import { addRoute, handleRoute, navigateTo, setTitle } from "@app/routing"; import { showError, showSuccess } from "@app/toast"; import page from "./profile.html?raw"; import { updateUser } from "@app/auth"; import { isNullish } from "@app/utils"; import client from "@app/api"; import QRCode from "qrcode"; /* * Renders an OAuth2-compatible TOTP QR code into a canvas. * * @param canvas HTMLCanvasElement to draw into * @param secret Base32-encoded shared secret * @param options Meta data for QR (label, issuer, etc.) */ export async function renderOAuth2QRCode( canvas: HTMLCanvasElement, secret: string, ): Promise { // Encode the otpauth:// URL const otpauthUrl = new URL(`otpauth://totp/ft_boule:totp`); otpauthUrl.searchParams.set("secret", secret.replace(/=+$/, "")); otpauthUrl.searchParams.set("issuer", "ft_boule"); // Render QR code into the canvas await QRCode.toCanvas(canvas, otpauthUrl.toString(), { margin: 1, scale: 5, }); canvas.style.width = ""; canvas.style.height = ""; } function removeBgColor(...elem: HTMLElement[]) { for (let e of elem) { for (let c of e.classList.values()) { if (c.startsWith("bg-") || c.startsWith("hover:bg-")) e.classList.remove(c); } } } async function route(url: string, _args: { [k: string]: string }) { setTitle("Edit Profile"); return { html: page, postInsert: async (app: HTMLElement | undefined) => { const user = await updateUser(); if (isNullish(user)) return showError("No User"); if (isNullish(app)) return showError("Failed to render"); let totpState = await (async () => { let res = await client.statusOtp(); if (res.kind === "success") return { enabled: (res.msg as string) === "statusOtp.success.enabled", secret: (res.msg as string) === "statusOtp.success.enabled" ? res.payload.secret : null, }; else { showError("Failed to get OTP status"); return { enabled: false, secret: null, }; } })(); // ---- Simulated State ---- let totpEnabled = totpState.enabled; let totpSecret = totpState.secret; // would come from backend let guestBox = app.querySelector("#isGuestBox")!; let displayNameWrapper = app.querySelector( "#displayNameWrapper", )!; let displayNameBox = app.querySelector("#displayNameBox")!; let displayNameButton = app.querySelector("#displayNameButton")!; let loginNameWrapper = app.querySelector("#loginNameWrapper")!; let loginNameBox = app.querySelector("#loginNameBox")!; let passwordWrapper = app.querySelector("#passwordWrapper")!; let passwordBox = app.querySelector("#passwordBox")!; let passwordButton = app.querySelector("#passwordButton")!; let providerWrapper = app.querySelector("#providerWrapper")!; let providerNameBox = app.querySelector("#providerNameBox")!; let providerUserBox = app.querySelector("#providerUserBox")!; let accountTypeBox = app.querySelector("#accountType")!; displayNameBox.value = user.name; guestBox.hidden = !user.guest; // ---- DOM Elements ---- const totpStatusText = app.querySelector("#totpStatusText")!; const enableBtn = app.querySelector("#enableTotp")!; const disableBtn = app.querySelector("#disableTotp")!; const showSecretBtn = app.querySelector("#showSecret")!; const secretBox = app.querySelector("#totpSecretBox")!; const secretText = app.querySelector("#totpSecretText")!; const secretCanvas = app.querySelector("#totpSecretCanvas")!; let totpWrapper = app.querySelector("#totpWrapper")!; if (user.guest) { for (let c of passwordButton.classList.values()) { if (c.startsWith("bg-") || c.startsWith("hover:bg-")) passwordButton.classList.remove(c); } } if (user.guest) { removeBgColor( passwordButton, displayNameButton, enableBtn, disableBtn, showSecretBtn, ); passwordButton.classList.add( "bg-gray-700", "hover:bg-gray-700", ); passwordBox.disabled = true; passwordBox.classList.add("color-white"); displayNameButton.disabled = true; displayNameButton.classList.add("bg-gray-700", "color-white"); displayNameBox.disabled = true; displayNameBox.classList.add("color-white"); enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); enableBtn.disabled = true; disableBtn.disabled = true; showSecretBtn.disabled = true; accountTypeBox.innerText = "Guest"; } else if (!isNullish(user.selfInfo?.loginName)) { loginNameWrapper.hidden = false; loginNameBox.innerText = user.selfInfo.loginName; totpWrapper.hidden = false; passwordWrapper.hidden = false; accountTypeBox.innerText = "Normal"; } else if ( !isNullish(user.selfInfo?.providerId) && !isNullish(user.selfInfo?.providerUser) ) { providerWrapper.hidden = false; providerNameBox.innerText = user.selfInfo.providerId; providerUserBox.innerText = user.selfInfo.providerUser; enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); enableBtn.disabled = true; disableBtn.disabled = true; showSecretBtn.disabled = true; removeBgColor(enableBtn, disableBtn, showSecretBtn); passwordWrapper.hidden = true; totpWrapper.hidden = true; accountTypeBox.innerText = "Provider"; } // ---- Update UI ---- function refreshTotpUI() { if (totpEnabled) { totpStatusText.textContent = "Status: Enabled"; enableBtn.classList.add("hidden"); disableBtn.classList.remove("hidden"); showSecretBtn.classList.remove("hidden"); } else { totpStatusText.textContent = "Status: Disabled"; enableBtn.classList.remove("hidden"); disableBtn.classList.add("hidden"); showSecretBtn.classList.add("hidden"); secretBox.classList.add("hidden"); } } // ---- Button Events ---- enableBtn.onclick = async () => { let res = await client.enableOtp(); if (res.kind === "success") { navigateTo(url); } else { showError(`failed to activate OTP: ${res.msg}`); } }; disableBtn.onclick = async () => { let res = await client.disableOtp(); if (res.kind === "success") { navigateTo(url); } else { showError(`failed to deactivate OTP: ${res.msg}`); } }; showSecretBtn.onclick = () => { if (!isNullish(totpSecret)) { secretText.textContent = totpSecret; renderOAuth2QRCode(secretCanvas, totpSecret); } secretBox.classList.toggle("hidden"); }; displayNameButton.onclick = async () => { let req = await client.changeDisplayName({ changeDisplayNameRequest: { name: displayNameBox.value, }, }); if (req.kind === "success") { showSuccess("Successfully changed display name"); handleRoute(); } else { showError(`Failed to update: ${req.msg}`); } }; passwordButton.onclick = async () => { let req = await client.changePassword({ changePasswordRequest: { newPassword: passwordBox.value, }, }); if (req.kind === "success") { showSuccess("Successfully changed password"); handleRoute(); } else { showError(`Failed to update: ${req.msg}`); } }; // Initialize UI state refreshTotpUI(); }, }; } addRoute("/profile", route);