account recovery and inital status stuff

This commit is contained in:
MathMan05 2025-04-03 14:14:37 -05:00
parent 4b087cb98b
commit acbce08e49
11 changed files with 370 additions and 92 deletions

View file

@ -169,7 +169,11 @@ class Contextmenu<x, y> {
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);

View file

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

View file

@ -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<Localuser, void>("");
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)) {

View file

@ -48,6 +48,7 @@
<button type="submit" id="loginButton">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
<div id="recover"></div>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script>

View file

@ -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");

119
src/webpage/recover.ts Normal file
View file

@ -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();
}

27
src/webpage/reset.html Normal file
View file

@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jank Client</title>
<meta content="Jank Client" property="og:title" />
<meta content="A spacebar client that has DMs, replying and more" property="og:description" />
<meta content="/logo.webp" property="og:image" />
<meta content="#4b458c" data-react-helmet="true" name="theme-color" />
<link href="/style.css" rel="stylesheet" />
<link href="/themes.css" rel="stylesheet" id="lightcss" />
<style>
body.no-theme {
background: #16191b;
}
@media (prefers-color-scheme: light) {
body.no-theme {
background: #9397bd;
}
}
</style>
</head>
<body class="no-theme">
<script src="/recover.js" type="module"></script>
</body>
</html>

View file

@ -351,7 +351,7 @@ class SelectInput implements OptionsElement<number> {
readonly label: string;
readonly owner: Options;
readonly onSubmit: (str: number) => void;
options: string[];
options: readonly string[];
index: number;
select!: WeakRef<HTMLSelectElement>;
radio: boolean;
@ -361,7 +361,7 @@ class SelectInput implements OptionsElement<number> {
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<void> {
addSelect(
label: string,
onSubmit: (str: number) => void,
selections: string[],
selections: readonly string[],
{defaultIndex = 0, radio = false} = {},
) {
const select = new SelectInput(label, onSubmit, selections, this, {

View file

@ -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":

View file

@ -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() {

View file

@ -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",