feat: implement multi-factor authentication (MFA) setup and verification flow

- Add SecurityPage component for managing MFA settings, including enrollment and verification.
- Create MfaLoginGate to handle MFA verification state during login.
- Develop MfaVerificationPopup for user input of verification codes.
- Implement VerifyMfaPage for the MFA verification process after login.
- Introduce reusable VerificationCodeInput component for entering verification codes.
- Integrate Supabase MFA API for managing factors and verification.
- Add loading states and error handling for a better user experience.
This commit is contained in:
willchen96 2026-06-10 03:48:08 +08:00
parent 15c96b0dd4
commit 3a10943200
32 changed files with 3704 additions and 311 deletions

View file

@ -14,7 +14,9 @@ import {
type ApiKeyProvider,
type UserProfile as ApiUserProfile,
getUserProfile,
isMfaRequiredError,
saveApiKey,
updateUserMfaOnLogin,
updateUserProfile,
} from "@/app/lib/mikeApi";
@ -27,6 +29,7 @@ interface UserProfile {
tier: string;
titleModel: string;
tabularModel: string;
mfaOnLogin: boolean;
apiKeys: ApiKeyState;
}
@ -39,6 +42,7 @@ interface UserProfileContextType {
field: "titleModel" | "tabularModel",
value: string,
) => Promise<boolean>;
updateMfaOnLogin: (enabled: boolean) => Promise<boolean>;
updateApiKey: (
provider: ApiKeyProvider,
value: string | null,
@ -83,6 +87,7 @@ function toProfile(data: ApiUserProfile): UserProfile {
return {
...profile,
mfaOnLogin: profile.mfaOnLogin === true,
apiKeys,
};
}
@ -111,6 +116,7 @@ export function UserProfileProvider({ children }: { children: ReactNode }) {
tier: "Free",
titleModel: "gemini-3.1-flash-lite-preview",
tabularModel: "gemini-3-flash-preview",
mfaOnLogin: false,
apiKeys: emptyApiKeys(),
});
} finally {
@ -156,7 +162,8 @@ export function UserProfileProvider({ children }: { children: ReactNode }) {
prev ? { ...prev, ...toProfile(updated) } : null,
);
return true;
} catch {
} catch (error) {
if (isMfaRequiredError(error)) throw error;
return false;
}
},
@ -184,6 +191,23 @@ export function UserProfileProvider({ children }: { children: ReactNode }) {
[user],
);
const updateMfaOnLogin = useCallback(
async (enabled: boolean): Promise<boolean> => {
if (!user) return false;
try {
const updated = await updateUserMfaOnLogin(enabled);
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
);
return true;
} catch (error) {
if (isMfaRequiredError(error)) throw error;
return false;
}
},
[user],
);
const updateApiKey = useCallback(
async (
provider: ApiKeyProvider,
@ -208,7 +232,8 @@ export function UserProfileProvider({ children }: { children: ReactNode }) {
: null,
);
return true;
} catch {
} catch (error) {
if (isMfaRequiredError(error)) throw error;
return false;
}
},
@ -242,6 +267,7 @@ export function UserProfileProvider({ children }: { children: ReactNode }) {
updateDisplayName,
updateOrganisation,
updateModelPreference,
updateMfaOnLogin,
updateApiKey,
reloadProfile,
incrementMessageCredits,