package updates and build time error fix in the main app

This commit is contained in:
user
2026-02-28 15:41:44 +02:00
parent b731b68918
commit 9328f79c92
9 changed files with 1133 additions and 317 deletions

View 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();