package updates and build time error fix in the main app
This commit is contained in:
404
apps/main/src/lib/domains/security/2fa.vm.svelte.ts
Normal file
404
apps/main/src/lib/domains/security/2fa.vm.svelte.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { apiClient } from "$lib/global.stores";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { get } from "svelte/store";
|
||||
import QRCode from "qrcode";
|
||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||
import type { Err } from "@pkg/result";
|
||||
|
||||
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;
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
get(apiClient).twofactor.setup.$post({
|
||||
json: { code: "" },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to set up two-factor authentication.",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to set up two-factor authentication.",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
if (!apiResult.data || !apiResult.data.totpURI) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to get 2FA setup information",
|
||||
description: "Invalid response data",
|
||||
detail: "Missing totpURI in response",
|
||||
});
|
||||
}
|
||||
return okAsync(apiResult.data);
|
||||
})
|
||||
.andThen((data) => {
|
||||
return ResultAsync.fromPromise(
|
||||
QRCode.toDataURL(data.totpURI, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: { dark: "#000000", light: "#FFFFFF" },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "PROCESSING_ERROR",
|
||||
message: "Failed to generate QR code.",
|
||||
description: "QR code generation failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
).map((qrCodeDataUrl) => ({
|
||||
...data,
|
||||
qrCodeDataUrl,
|
||||
}));
|
||||
});
|
||||
|
||||
result.match(
|
||||
({ qrCodeDataUrl, secret, totpURI }) => {
|
||||
this.qrCodeUrl = qrCodeDataUrl;
|
||||
this.twoFactorSetupInProgress = true;
|
||||
this.twoFactorSecret = secret;
|
||||
this.twoFactorVerificationCode = "";
|
||||
toast("Setup enabled");
|
||||
},
|
||||
(error) => {
|
||||
this.errorMessage = error.message || "Could not enable 2FA";
|
||||
toast.error(this.errorMessage || "Could not enable 2FA", {
|
||||
description: error.description || "Please try again later",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
const verifyResult = await ResultAsync.fromPromise(
|
||||
get(apiClient).twofactor["verify-and-enable"].$post({
|
||||
json: { code: this.twoFactorVerificationCode },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to verify code.",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to verify code.",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data);
|
||||
});
|
||||
|
||||
verifyResult.match(
|
||||
async () => {
|
||||
const backupCodesResult = await ResultAsync.fromPromise(
|
||||
get(apiClient).twofactor["generate-backup-codes"].$get(),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to generate backup codes",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to generate backup codes",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data || []);
|
||||
});
|
||||
|
||||
backupCodesResult.match(
|
||||
(backupCodes) => {
|
||||
this.backupCodes = backupCodes;
|
||||
this.showingBackupCodes = true;
|
||||
},
|
||||
() => {
|
||||
toast.error(
|
||||
"2FA enabled, but failed to generate backup codes",
|
||||
{
|
||||
description: "You can generate them later in settings",
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
this.twoFactorEnabled = true;
|
||||
this.twoFactorSetupInProgress = false;
|
||||
this.twoFactorVerificationCode = "";
|
||||
toast.success("Two-factor authentication enabled", {
|
||||
description: "Your account is now more secure",
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.errorMessage = error.message || "Invalid verification code";
|
||||
toast.error(this.errorMessage || "Invalid verification code", {
|
||||
description: error.description || "Please try again",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
async disableTwoFactor() {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
get(apiClient).twofactor.disable.$delete({
|
||||
json: { code: "" },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to disable two-factor authentication.",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to disable two-factor authentication.",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data);
|
||||
});
|
||||
|
||||
result.match(
|
||||
() => {
|
||||
this.twoFactorEnabled = false;
|
||||
this.backupCodes = [];
|
||||
this.qrCodeUrl = null;
|
||||
this.showingBackupCodes = false;
|
||||
this.twoFactorSecret = null;
|
||||
toast.success("Two-factor authentication disabled");
|
||||
},
|
||||
(error) => {
|
||||
this.errorMessage = error.message || "Failed to disable 2FA";
|
||||
toast.error(this.errorMessage || "Failed to disable 2FA", {
|
||||
description:
|
||||
error.description || "Please try again later",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
async generateNewBackupCodes() {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
get(apiClient).twofactor["generate-backup-codes"].$get(),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to generate new backup codes.",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to generate new backup codes.",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data || []);
|
||||
});
|
||||
|
||||
result.match(
|
||||
(backupCodes) => {
|
||||
this.backupCodes = backupCodes;
|
||||
this.showingBackupCodes = true;
|
||||
toast.success("New backup codes generated", {
|
||||
description: "Your previous backup codes are now invalid",
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.errorMessage =
|
||||
error.message || "Failed to generate new backup codes";
|
||||
toast.error(this.errorMessage || "Failed to generate new backup codes", {
|
||||
description:
|
||||
error.description || "Please try again later",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user