Update MFA UI flags and lint guidance

This commit is contained in:
Haitao Pan 2026-01-25 19:07:57 +08:00
parent e6817ab1e0
commit 9ac93eece5
5 changed files with 74 additions and 55 deletions

View File

@ -28,7 +28,8 @@ yarn preview # Build and start production server
### Code Quality ### Code Quality
```bash ```bash
yarn lint # Run ESLint yarn lint # Run ESLint (currently fails under Next 16 CLI; use eslint command below)
./node_modules/.bin/eslint . --no-eslintrc --config .eslintrc.json --resolve-plugins-relative-to . # Run ESLint directly (ignore parent configs)
yarn typecheck # TypeScript type checking yarn typecheck # TypeScript type checking
yarn format # Format code with Prettier yarn format # Format code with Prettier
``` ```
@ -246,6 +247,7 @@ MUST NOT:
- No @/ imports inside packages - No @/ imports inside packages
- Never "fix" libraries by polluting the app - Never "fix" libraries by polluting the app
- Always run `yarn lint` and `yarn typecheck` before completing tasks - Always run `yarn lint` and `yarn typecheck` before completing tasks
- If `yarn lint` fails with "Invalid project directory .../lint" (Next 16 CLI), use `./node_modules/.bin/eslint .` instead
--- ---

View File

@ -4,7 +4,8 @@
Required: Required:
- yarn lint - yarn lint (currently fails under Next 16 CLI; use eslint command below)
- ./node_modules/.bin/eslint . --no-eslintrc --config .eslintrc.json --resolve-plugins-relative-to .
- yarn typecheck - yarn typecheck
- yarn build - yarn build

View File

@ -96,7 +96,11 @@ function formatTimestamp(value?: string) {
return date.toLocaleString() return date.toLocaleString()
} }
export default function MfaSetupPanel() { type MfaSetupPanelProps = {
showSummary?: boolean
}
export default function MfaSetupPanel({ showSummary = true }: MfaSetupPanelProps) {
const { language } = useLanguage() const { language } = useLanguage()
const copy = translations[language].userCenter.mfa const copy = translations[language].userCenter.mfa
const router = useRouter() const router = useRouter()
@ -435,6 +439,9 @@ export default function MfaSetupPanel() {
}, []) }, [])
if (!user) { if (!user) {
if (!showSummary) {
return null
}
return ( return (
<Card> <Card>
<h2 className="text-xl font-semibold text-[var(--color-text)]">{copy.title}</h2> <h2 className="text-xl font-semibold text-[var(--color-text)]">{copy.title}</h2>
@ -451,42 +458,44 @@ export default function MfaSetupPanel() {
return ( return (
<> <>
<Card> {showSummary ? (
<div className="space-y-6"> <Card>
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between"> <div className="space-y-6">
<div> <div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
<h2 className="text-xl font-semibold text-[var(--color-text)]">{copy.title}</h2> <div>
<p className="mt-1 text-sm text-[var(--color-text-subtle)]">{copy.summary.description}</p> <h2 className="text-xl font-semibold text-[var(--color-text)]">{copy.title}</h2>
<dl className="mt-4 grid gap-4 text-xs text-[var(--color-text-subtle)] sm:grid-cols-2"> <p className="mt-1 text-sm text-[var(--color-text-subtle)]">{copy.summary.description}</p>
<div> <dl className="mt-4 grid gap-4 text-xs text-[var(--color-text-subtle)] sm:grid-cols-2">
<dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.summary.statusLabel}</dt> <div>
<dd className="mt-1 text-sm text-[var(--color-text)]">{statusLabel}</dd> <dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.summary.statusLabel}</dt>
</div> <dd className="mt-1 text-sm text-[var(--color-text)]">{statusLabel}</dd>
<div> </div>
<dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.status.issuedAt}</dt> <div>
<dd className="mt-1 text-sm text-[var(--color-text)]">{formatTimestamp(displayStatus?.totpSecretIssuedAt)}</dd> <dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.status.issuedAt}</dt>
</div> <dd className="mt-1 text-sm text-[var(--color-text)]">{formatTimestamp(displayStatus?.totpSecretIssuedAt)}</dd>
<div> </div>
<dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.status.confirmedAt}</dt> <div>
<dd className="mt-1 text-sm text-[var(--color-text)]">{formatTimestamp(displayStatus?.totpConfirmedAt)}</dd> <dt className="font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.status.confirmedAt}</dt>
</div> <dd className="mt-1 text-sm text-[var(--color-text)]">{formatTimestamp(displayStatus?.totpConfirmedAt)}</dd>
</dl> </div>
</div> </dl>
<div className="flex flex-col items-start gap-3 sm:items-end"> </div>
<button <div className="flex flex-col items-start gap-3 sm:items-end">
type="button" <button
onClick={openDialog} type="button"
className="inline-flex items-center justify-center rounded-md bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-[var(--color-primary-foreground)] shadow-[var(--shadow-sm)] transition hover:bg-[var(--color-primary-hover)]" onClick={openDialog}
> className="inline-flex items-center justify-center rounded-md bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-[var(--color-primary-foreground)] shadow-[var(--shadow-sm)] transition hover:bg-[var(--color-primary-hover)]"
{displayStatus?.totpEnabled ? copy.summary.manage : copy.summary.bind} >
</button> {displayStatus?.totpEnabled ? copy.summary.manage : copy.summary.bind}
{requiresSetup ? ( </button>
<p className="text-xs text-[var(--color-warning-foreground)]">{copy.lockedMessage}</p> {requiresSetup ? (
) : null} <p className="text-xs text-[var(--color-warning-foreground)]">{copy.lockedMessage}</p>
) : null}
</div>
</div> </div>
</div> </div>
</div> </Card>
</Card> ) : null}
{isDialogOpen ? ( {isDialogOpen ? (
<div <div

View File

@ -34,7 +34,11 @@ function resolveDisplayName(
return user.email return user.email
} }
export default function UserOverview() { type UserOverviewProps = {
hideMfaMainPrompt?: boolean
}
export default function UserOverview({ hideMfaMainPrompt = false }: UserOverviewProps) {
const router = useRouter() const router = useRouter()
const { language } = useLanguage() const { language } = useLanguage()
const copy = translations[language].userCenter.overview const copy = translations[language].userCenter.overview
@ -114,7 +118,7 @@ export default function UserOverview() {
<p className="mt-1 text-xs text-[var(--color-text-subtle)] opacity-80">{copy.uuidNote}</p> <p className="mt-1 text-xs text-[var(--color-text-subtle)] opacity-80">{copy.uuidNote}</p>
</div> </div>
{requiresSetup ? ( {!hideMfaMainPrompt && requiresSetup ? (
<div className="rounded-[var(--radius-xl)] border border-[color:var(--color-warning-muted)] bg-[var(--color-warning-muted)] p-4 text-sm text-[var(--color-warning-foreground)] transition-colors"> <div className="rounded-[var(--radius-xl)] border border-[color:var(--color-warning-muted)] bg-[var(--color-warning-muted)] p-4 text-sm text-[var(--color-warning-foreground)] transition-colors">
<p className="text-base font-semibold">{copy.lockBanner.title}</p> <p className="text-base font-semibold">{copy.lockBanner.title}</p>
<p className="mt-1 text-sm">{copy.lockBanner.body}</p> <p className="mt-1 text-sm">{copy.lockBanner.body}</p>
@ -180,21 +184,24 @@ export default function UserOverview() {
<p className="mt-3 text-xs text-[var(--color-text-subtle)]">{copy.cards.email.description}</p> <p className="mt-3 text-xs text-[var(--color-text-subtle)]">{copy.cards.email.description}</p>
</Card> </Card>
<Card> {!hideMfaMainPrompt ? (
<div className="flex items-start justify-between gap-4"> <Card>
<div> <div className="flex items-start justify-between gap-4">
<p className="text-xs font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.cards.mfa.label}</p> <div>
<p className="mt-1 text-base font-medium text-[var(--color-text)]">{mfaStatusLabel}</p> <p className="text-xs font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.cards.mfa.label}</p>
<p className="mt-3 text-xs text-[var(--color-text-subtle)]">{copy.cards.mfa.description}</p> <p className="mt-1 text-base font-medium text-[var(--color-text)]">{mfaStatusLabel}</p>
<p className="mt-3 text-xs text-[var(--color-text-subtle)]">{copy.cards.mfa.description}</p>
</div>
<Link
href="/panel/account?setupMfa=1"
className="inline-flex items-center justify-center rounded-full border border-[color:var(--color-primary-border)] px-3 py-1 text-xs font-medium text-[var(--color-primary)] transition-colors hover:border-[color:var(--color-primary)] hover:bg-[var(--color-primary-muted)]"
>
{copy.cards.mfa.action}
</Link>
</div> </div>
<Link </Card>
href="/panel/account?setupMfa=1" ) : null}
className="inline-flex items-center justify-center rounded-full border border-[color:var(--color-primary-border)] px-3 py-1 text-xs font-medium text-[var(--color-primary)] transition-colors hover:border-[color:var(--color-primary)] hover:bg-[var(--color-primary-muted)]"
>
{copy.cards.mfa.action}
</Link>
</div>
</Card>
</div> </div>
</div> </div>
) )

View File

@ -5,8 +5,8 @@ import UserOverview from '../components/UserOverview'
export default function UserCenterAccountRoute() { export default function UserCenterAccountRoute() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<UserOverview /> <UserOverview hideMfaMainPrompt />
<MfaSetupPanel /> <MfaSetupPanel showSummary={false} />
<SubscriptionPanel /> <SubscriptionPanel />
</div> </div>
) )