254 lines
7.5 KiB
TypeScript
254 lines
7.5 KiB
TypeScript
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<void> {
|
|
// 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<HTMLDivElement>("#isGuestBox")!;
|
|
let displayNameWrapper = app.querySelector<HTMLDivElement>(
|
|
"#displayNameWrapper",
|
|
)!;
|
|
let displayNameBox =
|
|
app.querySelector<HTMLInputElement>("#displayNameBox")!;
|
|
let displayNameButton =
|
|
app.querySelector<HTMLButtonElement>("#displayNameButton")!;
|
|
let loginNameWrapper =
|
|
app.querySelector<HTMLDivElement>("#loginNameWrapper")!;
|
|
let loginNameBox =
|
|
app.querySelector<HTMLDivElement>("#loginNameBox")!;
|
|
let passwordWrapper =
|
|
app.querySelector<HTMLDivElement>("#passwordWrapper")!;
|
|
let passwordBox =
|
|
app.querySelector<HTMLInputElement>("#passwordBox")!;
|
|
let passwordButton =
|
|
app.querySelector<HTMLButtonElement>("#passwordButton")!;
|
|
|
|
let providerWrapper =
|
|
app.querySelector<HTMLDivElement>("#providerWrapper")!;
|
|
let providerNameBox =
|
|
app.querySelector<HTMLDivElement>("#providerNameBox")!;
|
|
let providerUserBox =
|
|
app.querySelector<HTMLDivElement>("#providerUserBox")!;
|
|
|
|
let accountTypeBox =
|
|
app.querySelector<HTMLDivElement>("#accountType")!;
|
|
displayNameBox.value = user.name;
|
|
|
|
guestBox.hidden = !user.guest;
|
|
|
|
// ---- DOM Elements ----
|
|
const totpStatusText = app.querySelector("#totpStatusText")!;
|
|
const enableBtn =
|
|
app.querySelector<HTMLButtonElement>("#enableTotp")!;
|
|
const disableBtn =
|
|
app.querySelector<HTMLButtonElement>("#disableTotp")!;
|
|
const showSecretBtn =
|
|
app.querySelector<HTMLButtonElement>("#showSecret")!;
|
|
const secretBox = app.querySelector("#totpSecretBox")!;
|
|
const secretText =
|
|
app.querySelector<HTMLDivElement>("#totpSecretText")!;
|
|
const secretCanvas =
|
|
app.querySelector<HTMLCanvasElement>("#totpSecretCanvas")!;
|
|
let totpWrapper =
|
|
app.querySelector<HTMLDivElement>("#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}`);
|
|
}
|
|
};
|
|
|
|
// Initialize UI state
|
|
refreshTotpUI();
|
|
},
|
|
};
|
|
}
|
|
|
|
addRoute("/profile", route);
|