Files
lab/src/app/(dashboard)/settings/security/page.tsx
T
kovakmedya 3e15d9f937 feat(security): two-factor authentication (TOTP)
Hesap güvenliği için authenticator app (Google Authenticator, 1Password,
Authy etc.) based TOTP. SMS yok — sadece app-based per user request.

Enroll flow (/settings/security)
  - startMfaEnrollAction → account.createMFAAuthenticator('totp'),
    returns otpauth URI + plain secret as backup.
  - MfaPanel client island: starts the flow, shows the QR (rendered via
    api.qrserver.com for zero deps) plus the secret as text. Picks the
    6-digit code → verifyMfaEnrollAction calls
    updateMFAAuthenticator(totp, otp) + updateMFA(true) +
    createMFARecoveryCodes(). The recovery codes are surfaced once on
    success with a 'save these now' warning.
  - disableMfaAction + regenerateRecoveryCodesAction give the same
    panel a disable + 'yeni yedek kodlar' option once MFA is active.
  - settings-nav now has 'Güvenlik' between 'Görünüm' and 'Hesap
    Aktivitesi'.

Sign-in flow
  - signInAction:
      1. createEmailPasswordSession (sets cookie as before)
      2. users.get(userId).mfa? If yes:
         a. otp empty → return { mfaRequired: true, error }
         b. otp present → createMfaChallenge({factor: totp}) +
            updateMfaChallenge(challengeId, otp). Failure tears the
            partial session down and bounces back with mfaRequired.
  - AuthState gained an mfaRequired field. The login form watches it
    and reveals an autofocused 6-digit OTP input on the next render.
    User types the code, submits the form again, the same action
    finishes the challenge and redirects.

Existing accounts without MFA are unaffected — they never hit the
challenge branch.
2026-05-22 16:25:26 +03:00

57 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { redirect } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { createSessionClient } from "@/lib/appwrite/server";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { MfaPanel } from "./components/mfa-panel";
export const metadata = {
title: "DLS — Güvenlik",
};
export default async function SecurityPage() {
let ctx;
try {
ctx = await requireTenant();
} catch {
redirect("/onboarding");
}
// Look up the user's current MFA status straight from the session
// client so the panel knows whether to offer enroll or disable.
let mfaEnabled = false;
try {
const { account } = await createSessionClient();
const user = await account.get();
mfaEnabled = Boolean(user.mfa);
} catch {
// ignore — panel will treat as not enabled
}
return (
<div className="flex-1 space-y-6 px-6">
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
<h1 className="text-2xl font-bold tracking-tight">Güvenlik</h1>
<p className="text-muted-foreground text-sm">
Hesap erişiminizi koruyan ayarlar. İki adımlı doğrulamayı açtığınızda
giriş yaparken authenticator uygulamanızdaki 6 haneli kod istenir.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>İki Adımlı Doğrulama</CardTitle>
<CardDescription>
Authenticator uygulaması (Google Authenticator, 1Password, Authy, vs.)
ile TOTP. SMS desteklenmiyor.
</CardDescription>
</CardHeader>
<CardContent>
<MfaPanel initiallyEnabled={mfaEnabled} />
</CardContent>
</Card>
</div>
);
}