Files
illusory-mapp/apps/main/src/lib/domains/security/2fa.vm.svelte.ts

235 lines
7.9 KiB
TypeScript

import {
disableTwoFactorSC,
generateBackupCodesSQ,
setupTwoFactorSC,
verifyAndEnableTwoFactorSC,
} from "$lib/domains/security/twofa.remote";
import { toast } from "svelte-sonner";
import QRCode from "qrcode";
class TwoFactorViewModel {
twoFactorEnabled = $state(false);
twoFactorSetupInProgress = $state(false);
showingBackupCodes = $state(false);
qrCodeUrl = $state<string | null>(null);
twoFactorSecret = $state<string | null>(null);
backupCodes = $state<string[]>([]);
twoFactorVerificationCode = $state("");
isLoading = $state(false);
errorMessage = $state<string | null>(null);
async startTwoFactorSetup() {
this.isLoading = true;
this.errorMessage = null;
try {
const result = await setupTwoFactorSC({});
if (result?.error || !result?.data?.totpURI) {
this.errorMessage =
result?.error?.message || "Could not enable 2FA";
toast.error(this.errorMessage, {
description:
result?.error?.description || "Please try again later",
});
return;
}
const qrCodeDataUrl = await QRCode.toDataURL(result.data.totpURI, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#FFFFFF" },
});
this.qrCodeUrl = qrCodeDataUrl;
this.twoFactorSetupInProgress = true;
this.twoFactorSecret = result.data.secret;
this.twoFactorVerificationCode = "";
toast("Setup enabled");
} catch (error) {
this.errorMessage = "Could not enable 2FA";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
async completeTwoFactorSetup() {
if (!this.twoFactorVerificationCode) {
this.errorMessage =
"Please enter the verification code from your authenticator app.";
return;
}
this.isLoading = true;
this.errorMessage = null;
try {
const verifyResult = await verifyAndEnableTwoFactorSC({
code: this.twoFactorVerificationCode,
});
if (verifyResult?.error) {
this.errorMessage =
verifyResult.error.message || "Invalid verification code";
toast.error(this.errorMessage, {
description:
verifyResult.error.description || "Please try again",
});
return;
}
const backupCodesResult = await generateBackupCodesSQ();
if (backupCodesResult?.error || !Array.isArray(backupCodesResult?.data)) {
toast.error("2FA enabled, but failed to generate backup codes", {
description: "You can generate them later in settings",
});
} else {
this.backupCodes = backupCodesResult.data;
this.showingBackupCodes = true;
}
this.twoFactorEnabled = true;
this.twoFactorSetupInProgress = false;
this.twoFactorVerificationCode = "";
toast.success("Two-factor authentication enabled", {
description: "Your account is now more secure",
});
} catch (error) {
this.errorMessage = "Invalid verification code";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
this.isLoading = false;
}
}
async disableTwoFactor() {
this.isLoading = true;
this.errorMessage = null;
try {
const result = await disableTwoFactorSC({ code: "" });
if (result?.error) {
this.errorMessage = result.error.message || "Failed to disable 2FA";
toast.error(this.errorMessage, {
description: result.error.description || "Please try again later",
});
return;
}
this.twoFactorEnabled = false;
this.backupCodes = [];
this.qrCodeUrl = null;
this.showingBackupCodes = false;
this.twoFactorSecret = null;
toast.success("Two-factor authentication disabled");
} catch (error) {
this.errorMessage = "Failed to disable 2FA";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
async generateNewBackupCodes() {
this.isLoading = true;
this.errorMessage = null;
try {
const result = await generateBackupCodesSQ();
if (result?.error || !Array.isArray(result?.data)) {
this.errorMessage =
result?.error?.message || "Failed to generate new backup codes";
toast.error(this.errorMessage, {
description:
result?.error?.description || "Please try again later",
});
return;
}
this.backupCodes = result.data;
this.showingBackupCodes = true;
toast.success("New backup codes generated", {
description: "Your previous backup codes are now invalid",
});
} catch (error) {
this.errorMessage = "Failed to generate new backup codes";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
copyAllBackupCodes() {
const codesText = this.backupCodes.join("\n");
navigator.clipboard.writeText(codesText);
toast.success("All backup codes copied to clipboard");
}
downloadBackupCodes() {
const codesText = this.backupCodes.join("\n");
const blob = new Blob(
[
`Two-Factor Authentication Backup Codes\n\nGenerated: ${new Date().toLocaleString()}\n\n${codesText}\n\nKeep these codes in a safe place. Each code can only be used once.`,
],
{
type: "text/plain",
},
);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `2fa-backup-codes-${new Date().toISOString().split("T")[0]}.txt`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success("Backup codes downloaded");
}
confirmBackupCodesSaved() {
this.showingBackupCodes = false;
toast.success("Great! Your backup codes are safely stored");
}
cancelSetup() {
this.twoFactorSetupInProgress = false;
this.twoFactorSecret = null;
this.qrCodeUrl = null;
this.twoFactorVerificationCode = "";
this.errorMessage = null;
this.showingBackupCodes = false;
}
copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
reset() {
this.twoFactorEnabled = false;
this.twoFactorSetupInProgress = false;
this.showingBackupCodes = false;
this.qrCodeUrl = null;
this.backupCodes = [];
this.twoFactorSecret = null;
this.twoFactorVerificationCode = "";
this.isLoading = false;
this.errorMessage = null;
}
}
export const twofactorVM = new TwoFactorViewModel();