diff --git a/dashboard-fresh/app/(auth)/email-verification/EmailVerificationContent.tsx b/dashboard-fresh/app/(auth)/email-verification/EmailVerificationContent.tsx deleted file mode 100644 index ec027f1..0000000 --- a/dashboard-fresh/app/(auth)/email-verification/EmailVerificationContent.tsx +++ /dev/null @@ -1,323 +0,0 @@ -'use client' - -import { - ChangeEvent, - FormEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' -import { useRouter, useSearchParams } from 'next/navigation' - -import { AuthLayout } from '@components/auth/AuthLayout' -import { useLanguage } from '@i18n/LanguageProvider' -import { translations } from '@i18n/translations' - -const VERIFICATION_CODE_LENGTH = 6 -const RESEND_COOLDOWN_SECONDS = 60 - -const EMAIL_QUERY_KEYS = ['email', 'address', 'identifier', 'account'] as const - -type AlertState = { type: 'error' | 'success' | 'info'; message: string } - -export default function EmailVerificationContent() { - const { language } = useLanguage() - const t = translations[language].auth.emailVerification - const router = useRouter() - const searchParams = useSearchParams() - const redirectTimeoutRef = useRef(null) - - const email = useMemo(() => { - for (const key of EMAIL_QUERY_KEYS) { - const value = searchParams.get(key) - if (typeof value === 'string' && value.trim().length > 0) { - return value.trim().toLowerCase() - } - } - return '' - }, [searchParams]) - - const statusParam = searchParams.get('status') - const errorParam = searchParams.get('error') - - const descriptionEmail = email || t.emailFallback || '' - const description = useMemo(() => { - if (!t.description.includes('{{email}}')) { - return t.description - } - return t.description.replace('{{email}}', descriptionEmail) - }, [descriptionEmail, t.description]) - - const initialAlert = useMemo(() => { - if (statusParam === 'sent') { - return { type: 'info', message: t.alerts.verificationSent } - } - if (statusParam === 'resent') { - return { - type: 'success', - message: t.alerts.verificationResent ?? t.alerts.verificationSent, - } - } - if (errorParam) { - const normalized = errorParam - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') - const errorMap: Record = { - missing_verification: t.alerts.codeRequired, - verification_failed: t.alerts.verificationFailed, - invalid_code: t.alerts.verificationFailed, - invalid_email: t.alerts.missingEmail, - code_required: t.alerts.codeRequired, - } - const message = errorMap[normalized] ?? t.alerts.genericError - return { type: normalized === 'already_verified' ? 'success' : 'error', message } - } - if (!email) { - return { type: 'info', message: t.alerts.missingEmail } - } - return null - }, [email, errorParam, statusParam, t.alerts]) - - const [alert, setAlert] = useState(initialAlert) - const [code, setCode] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const [isResending, setIsResending] = useState(false) - const [resendCooldown, setResendCooldown] = useState(0) - - useEffect(() => { - setAlert(initialAlert) - }, [initialAlert]) - - useEffect(() => { - if (resendCooldown <= 0) { - return undefined - } - - const timeoutId = window.setTimeout(() => { - setResendCooldown(previous => Math.max(previous - 1, 0)) - }, 1000) - - return () => { - window.clearTimeout(timeoutId) - } - }, [resendCooldown]) - - useEffect(() => { - return () => { - if (redirectTimeoutRef.current !== null) { - window.clearTimeout(redirectTimeoutRef.current) - } - } - }, []) - - const handleCodeChange = useCallback((event: ChangeEvent) => { - const digitsOnly = event.target.value.replace(/\D/g, '').slice(0, VERIFICATION_CODE_LENGTH) - setCode(digitsOnly) - }, []) - - const hasEmail = email.length > 0 - const isSubmitDisabled = - isSubmitting || !hasEmail || code.length !== VERIFICATION_CODE_LENGTH - const isResendDisabled = isResending || resendCooldown > 0 || !hasEmail - - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() - if (isSubmitting) { - return - } - if (!hasEmail) { - setAlert({ type: 'error', message: t.alerts.missingEmail }) - return - } - if (code.length !== VERIFICATION_CODE_LENGTH) { - setAlert({ type: 'error', message: t.alerts.codeRequired }) - return - } - - setIsSubmitting(true) - setAlert(null) - - try { - const response = await fetch('/api/auth/verify-email', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, code }), - }) - - const payload = (await response.json().catch(() => ({}))) as { - success?: boolean - error?: string | null - } - - if (!response.ok || payload?.success !== true) { - const errorCode = typeof payload?.error === 'string' ? payload.error : 'verification_failed' - const normalized = errorCode - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') - - if (normalized === 'already_verified') { - const message = t.alerts.verificationReady ?? t.alerts.verificationSent - setAlert({ type: 'success', message }) - redirectTimeoutRef.current = window.setTimeout(() => { - router.push('/login?registered=1') - }, 1200) - return - } - - const errorMap: Record = { - missing_verification: t.alerts.codeRequired, - invalid_code: t.alerts.verificationFailed, - verification_failed: t.alerts.verificationFailed, - invalid_email: t.alerts.missingEmail, - code_expired: t.alerts.verificationFailed, - } - const message = errorMap[normalized] ?? t.alerts.genericError - setAlert({ type: 'error', message }) - return - } - - const successMessage = t.alerts.verificationReady ?? t.alerts.verificationSent - setAlert({ type: 'success', message: successMessage }) - setCode('') - redirectTimeoutRef.current = window.setTimeout(() => { - router.push('/login?registered=1') - }, 1200) - } catch (error) { - console.error('Email verification request failed', error) - setAlert({ type: 'error', message: t.alerts.genericError }) - } finally { - setIsSubmitting(false) - } - }, [code, email, hasEmail, isSubmitting, router, t.alerts]) - - const handleResend = useCallback(async () => { - if (isResending || !hasEmail) { - if (!hasEmail) { - setAlert({ type: 'error', message: t.alerts.missingEmail }) - } - return - } - - setIsResending(true) - - try { - const response = await fetch('/api/auth/verify-email/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email }), - }) - - const payload = (await response.json().catch(() => ({}))) as { - success?: boolean - error?: string | null - } - - if (!response.ok || payload?.success !== true) { - const errorCode = typeof payload?.error === 'string' ? payload.error : 'verification_failed' - const normalized = errorCode - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') - - if (normalized === 'already_verified') { - const message = t.alerts.verificationReady ?? t.alerts.verificationSent - setAlert({ type: 'success', message }) - redirectTimeoutRef.current = window.setTimeout(() => { - router.push('/login?registered=1') - }, 1200) - return - } - - const errorMap: Record = { - invalid_email: t.alerts.missingEmail, - verification_failed: t.alerts.verificationFailed, - rate_limited: t.alerts.genericError, - } - const message = errorMap[normalized] ?? t.alerts.genericError - setAlert({ type: 'error', message }) - return - } - - const successMessage = t.alerts.verificationResent ?? t.alerts.verificationSent - setAlert({ type: 'success', message: successMessage }) - setResendCooldown(RESEND_COOLDOWN_SECONDS) - } catch (error) { - console.error('Email verification resend failed', error) - setAlert({ type: 'error', message: t.alerts.genericError }) - } finally { - setIsResending(false) - } - }, [email, hasEmail, isResending, router, t.alerts]) - - const resendLabel = isResending - ? t.resend.resending ?? t.resend.label - : resendCooldown > 0 - ? `${t.resend.label} (${resendCooldown}s)` - : t.resend.label - - return ( - -
-
- - - {t.form.helper ? ( -

- {t.form.helper} -

- ) : null} -
- -
- -
- ) -} diff --git a/dashboard-fresh/app/(auth)/email-verification/page.tsx b/dashboard-fresh/app/(auth)/email-verification/page.tsx deleted file mode 100644 index 3801132..0000000 --- a/dashboard-fresh/app/(auth)/email-verification/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -export const dynamic = 'force-dynamic' - -import { Suspense } from 'react' -import { notFound } from 'next/navigation' - -import { isFeatureEnabled } from '@lib/featureToggles' - -import EmailVerificationContent from './EmailVerificationContent' - -function EmailVerificationPageFallback() { - return
-} - -export default function EmailVerificationPage() { - if (!isFeatureEnabled('globalNavigation', '/email-verification')) { - notFound() - } - - return ( - }> - - - ) -} diff --git a/dashboard-fresh/app/(auth)/layout.tsx b/dashboard-fresh/app/(auth)/layout.tsx deleted file mode 100644 index 1b02a89..0000000 --- a/dashboard-fresh/app/(auth)/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { ReactNode } from 'react' - -import { AppShellBypass } from '@lib/appShellBypass' - -export default function AuthPagesLayout({ children }: { children: ReactNode }) { - return ( - -
- {children} -
-
- ) -} diff --git a/dashboard-fresh/app/(auth)/login/LoginContent.tsx b/dashboard-fresh/app/(auth)/login/LoginContent.tsx deleted file mode 100644 index f87dfb6..0000000 --- a/dashboard-fresh/app/(auth)/login/LoginContent.tsx +++ /dev/null @@ -1,347 +0,0 @@ -'use client' - -import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Link from 'next/link' -import { useRouter, useSearchParams } from 'next/navigation' -import { Github } from 'lucide-react' - -import { AuthLayout, AuthLayoutSocialButton } from '@components/auth/AuthLayout' -import { useLanguage } from '@i18n/LanguageProvider' -import { translations } from '@i18n/translations' - -import { WeChatIcon } from '../../components/icons/WeChatIcon' - -type LoginContentProps = { - accountServiceBaseUrl: string - children?: ReactNode -} - -export default function LoginContent({ accountServiceBaseUrl, children }: LoginContentProps) { - const { language } = useLanguage() - const t = translations[language].auth.login - const alerts = t.alerts - const searchParams = useSearchParams() - const router = useRouter() - - useEffect(() => { - const sensitiveKeys = ['username', 'password', 'email'] - const hasSensitiveParams = sensitiveKeys.some((key) => searchParams.has(key)) - - if (!hasSensitiveParams) { - return - } - - const sanitized = new URLSearchParams(searchParams.toString()) - sensitiveKeys.forEach((key) => sanitized.delete(key)) - - const queryString = sanitized.toString() - router.replace(queryString ? `/login?${queryString}` : '/login', { scroll: false }) - }, [router, searchParams]) - - const errorParam = searchParams.get('error') - const registeredParam = searchParams.get('registered') - const setupMfaParam = searchParams.get('setupMfa') - - const normalize = useCallback( - (value: string) => - value - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, ''), - [], - ) - - const githubAuthUrl = process.env.NEXT_PUBLIC_GITHUB_AUTH_URL || '/api/auth/github' - const wechatAuthUrl = process.env.NEXT_PUBLIC_WECHAT_AUTH_URL || '/api/auth/wechat' - const loginUrl = process.env.NEXT_PUBLIC_LOGIN_URL || `${accountServiceBaseUrl}/api/auth/login` - - const loginUrlRef = useRef(loginUrl) - - const deriveSameOriginLoginFallback = useCallback((url: string): string | undefined => { - if (typeof window === 'undefined') { - return undefined - } - - try { - const currentOrigin = window.location.origin - const parsed = new URL(url, currentOrigin) - - if (parsed.origin === currentOrigin) { - const relative = `${parsed.pathname}${parsed.search}${parsed.hash}` || '/api/auth/login' - return relative - } - - const localHostnames = new Set(['localhost', '127.0.0.1', '[::1]']) - const parsedHostname = parsed.hostname.toLowerCase() - const browserHostname = window.location.hostname.toLowerCase() - - const parsedIsLocal = localHostnames.has(parsedHostname) - const browserIsLocal = localHostnames.has(browserHostname) - - if (!browserIsLocal && parsedIsLocal) { - const relative = `${parsed.pathname}${parsed.search}${parsed.hash}` || '/api/auth/login' - return relative - } - - if ( - window.location.protocol === 'https:' && - parsed.protocol === 'http:' && - parsedHostname === browserHostname - ) { - parsed.protocol = 'https:' - return parsed.toString() - } - } catch (error) { - console.warn('Failed to derive same-origin login fallback', error) - } - - return undefined - }, []) - - useEffect(() => { - loginUrlRef.current = loginUrl - }, [loginUrl]) - - const socialButtonsDisabled = true - - const initialAlert = useMemo(() => { - const successMessages: string[] = [] - if (registeredParam === '1') { - successMessages.push(alerts.registered) - } - if (setupMfaParam === '1') { - const setupRequiredMessage = alerts.mfa?.setupRequired ?? alerts.genericError - if (setupRequiredMessage) { - successMessages.push(setupRequiredMessage) - } - } - - if (successMessages.length > 0) { - return { type: 'success', message: successMessages.join(' ') } as const - } - - if (!errorParam) { - return null - } - - const normalizedError = normalize(errorParam) - const errorMap: Record = { - missing_credentials: alerts.missingCredentials, - email_and_password_are_required: alerts.missingCredentials, - invalid_credentials: alerts.invalidCredentials, - user_not_found: alerts.userNotFound ?? alerts.genericError, - credentials_in_query: alerts.genericError, - invalid_request: alerts.genericError, - - } - const message = errorMap[normalizedError] ?? alerts.genericError - return { type: 'error', message } as const - }, [alerts, errorParam, normalize, registeredParam, setupMfaParam]) - - const [alert, setAlert] = useState(initialAlert) - const [isSubmitting, setIsSubmitting] = useState(false) - - useEffect(() => { - setAlert(initialAlert) - }, [initialAlert]) - - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() - if (isSubmitting) { - return - } - - const formData = new FormData(event.currentTarget) - const username = String(formData.get('username') ?? '').trim() - const password = String(formData.get('password') ?? '') - const remember = formData.get('remember') === 'on' - - if (!username || !password) { - setAlert({ type: 'error', message: alerts.missingCredentials }) - return - } - - setIsSubmitting(true) - setAlert(null) - - try { - const requestPayload = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ - username, - password, - remember, - }), - } as const - - let response: Response - let usedUrl = loginUrlRef.current - - try { - response = await fetch(usedUrl, requestPayload) - } catch (primaryError) { - const sameOriginFallback = deriveSameOriginLoginFallback(usedUrl) - if (sameOriginFallback && sameOriginFallback !== usedUrl) { - try { - response = await fetch(sameOriginFallback, requestPayload) - loginUrlRef.current = sameOriginFallback - usedUrl = sameOriginFallback - } catch (fallbackError) { - console.error('Primary login request failed, same-origin fallback also failed', fallbackError) - throw fallbackError - } - } else { - const httpsPattern = /^https:/i - if (httpsPattern.test(usedUrl)) { - const insecureUrl = usedUrl.replace(httpsPattern, 'http:') - - try { - response = await fetch(insecureUrl, requestPayload) - loginUrlRef.current = insecureUrl - usedUrl = insecureUrl - } catch (fallbackError) { - console.error('Primary login request failed, insecure fallback also failed', fallbackError) - throw fallbackError - } - } else { - throw primaryError - } - } - } - - if (!response.ok) { - let errorCode = 'invalid_credentials' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse login response', error) - } - - const errorMap: Record = { - invalid_credentials: alerts.invalidCredentials, - missing_credentials: alerts.missingCredentials, - user_not_found: alerts.userNotFound ?? alerts.genericError, - invalid_request: alerts.genericError, - credentials_in_query: alerts.genericError, - } - - setAlert({ type: 'error', message: errorMap[normalize(errorCode)] ?? alerts.genericError }) - return - } - - const data: { redirectTo?: string } = await response - .json() - .catch(() => ({})) - router.push(data?.redirectTo || '/') - router.refresh() - } catch (error) { - console.error('Failed to submit login request', error) - setAlert({ type: 'error', message: alerts.genericError }) - } finally { - setIsSubmitting(false) - } - }, - [alerts, deriveSameOriginLoginFallback, isSubmitting, normalize, router], - ) - - const socialButtons = useMemo(() => { - return [ - { - label: t.social.github, - href: githubAuthUrl, - icon: , - disabled: socialButtonsDisabled, - }, - { - label: t.social.wechat, - href: wechatAuthUrl, - icon: , - disabled: socialButtonsDisabled, - }, - ] - }, [githubAuthUrl, socialButtonsDisabled, t.social.github, t.social.wechat, wechatAuthUrl]) - - const formContent = useMemo(() => { - if (children) { - return children - } - - return ( -
-
- - -
-
-
- - - {t.forgotPassword} - -
- -
- - -
- ) - }, [children, handleSubmit, isSubmitting, t]) - return ( - - {formContent} - - ) -} diff --git a/dashboard-fresh/app/(auth)/login/LoginForm.tsx b/dashboard-fresh/app/(auth)/login/LoginForm.tsx deleted file mode 100644 index a25c76b..0000000 --- a/dashboard-fresh/app/(auth)/login/LoginForm.tsx +++ /dev/null @@ -1,349 +0,0 @@ -'use client' - -import { FormEvent, useEffect, useState } from 'react' -import Link from 'next/link' -import { useRouter } from 'next/navigation' - -import { useLanguage } from '@i18n/LanguageProvider' -import { translations } from '@i18n/translations' -import { useUser } from '@lib/userStore' - -export function LoginForm() { - const router = useRouter() - const { language } = useLanguage() - const pageCopy = translations[language].login - const authCopy = translations[language].auth.login - const navCopy = translations[language].nav.account - const { user, login } = useUser() - const userEmail = user?.email ?? '' - const [identifier, setIdentifier] = useState(() => userEmail) - const [password, setPassword] = useState('') - const [totpCode, setTotpCode] = useState('') - const [remember, setRemember] = useState(false) - const [error, setError] = useState(null) - const [isSubmitting, setIsSubmitting] = useState(false) - const [mfaRequirement, setMfaRequirement] = useState<'optional' | 'required'>(() => - user?.mfaEnabled ? 'required' : 'optional', - ) - - useEffect(() => { - if (userEmail && identifier.trim().length === 0) { - setIdentifier(userEmail) - } - }, [identifier, userEmail]) - - useEffect(() => { - setTotpCode('') - }, [identifier]) - - useEffect(() => { - if (mfaRequirement !== 'required' && totpCode !== '') { - setTotpCode('') - } - }, [mfaRequirement, totpCode]) - - useEffect(() => { - let isActive = true - const trimmedIdentifier = identifier.trim() - - if (!trimmedIdentifier) { - if (isActive) { - setMfaRequirement('optional') - } - return () => { - isActive = false - } - } - - const normalizedIdentifier = trimmedIdentifier.toLowerCase() - - const controller = new AbortController() - const signal = controller.signal - - const timeoutId = window.setTimeout(async () => { - try { - const response = await fetch( - `/api/auth/mfa/status?identifier=${encodeURIComponent(normalizedIdentifier)}`, - { - method: 'GET', - cache: 'no-store', - signal, - }, - ) - - if (!isActive || signal.aborted) { - return - } - - if (!response.ok) { - setMfaRequirement('optional') - return - } - - const payload = (await response.json().catch(() => ({}))) as { - mfa?: { totpEnabled?: boolean } - } - - const requiresMfa = Boolean(payload?.mfa?.totpEnabled) - setMfaRequirement(requiresMfa ? 'required' : 'optional') - } catch (lookupError) { - if ((lookupError as Error)?.name === 'AbortError' || signal.aborted) { - return - } - setMfaRequirement('optional') - } - }, 300) - - return () => { - isActive = false - controller.abort() - window.clearTimeout(timeoutId) - } - }, [identifier]) - - useEffect(() => { - if (user?.mfaEnabled) { - setMfaRequirement('required') - } - }, [user?.mfaEnabled]) - - const handleSubmit = async (event: FormEvent) => { - event.preventDefault() - - const trimmedIdentifier = identifier.trim() - if (!trimmedIdentifier) { - setError(pageCopy.missingUsername) - return - } - if (!password) { - setError(pageCopy.missingPassword) - return - } - const requiresTotp = mfaRequirement === 'required' - const sanitizedTotp = totpCode.replace(/\D/g, '') - - // If TOTP is provided, validate its format (but don't require it) - if (sanitizedTotp && sanitizedTotp.length !== 6) { - setError( - authCopy.alerts.mfa?.invalidFormat ?? - authCopy.alerts.mfa?.invalid ?? - pageCopy.missingTotp ?? - authCopy.alerts.missingCredentials, - ) - return - } - - setError(null) - setIsSubmitting(true) - try { - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ - email: trimmedIdentifier, - password, - totp: sanitizedTotp.length === 6 ? sanitizedTotp : undefined, - remember, - }), - credentials: 'include', - }) - - const payload = (await response.json().catch(() => ({}))) as { - success?: boolean - error?: string | null - needMfa?: boolean - } - - if (payload.needMfa) { - setMfaRequirement('required') - router.replace('/panel/account?setupMfa=1') - router.refresh() - return - } - - const isSuccessful = response.ok && (payload.success ?? true) - - if (!isSuccessful) { - const messageKey = payload.error ?? 'generic_error' - if ( - messageKey === 'mfa_code_required' || - messageKey === 'invalid_mfa_code' || - messageKey === 'mfa_required' || - messageKey === 'mfa_setup_required' || - messageKey === 'mfa_challenge_failed' - ) { - setMfaRequirement('required') - } - switch (messageKey) { - case 'missing_credentials': - setError(authCopy.alerts.missingCredentials) - break - case 'invalid_credentials': - setError(pageCopy.invalidCredentials) - break - case 'user_not_found': - setError(pageCopy.userNotFound) - break - case 'mfa_code_required': - setError(authCopy.alerts.mfa?.missing ?? pageCopy.missingTotp ?? authCopy.alerts.missingCredentials) - break - case 'invalid_mfa_code': - setError(authCopy.alerts.mfa?.invalid ?? pageCopy.genericError) - break - case 'mfa_challenge_failed': - setError(authCopy.alerts.mfa?.challengeFailed ?? pageCopy.genericError) - break - case 'account_service_unreachable': - setError(pageCopy.serviceUnavailable ?? pageCopy.genericError) - break - default: - setError(pageCopy.genericError) - break - } - return - } - - await login() - router.replace('/') - router.refresh() - } catch (submitError) { - console.warn('Login failed', submitError) - setError(pageCopy.genericError) - } finally { - setIsSubmitting(false) - } - } - - const handleGoHome = () => { - router.replace('/') - router.refresh() - } - - const handleLogout = () => { - router.push('/logout') - } - - const requiresTotpInput = mfaRequirement === 'required' - const mfaModeLabel = requiresTotpInput - ? authCopy.form.mfa.passwordAndTotp - : authCopy.form.mfa.passwordOnly - - return ( - <> - {user ? ( -
-

- {pageCopy.success.replace('{username}', user.username)} -

-
- - -
-
- ) : null} - - {!user ? ( -
-
- - setIdentifier(event.target.value)} - placeholder={authCopy.form.emailPlaceholder} - className="w-full rounded-2xl border border-slate-200 bg-white/90 px-4 py-2.5 text-slate-900 shadow-sm transition focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" - /> -
-
-

{authCopy.form.mfa.mode}

-
- {mfaModeLabel} -
-
-
-
- - - {authCopy.forgotPassword} - -
- setPassword(event.target.value)} - placeholder={authCopy.form.passwordPlaceholder} - className="w-full rounded-2xl border border-slate-200 bg-white/90 px-4 py-2.5 text-slate-900 shadow-sm transition focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" - /> -
- {requiresTotpInput ? ( -
- - { - const digits = event.target.value.replace(/\D/g, '').slice(0, 6) - setTotpCode(digits) - }} - placeholder={authCopy.form.mfa.codePlaceholder} - className="w-full rounded-2xl border border-slate-200 bg-white/90 px-4 py-2.5 text-slate-900 shadow-sm transition focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" - /> -
- ) : null} - - - {error ?

{error}

: null} - - -

* {pageCopy.disclaimer}

-
- ) : null} - - ) -} diff --git a/dashboard-fresh/app/(auth)/login/page.tsx b/dashboard-fresh/app/(auth)/login/page.tsx deleted file mode 100644 index 200abae..0000000 --- a/dashboard-fresh/app/(auth)/login/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -export const dynamic = 'error' - -import { Suspense } from 'react' -import { notFound } from 'next/navigation' -import { isFeatureEnabled } from '@lib/featureToggles' -import { getAccountServiceBaseUrl } from '@server/serviceConfig' -import { LoginForm } from './LoginForm' -import LoginContent from './LoginContent' - -function LoginPageFallback() { - return
-} - -export default function LoginPage() { - if (!isFeatureEnabled('globalNavigation', '/login')) { - notFound() - } - const accountServiceBaseUrl = getAccountServiceBaseUrl() - // 统一返回:容器包裹表单,兼容两边改动 - return ( - }> - - - - - ) -} diff --git a/dashboard-fresh/app/(auth)/register/RegisterContent.tsx b/dashboard-fresh/app/(auth)/register/RegisterContent.tsx deleted file mode 100644 index a77e2d6..0000000 --- a/dashboard-fresh/app/(auth)/register/RegisterContent.tsx +++ /dev/null @@ -1,931 +0,0 @@ -'use client' - -import Link from 'next/link' -import { Github } from 'lucide-react' -import { - ChangeEvent, - ClipboardEvent, - FormEvent, - KeyboardEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, - useId, -} from 'react' -import { useRouter, useSearchParams } from 'next/navigation' - -import { AuthLayout, AuthLayoutSocialButton } from '@components/auth/AuthLayout' -import { useLanguage } from '@i18n/LanguageProvider' -import { translations } from '@i18n/translations' - -import { WeChatIcon } from '../../components/icons/WeChatIcon' - -type AlertState = { type: 'error' | 'success' | 'info'; message: string } - -const VERIFICATION_CODE_LENGTH = 6 -const RESEND_COOLDOWN_SECONDS = 60 -const EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ -const PASSWORD_STRENGTH_PATTERN = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/ - -export default function RegisterContent() { - const { language } = useLanguage() - const t = translations[language].auth.register - const alerts = t.alerts - const searchParams = useSearchParams() - const router = useRouter() - - const githubAuthUrl = process.env.NEXT_PUBLIC_GITHUB_AUTH_URL || '/api/auth/github' - const wechatAuthUrl = process.env.NEXT_PUBLIC_WECHAT_AUTH_URL || '/api/auth/wechat' - const isSocialAuthVisible = false - - const socialButtons = useMemo(() => { - if (!isSocialAuthVisible) { - return [] - } - - return [ - { - label: t.social.github, - href: githubAuthUrl, - icon: , - }, - { - label: t.social.wechat, - href: wechatAuthUrl, - icon: , - }, - ] - }, [githubAuthUrl, isSocialAuthVisible, t.social.github, t.social.wechat, wechatAuthUrl]) - - useEffect(() => { - const sensitiveKeys = ['username', 'password', 'confirmPassword', 'email'] - const hasSensitiveParams = sensitiveKeys.some((key) => searchParams.has(key)) - - if (!hasSensitiveParams) { - return - } - - const sanitized = new URLSearchParams(searchParams.toString()) - sensitiveKeys.forEach((key) => sanitized.delete(key)) - - const queryString = sanitized.toString() - router.replace(queryString ? `/register?${queryString}` : '/register', { scroll: false }) - }, [router, searchParams]) - - const normalize = useCallback( - (value: string) => - value - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, ''), - [], - ) - - const initialAlert = useMemo(() => { - const errorParam = searchParams.get('error') - const successParam = searchParams.get('success') - - if (successParam === '1') { - return { type: 'success', message: alerts.success } - } - - if (!errorParam) { - return null - } - - const normalizedError = normalize(errorParam) - const errorMap: Record = { - missing_fields: alerts.missingFields, - email_and_password_are_required: alerts.missingFields, - password_mismatch: alerts.passwordMismatch, - user_already_exists: alerts.userExists, - email_must_be_a_valid_address: alerts.invalidEmail, - password_must_be_at_least_8_characters: alerts.weakPassword, - email_already_exists: alerts.userExists, - name_already_exists: alerts.usernameExists ?? alerts.userExists, - invalid_email: alerts.invalidEmail, - password_too_short: alerts.weakPassword, - invalid_name: alerts.invalidName ?? alerts.genericError, - name_required: alerts.invalidName ?? alerts.genericError, - credentials_in_query: alerts.genericError, - } - const message = errorMap[normalizedError] ?? alerts.genericError - return { type: 'error', message } - }, [alerts, normalize, searchParams]) - - const [alert, setAlert] = useState(initialAlert) - const [isSubmitting, setIsSubmitting] = useState(false) - const [codeDigits, setCodeDigits] = useState(() => Array(VERIFICATION_CODE_LENGTH).fill('')) - const [hasRequestedCode, setHasRequestedCode] = useState(false) - const [pendingEmail, setPendingEmail] = useState('') - const [pendingPassword, setPendingPassword] = useState('') - const [isResending, setIsResending] = useState(false) - const [resendCooldown, setResendCooldown] = useState(0) - const [isVerified, setIsVerified] = useState(false) - const [formValues, setFormValues] = useState({ - email: '', - password: '', - confirmPassword: '', - agreement: false, - }) - const [isFormReady, setIsFormReady] = useState(false) - const formRef = useRef(null) - const codeInputRefs = useRef<(HTMLInputElement | null)[]>([]) - - useEffect(() => { - setAlert(initialAlert) - }, [initialAlert]) - - useEffect(() => { - setIsFormReady(true) - }, []) - - useEffect(() => { - if (resendCooldown <= 0) { - return - } - - const timer = window.setInterval(() => { - setResendCooldown((current) => (current > 0 ? current - 1 : 0)) - }, 1000) - - return () => window.clearInterval(timer) - }, [resendCooldown]) - - const focusCodeInput = useCallback((index: number) => { - const input = codeInputRefs.current[index] - if (input) { - input.focus() - input.select() - } - }, []) - - const resetCodeDigits = useCallback(() => { - setCodeDigits(Array(VERIFICATION_CODE_LENGTH).fill('')) - }, []) - - const handleInputChange = useCallback( - (field: 'email' | 'password' | 'confirmPassword') => - (event: ChangeEvent) => { - const { value } = event.target - setFormValues((previous) => ({ ...previous, [field]: value })) - }, - [], - ) - - const handleAgreementChange = useCallback((event: ChangeEvent) => { - setFormValues((previous) => ({ ...previous, agreement: event.target.checked })) - }, []) - - const handleCodeChange = useCallback( - (index: number, value: string) => { - const sanitized = value.replace(/\D/g, '') - setCodeDigits((previous) => { - const next = [...previous] - next[index] = sanitized ? sanitized[sanitized.length - 1] ?? '' : '' - return next - }) - - if (sanitized && index < VERIFICATION_CODE_LENGTH - 1) { - focusCodeInput(index + 1) - } - }, - [focusCodeInput], - ) - - const handleCodeKeyDown = useCallback( - (index: number, event: KeyboardEvent) => { - if (event.key === 'Backspace' && !codeDigits[index] && index > 0) { - event.preventDefault() - setCodeDigits((previous) => { - const next = [...previous] - next[index - 1] = '' - return next - }) - focusCodeInput(index - 1) - return - } - - if (event.key === 'ArrowLeft' && index > 0) { - event.preventDefault() - focusCodeInput(index - 1) - return - } - - if (event.key === 'ArrowRight' && index < VERIFICATION_CODE_LENGTH - 1) { - event.preventDefault() - focusCodeInput(index + 1) - } - }, - [codeDigits, focusCodeInput], - ) - - const handleCodePaste = useCallback( - (index: number, event: ClipboardEvent) => { - event.preventDefault() - const clipboardValue = event.clipboardData.getData('text').replace(/\D/g, '') - if (!clipboardValue) { - return - } - - const digits = clipboardValue.slice(0, VERIFICATION_CODE_LENGTH - index).split('') - setCodeDigits((previous) => { - const next = [...previous] - digits.forEach((digit, offset) => { - const targetIndex = index + offset - if (targetIndex < VERIFICATION_CODE_LENGTH) { - next[targetIndex] = digit - } - }) - return next - }) - - const lastFilledIndex = Math.min(index + digits.length - 1, VERIFICATION_CODE_LENGTH - 1) - focusCodeInput(lastFilledIndex) - }, - [focusCodeInput], - ) - - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() - - if (isSubmitting) { - return - } - - formRef.current = event.currentTarget - - const formData = new FormData(event.currentTarget) - const emailInput = String(formData.get('email') ?? '').trim() - const normalizedEmail = emailInput.toLowerCase() - const password = String(formData.get('password') ?? '') - const confirmPassword = String(formData.get('confirmPassword') ?? '') - const agreementAccepted = formData.get('agreement') === 'on' - const verificationCode = codeDigits.join('') - - setFormValues((previous) => ({ - ...previous, - email: emailInput, - password, - confirmPassword, - agreement: agreementAccepted, - })) - - const showError = (message: string) => { - setAlert({ type: 'error', message }) - } - - const showStatus = (message: string) => { - setAlert({ type: 'info', message }) - } - - if (!hasRequestedCode) { - if (!emailInput || !EMAIL_PATTERN.test(emailInput)) { - showError(alerts.invalidEmail) - return - } - - if (!password || !confirmPassword) { - showError(alerts.missingFields) - return - } - - if (!PASSWORD_STRENGTH_PATTERN.test(password)) { - showError(alerts.weakPassword ?? alerts.genericError) - return - } - - if (password !== confirmPassword) { - showError(alerts.passwordMismatch) - return - } - - if (!agreementAccepted) { - showError(alerts.agreementRequired ?? alerts.missingFields) - return - } - - setIsSubmitting(true) - showStatus( - t.form.validation?.submitting ?? - t.form.submitting ?? - 'Submitting registration request…', - ) - - try { - const response = await fetch('/api/auth/register/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailInput }), - }) - - if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse verification send response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - invalid_email: alerts.invalidEmail, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - email_already_exists: alerts.userExists, - account_service_unreachable: alerts.genericError, - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } - - setPendingEmail(normalizedEmail) - setPendingPassword(password) - setHasRequestedCode(true) - setIsVerified(false) - resetCodeDigits() - focusCodeInput(0) - setResendCooldown(RESEND_COOLDOWN_SECONDS) - - const successMessage = alerts.verificationSent ?? alerts.genericError - setAlert({ type: 'success', message: successMessage }) - } catch (error) { - console.error('Failed to request verification code', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) - } - return - } - - const emailForVerification = pendingEmail || normalizedEmail - if (!emailForVerification) { - showError(alerts.invalidEmail) - return - } - - if (!isVerified) { - if (verificationCode.length !== VERIFICATION_CODE_LENGTH) { - showError(alerts.codeRequired ?? alerts.invalidCode ?? alerts.missingFields) - return - } - - setIsSubmitting(true) - showStatus( - t.form.validation?.verifying ?? - t.form.verifying ?? - t.form.verifySubmit ?? - t.form.submit, - ) - - try { - const response = await fetch('/api/auth/register/verify', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailForVerification, code: verificationCode }), - }) - - if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse verification response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - missing_verification: alerts.codeRequired ?? alerts.missingFields, - invalid_code: - alerts.verificationFailed ?? alerts.invalidCode ?? alerts.genericError, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } - - setIsVerified(true) - const successMessage = alerts.verificationReady ?? alerts.success - setAlert({ type: 'success', message: successMessage }) - } catch (error) { - console.error('Failed to verify email', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) - } - return - } - - if (!pendingPassword) { - showError(alerts.genericError) - return - } - - if (verificationCode.length !== VERIFICATION_CODE_LENGTH) { - showError(alerts.codeRequired ?? alerts.invalidCode ?? alerts.genericError) - return - } - - setIsSubmitting(true) - showStatus( - t.form.validation?.completing ?? - t.form.completing ?? - t.form.completeSubmit ?? - t.form.submit, - ) - - try { - const registerResponse = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: emailForVerification, - password: pendingPassword, - confirmPassword: pendingPassword, - code: verificationCode, - }), - }) - - let registerData: { success?: boolean; error?: string } | null = null - try { - registerData = await registerResponse.json() - } catch (error) { - registerData = null - } - - if (!registerResponse.ok || registerData?.success === false) { - const errorCode = - typeof registerData?.error === 'string' ? registerData.error : 'registration_failed' - const errorMap: Record = { - invalid_request: alerts.genericError, - missing_credentials: alerts.missingFields, - invalid_email: alerts.invalidEmail, - password_too_short: alerts.weakPassword, - email_already_exists: alerts.userExists, - name_already_exists: alerts.usernameExists ?? alerts.userExists, - invalid_name: alerts.invalidName ?? alerts.genericError, - name_required: alerts.invalidName ?? alerts.genericError, - hash_failure: alerts.genericError, - user_creation_failed: alerts.genericError, - credentials_in_query: alerts.genericError, - verification_required: alerts.codeRequired ?? alerts.genericError, - invalid_code: - alerts.verificationFailed ?? alerts.invalidCode ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } - - const loginResponse = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: emailForVerification, - password: pendingPassword, - }), - }) - - let loginData: - | { success?: boolean; needMfa?: boolean; error?: string; redirectTo?: string } - | null = null - try { - loginData = await loginResponse.json() - } catch (error) { - loginData = null - } - - if (!loginResponse.ok || !loginData?.success) { - const errorCode = typeof loginData?.error === 'string' ? loginData.error : 'generic_error' - const errorMap: Record = { - invalid_credentials: alerts.genericError, - missing_credentials: alerts.missingFields, - account_service_unreachable: alerts.genericError, - authentication_failed: alerts.genericError, - } - - if (loginData?.needMfa) { - router.push('/login?needMfa=1') - router.refresh() - setIsSubmitting(false) - return - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } - - const successMessage = alerts.registrationComplete ?? alerts.success - setAlert({ type: 'success', message: successMessage }) - - router.push(loginData?.redirectTo || '/') - router.refresh() - } catch (error) { - console.error('Failed to complete registration', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) - } - }, - [ - alerts, - codeDigits, - focusCodeInput, - hasRequestedCode, - isSubmitting, - isVerified, - normalize, - pendingEmail, - pendingPassword, - resetCodeDigits, - router, - t.form, - ], - ) - - const handleResend = useCallback(async () => { - if (isResending || resendCooldown > 0 || isVerified) { - return - } - - const emailFromFormRaw = - pendingEmail || - (formRef.current ? String(new FormData(formRef.current).get('email') ?? '').trim() : '') - - if (!emailFromFormRaw) { - setAlert({ type: 'error', message: alerts.invalidEmail }) - return - } - - const emailFromForm = emailFromFormRaw.trim() - - setIsResending(true) - const resendStatusMessage = - t.form.verificationCodeResending ?? - (t.form.verificationCodeResend ? `${t.form.verificationCodeResend}…` : 'Resending verification code…') - setAlert({ type: 'info', message: resendStatusMessage }) - - try { - const response = await fetch('/api/auth/register/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailFromForm }), - }) - - if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse resend response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - invalid_email: alerts.invalidEmail, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - already_verified: alerts.verificationFailed ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - email_already_exists: alerts.userExists, - } - - setAlert({ type: 'error', message: errorMap[normalize(errorCode)] ?? alerts.genericError }) - setIsResending(false) - return - } - - setPendingEmail(emailFromForm.toLowerCase()) - setHasRequestedCode(true) - setIsVerified(false) - resetCodeDigits() - focusCodeInput(0) - setResendCooldown(RESEND_COOLDOWN_SECONDS) - const message = alerts.verificationResent ?? alerts.verificationSent ?? 'Verification code resent.' - setAlert({ type: 'success', message }) - setIsResending(false) - } catch (error) { - console.error('Failed to resend verification code', error) - setAlert({ type: 'error', message: alerts.genericError }) - setIsResending(false) - } - }, [ - alerts, - focusCodeInput, - isResending, - isVerified, - normalize, - pendingEmail, - resetCodeDigits, - resendCooldown, - t.form.verificationCodeResend, - t.form.verificationCodeResending, - ]) - - const aboveForm = t.uuidNote ? ( -
- {t.uuidNote} -
- ) : null - - const isVerificationStep = hasRequestedCode && !isVerified - const submitLabel = isVerified - ? isSubmitting - ? t.form.completing ?? t.form.completeSubmit ?? t.form.submit - : t.form.completeSubmit ?? t.form.submit - : isVerificationStep - ? isSubmitting - ? t.form.verifying ?? t.form.verifySubmit ?? t.form.submit - : t.form.verifySubmit ?? t.form.submit - : isSubmitting - ? t.form.submitting ?? t.form.submit - : t.form.submit - const resendLabel = isResending - ? t.form.verificationCodeResending ?? t.form.verificationCodeResend - : resendCooldown > 0 - ? `${t.form.verificationCodeResend} (${resendCooldown}s)` - : t.form.verificationCodeResend - const verificationDescriptionId = useId() - const validationHints = t.form.validation - const validationState = useMemo(() => { - const messages: string[] = [] - - if (!isFormReady && validationHints?.initializing) { - return { disabled: true, messages: [validationHints.initializing] } - } - - if (isSubmitting) { - if (isVerified) { - messages.push( - validationHints?.completing ?? - t.form.completing ?? - t.form.completeSubmit ?? - t.form.submit, - ) - } else if (isVerificationStep) { - messages.push( - validationHints?.verifying ?? - t.form.verifying ?? - t.form.verifySubmit ?? - t.form.submit, - ) - } else { - messages.push(validationHints?.submitting ?? t.form.submitting ?? t.form.submit) - } - - return { disabled: true, messages } - } - - if (!hasRequestedCode) { - const emailValue = formValues.email.trim() - - if (!emailValue) { - messages.push(validationHints?.emailMissing ?? alerts.invalidEmail) - } else if (!EMAIL_PATTERN.test(emailValue)) { - messages.push(validationHints?.emailInvalid ?? alerts.invalidEmail) - } - - if (!formValues.password) { - messages.push(validationHints?.passwordMissing ?? alerts.missingFields) - } - - if (!formValues.confirmPassword) { - messages.push(validationHints?.confirmPasswordMissing ?? alerts.missingFields) - } - - if (formValues.password && !PASSWORD_STRENGTH_PATTERN.test(formValues.password)) { - messages.push(validationHints?.passwordWeak ?? alerts.weakPassword ?? alerts.genericError) - } - - if ( - formValues.password && - formValues.confirmPassword && - formValues.password !== formValues.confirmPassword - ) { - messages.push(validationHints?.passwordMismatch ?? alerts.passwordMismatch) - } - - if (!formValues.agreement) { - messages.push( - validationHints?.agreementRequired ?? alerts.agreementRequired ?? alerts.missingFields, - ) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - } - - if (!isVerified) { - if (codeDigits.some((digit) => !digit)) { - messages.push( - validationHints?.codeIncomplete ?? - alerts.codeRequired ?? - alerts.invalidCode ?? - alerts.missingFields, - ) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - } - - if (codeDigits.some((digit) => !digit)) { - messages.push( - validationHints?.codeIncomplete ?? - alerts.codeRequired ?? - alerts.invalidCode ?? - alerts.missingFields, - ) - } - - if (!pendingPassword) { - messages.push(validationHints?.passwordUnavailable ?? alerts.genericError) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - }, [ - alerts, - codeDigits, - formValues, - hasRequestedCode, - isFormReady, - isSubmitting, - isVerificationStep, - isVerified, - pendingPassword, - t.form.completeSubmit, - t.form.completing, - t.form.submit, - t.form.submitting, - t.form.verifySubmit, - t.form.verifying, - validationHints, - ]) - const isSubmitDisabled = validationState.disabled - const validationMessages = validationState.messages - - return ( - -
-
- - -
-
-
- - -
-
- - -
-
-
- - {t.form.verificationCodeDescription ? ( -

- {t.form.verificationCodeDescription} -

- ) : null} - {hasRequestedCode && !isVerified ? ( -
- 我们已向你的邮箱发送一封验证邮件,点击邮件中的链接即可完成注册。 - 验证链接有效期 10 分钟。 -
- 若未收到邮件,请检查垃圾箱或稍后重试。 -
- ) : null} -
- - {validationMessages.length > 0 ? ( -
-
    - {validationMessages.map((message) => ( -
  • {message}
  • - ))} -
-
- ) : null} - -
-
- ) -} diff --git a/dashboard-fresh/app/(auth)/register/page.tsx b/dashboard-fresh/app/(auth)/register/page.tsx deleted file mode 100644 index 7ffc5f9..0000000 --- a/dashboard-fresh/app/(auth)/register/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export const dynamic = 'force-dynamic' - -export const revalidate = 0 - -import { Suspense } from 'react' -import { notFound } from 'next/navigation' - -import { isFeatureEnabled } from '@lib/featureToggles' - -import RegisterContent from './RegisterContent' - -function RegisterPageFallback() { - return
-} - -export default function RegisterPage() { - if (!isFeatureEnabled('globalNavigation', '/register')) { - notFound() - } - - return ( - }> - - - ) -} diff --git a/dashboard-fresh/app/api/admin/settings/route.ts b/dashboard-fresh/app/api/admin/settings/route.ts deleted file mode 100644 index b0a3c6e..0000000 --- a/dashboard-fresh/app/api/admin/settings/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -export const dynamic = 'force-dynamic' - -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' -import type { AccountUserRole } from '@server/account/session' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -const READ_ROLES: AccountUserRole[] = ['admin', 'operator'] -const WRITE_ROLES: AccountUserRole[] = ['admin'] - -type ErrorPayload = { - error: string -} - -async function proxyAccountRequest(request: NextRequest, endpoint: string, method: string, token: string) { - const headers = new Headers({ - Authorization: `Bearer ${token}`, - Accept: 'application/json', - }) - - let body: string | undefined - if (method !== 'GET' && method !== 'HEAD') { - body = await request.text() - const contentType = request.headers.get('content-type') ?? 'application/json' - headers.set('Content-Type', contentType) - } - - const response = await fetch(endpoint, { - method, - headers, - body, - cache: 'no-store', - }) - - const payload = await response.json().catch(() => null) - if (payload === null) { - return NextResponse.json({ error: 'invalid_response' }, { status: 502 }) - } - - return NextResponse.json(payload, { status: response.status }) -} - -export async function GET(request: NextRequest) { - const session = await getAccountSession(request) - const user = session.user - - if (!user || !session.token) { - return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) - } - - if (!(await userHasRole(user, READ_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - return proxyAccountRequest(request, `${ACCOUNT_API_BASE}/admin/settings`, 'GET', session.token) -} - -export async function POST(request: NextRequest) { - const session = await getAccountSession(request) - const user = session.user - - if (!user || !session.token) { - return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) - } - - if (!(await userHasRole(user, WRITE_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - return proxyAccountRequest(request, `${ACCOUNT_API_BASE}/admin/settings`, 'POST', session.token) -} - diff --git a/dashboard-fresh/app/api/admin/users/[userId]/role/route.ts b/dashboard-fresh/app/api/admin/users/[userId]/role/route.ts deleted file mode 100644 index 6b5b59f..0000000 --- a/dashboard-fresh/app/api/admin/users/[userId]/role/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -export const dynamic = 'force-dynamic' - -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' -import type { AccountUserRole } from '@server/account/session' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() -const REQUIRED_ROLES: AccountUserRole[] = ['admin'] - -type ErrorPayload = { - error: string -} - -type RouteParams = { - params: { - userId: string - } -} - -function resolveUserId(param?: string): string | null { - if (!param) { - return null - } - const trimmed = param.trim() - return trimmed.length > 0 ? trimmed : null -} - -export async function POST(request: NextRequest, { params }: RouteParams) { - const session = await getAccountSession(request) - const user = session.user - - if (!user || !session.token) { - return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) - } - - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - const userId = resolveUserId(params?.userId) - if (!userId) { - return NextResponse.json({ error: 'invalid_user' }, { status: 400 }) - } - - const body = await request.text() - const headers = new Headers({ - Authorization: `Bearer ${session.token}`, - Accept: 'application/json', - }) - const contentType = request.headers.get('content-type') ?? 'application/json' - headers.set('Content-Type', contentType) - - const response = await fetch(`${ACCOUNT_API_BASE}/admin/users/${encodeURIComponent(userId)}/role`, { - method: 'POST', - headers, - body, - cache: 'no-store', - }) - - const payload = await response.json().catch(() => null) - if (payload === null) { - return NextResponse.json({ error: 'invalid_response' }, { status: 502 }) - } - - return NextResponse.json(payload, { status: response.status }) -} - diff --git a/dashboard-fresh/app/api/admin/users/metrics/route.ts b/dashboard-fresh/app/api/admin/users/metrics/route.ts deleted file mode 100644 index ac68394..0000000 --- a/dashboard-fresh/app/api/admin/users/metrics/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -export const dynamic = 'force-dynamic' - -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' -import type { AccountUserRole } from '@server/account/session' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -const ALLOWED_ROLES: AccountUserRole[] = ['admin', 'operator'] - -type MetricsErrorPayload = { - error: string -} - -export async function GET(request: NextRequest) { - const session = await getAccountSession(request) - const user = session.user - - if (!user || !session.token) { - return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) - } - - if (!(await userHasRole(user, ALLOWED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - const response = await fetch(`${ACCOUNT_API_BASE}/admin/users/metrics`, { - method: 'GET', - headers: { - Authorization: `Bearer ${session.token}`, - Accept: 'application/json', - }, - cache: 'no-store', - }) - - const payload = await response.json().catch(() => null) - if (payload === null) { - return NextResponse.json({ error: 'invalid_response' }, { status: 502 }) - } - - return NextResponse.json(payload, { status: response.status }) -} - diff --git a/dashboard-fresh/app/api/agent/[...segments]/route.ts b/dashboard-fresh/app/api/agent/[...segments]/route.ts deleted file mode 100644 index 04dabb8..0000000 --- a/dashboard-fresh/app/api/agent/[...segments]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const dynamic = 'force-dynamic' - -import type { NextRequest } from 'next/server' - -import { createUpstreamProxyHandler } from '@lib/apiProxy' -import { getInternalServerServiceBaseUrl } from '@server/serviceConfig' - -const AGENT_PREFIX = '/api/agent' - -function createHandler() { - const upstreamBaseUrl = getInternalServerServiceBaseUrl() - return createUpstreamProxyHandler({ - upstreamBaseUrl, - upstreamPathPrefix: AGENT_PREFIX, - }) -} - -const handler = createHandler() - -export function GET(request: NextRequest) { - return handler(request) -} - -export function POST(request: NextRequest) { - return handler(request) -} - -export function PUT(request: NextRequest) { - return handler(request) -} - -export function PATCH(request: NextRequest) { - return handler(request) -} - -export function DELETE(request: NextRequest) { - return handler(request) -} - -export function HEAD(request: NextRequest) { - return handler(request) -} - -export function OPTIONS(request: NextRequest) { - return handler(request) -} diff --git a/dashboard-fresh/app/api/askai/route.ts b/dashboard-fresh/app/api/askai/route.ts deleted file mode 100644 index 0019dd5..0000000 --- a/dashboard-fresh/app/api/askai/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getInternalServerServiceBaseUrl } from '@server/serviceConfig' - -const FORWARDED_HEADERS = ['authorization', 'cookie', 'x-account-session'] as const - -function buildForwardHeaders(req: Request) { - const headers = new Headers({ 'Content-Type': 'application/json' }) - - for (const name of FORWARDED_HEADERS) { - const value = req.headers.get(name) - if (value) { - headers.set(name, value) - } - } - - return headers -} - -export async function POST(req: Request) { - try { - const { question, history } = await req.json() - const apiBase = getInternalServerServiceBaseUrl() - const response = await fetch(`${apiBase}/api/askai`, { - method: 'POST', - headers: buildForwardHeaders(req), - body: JSON.stringify({ question, history }), - credentials: 'include' - }) - - const data = await response.json().catch(() => null) - if (data === null) { - return Response.json({ error: 'Invalid response from server' }, { - status: response.status - }) - } - - return Response.json(data, { status: response.status }) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return Response.json({ error: message }, { status: 500 }) - } -} - diff --git a/dashboard-fresh/app/api/auth/login/route.ts b/dashboard-fresh/app/api/auth/login/route.ts deleted file mode 100644 index d64a461..0000000 --- a/dashboard-fresh/app/api/auth/login/route.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { applyMfaCookie, applySessionCookie, clearMfaCookie, clearSessionCookie, deriveMaxAgeFromExpires, MFA_COOKIE_NAME } from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type LoginPayload = { - email?: string - password?: string - remember?: boolean - totp?: string - code?: string - token?: string -} - -type AccountLoginResponse = { - token?: string - expiresAt?: string - error?: string - mfaToken?: string - needMfa?: boolean - mfaEnabled?: boolean -} - -function normalizeEmail(value: unknown) { - return typeof value === 'string' ? value.trim().toLowerCase() : '' -} - -function normalizeCode(value: unknown) { - return typeof value === 'string' ? value.replace(/\D/g, '').slice(0, 6) : '' -} - -export async function POST(request: NextRequest) { - let payload: LoginPayload - try { - payload = (await request.json()) as LoginPayload - } catch (error) { - console.error('Failed to decode login payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) - } - - const email = normalizeEmail(payload?.email) - const password = typeof payload?.password === 'string' ? payload.password : '' - const totpCode = normalizeCode(payload?.totp ?? payload?.code) - const remember = Boolean(payload?.remember) - - if (!email || !password) { - return NextResponse.json({ success: false, error: 'missing_credentials', needMfa: false }, { status: 400 }) - } - - try { - const loginBody: Record = { email, password } - if (totpCode) { - loginBody.totpCode = totpCode - } - - const response = await fetch(`${ACCOUNT_API_BASE}/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(loginBody), - cache: 'no-store', - }) - - const data = (await response.json().catch(() => ({}))) as AccountLoginResponse - - if (response.ok && typeof data?.token === 'string' && data.token.length > 0) { - const maxAgeFromBackend = deriveMaxAgeFromExpires(data?.expiresAt) - const effectiveMaxAge = remember ? Math.max(maxAgeFromBackend, 60 * 60 * 24 * 30) : maxAgeFromBackend - const result = NextResponse.json({ success: true, error: null, needMfa: false }) - applySessionCookie(result, data.token, effectiveMaxAge) - clearMfaCookie(result) - return result - } - - const errorCode = typeof data?.error === 'string' ? data.error : 'authentication_failed' - const needsMfa = Boolean(data?.needMfa || errorCode === 'mfa_required' || errorCode === 'mfa_setup_required') - - if ((response.status === 401 || response.status === 403 || needsMfa) && typeof data?.mfaToken === 'string') { - const result = NextResponse.json({ success: false, error: errorCode, needMfa: true }, { status: 401 }) - applyMfaCookie(result, data.mfaToken) - clearSessionCookie(result) - return result - } - - const statusCode = response.status || 401 - const result = NextResponse.json({ success: false, error: errorCode, needMfa: false }, { status: statusCode }) - clearSessionCookie(result) - clearMfaCookie(result) - return result - } catch (error) { - console.error('Account service login proxy failed', error) - const result = NextResponse.json({ success: false, error: 'account_service_unreachable', needMfa: false }, { status: 502 }) - clearSessionCookie(result) - clearMfaCookie(result) - return result - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: false }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} - -export function DELETE() { - const cookieStore = cookies() - const response = NextResponse.json({ success: true, error: null, needMfa: false }) - if (cookieStore.has(MFA_COOKIE_NAME)) { - clearMfaCookie(response) - } - clearSessionCookie(response) - return response -} diff --git a/dashboard-fresh/app/api/auth/mfa/disable/route.ts b/dashboard-fresh/app/api/auth/mfa/disable/route.ts deleted file mode 100644 index cd0054a..0000000 --- a/dashboard-fresh/app/api/auth/mfa/disable/route.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { SESSION_COOKIE_NAME, clearSessionCookie } from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -export async function POST(request: NextRequest) { - void request - const token = cookies().get(SESSION_COOKIE_NAME)?.value?.trim() - - if (!token) { - return NextResponse.json({ success: false, error: 'session_required' }, { status: 401 }) - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/mfa/disable`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'mfa_disable_failed' - if (response.status === 401) { - const result = NextResponse.json({ success: false, error: errorCode }) - clearSessionCookie(result) - return result - } - return NextResponse.json({ success: false, error: errorCode }, { status: response.status || 400 }) - } - - return NextResponse.json({ success: true, error: null, data }) - } catch (error) { - console.error('Account service MFA disable proxy failed', error) - return NextResponse.json({ success: false, error: 'account_service_unreachable' }, { status: 502 }) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed' }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/mfa/setup/route.ts b/dashboard-fresh/app/api/auth/mfa/setup/route.ts deleted file mode 100644 index 6a804c6..0000000 --- a/dashboard-fresh/app/api/auth/mfa/setup/route.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { applyMfaCookie, MFA_COOKIE_NAME, SESSION_COOKIE_NAME } from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -// This Next.js route proxies MFA provisioning requests to the account service. -// The UI calls /api/auth/mfa/setup, which in turn forwards to the Go backend -// at /api/auth/mfa/totp/provision, keeping browser credentials opaque to the -// external service and letting us manage cookies centrally. - -type SetupPayload = { - token?: string - issuer?: string - account?: string -} - -function normalizeString(value: unknown) { - return typeof value === 'string' ? value.trim() : '' -} - -export async function POST(request: NextRequest) { - const cookieStore = cookies() - let payload: SetupPayload - try { - payload = (await request.json()) as SetupPayload - } catch (error) { - console.error('Failed to decode MFA setup payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: true }, { status: 400 }) - } - - const sessionToken = cookieStore.get(SESSION_COOKIE_NAME)?.value ?? '' - const cookieToken = cookieStore.get(MFA_COOKIE_NAME)?.value ?? '' - const token = normalizeString(payload?.token || cookieToken) - - if (!token && !sessionToken) { - return NextResponse.json({ success: false, error: 'mfa_token_required', needMfa: true }, { status: 400 }) - } - - const issuer = normalizeString(payload?.issuer) - const account = normalizeString(payload?.account) - - try { - const headers: Record = { - 'Content-Type': 'application/json', - } - if (sessionToken) { - headers.Authorization = `Bearer ${sessionToken}` - } - - const body: Record = {} - if (token) { - body.token = token - } - if (issuer) { - body.issuer = issuer - } - if (account) { - body.account = account - } - - const response = await fetch(`${ACCOUNT_API_BASE}/mfa/totp/provision`, { - method: 'POST', - headers, - body: JSON.stringify(body), - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'mfa_setup_failed' - return NextResponse.json({ success: false, error: errorCode, needMfa: true }, { status: response.status || 400 }) - } - - const result = NextResponse.json({ success: true, error: null, needMfa: true, data }) - const nextToken = normalizeString((data as { mfaToken?: string })?.mfaToken || token || cookieToken) - if (nextToken) { - applyMfaCookie(result, nextToken) - } - return result - } catch (error) { - console.error('Account service MFA setup proxy failed', error) - return NextResponse.json({ success: false, error: 'account_service_unreachable', needMfa: true }, { status: 502 }) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: true }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/mfa/status/route.ts b/dashboard-fresh/app/api/auth/mfa/status/route.ts deleted file mode 100644 index 07ee246..0000000 --- a/dashboard-fresh/app/api/auth/mfa/status/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { MFA_COOKIE_NAME, SESSION_COOKIE_NAME } from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -export async function GET(request: NextRequest) { - const cookieStore = cookies() - const sessionToken = cookieStore.get(SESSION_COOKIE_NAME)?.value ?? '' - const storedMfaToken = cookieStore.get(MFA_COOKIE_NAME)?.value ?? '' - - const url = new URL(request.url) - const queryToken = String(url.searchParams.get('token') ?? '').trim() - const token = queryToken || storedMfaToken - const identifier = String( - url.searchParams.get('identifier') ?? url.searchParams.get('email') ?? '', - ).trim() - - const headers: Record = { - Accept: 'application/json', - } - if (sessionToken) { - headers.Authorization = `Bearer ${sessionToken}` - } - - const params = new URLSearchParams() - if (token) { - params.set('token', token) - } - if (identifier) { - params.set('identifier', identifier.toLowerCase()) - } - - const endpointParams = params.toString() - const endpoint = endpointParams - ? `${ACCOUNT_API_BASE}/mfa/status?${endpointParams}` - : `${ACCOUNT_API_BASE}/mfa/status` - - const response = await fetch(endpoint, { - method: 'GET', - headers, - cache: 'no-store', - }) - - const payload = await response.json().catch(() => ({})) - return NextResponse.json(payload, { status: response.status }) -} diff --git a/dashboard-fresh/app/api/auth/mfa/verify/route.ts b/dashboard-fresh/app/api/auth/mfa/verify/route.ts deleted file mode 100644 index 8d1868e..0000000 --- a/dashboard-fresh/app/api/auth/mfa/verify/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { - applyMfaCookie, - applySessionCookie, - clearMfaCookie, - clearSessionCookie, - deriveMaxAgeFromExpires, - MFA_COOKIE_NAME, -} from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type VerifyPayload = { - token?: string - code?: string - totp?: string -} - -type AccountVerifyResponse = { - token?: string - expiresAt?: string - mfaToken?: string - error?: string - retryAt?: string - user?: Record | null - mfa?: Record | null -} - -function normalizeString(value: unknown) { - return typeof value === 'string' ? value.trim() : '' -} - -function normalizeCode(value: unknown) { - return typeof value === 'string' ? value.replace(/\D/g, '').slice(0, 6) : '' -} - -export async function POST(request: NextRequest) { - const cookieStore = cookies() - let payload: VerifyPayload - try { - payload = (await request.json()) as VerifyPayload - } catch (error) { - console.error('Failed to decode MFA verification payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: true }, { status: 400 }) - } - - const cookieToken = cookieStore.get(MFA_COOKIE_NAME)?.value ?? '' - const token = normalizeString(payload?.token || cookieToken) - const code = normalizeCode(payload?.code ?? payload?.totp) - - if (!token) { - return NextResponse.json({ success: false, error: 'mfa_token_required', needMfa: true }, { status: 400 }) - } - - if (!code) { - return NextResponse.json({ success: false, error: 'mfa_code_required', needMfa: true }, { status: 400 }) - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/mfa/totp/verify`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ token, code }), - cache: 'no-store', - }) - - const data = (await response.json().catch(() => ({}))) as AccountVerifyResponse - - if (response.ok && typeof data?.token === 'string' && data.token.length > 0) { - const result = NextResponse.json({ success: true, error: null, needMfa: false, data }) - applySessionCookie(result, data.token, deriveMaxAgeFromExpires(data?.expiresAt)) - clearMfaCookie(result) - return result - } - - const errorCode = typeof data?.error === 'string' ? data.error : 'mfa_verification_failed' - const result = NextResponse.json( - { success: false, error: errorCode, needMfa: true, data }, - { status: response.status || 400 }, - ) - - if (typeof data?.mfaToken === 'string' && data.mfaToken.trim()) { - applyMfaCookie(result, data.mfaToken) - } else { - applyMfaCookie(result, token) - } - - clearSessionCookie(result) - return result - } catch (error) { - console.error('Account service MFA verification proxy failed', error) - const result = NextResponse.json({ success: false, error: 'account_service_unreachable', needMfa: true }, { status: 502 }) - applyMfaCookie(result, token) - clearSessionCookie(result) - return result - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: true }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/register/route.ts b/dashboard-fresh/app/api/auth/register/route.ts deleted file mode 100644 index d9bf901..0000000 --- a/dashboard-fresh/app/api/auth/register/route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type RegistrationPayload = { - name?: string - email?: string - password?: string - confirmPassword?: string - code?: string -} - -function normalizeEmail(value: unknown) { - return typeof value === 'string' ? value.trim().toLowerCase() : '' -} - -function normalizeString(value: unknown) { - return typeof value === 'string' ? value.trim() : '' -} - -export async function POST(request: NextRequest) { - let payload: RegistrationPayload - try { - payload = (await request.json()) as RegistrationPayload - } catch (error) { - console.error('Failed to decode registration payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) - } - - const email = normalizeEmail(payload?.email) - const password = typeof payload?.password === 'string' ? payload.password : '' - const confirmPassword = - typeof payload?.confirmPassword === 'string' ? payload.confirmPassword : payload?.password ?? '' - const name = normalizeString(payload?.name) - const code = normalizeString(payload?.code) - - if (!email || !password) { - return NextResponse.json({ success: false, error: 'missing_credentials', needMfa: false }, { status: 400 }) - } - - if (password !== confirmPassword) { - return NextResponse.json({ success: false, error: 'password_mismatch', needMfa: false }, { status: 400 }) - } - - if (!code) { - return NextResponse.json({ success: false, error: 'verification_required', needMfa: false }, { status: 400 }) - } - - const body = { - email, - password, - code, - ...(name ? { name } : {}), - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'registration_failed' - return NextResponse.json( - { success: false, error: errorCode, needMfa: false }, - { status: response.status || 400 }, - ) - } - - return NextResponse.json({ success: true, error: null, needMfa: false }) - } catch (error) { - console.error('Account service registration proxy failed', error) - return NextResponse.json({ success: false, error: 'account_service_unreachable', needMfa: false }, { status: 502 }) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: false }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/register/send/route.ts b/dashboard-fresh/app/api/auth/register/send/route.ts deleted file mode 100644 index 78f8830..0000000 --- a/dashboard-fresh/app/api/auth/register/send/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type SendPayload = { - email?: string -} - -function normalizeEmail(value: unknown) { - return typeof value === 'string' ? value.trim().toLowerCase() : '' -} - -export async function POST(request: NextRequest) { - let payload: SendPayload - try { - payload = (await request.json()) as SendPayload - } catch (error) { - console.error('Failed to decode registration send payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) - } - - const email = normalizeEmail(payload?.email) - if (!email) { - return NextResponse.json({ success: false, error: 'invalid_email', needMfa: false }, { status: 400 }) - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/register/send`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email }), - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'verification_failed' - return NextResponse.json({ success: false, error: errorCode, needMfa: false }, { status: response.status || 400 }) - } - - return NextResponse.json({ success: true, error: null, needMfa: false }) - } catch (error) { - console.error('Account service registration send proxy failed', error) - return NextResponse.json( - { success: false, error: 'account_service_unreachable', needMfa: false }, - { status: 502 }, - ) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: false }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/register/verify/route.ts b/dashboard-fresh/app/api/auth/register/verify/route.ts deleted file mode 100644 index 0aa6689..0000000 --- a/dashboard-fresh/app/api/auth/register/verify/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST, GET } from '../../verify-email/route' diff --git a/dashboard-fresh/app/api/auth/session/route.ts b/dashboard-fresh/app/api/auth/session/route.ts deleted file mode 100644 index ee0aaef..0000000 --- a/dashboard-fresh/app/api/auth/session/route.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' - -import { SESSION_COOKIE_NAME, clearSessionCookie } from '@lib/authGateway' -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type AccountUser = { - id?: string - uuid?: string - name?: string - username?: string - email: string - mfaEnabled?: boolean - mfaPending?: boolean - mfa?: { - totpEnabled?: boolean - totpPending?: boolean - totpSecretIssuedAt?: string - totpConfirmedAt?: string - totpLockedUntil?: string - } - role?: string - groups?: string[] - permissions?: string[] - tenantId?: string - tenants?: Array<{ - id?: string - name?: string - role?: string - }> -} - -type SessionResponse = { - user?: AccountUser | null - error?: string -} - -async function fetchSession(token: string) { - try { - const response = await fetch(`${ACCOUNT_API_BASE}/session`, { - headers: { - Authorization: `Bearer ${token}`, - }, - cache: 'no-store', - }) - - const data = (await response.json().catch(() => ({}))) as SessionResponse - return { response, data } - } catch (error) { - console.error('Session lookup proxy failed', error) - return { response: null, data: null } - } -} - -export async function GET(request: NextRequest) { - void request - const token = cookies().get(SESSION_COOKIE_NAME)?.value - if (!token) { - return NextResponse.json({ user: null }) - } - - const { response, data } = await fetchSession(token) - if (!response || !response.ok || !data?.user) { - const res = NextResponse.json({ user: null }) - clearSessionCookie(res) - return res - } - - const rawUser = data.user as AccountUser - const identifier = - typeof rawUser.uuid === 'string' && rawUser.uuid.trim().length > 0 - ? rawUser.uuid.trim() - : typeof rawUser.id === 'string' - ? rawUser.id.trim() - : undefined - - const rawMfa = rawUser.mfa ?? {} - const derivedMfaEnabled = Boolean(rawUser.mfaEnabled ?? rawMfa.totpEnabled) - const derivedMfaPendingSource = - typeof rawUser.mfaPending === 'boolean' - ? rawUser.mfaPending - : typeof rawMfa.totpPending === 'boolean' - ? rawMfa.totpPending - : false - const derivedMfaPending = derivedMfaPendingSource && !derivedMfaEnabled - - const normalizedRole = - typeof rawUser.role === 'string' && rawUser.role.trim().length > 0 - ? rawUser.role.trim().toLowerCase() - : 'user' - const normalizedGroups = Array.isArray(rawUser.groups) - ? rawUser.groups - .filter((value): value is string => typeof value === 'string' && value.trim().length > 0) - .map((value) => value.trim()) - : [] - const normalizedPermissions = Array.isArray(rawUser.permissions) - ? rawUser.permissions - .filter((value): value is string => typeof value === 'string' && value.trim().length > 0) - .map((value) => value.trim()) - : [] - const normalizedTenantId = - typeof rawUser.tenantId === 'string' && rawUser.tenantId.trim().length > 0 - ? rawUser.tenantId.trim() - : undefined - const normalizedTenants = Array.isArray(rawUser.tenants) - ? rawUser.tenants - .map((tenant) => { - if (!tenant || typeof tenant !== 'object') { - return null - } - - const identifier = - typeof tenant.id === 'string' && tenant.id.trim().length > 0 - ? tenant.id.trim() - : undefined - if (!identifier) { - return null - } - - const normalizedTenant: { id: string; name?: string; role?: string } = { - id: identifier, - } - - if (typeof tenant.name === 'string' && tenant.name.trim().length > 0) { - normalizedTenant.name = tenant.name.trim() - } - - if (typeof tenant.role === 'string' && tenant.role.trim().length > 0) { - normalizedTenant.role = tenant.role.trim().toLowerCase() - } - - return normalizedTenant - }) - .filter((tenant): tenant is { id: string; name?: string; role?: string } => Boolean(tenant)) - : undefined - - const normalizedMfa = Object.keys(rawMfa).length - ? { - ...rawMfa, - totpEnabled: Boolean(rawMfa.totpEnabled ?? derivedMfaEnabled), - totpPending: Boolean(rawMfa.totpPending ?? derivedMfaPending), - } - : { - totpEnabled: derivedMfaEnabled, - totpPending: derivedMfaPending, - } - - const normalizedUser = identifier ? { ...rawUser, id: identifier, uuid: identifier } : rawUser - - return NextResponse.json({ - user: { - ...normalizedUser, - mfaEnabled: derivedMfaEnabled, - mfaPending: derivedMfaPending, - mfa: normalizedMfa, - role: normalizedRole, - groups: normalizedGroups, - permissions: normalizedPermissions, - tenantId: normalizedTenantId, - tenants: normalizedTenants, - }, - }) -} - -export async function DELETE(request: NextRequest) { - void request - const cookieStore = cookies() - const token = cookieStore.get(SESSION_COOKIE_NAME)?.value - if (token) { - await fetch(`${ACCOUNT_API_BASE}/session`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, - cache: 'no-store', - }).catch(() => null) - } - - const response = NextResponse.json({ success: true }) - clearSessionCookie(response) - return response -} diff --git a/dashboard-fresh/app/api/auth/verify-email/route.ts b/dashboard-fresh/app/api/auth/verify-email/route.ts deleted file mode 100644 index 5fe38da..0000000 --- a/dashboard-fresh/app/api/auth/verify-email/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type VerifyPayload = { - email?: string - code?: string -} - -function normalizeEmail(value: unknown) { - return typeof value === 'string' ? value.trim().toLowerCase() : '' -} - -function normalizeCode(value: unknown) { - return typeof value === 'string' ? value.trim() : '' -} - -export async function POST(request: NextRequest) { - let payload: VerifyPayload - try { - payload = (await request.json()) as VerifyPayload - } catch (error) { - console.error('Failed to decode verification payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) - } - - const email = normalizeEmail(payload?.email) - const code = normalizeCode(payload?.code) - - if (!email || !code) { - return NextResponse.json({ success: false, error: 'missing_verification', needMfa: false }, { status: 400 }) - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/register/verify`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, code }), - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'verification_failed' - return NextResponse.json({ success: false, error: errorCode, needMfa: false }, { status: response.status || 400 }) - } - - return NextResponse.json({ success: true, error: null, needMfa: false }) - } catch (error) { - console.error('Account service verification proxy failed', error) - return NextResponse.json({ success: false, error: 'account_service_unreachable', needMfa: false }, { status: 502 }) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: false }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/auth/verify-email/send/route.ts b/dashboard-fresh/app/api/auth/verify-email/send/route.ts deleted file mode 100644 index cc8761a..0000000 --- a/dashboard-fresh/app/api/auth/verify-email/send/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' - -const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() - -type SendPayload = { - email?: string -} - -function normalizeEmail(value: unknown) { - return typeof value === 'string' ? value.trim().toLowerCase() : '' -} - -export async function POST(request: NextRequest) { - let payload: SendPayload - try { - payload = (await request.json()) as SendPayload - } catch (error) { - console.error('Failed to decode verification send payload', error) - return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) - } - - const email = normalizeEmail(payload?.email) - if (!email) { - return NextResponse.json({ success: false, error: 'invalid_email', needMfa: false }, { status: 400 }) - } - - try { - const response = await fetch(`${ACCOUNT_API_BASE}/register/send`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email }), - cache: 'no-store', - }) - - const data = await response.json().catch(() => ({})) - if (!response.ok) { - const errorCode = typeof (data as { error?: string })?.error === 'string' ? data.error : 'verification_failed' - return NextResponse.json({ success: false, error: errorCode, needMfa: false }, { status: response.status || 400 }) - } - - return NextResponse.json({ success: true, error: null, needMfa: false }) - } catch (error) { - console.error('Account service verification send proxy failed', error) - return NextResponse.json( - { success: false, error: 'account_service_unreachable', needMfa: false }, - { status: 502 }, - ) - } -} - -export function GET() { - return NextResponse.json( - { success: false, error: 'method_not_allowed', needMfa: false }, - { - status: 405, - headers: { - Allow: 'POST', - }, - }, - ) -} diff --git a/dashboard-fresh/app/api/content-meta/route.ts b/dashboard-fresh/app/api/content-meta/route.ts deleted file mode 100644 index 6aafdc9..0000000 --- a/dashboard-fresh/app/api/content-meta/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { ContentNotFoundError, getContentCommitMeta } from '../../../api/content-meta' - -export const runtime = 'nodejs' - -export async function GET(request: NextRequest) { - const path = request.nextUrl.searchParams.get('path') - if (!path) { - return NextResponse.json({ error: 'Missing path parameter' }, { status: 400 }) - } - - try { - const result = await getContentCommitMeta(path) - return NextResponse.json(result, { status: 200 }) - } catch (error) { - if (error instanceof ContentNotFoundError) { - return NextResponse.json({ error: 'Content file not found' }, { status: 404 }) - } - console.error('Failed to load content metadata:', error) - return NextResponse.json({ error: 'Failed to load metadata' }, { status: 500 }) - } -} diff --git a/dashboard-fresh/app/api/mail/ai/classify/route.ts b/dashboard-fresh/app/api/mail/ai/classify/route.ts deleted file mode 100644 index 32042d0..0000000 --- a/dashboard-fresh/app/api/mail/ai/classify/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId: string } - const message = getMessage(tenantId, body.messageId) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - - const labels = Array.from(new Set([...message.labels, 'AI-Reviewed'])) - return NextResponse.json({ labels }) -} diff --git a/dashboard-fresh/app/api/mail/ai/reply-suggest/route.ts b/dashboard-fresh/app/api/mail/ai/reply-suggest/route.ts deleted file mode 100644 index 491ce96..0000000 --- a/dashboard-fresh/app/api/mail/ai/reply-suggest/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId: string; style?: string; language?: string } - const message = body?.messageId ? getMessage(tenantId, body.messageId) : null - - const base = message?.aiInsights?.suggestions ?? [ - '收到,我们将安排同事跟进。', - '感谢提醒,我们将及时回复。', - '请告知是否需要更多信息。', - ] - - return NextResponse.json({ suggestions: base }) -} diff --git a/dashboard-fresh/app/api/mail/ai/summarize/route.ts b/dashboard-fresh/app/api/mail/ai/summarize/route.ts deleted file mode 100644 index 0834a4f..0000000 --- a/dashboard-fresh/app/api/mail/ai/summarize/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId?: string; raw?: string } - if (!body.messageId && !body.raw) { - return NextResponse.json({ error: 'messageId or raw is required' }, { status: 400 }) - } - - if (body.messageId) { - const message = getMessage(tenantId, body.messageId) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - if (message.aiInsights) { - return NextResponse.json(message.aiInsights) - } - } - - return NextResponse.json({ - summary: '示例摘要:邮件内容将提炼为关键句子。', - bullets: ['示例要点一', '示例要点二'], - actions: ['示例行动一'], - tone: '信息', - }) -} diff --git a/dashboard-fresh/app/api/mail/inbox/route.ts b/dashboard-fresh/app/api/mail/inbox/route.ts deleted file mode 100644 index f33d34f..0000000 --- a/dashboard-fresh/app/api/mail/inbox/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getInbox, resolveTenantId } from '../mockData' - -export async function GET(request: NextRequest) { - const tenantHeader = request.headers.get('x-tenant-id') - const tenantQuery = request.nextUrl.searchParams.get('tenantId') - const tenantId = resolveTenantId(tenantHeader ?? tenantQuery) - - const inbox = getInbox(tenantId) - - const label = request.nextUrl.searchParams.get('label') - const query = request.nextUrl.searchParams.get('q')?.toLowerCase().trim() - - let filtered = inbox.messages - if (label === 'unread') { - filtered = filtered.filter((item) => item.unread) - } else if (label === 'starred') { - filtered = filtered.filter((item) => item.starred) - } else if (label && label !== 'important') { - filtered = filtered.filter((item) => item.labels.includes(label)) - } - if (query) { - filtered = filtered.filter((item) => - [item.subject, item.snippet, item.from.email, item.from.name] - .filter(Boolean) - .some((field) => field!.toLowerCase().includes(query)), - ) - } - - return NextResponse.json({ - ...inbox, - messages: filtered, - }) -} diff --git a/dashboard-fresh/app/api/mail/message/[id]/route.ts b/dashboard-fresh/app/api/mail/message/[id]/route.ts deleted file mode 100644 index cbe3723..0000000 --- a/dashboard-fresh/app/api/mail/message/[id]/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function GET(request: NextRequest, { params }: { params: { id: string } }) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const message = getMessage(tenantId, params.id) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - return NextResponse.json(message) -} - -export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const message = getMessage(tenantId, params.id) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - return NextResponse.json({ success: true }) -} diff --git a/dashboard-fresh/app/api/mail/mockData.ts b/dashboard-fresh/app/api/mail/mockData.ts deleted file mode 100644 index 27ec606..0000000 --- a/dashboard-fresh/app/api/mail/mockData.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { MailInboxResponse, MailListMessage, MailMessageDetail, NamespacePolicy } from '@lib/mail/types' - -type TenantMailData = { - inbox: MailListMessage[] - messages: Record - namespace: NamespacePolicy -} - -const now = Date.now() - -const baseMessages: MailListMessage[] = [ - { - id: 'msg-1001', - subject: '【故障通报】核心链路延迟恢复通知', - snippet: '生产集群延迟已恢复至正常指标,详见行动项。', - from: { name: 'SRE 值班', email: 'sre@svc.plus' }, - to: [{ name: 'Ops 团队', email: 'ops@tenant.io' }], - date: new Date(now - 5 * 60 * 1000).toISOString(), - unread: true, - starred: true, - labels: ['Incident', 'Priority'], - hasAttachments: true, - aiSummary: { - preview: '延迟恢复,需确认追踪指标。', - tone: '紧急', - }, - }, - { - id: 'msg-1002', - subject: '月度账单与消耗对账单', - snippet: '附件包含 5 月份资源使用与费用明细,请于本周内确认。', - from: { name: 'Finance Robot', email: 'billing@svc.plus' }, - to: [{ name: 'Finance', email: 'finance@tenant.io' }], - date: new Date(now - 2 * 60 * 60 * 1000).toISOString(), - unread: false, - labels: ['Billing'], - hasAttachments: true, - aiSummary: { - preview: '账单结算提醒,需核对折扣。', - tone: '正式', - }, - }, - { - id: 'msg-1003', - subject: 'AI 助手联调会议记录', - snippet: '会议纪要包含下一步联调行动项与 SLA 讨论。', - from: { name: '产品经理', email: 'pm@svc.plus' }, - to: [{ name: 'AI 团队', email: 'ai@tenant.io' }], - date: new Date(now - 5 * 60 * 60 * 1000).toISOString(), - unread: false, - labels: ['Product'], - aiSummary: { - preview: '提炼三条关键任务。', - tone: '合作', - }, - }, - { - id: 'msg-1004', - subject: '【提醒】IAM 权限矩阵变更审批', - snippet: '审批单待确认,涉及新的只读角色授权,请于 24 小时内处理。', - from: { name: 'Access Bot', email: 'iam@svc.plus' }, - to: [{ name: 'Security', email: 'sec@tenant.io' }], - date: new Date(now - 12 * 60 * 60 * 1000).toISOString(), - unread: true, - labels: ['Security'], - aiSummary: { - preview: '审批截止前需确认。', - tone: '提醒', - }, - }, -] - -const detailMap: Record = { - 'msg-1001': { - ...baseMessages[0], - text: '生产链路延迟恢复。请确认后续监控指标与复盘会议安排。', - html: '

生产链路延迟已恢复。

  • 核对 Prometheus 延迟指标
  • 更新状态页面
  • 准备 18:00 复盘会议
', - attachments: [ - { - id: 'att-1', - fileName: 'incident-report.pdf', - contentType: 'application/pdf', - size: 234567, - downloadUrl: '#', - }, - ], - aiInsights: { - summary: '生产链路延迟恢复,需跟进指标及复盘会议。', - bullets: ['Prometheus 延迟恢复', '状态页面需更新', '18:00 复盘会议'], - actions: ['确认状态页', '同步客户邮件', '准备复盘材料'], - tone: '紧急', - suggestions: [ - '感谢通知,已安排团队核查 Prometheus 指标。', - '收到,我们将于 18:00 准备复盘材料。', - '请同步可能影响的客户列表,方便统一公告。', - ], - }, - }, - 'msg-1002': { - ...baseMessages[1], - text: '随信附上 5 月份账单,包含折扣与超额费用明细,请在本周内完成对账。', - attachments: [ - { - id: 'att-2', - fileName: 'may-usage.xlsx', - contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - size: 54567, - downloadUrl: '#', - }, - ], - aiInsights: { - summary: '账单需要财务团队在本周内确认。', - bullets: ['包含折扣明细', '有部分资源超额', '需在周五前回复'], - actions: ['核对折扣', '确认超额原因', '回邮确认'], - tone: '正式', - suggestions: ['已收悉,我们将在周四前完成对账并回复。'], - }, - }, - 'msg-1003': { - ...baseMessages[2], - html: '

联调会议要点:

  1. 六月上线 Beta,需补充监控指标
  2. AI 模型回退策略需评审
  3. 下一次联调会议安排在周五上午
', - aiInsights: { - summary: '会议聚焦上线计划、模型回退与下次会议时间。', - bullets: ['六月 Beta 上线', '确认模型回退策略', '周五上午继续联调'], - actions: ['同步监控指标清单', '准备回退方案文档', '发送会议邀请'], - tone: '合作', - }, - }, - 'msg-1004': { - ...baseMessages[3], - text: 'IAM 角色矩阵变更涉及新建只读角色,需要安全团队审批。', - aiInsights: { - summary: '安全团队需在 24 小时内确认新角色审批。', - bullets: ['新增只读角色', '审批截止 24 小时内', '需评估权限边界'], - actions: ['审阅角色权限', '评估风险', '确认审批或驳回'], - tone: '提醒', - }, - }, -} - -const TENANT_DATA: Record = { - 'tenant-alpha': { - inbox: baseMessages, - messages: detailMap, - namespace: { - model: 'gpt-4o-mini', - temperature: 0.3, - maxTokens: 2048, - rateLimitPerMinute: 60, - vectorIndex: 's3://tenant-alpha-mail', - policy: '{"blockedKeywords": ["NDA", "秘密"]}', - updatedAt: new Date(now - 3600 * 1000).toISOString(), - }, - }, - default: { - inbox: baseMessages, - messages: detailMap, - namespace: { - model: 'gpt-4o-mini', - temperature: 0.5, - maxTokens: 2048, - rateLimitPerMinute: 30, - vectorIndex: 's3://default-mail', - policy: '{"allowExternal": true}', - updatedAt: new Date(now - 7200 * 1000).toISOString(), - }, - }, -} - -export function resolveTenantId(raw: string | null | undefined) { - if (!raw) { - return 'default' - } - return TENANT_DATA[raw] ? raw : 'default' -} - -export function getInbox(tenantId: string): MailInboxResponse { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return { - messages: data.inbox, - labels: [ - { id: 'Incident', name: 'Incident', color: '#f97316', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Incident')).length }, - { id: 'Billing', name: 'Billing', color: '#2563eb', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Billing')).length }, - { id: 'Security', name: 'Security', color: '#7c3aed', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Security')).length }, - { id: 'Product', name: 'Product', color: '#0f766e', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Product')).length }, - ], - unreadCount: data.inbox.filter((item) => item.unread).length, - nextCursor: null, - } -} - -export function getMessage(tenantId: string, id: string): MailMessageDetail | null { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return data.messages[id] ?? null -} - -export function getNamespace(tenantId: string): NamespacePolicy { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return data.namespace -} - -export function updateNamespace(tenantId: string, patch: Partial): NamespacePolicy { - const key = TENANT_DATA[tenantId] ? tenantId : 'default' - const current = TENANT_DATA[key].namespace - const next = { ...current, ...patch, updatedAt: new Date().toISOString() } - TENANT_DATA[key].namespace = next - return next -} diff --git a/dashboard-fresh/app/api/mail/namespace/route.ts b/dashboard-fresh/app/api/mail/namespace/route.ts deleted file mode 100644 index 45a3828..0000000 --- a/dashboard-fresh/app/api/mail/namespace/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getNamespace, resolveTenantId, updateNamespace } from '../mockData' - -export async function GET(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - return NextResponse.json(getNamespace(tenantId)) -} - -export async function PUT(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const patch = (await request.json()) as Record - return NextResponse.json(updateNamespace(tenantId, patch)) -} diff --git a/dashboard-fresh/app/api/mail/send/route.ts b/dashboard-fresh/app/api/mail/send/route.ts deleted file mode 100644 index ec3fb9d..0000000 --- a/dashboard-fresh/app/api/mail/send/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import type { ComposePayload } from '@lib/mail/types' - -export async function POST(request: NextRequest) { - const payload = (await request.json()) as ComposePayload - void payload - return NextResponse.json({ success: true }) -} diff --git a/dashboard-fresh/app/api/ping/route.ts b/dashboard-fresh/app/api/ping/route.ts deleted file mode 100644 index 1ca0387..0000000 --- a/dashboard-fresh/app/api/ping/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from 'next/server' - -import { loadRuntimeConfig } from '@server/runtime-loader' - -export async function GET(request: Request) { - const hostnameHeader = request.headers.get('host') ?? undefined - const runtimeConfig = loadRuntimeConfig({ hostname: hostnameHeader }) - - const payload = { - status: 'ok' as const, - environment: runtimeConfig.environment, - region: runtimeConfig.region, - apiBaseUrl: runtimeConfig.apiBaseUrl, - authUrl: runtimeConfig.authUrl, - dashboardUrl: runtimeConfig.dashboardUrl, - logLevel: runtimeConfig.logLevel, - } - - console.info('[runtime-config] /api/ping resolved config snippet', payload) - - return NextResponse.json(payload) -} diff --git a/dashboard-fresh/app/api/rag/query/route.ts b/dashboard-fresh/app/api/rag/query/route.ts deleted file mode 100644 index 17f9fb0..0000000 --- a/dashboard-fresh/app/api/rag/query/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getInternalServerServiceBaseUrl } from '@server/serviceConfig' - -const FORWARDED_HEADERS = ['authorization', 'cookie', 'x-account-session'] as const - -function buildForwardHeaders(req: Request) { - const headers = new Headers({ 'Content-Type': 'application/json' }) - - for (const name of FORWARDED_HEADERS) { - const value = req.headers.get(name) - if (value) { - headers.set(name, value) - } - } - - return headers -} - -export async function POST(req: Request) { - try { - const { question, history } = await req.json() - const apiBase = getInternalServerServiceBaseUrl() - const response = await fetch(`${apiBase}/api/rag/query`, { - method: 'POST', - headers: buildForwardHeaders(req), - body: JSON.stringify({ question, history }), - credentials: 'include' - }) - - const data = await response.json().catch(() => null) - return Response.json(data ?? { error: 'Invalid response from server' }, { - status: response.status - }) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return Response.json({ error: message }, { status: 500 }) - } -} diff --git a/dashboard-fresh/app/api/render-markdown/route.ts b/dashboard-fresh/app/api/render-markdown/route.ts deleted file mode 100644 index a3fc8a2..0000000 --- a/dashboard-fresh/app/api/render-markdown/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { ContentNotFoundError, renderMarkdownFile } from '../../../api/render-markdown' - -export const runtime = 'nodejs' - -export async function GET(request: NextRequest) { - const path = request.nextUrl.searchParams.get('path') - if (!path) { - return NextResponse.json({ error: 'Missing path parameter' }, { status: 400 }) - } - - try { - const result = await renderMarkdownFile(path) - return NextResponse.json(result, { status: 200 }) - } catch (error) { - if (error instanceof ContentNotFoundError) { - return NextResponse.json({ error: 'Markdown file not found' }, { status: 404 }) - } - console.error('Failed to render markdown:', error) - return NextResponse.json({ error: 'Failed to render markdown' }, { status: 500 }) - } -} diff --git a/dashboard-fresh/app/api/task/[...segments]/route.ts b/dashboard-fresh/app/api/task/[...segments]/route.ts deleted file mode 100644 index bab800d..0000000 --- a/dashboard-fresh/app/api/task/[...segments]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const dynamic = 'force-dynamic' - -import type { NextRequest } from 'next/server' - -import { createUpstreamProxyHandler } from '@lib/apiProxy' -import { getInternalServerServiceBaseUrl } from '@server/serviceConfig' - -const TASK_PREFIX = '/api/task' - -function createHandler() { - const upstreamBaseUrl = getInternalServerServiceBaseUrl() - return createUpstreamProxyHandler({ - upstreamBaseUrl, - upstreamPathPrefix: TASK_PREFIX, - }) -} - -const handler = createHandler() - -export function GET(request: NextRequest) { - return handler(request) -} - -export function POST(request: NextRequest) { - return handler(request) -} - -export function PUT(request: NextRequest) { - return handler(request) -} - -export function PATCH(request: NextRequest) { - return handler(request) -} - -export function DELETE(request: NextRequest) { - return handler(request) -} - -export function HEAD(request: NextRequest) { - return handler(request) -} - -export function OPTIONS(request: NextRequest) { - return handler(request) -} diff --git a/dashboard-fresh/app/api/users/route.ts b/dashboard-fresh/app/api/users/route.ts deleted file mode 100644 index 8700dd4..0000000 --- a/dashboard-fresh/app/api/users/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -export const dynamic = 'force-dynamic' - -import { NextResponse } from 'next/server' - -import { getInternalServerServiceBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' -import type { AccountUserRole } from '@server/account/session' - -const SERVER_API_BASE = getInternalServerServiceBaseUrl() -const SERVER_USERS_ENDPOINT = `${SERVER_API_BASE}/api/users` - -const ALLOWED_ROLES: AccountUserRole[] = ['admin', 'operator'] - -type ErrorPayload = { - error: string -} - -type PermissionAwareHeaders = { - 'X-User-Role': string - 'X-User-Permissions'?: string -} - -function buildForwardHeaders(role: string, permissions: string[]): PermissionAwareHeaders { - const headers: PermissionAwareHeaders = { - 'X-User-Role': role, - } - if (permissions.length > 0) { - headers['X-User-Permissions'] = permissions.join(',') - } - return headers -} - -export async function GET() { - const session = await getAccountSession() - const user = session.user - - if (!user) { - return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) - } - - if (!(await userHasRole(user, ALLOWED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - const headers = new Headers({ - Accept: 'application/json', - ...buildForwardHeaders(user.role, user.permissions), - }) - - const response = await fetch(SERVER_USERS_ENDPOINT, { - method: 'GET', - headers, - cache: 'no-store', - }) - - const payload = await response.json().catch(() => null) - if (payload === null) { - return NextResponse.json({ error: 'invalid_response' }, { status: 502 }) - } - - return NextResponse.json(payload, { status: response.status }) -} -