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:
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { SystemStatus } from '../types'
|
||||
|
||||
interface LegalConsentProps {
|
||||
status: SystemStatus | null
|
||||
checked: boolean
|
||||
onCheckedChange: (nextValue: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LegalConsent({
|
||||
status,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
className,
|
||||
}: LegalConsentProps) {
|
||||
const { t } = useTranslation()
|
||||
const hasUserAgreement = Boolean(status?.user_agreement_enabled)
|
||||
const hasPrivacyPolicy = Boolean(status?.privacy_policy_enabled)
|
||||
|
||||
if (!hasUserAgreement && !hasPrivacyPolicy) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
onCheckedChange(value === true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-border/60 bg-muted/40 flex items-start gap-3 rounded-md border p-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id='legal-consent'
|
||||
checked={checked}
|
||||
onCheckedChange={handleChange}
|
||||
className='mt-0.5'
|
||||
/>
|
||||
<Label
|
||||
htmlFor='legal-consent'
|
||||
className='text-muted-foreground items-start gap-1 text-left text-xs leading-5 font-normal'
|
||||
>
|
||||
<span>
|
||||
{t('I have read and agree to the')}{' '}
|
||||
{hasUserAgreement && (
|
||||
<a
|
||||
href='/user-agreement'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-primary hover:underline'
|
||||
>
|
||||
{t('User Agreement')}
|
||||
</a>
|
||||
)}
|
||||
{hasUserAgreement && hasPrivacyPolicy && ' and the '}
|
||||
{hasPrivacyPolicy && (
|
||||
<a
|
||||
href='/privacy-policy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-primary hover:underline'
|
||||
>
|
||||
{t('Privacy Policy')}
|
||||
</a>
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
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 { Loader2, Send, Shield, UserRound, type LucideIcon } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SiGithub, SiLinux, SiWechat } from 'react-icons/si'
|
||||
|
||||
import { AuthLayout } from '../auth-layout'
|
||||
|
||||
type OAuthCallbackScreenProps = {
|
||||
provider: string
|
||||
mode: 'login' | 'bind'
|
||||
}
|
||||
|
||||
type ProviderMeta = {
|
||||
label: string
|
||||
Icon: LucideIcon | ((props: { className?: string }) => React.JSX.Element)
|
||||
}
|
||||
|
||||
const providerDictionary: Record<string, ProviderMeta> = {
|
||||
github: {
|
||||
label: 'GitHub',
|
||||
Icon: (props: { className?: string }) => (
|
||||
<SiGithub className={props.className} focusable='false' />
|
||||
),
|
||||
},
|
||||
oidc: { label: 'OIDC', Icon: Shield },
|
||||
linuxdo: {
|
||||
label: 'LinuxDO',
|
||||
Icon: (props: { className?: string }) => (
|
||||
<SiLinux className={props.className} focusable='false' />
|
||||
),
|
||||
},
|
||||
telegram: { label: 'Telegram', Icon: Send },
|
||||
wechat: {
|
||||
label: 'WeChat',
|
||||
Icon: (props: { className?: string }) => (
|
||||
<SiWechat className={props.className} focusable='false' />
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export function OAuthCallbackScreen({
|
||||
provider,
|
||||
mode,
|
||||
}: OAuthCallbackScreenProps) {
|
||||
const { t } = useTranslation()
|
||||
const { label, Icon } = useMemo(() => {
|
||||
const normalized = provider?.toLowerCase() ?? ''
|
||||
return (
|
||||
providerDictionary[normalized] || {
|
||||
label: 'account',
|
||||
Icon: UserRound,
|
||||
}
|
||||
)
|
||||
}, [provider])
|
||||
|
||||
const providerLabel = t(label)
|
||||
const isBindMode = mode === 'bind'
|
||||
|
||||
const headline = isBindMode
|
||||
? t('Binding your {{provider}} account', { provider: providerLabel })
|
||||
: t('Signing you in with {{provider}}', { provider: providerLabel })
|
||||
|
||||
const description = isBindMode
|
||||
? t('Hang tight while we securely link this account to your profile.')
|
||||
: t('Hang tight while we finish connecting your account.')
|
||||
|
||||
const secondaryNote = isBindMode
|
||||
? t(
|
||||
'You can close this tab once the binding completes or a success message appears in the original window.'
|
||||
)
|
||||
: t(
|
||||
"You'll be redirected automatically. You can return to the previous page if nothing happens after a few seconds."
|
||||
)
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div className='w-full space-y-8'>
|
||||
<div className='flex flex-col items-center space-y-4 text-center'>
|
||||
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
|
||||
<Icon className='h-8 w-8' />
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight'>
|
||||
{headline}
|
||||
</h2>
|
||||
<p className='text-muted-foreground text-sm sm:text-base'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4 text-center'>
|
||||
<div className='flex items-center justify-center gap-2 text-sm font-medium'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
<span>{t('Processing OAuth response...')}</span>
|
||||
</div>
|
||||
<p className='text-muted-foreground text-sm'>{secondaryNote}</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t(
|
||||
'This may take a few moments while we validate the request and update your session.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
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 { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
IconDiscord,
|
||||
IconGithub,
|
||||
IconLinuxDo,
|
||||
IconTelegram,
|
||||
IconWeChat,
|
||||
} from '@/assets/brand-icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { useOAuthLogin } from '../hooks/use-oauth-login'
|
||||
import type { SystemStatus } from '../types'
|
||||
import { TelegramLoginDialog } from './telegram-login-dialog'
|
||||
|
||||
type OAuthProvidersProps = {
|
||||
status: SystemStatus | null
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
onWeChatLogin?: () => void
|
||||
isWeChatLoading?: boolean
|
||||
redirectTo?: string
|
||||
}
|
||||
|
||||
type ProviderButton = {
|
||||
key: string
|
||||
label: string
|
||||
onClick: () => void
|
||||
icon?: ReactNode
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function OAuthProviders({
|
||||
status,
|
||||
disabled = false,
|
||||
className,
|
||||
onWeChatLogin,
|
||||
isWeChatLoading = false,
|
||||
redirectTo,
|
||||
}: OAuthProvidersProps) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isLoading,
|
||||
githubButtonText,
|
||||
githubButtonDisabled,
|
||||
handleGitHubLogin,
|
||||
handleDiscordLogin,
|
||||
handleOIDCLogin,
|
||||
handleLinuxDOLogin,
|
||||
handleTelegramLogin,
|
||||
handleCustomOAuthLogin,
|
||||
isTelegramDialogOpen,
|
||||
isTelegramPending,
|
||||
handleTelegramAuthorization,
|
||||
setIsTelegramDialogOpen,
|
||||
} = useOAuthLogin(status, redirectTo)
|
||||
|
||||
const providerButtons: ProviderButton[] = []
|
||||
|
||||
if (status?.wechat_login && onWeChatLogin) {
|
||||
providerButtons.push({
|
||||
key: 'wechat',
|
||||
label: t('Continue with WeChat'),
|
||||
onClick: onWeChatLogin,
|
||||
icon: <IconWeChat className='h-4 w-4' />,
|
||||
disabled: isWeChatLoading,
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.github_oauth) {
|
||||
providerButtons.push({
|
||||
key: 'github',
|
||||
label: githubButtonText || t('Continue with GitHub'),
|
||||
onClick: handleGitHubLogin,
|
||||
icon: <IconGithub className='h-4 w-4' />,
|
||||
disabled: githubButtonDisabled,
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.discord_oauth) {
|
||||
providerButtons.push({
|
||||
key: 'discord',
|
||||
label: t('Continue with Discord'),
|
||||
onClick: handleDiscordLogin,
|
||||
icon: <IconDiscord className='h-4 w-4' />,
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.oidc_enabled) {
|
||||
providerButtons.push({
|
||||
key: 'oidc',
|
||||
label: t('Continue with OIDC'),
|
||||
onClick: handleOIDCLogin,
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.linuxdo_oauth) {
|
||||
providerButtons.push({
|
||||
key: 'linuxdo',
|
||||
label: t('Continue with LinuxDO'),
|
||||
onClick: handleLinuxDOLogin,
|
||||
icon: <IconLinuxDo className='h-4 w-4' />,
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.telegram_oauth) {
|
||||
providerButtons.push({
|
||||
key: 'telegram',
|
||||
label: t('Continue with Telegram'),
|
||||
onClick: handleTelegramLogin,
|
||||
icon: <IconTelegram data-icon='inline-start' />,
|
||||
})
|
||||
}
|
||||
|
||||
// Custom OAuth providers
|
||||
const customProviders = status?.custom_oauth_providers
|
||||
if (customProviders && customProviders.length > 0) {
|
||||
for (const provider of customProviders) {
|
||||
providerButtons.push({
|
||||
key: `custom-${provider.slug}`,
|
||||
label: t('Continue with {{name}}', { name: provider.name }),
|
||||
onClick: () => handleCustomOAuthLogin(provider),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (providerButtons.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className='relative'>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<span className='w-full border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-xs uppercase'>
|
||||
<span className='bg-background text-muted-foreground px-2'>
|
||||
{t('Or continue with')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
{providerButtons.map(
|
||||
({ key, label, onClick, icon, disabled: extraDisabled }) => (
|
||||
<Button
|
||||
key={key}
|
||||
variant='outline'
|
||||
type='button'
|
||||
disabled={disabled || isLoading || extraDisabled}
|
||||
onClick={onClick}
|
||||
className='h-11 w-full justify-center gap-2 rounded-lg'
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TelegramLoginDialog
|
||||
open={isTelegramDialogOpen}
|
||||
botName={status?.telegram_bot_name ?? ''}
|
||||
pending={isTelegramPending}
|
||||
onOpenChange={setIsTelegramDialogOpen}
|
||||
onAuthorization={handleTelegramAuthorization}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
|
||||
type TelegramLoginDialogProps = {
|
||||
open: boolean
|
||||
botName: string
|
||||
pending: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onAuthorization: (authorization: unknown) => void
|
||||
}
|
||||
|
||||
let telegramCallbackSequence = 0
|
||||
|
||||
export function TelegramLoginDialog(props: TelegramLoginDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const widgetContainer = useRef<HTMLDivElement | null>(null)
|
||||
const authorizationHandler = useRef(props.onAuthorization)
|
||||
const [callbackName] = useState(
|
||||
() => `newApiTelegramLogin${++telegramCallbackSequence}`
|
||||
)
|
||||
const [widgetState, setWidgetState] = useState<
|
||||
'idle' | 'loading' | 'ready' | 'failed'
|
||||
>('idle')
|
||||
|
||||
useEffect(() => {
|
||||
authorizationHandler.current = props.onAuthorization
|
||||
}, [props.onAuthorization])
|
||||
|
||||
useEffect(() => {
|
||||
const container = widgetContainer.current
|
||||
const botName = props.botName.trim()
|
||||
if (!props.open || !container || !botName) return
|
||||
|
||||
setWidgetState('loading')
|
||||
const callback = (authorization: unknown) => {
|
||||
authorizationHandler.current(authorization)
|
||||
}
|
||||
const browserWindow = window as unknown as Record<string, unknown>
|
||||
browserWindow[callbackName] = callback
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = 'https://telegram.org/js/telegram-widget.js?22'
|
||||
script.dataset.telegramLogin = botName
|
||||
script.dataset.size = 'large'
|
||||
script.dataset.radius = '8'
|
||||
script.dataset.onauth = `${callbackName}(user)`
|
||||
const handleLoad = () => setWidgetState('ready')
|
||||
const handleError = () => setWidgetState('failed')
|
||||
script.addEventListener('load', handleLoad)
|
||||
script.addEventListener('error', handleError)
|
||||
container.replaceChildren(script)
|
||||
|
||||
return () => {
|
||||
script.removeEventListener('load', handleLoad)
|
||||
script.removeEventListener('error', handleError)
|
||||
container.replaceChildren()
|
||||
delete browserWindow[callbackName]
|
||||
}
|
||||
}, [callbackName, props.botName, props.open])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
title={t('Telegram Login Widget')}
|
||||
description={t('Continue with Telegram')}
|
||||
contentClassName='max-w-sm'
|
||||
contentHeight='auto'
|
||||
bodyClassName='space-y-4'
|
||||
>
|
||||
<div
|
||||
className='flex min-h-12 items-center justify-center'
|
||||
aria-busy={widgetState === 'loading' || props.pending}
|
||||
>
|
||||
{(widgetState === 'loading' || props.pending) && <Spinner />}
|
||||
{widgetState === 'failed' && (
|
||||
<p className='text-destructive text-sm'>{t('Login failed')}</p>
|
||||
)}
|
||||
<div
|
||||
ref={widgetContainer}
|
||||
className={
|
||||
widgetState === 'ready' && !props.pending ? 'block' : 'hidden'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { SystemStatus } from '../types'
|
||||
|
||||
interface TermsFooterProps {
|
||||
variant?: 'sign-in' | 'sign-up'
|
||||
className?: string
|
||||
status?: SystemStatus | null
|
||||
}
|
||||
|
||||
export function TermsFooter({
|
||||
variant = 'sign-in',
|
||||
className,
|
||||
status,
|
||||
}: TermsFooterProps) {
|
||||
const { t } = useTranslation()
|
||||
const text =
|
||||
variant === 'sign-in'
|
||||
? 'By clicking sign in, you agree to our'
|
||||
: 'By creating an account, you agree to our'
|
||||
|
||||
const hasUserAgreement = Boolean(status?.user_agreement_enabled)
|
||||
const hasPrivacyPolicy = Boolean(status?.privacy_policy_enabled)
|
||||
|
||||
if (!hasUserAgreement && !hasPrivacyPolicy) {
|
||||
return null
|
||||
}
|
||||
|
||||
const agreementLink = {
|
||||
label: 'User Agreement',
|
||||
href: '/user-agreement',
|
||||
}
|
||||
const privacyLink = {
|
||||
label: 'Privacy Policy',
|
||||
href: '/privacy-policy',
|
||||
}
|
||||
|
||||
const activeLinks =
|
||||
hasUserAgreement || hasPrivacyPolicy
|
||||
? ([
|
||||
hasUserAgreement ? agreementLink : null,
|
||||
hasPrivacyPolicy ? privacyLink : null,
|
||||
].filter(Boolean) as Array<{ label: string; href: string }>)
|
||||
: [agreementLink, privacyLink]
|
||||
|
||||
const [firstLink, secondLink] = activeLinks
|
||||
|
||||
return (
|
||||
<p className={cn('text-muted-foreground text-center text-xs', className)}>
|
||||
{text}{' '}
|
||||
{firstLink && (
|
||||
<a
|
||||
href={firstLink.href}
|
||||
className='hover:text-primary underline underline-offset-4'
|
||||
>
|
||||
{firstLink.label}
|
||||
</a>
|
||||
)}
|
||||
{secondLink && (
|
||||
<>
|
||||
{' '}
|
||||
{t('and')}{' '}
|
||||
<a
|
||||
href={secondLink.href}
|
||||
className='hover:text-primary underline underline-offset-4'
|
||||
>
|
||||
{secondLink.label}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user