/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import i18next from 'i18next' import type { ApiResponse } from '@/features/auth/types' import { api, get2FAStatus } from '@/lib/api' import { buildAssertionResult, prepareCredentialRequestOptions, isPasskeySupported as detectPasskeySupport, } from '@/lib/passkey' import { beginPasskeyVerification, finishPasskeyVerification, getPasskeyStatus, } from '../passkey' import type { SecurityProof, SecurityProofScope, VerificationMethod, VerificationMethods, } from './types' /** * Fetch available verification methods for the current user. */ export async function checkVerificationMethods(): Promise { try { const [twoFAResponse, passkeyResponse, passkeySupported] = await Promise.all([ get2FAStatus(), getPasskeyStatus(), detectPasskeySupport(), ]) const has2FA = Boolean(twoFAResponse?.success) && Boolean(twoFAResponse?.data?.enabled) const hasPasskey = Boolean(passkeyResponse?.success) && Boolean(passkeyResponse?.data?.enabled) return { has2FA, hasPasskey, passkeySupported, } } catch (error) { // eslint-disable-next-line no-console console.error('[Secure Verification] Failed to check methods', error) return { has2FA: false, hasPasskey: false, passkeySupported: false, } } } /** * Execute a verification flow based on the method type. */ export async function verify( method: VerificationMethod, scope: SecurityProofScope, code?: string ): Promise { switch (method) { case '2fa': return verifyTwoFA(scope, code) case 'passkey': return verifyPasskey(scope) default: throw new Error( i18next.t('Unsupported verification method: {{method}}', { method }) ) } } /** * Perform 2FA verification flow. */ async function verifyTwoFA( scope: SecurityProofScope, code?: string | null ): Promise { const trimmed = code?.trim() if (!trimmed) { throw new Error( i18next.t('Please enter the verification code or backup code') ) } const res = await api.post>('/api/verify', { method: '2fa', code: trimmed, scope, }) if (!res.data?.success) { throw new Error(res.data?.message || i18next.t('Verification failed')) } if (!res.data.data?.proof_token) { throw new Error(i18next.t('Verification proof was not returned')) } return res.data.data } /** * Perform Passkey verification flow. */ async function verifyPasskey( scope: SecurityProofScope ): Promise { if (typeof navigator === 'undefined' || !navigator.credentials) { throw new Error( i18next.t('Passkey verification is not supported in this environment') ) } try { const beginResponse = await beginPasskeyVerification(scope) if (!beginResponse.success) { throw new Error( beginResponse.message || i18next.t('Failed to start verification') ) } const publicKey = prepareCredentialRequestOptions( beginResponse.data?.options ?? beginResponse.data ) const flowToken = beginResponse.data?.flow_token if (!flowToken) { throw new Error(i18next.t('Verification flow expired')) } const credential = (await navigator.credentials.get({ publicKey, })) as PublicKeyCredential | null if (!credential) { throw new Error(i18next.t('Passkey verification was cancelled')) } const assertion = buildAssertionResult(credential) if (!assertion) { throw new Error(i18next.t('Unable to build Passkey assertion')) } const finishResponse = await finishPasskeyVerification(flowToken, assertion) if (!finishResponse.success) { throw new Error( finishResponse.message || i18next.t('Passkey verification failed') ) } if (!finishResponse.data?.proof_token) { throw new Error(i18next.t('Verification proof was not returned')) } return finishResponse.data } catch (error: unknown) { if (error instanceof DOMException && error.name === 'NotAllowedError') { throw new Error( i18next.t('Passkey verification was cancelled or timed out'), { cause: error } ) } if (error instanceof DOMException && error.name === 'InvalidStateError') { throw new Error( i18next.t('Passkey verification is not available in the current state'), { cause: error } ) } throw error } }