almost done

This commit is contained in:
Maieul BOYER 2025-12-10 16:50:31 +01:00 committed by apetitco
parent 37a33d8a73
commit 23baa4af56
2 changed files with 232 additions and 249 deletions

View file

@ -1,18 +1,14 @@
<div class="grid h-full place-items-center"> <div class="grid h-full place-items-center">
<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md"> <div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
<h1 class="text-2xl font-semibold mb-6 text-gray-700">Edit Profile</h1> <h1 class="text-2xl font-semibold mb-6 text-gray-700">Edit Profile</h1>
<div id="isGuestBox" class="border-red-600 rounded-2xl border-2" hidden> <div id="isGuestBox" class="border-red-600 rounded-2xl border-2" hidden>
<h2 class="text-2xl font-semibold text-red-600"> This is a guest Account</h2> <h2 class="text-2xl font-semibold text-red-600">This is a guest Account</h2>
<span class="text-red-600"> You can't change anything here </span> <span class="text-red-600">You can't change anything here</span>
</div> </div>
<div class="mb-1 text-gray-700 rounded-sm border-2 outline-lime-100"> <div class="mb-1 text-gray-700 rounded-sm border-2 outline-lime-100">
<label class="inline font-medium mb-1 text-gray-700">AccountType: </label> <label class="inline font-medium mb-1 text-gray-700">AccountType:</label>
<span id="accountType" class="font-medium"></span> <span id="accountType" class="font-medium"><span class="text-red-600">Unknown</span></span>
</div> </div>
<!-- Login Name --> <!-- Login Name -->
<div id="loginNameWrapper" class="py-2" hidden> <div id="loginNameWrapper" class="py-2" hidden>
<label class="block font-medium mb-1 text-gray-700">Login Name</label> <label class="block font-medium mb-1 text-gray-700">Login Name</label>
@ -20,17 +16,18 @@
</div> </div>
<!-- Login Name --> <!-- Login Name -->
<div id="providerWrapper" class="py-2 mb-1 border-2 border-green-400 rounded-sm" hidden> <div id="providerWrapper" class="py-2 mb-1 border-2 border-green-400 rounded-sm" hidden>
<span class="py-2 mb-1 text-gray-700 text-lg">Provider</span>
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
<div> <div class="flex-1">
<label class="block font-medium mb-1 text-gray-700">Name</label> <label class="block font-medium mb-1 text-gray-700">Name</label>
<div id="providerNameBox" <div id="providerNameBox"
class="font-medium mb-1 text-gray-700"></div> class="max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div>
</div> </div>
<div> <div class="flex-1">
<label class="block font-medium mb-1 text-gray-700">User</label> <label class="block font-medium mb-1 text-gray-700">User</label>
<div id="providerUserBox" <div id="providerUserBox"
class="font-medium mb-1 text-gray-700"></div> class="max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div>
</div> </div>
</div> </div>
</div> </div>
@ -39,54 +36,42 @@
<label class="block font-medium mb-1 text-gray-700">Display Name</label> <label class="block font-medium mb-1 text-gray-700">Display Name</label>
<input id="displayNameBox" type="text" placeholder="Display Name" name="DisplayName" <input id="displayNameBox" type="text" placeholder="Display Name" name="DisplayName"
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" /> class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="displayNameButton" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"> <button id="displayNameButton"
Update class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
</button>
</div> </div>
<!-- Password --> <!-- Password -->
<div id="passwordWrapper" class="py-2"> <div id="passwordWrapper" class="py-2" hidden hidden>
<label class="block font-medium mb-1 text-gray-700">Change Password</label> <label class="block font-medium mb-1 text-gray-700">Change Password</label>
<input id="passwordBox" type="password" placeholder="New Password" name="Password" <input id="passwordBox" type="password" placeholder="New Password" name="Password"
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" /> class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="passwordButton" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"> <button id="passwordButton"
Update class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
</button>
</div> </div>
<!-- TOTP --> <!-- TOTP -->
<div class="border rounded p-4" id="totpWrapper"> <div class="border rounded p-4" id="totpWrapper" hidden>
<h2 class="font-semibold text-lg mb-2">Two-Factor Authentication (TOTP)</h2> <h2 class="font-semibold text-lg mb-2">Two-Factor Authentication (TOTP)</h2>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span id="totpStatusText" class="font-medium text-gray-700">Status: Disabled</span> <span id="totpStatusText" class="font-medium text-gray-700">Status: Disabled</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button id="enableTotp" type="button" <button id="enableTotp" type="button"
class="bg-green-600 text-white-700 px-3 py-1 rounded hover:bg-green-700"> class="bg-green-600 text-white-700 px-3 py-1 rounded hover:bg-green-700">Enable</button>
Enable
</button>
<button id="disableTotp" type="button" <button id="disableTotp" type="button"
class="bg-red-600 text-white-700 px-3 py-1 rounded hover:bg-red-700 hidden"> class="bg-red-600 text-white-700 px-3 py-1 rounded hover:bg-red-700 hidden">
Disable Disable
</button> </button>
<button id="showSecret" type="button" <button id="showSecret" type="button"
class="bg-blue-600 text-white-700 px-3 py-1 rounded hover:bg-blue-700 hidden"> class="bg-blue-600 text-white-700 px-3 py-1 rounded hover:bg-blue-700 hidden">
Show Secret Show Secret
</button> </button>
</div> </div>
</div> </div>
<div id="totpSecretBox" class="mt-3 text-sm bg-gray-100 border p-2 rounded hidden"> <div id="totpSecretBox" class="mt-3 text-sm bg-gray-100 border p-2 rounded hidden">
<canvas id="totpSecretCanvas" class="w-full h-full block"> </canvas> <canvas id="totpSecretCanvas" class="w-full h-full block">
</canvas>
<div id="totpSecretText" <div id="totpSecretText"
class="w-full max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap"> class="w-full max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,15 +6,7 @@ import { isNullish } from "@app/utils";
import client from "@app/api"; import client from "@app/api";
import QRCode from "qrcode"; import QRCode from "qrcode";
type OAuthQRCodeOptions = { /*
label?: string; // e.g. your-app:user@example.com
issuer?: string; // e.g. "YourApp"
algorithm?: "SHA1" | "SHA256" | "SHA512";
digits?: number;
period?: number;
};
/**
* Renders an OAuth2-compatible TOTP QR code into a canvas. * Renders an OAuth2-compatible TOTP QR code into a canvas.
* *
* @param canvas HTMLCanvasElement to draw into * @param canvas HTMLCanvasElement to draw into
@ -38,219 +30,225 @@ export async function renderOAuth2QRCode(
}); });
canvas.style.width = ""; canvas.style.width = "";
canvas.style.height = ""; canvas.style.height = "";
function removeBgColor(...elem: HTMLElement[]) { }
for (let e of elem) { function removeBgColor(...elem: HTMLElement[]) {
for (let c of e.classList.values()) { for (let e of elem) {
if (c.startsWith("bg-") || c.startsWith("hover:bg-")) for (let c of e.classList.values()) {
e.classList.remove(c); if (c.startsWith("bg-") || c.startsWith("hover:bg-"))
} e.classList.remove(c);
} }
} }
}
async function route(url: string, _args: { [k: string]: string }) { async function route(url: string, _args: { [k: string]: string }) {
setTitle("Edit Profile"); setTitle("Edit Profile");
return { return {
html: page, html: page,
postInsert: async (app: HTMLElement | undefined) => { postInsert: async (app: HTMLElement | undefined) => {
const user = await updateUser(); const user = await updateUser();
if (isNullish(user)) return showError("No User"); if (isNullish(user)) return showError("No User");
if (isNullish(app)) return showError("Failed to render"); if (isNullish(app)) return showError("Failed to render");
let totpState = await (async () => { let totpState = await (async () => {
let res = await client.statusOtp(); let res = await client.statusOtp();
if (res.kind === "success") if (res.kind === "success")
return { return {
enabled: enabled:
(res.msg as string) === "statusOtp.success.enabled", (res.msg as string) === "statusOtp.success.enabled",
secret: secret:
(res.msg as string) === "statusOtp.success.enabled" (res.msg as string) === "statusOtp.success.enabled"
? res.payload.secret ? res.payload.secret
: null, : 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")!;
if (user.guest) {
for (let c of passwordButton.classList.values()) {
if (c.startsWith("bg-") || c.startsWith("hover:bg-"))
passwordButton.classList.remove(c);
}
let totpWrapper = app.querySelector<HTMLDivElement>("#totpWrapper")!;
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;
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}`);
}
}; };
else {
disableBtn.onclick = async () => { showError("Failed to get OTP status");
let res = await client.disableOtp(); return {
if (res.kind === "success") { enabled: false,
navigateTo(url); secret: null,
} else {
showError(`failed to deactivate OTP: ${res.msg}`);
}
}; };
}
})();
// ---- Simulated State ----
let totpEnabled = totpState.enabled;
let totpSecret = totpState.secret; // would come from backend
showSecretBtn.onclick = () => { let guestBox = app.querySelector<HTMLDivElement>("#isGuestBox")!;
if (!isNullish(totpSecret)) { let displayNameWrapper = app.querySelector<HTMLDivElement>(
secretText.textContent = totpSecret; "#displayNameWrapper",
renderOAuth2QRCode(secretCanvas, totpSecret); )!;
} let displayNameBox =
secretBox.classList.toggle("hidden"); 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")!;
displayNameButton.onclick = async () => { let providerWrapper =
let req = await client.changeDisplayName({ app.querySelector<HTMLDivElement>("#providerWrapper")!;
changeDisplayNameRequest: { name: displayNameBox.value }, let providerNameBox =
}); app.querySelector<HTMLDivElement>("#providerNameBox")!;
if (req.kind === "success") { let providerUserBox =
showSuccess("Successfully changed display name"); app.querySelector<HTMLDivElement>("#providerUserBox")!;
handleRoute();
} else {
showError(`Failed to update: ${req.msg}`);
}
};
// Initialize UI state let accountTypeBox =
refreshTotpUI(); 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}`);
}
}; };
}
addRoute("/profile", route); 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);