From acbce08e491afc6a85167fadc0416a3228838e0f Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 3 Apr 2025 14:14:37 -0500 Subject: [PATCH] account recovery and inital status stuff --- src/webpage/contextmenu.ts | 6 +- src/webpage/index.ts | 83 ++------------------ src/webpage/localuser.ts | 151 +++++++++++++++++++++++++++++++++++-- src/webpage/login.html | 1 + src/webpage/login.ts | 30 +++++++- src/webpage/recover.ts | 119 +++++++++++++++++++++++++++++ src/webpage/reset.html | 27 +++++++ src/webpage/settings.ts | 12 +-- src/webpage/user.ts | 4 + src/webpage/utils/utils.ts | 13 +++- translations/en.json | 16 +++- 11 files changed, 370 insertions(+), 92 deletions(-) create mode 100644 src/webpage/recover.ts create mode 100644 src/webpage/reset.html diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 57e6e63..f999f7f 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -169,7 +169,11 @@ class Contextmenu { if (Contextmenu.currentmenu !== "") { Contextmenu.currentmenu.remove(); } - div.style.top = y + "px"; + if (y > 0) { + div.style.top = y + "px"; + } else { + div.style.bottom = y * -1 + "px"; + } div.style.left = x + "px"; document.body.appendChild(div); Contextmenu.keepOnScreen(div); diff --git a/src/webpage/index.ts b/src/webpage/index.ts index 9244a7c..78b02c5 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -8,8 +8,8 @@ import {File} from "./file.js"; import {I18n} from "./i18n.js"; (async () => { await I18n.done; - const users = getBulkUsers(); - if (!users.currentuser) { + + if (!Localuser.users.currentuser) { window.location.href = "/login.html"; return; } @@ -26,94 +26,27 @@ import {I18n} from "./i18n.js"; } } I18n; - function showAccountSwitcher(): void { - const table = document.createElement("div"); - table.classList.add("flexttb", "accountSwitcher"); - - for (const user of Object.values(users.users)) { - const specialUser = user as Specialuser; - const userInfo = document.createElement("div"); - userInfo.classList.add("flexltr", "switchtable"); - - const pfp = document.createElement("img"); - pfp.src = specialUser.pfpsrc; - pfp.classList.add("pfp"); - userInfo.append(pfp); - - const userDiv = document.createElement("div"); - userDiv.classList.add("userinfo"); - userDiv.textContent = specialUser.username; - userDiv.append(document.createElement("br")); - - const span = document.createElement("span"); - span.textContent = specialUser.serverurls.wellknown - .replace("https://", "") - .replace("http://", ""); - span.classList.add("serverURL"); - userDiv.append(span); - - userInfo.append(userDiv); - table.append(userInfo); - - userInfo.addEventListener("click", () => { - thisUser.unload(); - thisUser.swapped = true; - const loading = document.getElementById("loading") as HTMLDivElement; - loading.classList.remove("doneloading"); - loading.classList.add("loading"); - - thisUser = new Localuser(specialUser); - users.currentuser = specialUser.uid; - sessionStorage.setItem("currentuser", specialUser.uid); - localStorage.setItem("userinfos", JSON.stringify(users)); - - thisUser.initwebsocket().then(() => { - thisUser.loaduser(); - thisUser.init(); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - - userInfo.remove(); - }); - } - - const switchAccountDiv = document.createElement("div"); - switchAccountDiv.classList.add("switchtable"); - switchAccountDiv.textContent = I18n.getTranslation("switchAccounts"); - switchAccountDiv.addEventListener("click", () => { - window.location.href = "/login.html"; - }); - table.append(switchAccountDiv); - - if (Contextmenu.currentmenu) { - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu = table; - document.body.append(table); - } const userInfoElement = document.getElementById("userinfo") as HTMLDivElement; userInfoElement.addEventListener("click", (event) => { event.stopImmediatePropagation(); - showAccountSwitcher(); + const rect = userInfoElement.getBoundingClientRect(); + Localuser.userMenu.makemenu(rect.x, rect.top - 10 - window.innerHeight, thisUser); }); const switchAccountsElement = document.getElementById("switchaccounts") as HTMLDivElement; switchAccountsElement.addEventListener("click", (event) => { event.stopImmediatePropagation(); - showAccountSwitcher(); + Localuser.showAccountSwitcher(thisUser); }); let thisUser: Localuser; try { - const current = sessionStorage.getItem("currentuser") || users.currentuser; - console.log(users.users, current); - if (!users.users[current]) { + const current = sessionStorage.getItem("currentuser") || Localuser.users.currentuser; + if (!Localuser.users.users[current]) { window.location.href = "/login"; } - thisUser = new Localuser(users.users[current]); + thisUser = new Localuser(Localuser.users.users[current]); thisUser.initwebsocket().then(() => { thisUser.loaduser(); thisUser.init(); diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 0ea789e..6ac112d 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -3,7 +3,7 @@ import {Channel} from "./channel.js"; import {Direct} from "./direct.js"; import {AVoice} from "./audio/voice.js"; import {User} from "./user.js"; -import {getapiurls, SW} from "./utils/utils.js"; +import {getapiurls, getBulkUsers, SW} from "./utils/utils.js"; import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js"; import { channeljson, @@ -30,6 +30,7 @@ import {Play} from "./audio/play.js"; import {Message} from "./message.js"; import {badgeArr} from "./Dbadges.js"; import {Rights} from "./rights.js"; +import {Contextmenu} from "./contextmenu.js"; const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]); @@ -75,6 +76,146 @@ class Localuser { set perminfo(e) { this.userinfo.localuserStore = e; } + static users = getBulkUsers(); + static showAccountSwitcher(thisUser: Localuser): void { + const table = document.createElement("div"); + table.classList.add("flexttb", "accountSwitcher"); + + for (const user of Object.values(this.users.users)) { + const specialUser = user as Specialuser; + const userInfo = document.createElement("div"); + userInfo.classList.add("flexltr", "switchtable"); + + const pfp = document.createElement("img"); + pfp.src = specialUser.pfpsrc; + pfp.classList.add("pfp"); + userInfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = specialUser.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = specialUser.serverurls.wellknown + .replace("https://", "") + .replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userInfo.append(userDiv); + table.append(userInfo); + + userInfo.addEventListener("click", () => { + thisUser.unload(); + thisUser.swapped = true; + const loading = document.getElementById("loading") as HTMLDivElement; + loading.classList.remove("doneloading"); + loading.classList.add("loading"); + + thisUser = new Localuser(specialUser); + Localuser.users.currentuser = specialUser.uid; + sessionStorage.setItem("currentuser", specialUser.uid); + localStorage.setItem("userinfos", JSON.stringify(Localuser.users)); + + thisUser.initwebsocket().then(() => { + thisUser.loaduser(); + thisUser.init(); + loading.classList.add("doneloading"); + loading.classList.remove("loading"); + console.log("done loading"); + }); + + userInfo.remove(); + }); + } + + const switchAccountDiv = document.createElement("div"); + switchAccountDiv.classList.add("switchtable"); + switchAccountDiv.textContent = I18n.getTranslation("switchAccounts"); + switchAccountDiv.addEventListener("click", () => { + window.location.href = "/login.html"; + }); + table.append(switchAccountDiv); + + if (Contextmenu.currentmenu) { + Contextmenu.currentmenu.remove(); + } + Contextmenu.currentmenu = table; + document.body.append(table); + } + static userMenu = this.generateUserMenu(); + static generateUserMenu() { + const menu = new Contextmenu(""); + menu.addButton( + () => I18n.localuser.addStatus(), + function () { + const d = new Dialog(I18n.localuser.status()); + const opt = d.float.options.addForm( + "", + () => { + const status = cust.value; + sessionStorage.setItem("cstatus", JSON.stringify({text: status})); + //this.user.setstatus(status); + d.hide(); + }, + { + fetchURL: this.info.api + "/users/@me/settings", + method: "PATCH", + headers: this.headers, + }, + ); + opt.addText(I18n.localuser.customStatusWarn()); + opt.addPreprocessor((obj) => { + if ("custom_status" in obj) { + obj.custom_status = {text: obj.custom_status}; + } + }); + const cust = opt.addTextInput(I18n.localuser.status(), "custom_status", {}); + d.show(); + }, + ); + menu.addButton( + () => I18n.localuser.status(), + function () { + const d = new Dialog(I18n.localuser.status()); + const opt = d.float.options; + const selection = ["online", "invisible", "dnd", "idle"] as const; + opt.addText(I18n.localuser.statusWarn()); + const smap = selection.map((_) => I18n.user[_]()); + let index = selection.indexOf( + sessionStorage.getItem("status") as "online" | "invisible" | "dnd" | "idle", + ); + if (index === -1) { + index = 0; + } + opt + .addSelect("", () => {}, smap, { + defaultIndex: index, + }) + .watchForChange(async (i) => { + const status = selection[i]; + await fetch(this.info.api + "/users/@me/settings", { + body: JSON.stringify({ + status, + }), + headers: this.headers, + method: "PATCH", + }); + sessionStorage.setItem("status", status); + this.user.setstatus(status); + }); + d.show(); + }, + ); + menu.addButton( + () => I18n.switchAccounts(), + function () { + Localuser.showAccountSwitcher(this); + }, + ); + return menu; + } constructor(userinfo: Specialuser | -1) { Play.playURL("/audio/sounds.jasf").then((_) => { this.play = _; @@ -104,7 +245,7 @@ class Localuser { this.guilds = []; this.guildids = new Map(); this.user = new User(ready.d.user, this); - this.user.setstatus("online"); + this.user.setstatus(sessionStorage.getItem("status") || "online"); this.resume_gateway_url = ready.d.resume_gateway_url; this.session_id = ready.d.session_id; @@ -240,7 +381,7 @@ class Localuser { }, compress: Boolean(DecompressionStream), presence: { - status: "online", + status: sessionStorage.getItem("status") || "online", since: null, //new Date().getTime() activities: [], afk: false, @@ -756,13 +897,13 @@ class Localuser { for (const [role, list] of elms) { members.forEach((member) => { if (role === "offline") { - if (member.user.getStatus() === "offline") { + if (member.user.getStatus() === "offline" || member.user.getStatus() === "invisible") { list.push(member); members.delete(member); } return; } - if (member.user.getStatus() === "offline") { + if (member.user.getStatus() === "offline" || member.user.getStatus() === "invisible") { return; } if (role !== "online" && member.hasRole(role.id)) { diff --git a/src/webpage/login.html b/src/webpage/login.html index faa9387..2b1e5b4 100644 --- a/src/webpage/login.html +++ b/src/webpage/login.html @@ -48,6 +48,7 @@ Don't have an account? +
diff --git a/src/webpage/login.ts b/src/webpage/login.ts index d931886..34e1e91 100644 --- a/src/webpage/login.ts +++ b/src/webpage/login.ts @@ -2,9 +2,35 @@ import {getBulkInfo, Specialuser} from "./utils/utils.js"; import {I18n} from "./i18n.js"; import {Dialog, FormError} from "./settings.js"; import {checkInstance} from "./utils/utils.js"; - +function generateRecArea() { + const recover = document.getElementById("recover"); + if (!recover) return; + const can = localStorage.getItem("canRecover"); + if (can) { + const a = document.createElement("a"); + a.textContent = I18n.login.recover(); + a.href = "/reset"; + recover.append(a); + } +} +checkInstance.alt = async (e) => { + const recover = document.getElementById("recover"); + if (!recover) return; + recover.innerHTML = ""; + try { + const json = (await (await fetch(e.api + "/policies/instance/config")).json()) as { + can_recover_account: boolean; + }; + if (!json || !json.can_recover_account) throw Error("can't recover account"); + localStorage.setItem("canRecover", "true"); + generateRecArea(); + } catch { + localStorage.removeItem("canRecover"); + generateRecArea(); + } +}; await I18n.done; - +generateRecArea(); (async () => { await I18n.done; const instanceField = document.getElementById("instanceField"); diff --git a/src/webpage/recover.ts b/src/webpage/recover.ts new file mode 100644 index 0000000..ed8131e --- /dev/null +++ b/src/webpage/recover.ts @@ -0,0 +1,119 @@ +import {I18n} from "./i18n.js"; +import {adduser} from "./login.js"; +import {Dialog, FormError} from "./settings.js"; +await I18n.done; +const info = JSON.parse(localStorage.getItem("instanceinfo") as string); + +function makeMenu2(email: string | void) { + const d2 = new Dialog(I18n.login.recovery()); + const headers = { + "Content-Type": "application/json", + }; + const opt = d2.float.options.addForm( + "", + async (obj) => { + const serverurls = JSON.parse(localStorage.getItem("instanceinfo") as string); + + if ("token" in obj && typeof obj.token === "string") { + if (email === undefined) { + const user = await ( + await fetch(serverurls.api + "/users/@me", { + headers: { + Authorization: obj.token, + }, + }) + ).json(); + if ("email" in user && typeof user.email === "string") { + email = user.email; + } else { + throw new Error("stupid"); + } + } + const username = email; + adduser({ + serverurls, + email: username, + token: obj.token, + }).username = username; + } + }, + { + fetchURL: info.api + "/auth/reset", + method: "POST", + headers, + }, + ); + if (email !== undefined) { + opt.addTextInput(I18n.login.pasteInfo(), "token"); + } + opt.addTextInput(I18n.login.newPassword(), "password", {password: true}); + const p2 = opt.addTextInput(I18n.login.enterPAgain(), "password2", {password: true}); + opt.addPreprocessor((e) => { + const obj = e as unknown as {password: string; password2?: string; token?: string}; + const token = obj.token || window.location.href; + if (URL.canParse(token)) { + obj.token = new URLSearchParams(token.split("#")[1]).get("token") as string; + } + + if (obj.password !== obj.password2) { + throw new FormError(p2, I18n.localuser.PasswordsNoMatch()); + } + delete obj.password2; + }); + d2.show(false); +} +function makeMenu1() { + const d = new Dialog(I18n.login.recovery()); + let area: HTMLElement | undefined = undefined; + const opt = d.float.options.addForm( + "", + (e) => { + if (Object.keys(e).length === 0) { + d.hide(); + makeMenu2(email.value); + } else if ("captcha_sitekey" in e && typeof e.captcha_sitekey === "string") { + if (area) { + eval("hcaptcha.reset()"); + } else { + area = document.createElement("div"); + opt.addHTMLArea(area); + const capty = document.createElement("div"); + capty.classList.add("h-captcha"); + + capty.setAttribute("data-sitekey", e.captcha_sitekey); + const script = document.createElement("script"); + script.src = "https://js.hcaptcha.com/1/api.js"; + area.append(script); + area.append(capty); + } + } + }, + { + fetchURL: info.api + "/auth/forgot", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + const email = opt.addTextInput(I18n.htmlPages.emailField(), "login"); + opt.addPreprocessor((e) => { + if (area) { + try { + //@ts-expect-error + e.captcha_key = area.children[1].children[1].value; + } catch (e) { + console.error(e); + } + } + }); + d.show(false); +} +if ( + window.location.href.split("#").length == 2 && + new URLSearchParams(window.location.href.split("#")[1]).has("token") +) { + makeMenu2(); +} else { + makeMenu1(); +} diff --git a/src/webpage/reset.html b/src/webpage/reset.html new file mode 100644 index 0000000..8da1f5b --- /dev/null +++ b/src/webpage/reset.html @@ -0,0 +1,27 @@ + + + + + + Jank Client + + + + + + + + + + + + diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index d894b62..f20f0b5 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -351,7 +351,7 @@ class SelectInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: number) => void; - options: string[]; + options: readonly string[]; index: number; select!: WeakRef; radio: boolean; @@ -361,7 +361,7 @@ class SelectInput implements OptionsElement { constructor( label: string, onSubmit: (str: number) => void, - options: string[], + options: readonly string[], owner: Options, {defaultIndex = 0, radio = false} = {}, ) { @@ -619,7 +619,7 @@ class Dialog { constructor(name: string, {ltr = false, noSubmit = true} = {}) { this.float = new Float(name, {ltr, noSubmit}); } - show() { + show(hideOnClick = true) { const background = document.createElement("div"); background.classList.add("background"); const center = this.float.generateHTML(); @@ -632,7 +632,9 @@ class Dialog { document.body.append(background); this.background = new WeakRef(background); background.onclick = (_) => { - background.remove(); + if (hideOnClick) { + background.remove(); + } }; } hide() { @@ -738,7 +740,7 @@ class Options implements OptionsElement { addSelect( label: string, onSubmit: (str: number) => void, - selections: string[], + selections: readonly string[], {defaultIndex = 0, radio = false} = {}, ) { const select = new SelectInput(label, onSubmit, selections, this, { diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 3aee913..6edd6e1 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -124,6 +124,9 @@ class User extends SnowFlake { return this.status && this.status != "offline"; } setstatus(status: string): void { + if (this.id === this.localuser.user.id) { + console.warn(status); + } this.status = status; } @@ -482,6 +485,7 @@ class User extends SnowFlake { status.classList.add("statusDiv"); switch (await this.getStatus()) { case "offline": + case "invisible": status.classList.add("offlinestatus"); break; case "online": diff --git a/src/webpage/utils/utils.ts b/src/webpage/utils/utils.ts index cd1fc5e..f30dd54 100644 --- a/src/webpage/utils/utils.ts +++ b/src/webpage/utils/utils.ts @@ -614,7 +614,7 @@ const checkInstance = Object.assign( verify!.textContent = I18n.getTranslation("login.allGood"); loginButton.disabled = false; if (checkInstance.alt) { - checkInstance.alt(); + checkInstance.alt(instanceinfo); } setTimeout((_: any) => { console.log(verify!.textContent); @@ -630,7 +630,16 @@ const checkInstance = Object.assign( loginButton.disabled = true; } }, - {} as {alt?: Function}, + {} as { + alt?: (e: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + value: string; + }) => void; + }, ); export {checkInstance}; export function getInstances() { diff --git a/translations/en.json b/translations/en.json index b7fdfe1..5c7e5f7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -295,6 +295,10 @@ "save": "Save changes" }, "localuser": { + "addStatus": "Add status", + "status": "Status", + "statusWarn": "Spacebar has bugs with this feature and will only update after a refresh, and will often not respect it", + "customStatusWarn": "Spacebar does not support custom status being displayed at this time so while it'll accept the status, it will not do anything with it", "settings": "Settings", "userSettings": "User Settings", "themesAndSounds": "Themes & Sounds", @@ -331,7 +335,7 @@ "changePassword": "Change password", "oldPassword:": "Old password:", "newPassword:": "New password:", - "PasswordsNoMatch": "Password don't match", + "PasswordsNoMatch": "Passwords don't match", "disableConnection": "This connection has been disabled server-side", "devPortal": "Developer Portal", "createApp": "Create application", @@ -455,6 +459,9 @@ "copyId": "Copy user ID", "online": "Online", "offline": "Offline", + "invisible": "Invisible", + "dnd": "Do Not Disturb", + "idle": "Idle", "message": "Message user", "block": "Block user", "unblock": "Unblock user", @@ -471,7 +478,12 @@ "checking": "Checking Instance", "allGood": "All good", "invalid": "Invalid Instance, try again", - "waiting": "Waiting to check Instance" + "waiting": "Waiting to check Instance", + "recover": "Forgot password?", + "pasteInfo": "Paste the recovery URL here:", + "newPassword": "New password:", + "enterPAgain": "Enter new password again:", + "recovery": "Forgotten password" }, "member": { "kick": "Kick $1 from $2",