235 lines
7.9 KiB
TypeScript
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();
|