import { useState } from 'react'; import { QRCodeSVG } from 'qrcode.react'; import { Alert, Badge, Button, Card, FormField, Input, Spinner, useToast, } from '@cameleer/design-system'; import { useTenantSettings, useChangeOwnPassword, useResetServerAdminPassword, useMfaStatus, useMfaSetup, useMfaVerify, useMfaBackupCodes, useMfaRemove, useUpdateTenantSettings, } from '../../api/tenant-hooks'; import { useScopes } from '../../auth/useScopes'; import { tierColor } from '../../utils/tier'; import styles from '../../styles/platform.module.css'; function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' { switch (status?.toUpperCase()) { case 'ACTIVE': return 'success'; case 'SUSPENDED': return 'warning'; case 'PROVISIONING': return 'auto'; case 'ERROR': return 'error'; default: return 'auto'; } } function MfaSection() { const { toast } = useToast(); const { data: mfaStatus, isLoading: statusLoading } = useMfaStatus(); const setup = useMfaSetup(); const verify = useMfaVerify(); const backupCodes = useMfaBackupCodes(); const remove = useMfaRemove(); const [setupData, setSetupData] = useState<{ secret: string; secretQrCode: string } | null>(null); const [verifyCode, setVerifyCode] = useState(''); const [codes, setCodes] = useState(null); const [codesSaved, setCodesSaved] = useState(false); const [confirmRemove, setConfirmRemove] = useState(false); async function handleStartSetup() { try { const data = await setup.mutateAsync(); setSetupData(data); setVerifyCode(''); } catch (err) { toast({ title: 'Failed to start MFA setup', description: String(err), variant: 'error' }); } } async function handleVerify(e: React.FormEvent) { e.preventDefault(); if (!setupData) return; try { const result = await verify.mutateAsync({ secret: setupData.secret, code: verifyCode }); if (result.verified) { const bc = await backupCodes.mutateAsync(); setCodes(bc.codes); setSetupData(null); setVerifyCode(''); setCodesSaved(false); toast({ title: 'MFA enabled successfully', variant: 'success' }); } else { toast({ title: 'Invalid code. Please try again.', variant: 'error' }); } } catch (err) { toast({ title: 'Verification failed', description: String(err), variant: 'error' }); } } async function handleRegenerateCodes() { try { const bc = await backupCodes.mutateAsync(); setCodes(bc.codes); setCodesSaved(false); toast({ title: 'Backup codes regenerated', variant: 'success' }); } catch (err) { toast({ title: 'Failed to regenerate backup codes', description: String(err), variant: 'error' }); } } async function handleRemove() { try { await remove.mutateAsync(); setConfirmRemove(false); setCodes(null); setSetupData(null); toast({ title: 'MFA removed', variant: 'success' }); } catch (err) { toast({ title: 'Failed to remove MFA', description: String(err), variant: 'error' }); } } function handleCopyAll() { if (!codes) return; navigator.clipboard.writeText(codes.join('\n')); toast({ title: 'Backup codes copied to clipboard', variant: 'success' }); } function handleDownload() { if (!codes) return; const blob = new Blob([codes.join('\n')], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'cameleer-mfa-backup-codes.txt'; a.click(); URL.revokeObjectURL(url); } if (statusLoading) { return (
); } // Backup codes display if (codes) { return ( These codes can be used to sign in if you lose access to your authenticator app. Each code can only be used once. Store them in a safe place.
{codes.map((code) => ( {code} ))}
); } // Setup flow — QR code + verification if (setupData) { return (

Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.), then enter the 6-digit code below.

{setupData.secret}
setVerifyCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="Enter 6-digit code" required autoComplete="one-time-code" />
); } // Main view — enrolled or not return (
Status: {mfaStatus?.enrolled ? ( ) : ( )}
{mfaStatus?.enrolled ? ( <>

Your account is protected with a TOTP authenticator app.

{confirmRemove ? (
) : ( )}
) : ( <>

Add an extra layer of security to your account by enabling multi-factor authentication with an authenticator app.

)}
); } function MfaEnforcementToggle() { const scopes = useScopes(); const { toast } = useToast(); const { data: settings } = useTenantSettings(); const updateSettings = useUpdateTenantSettings(); const [confirmEnable, setConfirmEnable] = useState(false); if (!scopes.has('tenant:manage')) return null; const mfaRequired = settings?.mfaRequired ?? false; async function handleToggle() { if (!mfaRequired) { setConfirmEnable(true); return; } try { await updateSettings.mutateAsync({ mfaRequired: false }); toast({ title: 'MFA requirement disabled for all members', variant: 'success' }); } catch (err) { toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' }); } } async function handleConfirmEnable() { try { await updateSettings.mutateAsync({ mfaRequired: true }); setConfirmEnable(false); toast({ title: 'MFA is now required for all members', variant: 'success' }); } catch (err) { toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' }); } } return (

When enabled, all team members will be required to set up multi-factor authentication before accessing this tenant.

Require MFA for all members
{confirmEnable ? (
All team members who have not enrolled in MFA will need to set it up on their next login. Are you sure?
) : (
)}
); } export function SettingsPage() { const { data, isLoading, isError } = useTenantSettings(); const changePassword = useChangeOwnPassword(); const resetServerAdmin = useResetServerAdminPassword(); const { toast } = useToast(); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [serverAdminPw, setServerAdminPw] = useState(''); async function handleChangePassword(e: React.FormEvent) { e.preventDefault(); if (newPassword.length < 8) { toast({ title: 'Password must be at least 8 characters', variant: 'error' }); return; } if (newPassword !== confirmPassword) { toast({ title: 'Passwords do not match', variant: 'error' }); return; } try { await changePassword.mutateAsync(newPassword); toast({ title: 'Password changed successfully', variant: 'success' }); setNewPassword(''); setConfirmPassword(''); } catch (err) { toast({ title: 'Failed to change password', description: String(err), variant: 'error' }); } } if (isLoading) { return (
); } if (isError || !data) { return (
Could not fetch settings. Please refresh.
); } return (

Settings

Name {data.name}
Slug {data.slug}
Tier
Status
Server Endpoint {data.serverEndpoint ?? '—'}
Created {new Date(data.createdAt).toLocaleDateString()}

To change your tier or other billing-related settings, please contact support.

Update your login password. Minimum 8 characters.

setNewPassword(e.target.value)} placeholder="Enter new password" required minLength={8} /> setConfirmPassword(e.target.value)} placeholder="Confirm new password" required minLength={8} />

Reset the built-in admin password for your server dashboard (local login at /login?local).

{ e.preventDefault(); if (serverAdminPw.length < 8) { toast({ title: 'Password must be at least 8 characters', variant: 'error' }); return; } try { await resetServerAdmin.mutateAsync(serverAdminPw); toast({ title: 'Server admin password reset successfully', variant: 'success' }); setServerAdminPw(''); } catch (err) { toast({ title: 'Failed to reset server admin password', description: String(err), variant: 'error' }); } }} style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }} > setServerAdminPw(e.target.value)} placeholder="Enter new admin password" required minLength={8} />
); }