refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)

* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
This commit is contained in:
Calcium-Ion
2026-07-20 16:48:43 +08:00
committed by GitHub
parent 5a6c53d496
commit 31d70fca39
1605 changed files with 17511 additions and 147913 deletions
+185
View File
@@ -0,0 +1,185 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export type JsonParseResult<T> =
| { success: true; data: T }
| { success: false; error: string }
export interface SafeJsonParseOptions<T> {
fallback?: T
silent?: boolean
context?: string
}
export interface SafeJsonParseWithValidationOptions<T> {
fallback: T
validator: (data: unknown) => data is T
validatorMessage?: string
context?: string
silent?: boolean
}
interface JsonErrorPosition {
line?: number
column?: number
position?: number
}
function extractErrorPosition(
error: unknown,
jsonString: string
): JsonErrorPosition {
if (!(error instanceof Error)) return {}
const message = error.message
// Try to extract position from error message
// Format 1: "Unexpected token } in JSON at position 15"
const positionMatch = message.match(/at position (\d+)/i)
if (positionMatch) {
const position = parseInt(positionMatch[1], 10)
const { line, column } = getLineAndColumn(jsonString, position)
return { line, column, position }
}
// Format 2: "JSON.parse: ... at line 2 column 3"
const lineColMatch = message.match(/at line (\d+) column (\d+)/i)
if (lineColMatch) {
return {
line: parseInt(lineColMatch[1], 10),
column: parseInt(lineColMatch[2], 10),
}
}
return {}
}
function getLineAndColumn(
text: string,
position: number
): { line: number; column: number } {
const lines = text.substring(0, position).split('\n')
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
}
}
function formatErrorDescription(
error: unknown,
jsonString: string
): string | undefined {
if (!(error instanceof Error)) return undefined
const position = extractErrorPosition(error, jsonString)
const message = error.message
// Check if it's a "missing comma" type error
const isMissingCommaError =
message.includes("Expected ','") ||
message.includes('Expected property name') ||
message.includes('Unexpected string')
if (position.line && position.column) {
let hint = ''
if (isMissingCommaError && position.line > 1) {
hint = ` (check line ${position.line - 1} for missing comma)`
}
return `Error at line ${position.line}, column ${position.column}: ${message}${hint}`
}
if (position.position !== undefined) {
return `Error at position ${position.position}: ${message}`
}
return message
}
export function safeJsonParse<T = unknown>(
value: string | undefined | null,
options: SafeJsonParseOptions<T> = {}
): T {
const { fallback, silent = false, context } = options
if (!value || value.trim() === '') {
return (fallback ?? null) as T
}
const trimmedValue = value.trim()
try {
return JSON.parse(trimmedValue) as T
} catch (error) {
// Log error for debugging in development
if (import.meta.env.DEV && !silent) {
const message = context
? `Failed to parse ${context}`
: 'Invalid JSON format'
const description = formatErrorDescription(error, trimmedValue)
// eslint-disable-next-line no-console
console.error(`[JSON Parse Error] ${message}:`, description)
}
return (fallback ?? null) as T
}
}
export function safeJsonParseWithValidation<T>(
value: string | undefined | null,
options: SafeJsonParseWithValidationOptions<T>
): T {
const {
fallback,
validator,
validatorMessage,
context,
silent = false,
} = options
const parsed = safeJsonParse(value, { fallback, silent: true, context })
if (!validator(parsed)) {
// Log error for debugging in development
if (import.meta.env.DEV && !silent) {
const message =
validatorMessage ??
(context ? `Invalid ${context} structure` : 'Invalid data structure')
// eslint-disable-next-line no-console
console.error(`[JSON Validation Error] ${message}`, { parsed })
}
return fallback
}
return parsed
}
export function tryJsonParse<T = unknown>(
value: string | undefined | null
): JsonParseResult<T> {
if (!value || value.trim() === '') {
return { success: false, error: 'Empty value' }
}
try {
const data = JSON.parse(value.trim()) as T
return { success: true, data }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
@@ -0,0 +1,53 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export const isObjectRecord = (
data: unknown
): data is Record<string, unknown> =>
typeof data === 'object' && data !== null && !Array.isArray(data)
export const isArray = (data: unknown): data is unknown[] => Array.isArray(data)
export const isStringArray = (data: unknown): data is string[] =>
Array.isArray(data) && data.every((item) => typeof item === 'string')
export const isNumberArray = (data: unknown): data is number[] =>
Array.isArray(data) && data.every((item) => typeof item === 'number')
export const isObjectArray = (
data: unknown
): data is Record<string, unknown>[] =>
Array.isArray(data) && data.every((item) => isObjectRecord(item))
export function createObjectValidator<T extends Record<string, unknown>>(
requiredKeys: (keyof T)[]
): (data: unknown) => data is T {
return (data): data is T => {
if (!isObjectRecord(data)) return false
return requiredKeys.every((key) => key in data)
}
}
export function createArrayValidator<T>(
itemValidator: (item: unknown) => item is T
): (data: unknown) => data is T[] {
return (data): data is T[] => {
if (!Array.isArray(data)) return false
return data.every(itemValidator)
}
}
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ChangeEvent } from 'react'
import type {
ControllerRenderProps,
FieldPath,
FieldValues,
} from 'react-hook-form'
/**
* Props produced by {@link safeNumberFieldProps} for a native
* `<input type="number">`. They are intentionally narrow so consumers can
* spread them onto our shared `Input` component without leaking the
* react-hook-form internals (e.g. `disabled`) that need overriding per call.
*/
export type SafeNumberFieldProps = {
value: number | ''
onChange: (event: ChangeEvent<HTMLInputElement>) => void
onBlur: () => void
name: string
ref: (instance: HTMLInputElement | null) => void
}
/**
* Adapter for binding a react-hook-form numeric field to a native
* `<input type="number">` without ever putting `NaN` into form state.
*
* Why this exists:
* - `<input type="number">` reports `valueAsNumber === NaN` whenever the field
* is empty or holds an in-progress non-numeric token (e.g. just a minus
* sign or a trailing dot). Forwarding `NaN` to `field.onChange` makes Zod
* numeric validators (`z.number().min(...)`, `z.coerce.number()`, etc.)
* fail at submit time, so `form.handleSubmit` silently refuses to call
* `onSubmit` — the save button appears frozen with no toast and no error.
* - Numeric inputs should snap back to the previous valid number instead of
* keeping `NaN`. We preserve that behaviour by ignoring `NaN`
* updates: React's controlled-input reconciliation will restore the last
* valid value to the DOM on the next render.
*
* Display:
* - When the underlying state is not a finite number, the prop returns `''`
* so the input visibly renders empty instead of literal "NaN".
*
* Usage:
* ```tsx
* <FormField
* control={form.control}
* name='performance_setting.monitor_cpu_threshold'
* render={({ field }) => (
* <Input type='number' min={0} {...safeNumberFieldProps(field)} />
* )}
* />
* ```
*/
export function safeNumberFieldProps<
TFieldValues extends FieldValues,
TName extends FieldPath<TFieldValues>,
>(field: ControllerRenderProps<TFieldValues, TName>): SafeNumberFieldProps {
const raw = field.value as unknown
const display: number | '' =
typeof raw === 'number' && Number.isFinite(raw) ? raw : ''
return {
value: display,
onChange: (event) => {
const next = event.target.valueAsNumber
if (Number.isFinite(next)) {
;(field.onChange as (value: number) => void)(next)
}
},
onBlur: field.onBlur,
name: field.name,
ref: field.ref,
}
}
+110
View File
@@ -0,0 +1,110 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { redirect } from '@tanstack/react-router'
import * as z from 'zod'
/**
* Create search schema for settings routes with section parameter
*/
export function createSectionSearchSchema<TSectionId extends string>(
sectionIds: readonly [TSectionId, ...TSectionId[]],
defaultSection: TSectionId
) {
return z.object({
section: z
.enum(sectionIds as unknown as [string, ...string[]])
.optional()
.catch(defaultSection),
})
}
/**
* Configuration for creating a settings route config
*/
export type SettingsRouteConfigOptions<
TSectionId extends string,
TComponent = unknown,
> = {
/** Section IDs array from section-registry */
sectionIds: readonly [TSectionId, ...TSectionId[]]
/** Default section ID */
defaultSection: TSectionId
/** Settings component to render */
component: TComponent
/** Route path for redirect (e.g., '/system-settings/site') */
routePath: string
/** Whether to redirect to default section if no section is provided (default: false) */
redirectToDefault?: boolean
}
/**
* Create a settings route configuration with common setup
* This abstracts the repetitive pattern of:
* - Creating search schema
* - Setting up validateSearch
* - Optionally redirecting to default section
*
* @example
* ```tsx
* export const Route = createFileRoute('/_authenticated/system-settings/site')(
* createSettingsRouteConfig({
* sectionIds: SITE_SECTION_IDS,
* defaultSection: SITE_DEFAULT_SECTION,
* component: SiteSettings,
* routePath: '/system-settings/site',
* redirectToDefault: true,
* })
* )
* ```
*/
export function createSettingsRouteConfig<
TSectionId extends string,
TComponent = unknown,
>(options: SettingsRouteConfigOptions<TSectionId, TComponent>) {
const {
sectionIds,
defaultSection,
component,
routePath,
redirectToDefault = false,
} = options
const searchSchema = createSectionSearchSchema(sectionIds, defaultSection)
const routeConfig = {
validateSearch: searchSchema,
component,
...(redirectToDefault && {
beforeLoad: ({
search,
}: {
search?: { section?: TSectionId | string }
}) => {
if (!search?.section) {
throw redirect({
to: routePath,
search: { section: defaultSection } as Record<string, unknown>,
})
}
},
}),
}
return routeConfig
}
@@ -0,0 +1,100 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { TFunction } from 'i18next'
import type { ReactNode } from 'react'
/**
* Section definition for settings pages
*/
export type SectionDefinition<TSettings, TExtraArgs extends unknown[] = []> = {
id: string
titleKey: string
build: (settings: TSettings, ...extraArgs: TExtraArgs) => ReactNode
}
/**
* Section registry configuration
*/
export type SectionRegistryConfig<
TSectionId extends string,
TSettings,
TExtraArgs extends unknown[] = [],
> = {
sections: readonly SectionDefinition<TSettings, TExtraArgs>[]
defaultSection: TSectionId
basePath: string
/** 'query' = `${basePath}?section=${id}`, 'path' = `${basePath}/${id}` */
urlStyle?: 'query' | 'path'
}
/**
* Create a section registry with helper functions
*/
export function createSectionRegistry<
TSectionId extends string,
TSettings,
TExtraArgs extends unknown[] = [],
>(config: SectionRegistryConfig<TSectionId, TSettings, TExtraArgs>) {
const { sections, defaultSection, basePath, urlStyle = 'query' } = config
type SectionId = TSectionId
const sectionIds = sections.map((section) => section.id) as [
SectionId,
...SectionId[],
]
/**
* Get navigation items for sidebar
*/
function getSectionNavItems(t: TFunction) {
return sections.map((section) => ({
title: t(section.titleKey),
url:
urlStyle === 'path'
? `${basePath}/${section.id}`
: `${basePath}?section=${section.id}`,
}))
}
/**
* Get section content by section ID
*/
function getSectionContent(
sectionId: SectionId,
settings: TSettings,
...extraArgs: TExtraArgs
) {
return getSectionMeta(sectionId).build(settings, ...extraArgs)
}
function getSectionMeta(sectionId: SectionId) {
const section =
sections.find((item) => item.id === sectionId) ?? sections[0]
return section
}
return {
sectionIds,
defaultSection,
getSectionNavItems,
getSectionContent,
getSectionMeta,
}
}