redo login/register pages and install button

This commit is contained in:
MathMan05 2025-04-29 14:30:46 -05:00
parent e776bf6d3f
commit ba5ab05408
11 changed files with 698 additions and 600 deletions

View file

@ -1,4 +1,5 @@
import {I18n} from "./i18n.js";
import {makeRegister} from "./register.js";
import {mobile} from "./utils/utils.js";
console.log(mobile);
const serverbox = document.getElementById("instancebox") as HTMLDivElement;
@ -127,7 +128,7 @@ fetch("/instances.json")
div.append(statbox);
div.onclick = (_) => {
if (instance.online) {
window.location.href = "/register.html?instance=" + encodeURI(instance.name);
makeRegister(true, instance.name);
} else {
alert(I18n.getTranslation("home.warnOffiline"));
}

View file

@ -10,7 +10,7 @@ import {I18n} from "./i18n.js";
let templateID = new URLSearchParams(window.location.search).get("templateID");
await I18n.done;
if (!Localuser.users.currentuser) {
if (!(sessionStorage.getItem("currentuser") || Localuser.users.currentuser)) {
window.location.href = "/login.html";
return;
}

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 {createImg, getapiurls, getBulkUsers, SW} from "./utils/utils.js";
import {createImg, getapiurls, getBulkUsers, installPGet, SW} from "./utils/utils.js";
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
import {
channeljson,
@ -1719,7 +1719,8 @@ class Localuser {
required: true,
password: true,
});
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true});
form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
debugger;
form.setValue("secret", secret);
});
}
@ -2197,6 +2198,15 @@ class Localuser {
this.instanceStats();
});
})();
const installP = installPGet();
if (installP) {
const c = settings.addButton(I18n.localuser.install());
c.addText(I18n.localuser.installDesc());
c.addButtonInput("", I18n.localuser.installJank(), async () => {
//@ts-expect-error have to do this :3
await installP.prompt();
});
}
settings.show();
}
readonly botTokens: Map<string, string> = new Map();

View file

@ -22,35 +22,6 @@
</style>
</head>
<body class="no-theme">
<div id="logindiv">
<h1>Login</h1>
<form id="form" submit="check(e)">
<label for="instance" id="instanceField"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
required
/>
<label for="uname" id="emailField"><b>Email:</b></label>
<input type="text" placeholder="Enter email address" name="uname" id="uname" required />
<label for="psw" id="pwField"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required />
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<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>
</body>
</html>

View file

@ -1,10 +1,10 @@
import {getBulkInfo, Specialuser} from "./utils/utils.js";
import {instanceinfo, adduser} 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");
import {Dialog} from "./settings.js";
import {makeRegister} from "./register.js";
function generateRecArea(recover = document.getElementById("recover")) {
if (!recover) return;
recover.innerHTML = "";
const can = localStorage.getItem("canRecover");
if (can) {
const a = document.createElement("a");
@ -13,248 +13,98 @@ function generateRecArea() {
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");
const emailField = document.getElementById("emailField");
const pwField = document.getElementById("pwField");
const loginButton = document.getElementById("loginButton");
const noAccount = document.getElementById("switch");
if (instanceField && emailField && pwField && loginButton && noAccount) {
instanceField.textContent = I18n.getTranslation("htmlPages.instanceField");
emailField.textContent = I18n.getTranslation("htmlPages.emailField");
pwField.textContent = I18n.getTranslation("htmlPages.pwField");
loginButton.textContent = I18n.getTranslation("htmlPages.loginButton");
noAccount.textContent = I18n.getTranslation("htmlPages.noAccount");
}
})();
function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
const recMap = new Map<string, Promise<boolean>>();
async function recover(e: instanceinfo, recover = document.getElementById("recover")) {
const prom = new Promise<boolean>(async (res) => {
if (!recover) {
res(false);
return;
}
wellknown = (user.id || user.email) + "@" + wellknown;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
} else {
delete json.users[thing];
recover.innerHTML = "";
try {
if (!(await recMap.get(e.api))) {
if (recMap.has(e.api)) {
throw Error("can't recover");
}
recMap.set(e.api, prom);
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");
}
} else {
map.set(wellknown, [thing, user]);
res(true);
localStorage.setItem("canRecover", "true");
generateRecArea(recover);
} catch {
res(false);
localStorage.removeItem("canRecover");
generateRecArea(recover);
} finally {
res(false);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
function adduser(user: typeof Specialuser.prototype.json) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
sessionStorage.setItem("currentuser", user.uid);
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
const instancein = document.getElementById("instancein") as HTMLInputElement;
let timeout: ReturnType<typeof setTimeout> | string | number | undefined | null = null;
// let instanceinfo;
const switchurl = document.getElementById("switch") as HTMLAreaElement;
if (switchurl) {
switchurl.href += window.location.search;
const instance = new URLSearchParams(window.location.search).get("instance");
console.log(instance);
if (instance) {
instancein.value = instance;
checkInstance(instance);
}
}
if (instancein) {
console.log(instancein);
instancein.addEventListener("keydown", () => {
const verify = document.getElementById("verify");
verify!.textContent = I18n.getTranslation("login.waiting");
if (timeout !== null && typeof timeout !== "string") {
clearTimeout(timeout);
}
timeout = setTimeout(() => checkInstance((instancein as HTMLInputElement).value), 1000);
});
if (
localStorage.getItem("instanceinfo") &&
!new URLSearchParams(window.location.search).get("instance")
) {
const json = JSON.parse(localStorage.getItem("instanceinfo")!);
if (json.value) {
(instancein as HTMLInputElement).value = json.value;
} else {
(instancein as HTMLInputElement).value = json.wellknown;
}
}
}
async function login(username: string, password: string, captcha: string) {
if (captcha === "") {
captcha = "";
}
const options = {
method: "POST",
body: JSON.stringify({
login: username,
password,
undelete: false,
captcha_key: captcha,
}),
headers: {
"Content-type": "application/json; charset=UTF-8",
export async function makeLogin(trasparentBg = false, instance = "") {
const dialog = new Dialog("");
const opt = dialog.options;
opt.addTitle(I18n.login.login());
const picker = opt.addInstancePicker(
(info) => {
const api = info.login + (info.login.startsWith("/") ? "/" : "");
form.fetchURL = api + "/auth/login";
recover(info, rec);
},
{
instance,
},
};
try {
const info = JSON.parse(localStorage.getItem("instanceinfo")!);
const api = info.login + (info.login.startsWith("/") ? "/" : "");
return await fetch(api + "/auth/login", options)
.then((response) => response.json())
.then((response) => {
console.log(response, response.message);
if (response.message === "Invalid Form Body") {
return response.errors.login._errors[0].message;
console.log("test");
}
//this.serverurls||!this.email||!this.token
console.log(response);
if (response.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (capt!.children.length) {
eval("hcaptcha.reset()");
} else {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", response.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt!.append(script);
capt!.append(capty);
}
} else {
console.log(response);
if (response.ticket) {
const better = new Dialog("");
const form = better.options.addForm(
"",
(res: any) => {
if (res.message) {
throw new FormError(ti, res.message);
} else {
console.warn(res);
if (!res.token) return;
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string),
email: username,
token: res.token,
}).username = username;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
}
},
{
fetchURL: api + "/auth/mfa/totp",
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
);
form.addTitle(I18n.getTranslation("2faCode"));
const ti = form.addTextInput("", "code");
better.show();
} else {
console.warn(response);
if (!response.token) return;
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo")!),
email: username,
token: response.token,
}).username = username;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
return "";
}
}
});
} catch (error) {
console.error("Error:", error);
}
}
async function check(e: SubmitEvent) {
e.preventDefault();
const target = e.target as HTMLFormElement;
const h = await login(
(target[1] as HTMLInputElement).value,
(target[2] as HTMLInputElement).value,
(target[3] as HTMLInputElement).value,
);
const wrongElement = document.getElementById("wrong");
if (wrongElement) {
wrongElement.textContent = h;
}
console.log(h);
}
if (document.getElementById("form")) {
const form = document.getElementById("form");
if (form) {
form.addEventListener("submit", (e: SubmitEvent) => check(e));
}
}
//this currently does not work, and need to be implemented better at some time.
if (!localStorage.getItem("SWMode")) {
localStorage.setItem("SWMode", "SWOn");
}
dialog.show(trasparentBg);
trimswitcher();
const form = opt.addForm(
"",
(res) => {
if ("token" in res && typeof res.token == "string") {
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string),
email: email.value,
token: res.token,
}).username = email.value;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
}
},
{
submitText: I18n.login.login(),
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
vsmaller: true,
},
);
const button = form.button.deref();
picker.giveButton(button);
button?.classList.add("createAccount");
export {adduser};
const email = form.addTextInput(I18n.htmlPages.userField(), "login");
form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true});
form.addCaptcha();
const a = document.createElement("a");
a.onclick = () => {
dialog.hide();
makeRegister(trasparentBg);
};
a.textContent = I18n.htmlPages.noAccount();
const rec = document.createElement("div");
form.addHTMLArea(rec);
form.addHTMLArea(a);
}
await I18n.done;
if (window.location.pathname.startsWith("/login")) {
makeLogin();
}

View file

@ -22,64 +22,6 @@
</style>
</head>
<body class="no-theme">
<div id="logindiv">
<h1>Create an account</h1>
<form id="register" submit="registertry(e)">
<div>
<label for="instance" id="instanceField"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
id="instancein"
name="instance"
value=""
required
/>
</div>
<div>
<label for="uname" id="emailField"><b>Email:</b></label>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required />
</div>
<div>
<label for="uname" id="userField"><b>Username:</b></label>
<input type="text" placeholder="Enter Username" name="username" id="username" required />
</div>
<div>
<label for="psw" id="pwField"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required />
</div>
<div>
<label for="psw2" id="pw2Field"><b>Enter password again:</b></label>
<input
type="password"
placeholder="Enter Password Again"
name="psw2"
id="psw2"
required
/>
</div>
<div>
<label for="date" id="dobField"><b>Date of birth:</b></label>
<input type="date" id="date" name="date" />
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS" />
</div>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<button type="submit" class="dontgrow" id="createAccount">Create account</button>
</form>
<a href="/login.html" id="switch" id="alreadyHave">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script>
</body>
</html>

View file

@ -1,169 +1,100 @@
import {I18n} from "./i18n.js";
import {checkInstance} from "./utils/utils.js";
import {adduser} from "./login.js";
import {adduser} from "./utils/utils.js";
import {makeLogin} from "./login.js";
import {MarkDown} from "./markdown.js";
await I18n.done;
const registerElement = document.getElementById("register");
if (registerElement) {
registerElement.addEventListener("submit", registertry);
}
(async () => {
await I18n.done;
const userField = document.getElementById("userField");
const pw2Field = document.getElementById("pw2Field");
const dobField = document.getElementById("dobField");
const createAccount = document.getElementById("createAccount");
const alreadyHave = document.getElementById("alreadyHave");
if (userField && pw2Field && alreadyHave && createAccount && dobField) {
userField.textContent = I18n.getTranslation("htmlPages.userField");
pw2Field.textContent = I18n.getTranslation("htmlPages.pw2Field");
dobField.textContent = I18n.getTranslation("htmlPages.dobField");
createAccount.textContent = I18n.getTranslation("htmlPages.createAccount");
alreadyHave.textContent = I18n.getTranslation("htmlPages.alreadyHave");
}
})();
async function registertry(e: Event) {
e.preventDefault();
const elements = (e.target as HTMLFormElement).elements as HTMLFormControlsCollection;
const email = (elements[1] as HTMLInputElement).value;
const username = (elements[2] as HTMLInputElement).value;
const password = (elements[3] as HTMLInputElement).value;
const confirmPassword = (elements[4] as HTMLInputElement).value;
const dateofbirth = (elements[5] as HTMLInputElement).value;
const consent = (elements[6] as HTMLInputElement).checked;
const captchaKey = (elements[7] as HTMLInputElement)?.value;
import {Dialog, FormError} from "./settings.js";
export async function makeRegister(trasparentBg = false, instance = "") {
const dialog = new Dialog("");
const opt = dialog.options;
opt.addTitle(I18n.htmlPages.createAccount());
const picker = opt.addInstancePicker(
(info) => {
const api = info.login + (info.login.startsWith("/") ? "/" : "");
form.fetchURL = api + "/auth/register";
tosLogic(md);
},
{instance},
);
dialog.show(trasparentBg);
if (password !== confirmPassword) {
(document.getElementById("wrong") as HTMLElement).textContent = I18n.getTranslation(
"localuser.PasswordsNoMatch",
);
return;
}
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api);
let add = "";
const token = new URLSearchParams(window.location.search).get("token");
if (token) {
add = "?" + new URLSearchParams([["token", token]]).toString();
}
try {
const response = await fetch(apiurl + "/auth/register" + add, {
body: JSON.stringify({
date_of_birth: dateofbirth,
email,
username,
password,
consent,
captcha_key: captchaKey,
}),
headers: {
"content-type": "application/json",
Referrer: window.location.href,
},
method: "POST",
});
const data = await response.json();
if (data.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (capt && !capt.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", data.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
} else {
eval("hcaptcha.reset()");
const form = opt.addForm(
"",
(res) => {
if ("token" in res && typeof res.token == "string") {
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string),
email: email.value,
token: res.token,
}).username = user.value;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
}
return;
},
{
submitText: I18n.htmlPages.createAccount(),
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
vsmaller: true,
},
);
const button = form.button.deref();
picker.giveButton(button);
button?.classList.add("createAccount");
const email = form.addTextInput(I18n.htmlPages.emailField(), "email");
const user = form.addTextInput(I18n.htmlPages.userField(), "username");
const p1 = form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true});
const p2 = form.addTextInput(I18n.htmlPages.pw2Field(), "password2", {password: true});
form.addDateInput(I18n.htmlPages.dobField(), "date_of_birth");
form.addPreprocessor((e) => {
if (p1.value !== p2.value) {
throw new FormError(p2, I18n.localuser.PasswordsNoMatch());
}
//@ts-expect-error it's there
delete e.password2;
if (!check.checked) throw new FormError(checkbox, I18n.register.tos());
//@ts-expect-error it's there
e.consent = check.checked;
});
const toshtml = document.createElement("div");
const md = document.createElement("span");
const check = document.createElement("input");
check.type = "checkbox";
if (!data.token) {
handleErrors(data.errors, elements);
} else {
adduser({
serverurls: instanceInfo,
email,
token: data.token,
}).username = username;
localStorage.setItem("token", data.token);
const redir = new URLSearchParams(window.location.search).get("goback");
window.location.href = redir ? redir : "/channels/@me";
}
} catch (error) {
console.error("Registration failed:", error);
}
toshtml.append(md, check);
const checkbox = form.addHTMLArea(toshtml);
form.addCaptcha();
const a = document.createElement("a");
a.onclick = () => {
dialog.hide();
makeLogin(trasparentBg);
};
a.textContent = I18n.htmlPages.alreadyHave();
form.addHTMLArea(a);
}
function handleErrors(errors: any, elements: HTMLFormControlsCollection) {
if (errors.consent) {
showError(elements[6] as HTMLElement, errors.consent._errors[0].message);
} else if (errors.password) {
showError(
elements[3] as HTMLElement,
I18n.getTranslation("register.passwordError:", errors.password._errors[0].message),
);
} else if (errors.username) {
showError(
elements[2] as HTMLElement,
I18n.getTranslation("register.usernameError", errors.username._errors[0].message),
);
} else if (errors.email) {
showError(
elements[1] as HTMLElement,
I18n.getTranslation("register.emailError", errors.email._errors[0].message),
);
} else if (errors.date_of_birth) {
showError(
elements[5] as HTMLElement,
I18n.getTranslation("register.DOBError", errors.date_of_birth._errors[0].message),
);
} else {
(document.getElementById("wrong") as HTMLElement).textContent =
errors[Object.keys(errors)[0]]._errors[0].message;
}
}
function showError(element: HTMLElement, message: string) {
const parent = element.parentElement!;
let errorElement = parent.getElementsByClassName("suberror")[0] as HTMLElement;
if (!errorElement) {
const div = document.createElement("div");
div.classList.add("suberror", "suberrora");
parent.append(div);
errorElement = div;
} else {
errorElement.classList.remove("suberror");
setTimeout(() => {
errorElement.classList.add("suberror");
}, 100);
}
errorElement.textContent = message;
}
async function tosLogic() {
async function tosLogic(box: HTMLElement) {
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api);
const urlstr = apiurl.toString();
const response = await fetch(urlstr + (urlstr.endsWith("/") ? "" : "/") + "ping");
const data = await response.json();
const tosPage = data.instance.tosPage;
if (!box) return;
if (tosPage) {
const box = document.getElementById("TOSbox");
if (!box) return;
box.innerHTML = "";
box.append(new MarkDown(I18n.getTranslation("register.agreeTOS", tosPage)).makeHTML());
} else {
document.getElementById("TOSbox")!.textContent = I18n.getTranslation("register.noTOS");
box.textContent = I18n.getTranslation("register.noTOS");
}
console.log(tosPage);
}
tosLogic();
checkInstance.alt = tosLogic;
if (window.location.pathname.startsWith("/register")) {
await I18n.done;
makeRegister();
}

View file

@ -1,3 +1,10 @@
import {
checkInstance,
getInstances,
getStringURLMapPair,
instancefetch,
instanceinfo,
} from "./utils/utils.js";
import {Emoji} from "./emoji.js";
import {I18n} from "./i18n.js";
import {Localuser} from "./localuser.js";
@ -166,6 +173,21 @@ class TextInput implements OptionsElement<string> {
this.onSubmit(this.value);
}
}
class DateInput extends TextInput {
generateHTML(): HTMLDivElement {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.value = this.value;
input.type = "date";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
}
const mdProm = import("./markdown.js");
class SettingsMDText implements OptionsElement<void> {
readonly onSubmit!: (str: string) => void;
@ -306,6 +328,7 @@ class ButtonInput implements OptionsElement<void> {
this.onClick = onClick;
this.textContent = textContent;
}
buttonHtml?: HTMLButtonElement;
generateHTML(): HTMLDivElement {
const div = document.createElement("div");
if (this.label) {
@ -317,6 +340,7 @@ class ButtonInput implements OptionsElement<void> {
const button = document.createElement("button");
button.textContent = this.textContent;
button.onclick = this.onClickEvent.bind(this);
this.buttonHtml = button;
div.append(button);
return div;
}
@ -712,6 +736,7 @@ class Dialog {
show(hideOnClick = true) {
const background = document.createElement("div");
background.classList.add("background");
if (!hideOnClick) background.classList.add("solidBackground");
const center = this.float.generateHTML();
center.classList.add("centeritem", "nonimagecenter");
center.classList.remove("titlediv");
@ -733,7 +758,141 @@ class Dialog {
background.remove();
}
}
class InstancePicker implements OptionsElement<instanceinfo | null> {
value: instanceinfo | null = null;
owner: Options | Form;
verify = document.createElement("p");
onchange = (_: instanceinfo) => {};
instance?: string;
watchForChange(func: (arg1: instanceinfo) => void) {
this.onchange = func;
}
constructor(
owner: Options | Form,
onchange?: InstancePicker["onchange"],
button?: HTMLButtonElement,
instance?: string,
) {
this.owner = owner;
this.instance = instance;
if (onchange) {
this.onchange = onchange;
}
this.button = button;
}
generateHTML(): HTMLElement {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = I18n.htmlPages.instanceField();
div.append(span);
const verify = this.verify;
verify.classList.add("verify");
div.append(verify);
const input = this.input;
input.type = "search";
input.setAttribute("list", "instances");
div.append(input);
let cur = 0;
input.onkeyup = async () => {
const thiscur = ++cur;
await new Promise((res) => setTimeout(res, 500));
if (thiscur !== cur) return;
const urls = await checkInstance(input.value, verify, this.button);
if (thiscur === cur && urls) {
this.onchange(urls);
}
};
InstancePicker.picker = this;
InstancePicker.genDataList();
return div;
}
button?: HTMLButtonElement;
input = document.createElement("input");
giveButton(button: HTMLButtonElement | undefined) {
this.button = button;
}
static picker?: InstancePicker;
static genDataList() {
let datalist = document.getElementById("instances");
if (!datalist) {
datalist = document.createElement("datalist");
datalist.setAttribute("id", "instances");
document.body.append(datalist);
}
const json = getInstances();
const [stringURLMap, stringURLsMap] = getStringURLMapPair();
if (!json) {
instancefetch.then(this.genDataList.bind(this));
return;
}
if (json.length !== 0) {
let name =
this.picker?.instance || new URLSearchParams(window.location.search).get("instance");
if (!name) {
const l = localStorage.getItem("instanceinfo");
if (l) {
const json = JSON.parse(l);
if (json.value) {
name = json.value;
} else {
name = json.wellknown;
}
}
}
if (!name) {
name = json[0].name;
}
if (this.picker) {
checkInstance(
name,
this.picker.verify,
this.picker.button || document.createElement("button"),
).then((e) => {
if (e) this.picker?.onchange(e);
});
this.picker.input.value = name;
}
}
if (datalist.childElementCount !== 0) {
return;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
} else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
} else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
} else {
option.label = instance.name;
}
datalist.append(option);
}
}
submit() {}
}
setTimeout(InstancePicker.genDataList.bind(InstancePicker), 0);
export {Dialog};
class Options implements OptionsElement<void> {
name: string;
@ -745,16 +904,18 @@ class Options implements OptionsElement<void> {
readonly html: WeakMap<OptionsElement<any>, WeakRef<HTMLDivElement>> = new WeakMap();
readonly noSubmit: boolean = false;
container: WeakRef<HTMLDivElement> = new WeakRef(document.createElement("div"));
vsmaller = false;
constructor(
name: string,
owner: Buttons | Options | Form | Float,
{ltr = false, noSubmit = false} = {},
{ltr = false, noSubmit = false, vsmaller = false} = {},
) {
this.name = name;
this.options = [];
this.owner = owner;
this.ltr = ltr;
this.noSubmit = noSubmit;
this.vsmaller = vsmaller;
}
removeAll() {
this.returnFromSub();
@ -837,6 +998,15 @@ class Options implements OptionsElement<void> {
this.generate(emoji);
return emoji;
}
addInstancePicker(
onchange?: InstancePicker["onchange"],
{button, instance}: {button?: HTMLButtonElement; instance?: string} = {},
) {
const instacePicker = new InstancePicker(this, onchange, button, instance);
this.options.push(instacePicker);
this.generate(instacePicker);
return instacePicker;
}
returnFromSub() {
this.subOptions = undefined;
this.genTop();
@ -861,6 +1031,14 @@ class Options implements OptionsElement<void> {
this.generate(FI);
return FI;
}
addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) {
const textInput = new DateInput(label, onSubmit, this, {
initText,
});
this.options.push(textInput);
this.generate(textInput);
return textInput;
}
addTextInput(
label: string,
onSubmit: (str: string) => void,
@ -938,6 +1116,7 @@ class Options implements OptionsElement<void> {
headers = {},
method = "POST",
traditionalSubmit = false,
vsmaller = false,
} = {},
) {
const options = new Form(name, this, onSubmit, {
@ -947,6 +1126,7 @@ class Options implements OptionsElement<void> {
headers,
method,
traditionalSubmit,
vsmaller,
});
this.options.push(options);
this.generate(options);
@ -969,6 +1149,7 @@ class Options implements OptionsElement<void> {
generateHTML(): HTMLElement {
const div = document.createElement("div");
div.classList.add("flexttb", "titlediv");
if (this.vsmaller) div.classList.add("vsmaller");
if (this.owner instanceof Options) {
div.classList.add("optionElement");
}
@ -1106,6 +1287,83 @@ class Options implements OptionsElement<void> {
}
}
}
class Captcha implements OptionsElement<string> {
owner: Form;
value: string = "";
constructor(owner: Form) {
this.owner = owner;
}
div?: HTMLElement;
generateHTML(): HTMLElement {
const div = document.createElement("div");
this.div = div;
return div;
}
submit() {}
onchange = (_: string) => {};
watchForChange(func: (arg1: string) => void) {
this.onchange = func;
}
static hcaptcha?: HTMLDivElement;
static async waitForCaptcha(ctype: "hcaptcha") {
switch (ctype) {
case "hcaptcha":
if (!this.hcaptcha) throw Error("no captcha found");
const hcaptcha = this.hcaptcha;
console.log(hcaptcha);
//@ts-expect-error
while (!hcaptcha.children[1].children.length || !hcaptcha.children[1].children[1].value) {
await new Promise<void>((res) => setTimeout(res, 100));
}
//@ts-expect-error
return hcaptcha.children[1].children[1].value;
}
}
async makeCaptcha({
captcha_sitekey,
captcha_service,
}: {
captcha_sitekey: string;
captcha_service: "hcaptcha";
}): Promise<string> {
if (!this.div) throw new Error("Div doesn't exist yet to give catpcha");
switch (captcha_service) {
case "hcaptcha":
if (Captcha.hcaptcha) {
this.div.append(Captcha.hcaptcha);
Captcha.hcaptcha.setAttribute("data-sitekey", captcha_sitekey);
eval("hcaptcha.reset()");
return Captcha.waitForCaptcha(captcha_service);
} else {
const capt = document.createElement("div");
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
Captcha.hcaptcha = capt;
this.div.append(capt);
return Captcha.waitForCaptcha(captcha_service);
}
}
}
static async makeCaptcha(json: {
captcha_sitekey: string;
captcha_service: "hcaptcha";
}): Promise<string> {
const float = new Dialog("", {noSubmit: true});
float.options.addTitle(I18n.form.captcha());
const cap = float.options.addForm("", () => {}, {traditionalSubmit: true}).addCaptcha();
float.show();
const ret = cap.makeCaptcha(json);
await ret;
float.hide();
return ret;
}
}
class FormError extends Error {
elem: OptionsElement<any>;
message: string;
@ -1115,6 +1373,53 @@ class FormError extends Error {
this.elem = elem;
}
}
async function handle2fa(json: any, api: string): Promise<false | any> {
if (json.ticket) {
return new Promise<boolean>((resolution) => {
const better = new Dialog("");
const form = better.options.addForm(
"",
(res: any) => {
if (res.message) {
throw new FormError(ti, res.message);
} else {
resolution(res);
better.hide();
}
},
{
fetchURL: api + "/auth/mfa/totp",
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
);
form.addTitle(I18n.getTranslation("2faCode"));
form.addPreprocessor((e) => {
//@ts-ignore
e.ticket = json.ticket;
});
const ti = form.addTextInput("", "code");
better.show();
});
} else {
return false;
}
}
async function handleCaptcha(json: any, build: any, cap: Captcha | undefined) {
if (json.captcha_sitekey) {
let token: string;
if (cap) {
token = await cap.makeCaptcha(json);
} else {
token = await Captcha.makeCaptcha(json);
}
build.captcha_key = token;
return true;
}
return false;
}
export {FormError};
class Form implements OptionsElement<object> {
name: string;
@ -1141,13 +1446,14 @@ class Form implements OptionsElement<object> {
headers = {},
method = "POST",
traditionalSubmit = false,
vsmaller = false,
} = {},
) {
this.traditionalSubmit = traditionalSubmit;
this.name = name;
this.method = method;
this.submitText = submitText;
this.options = new Options(name, this, {ltr});
this.options = new Options(name, this, {ltr, vsmaller});
this.owner = owner;
this.fetchURL = fetchURL;
this.headers = headers;
@ -1167,6 +1473,15 @@ class Form implements OptionsElement<object> {
addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) {
return this.options.addHTMLArea(html, onSubmit);
}
private captcha?: Captcha;
addCaptcha() {
if (this.captcha) throw new Error("only one captcha is allowed per form");
const cap = new Captcha(this);
this.options.options.push(cap);
this.options.generate(cap);
this.captcha = cap;
return cap;
}
addSubForm(
name: string,
onSubmit: (arg1: object, sent: object) => void,
@ -1247,7 +1562,16 @@ class Form implements OptionsElement<object> {
this.names.set(formName, emoji);
return emoji;
}
addDateInput(label: string, formName: string, {initText = "", required = false} = {}) {
const dateInput = this.options.addDateInput(label, (_) => {}, {
initText,
});
this.names.set(formName, dateInput);
if (required) {
this.required.add(dateInput);
}
return dateInput;
}
addTextInput(
label: string,
formName: string,
@ -1377,7 +1701,7 @@ class Form implements OptionsElement<object> {
return;
}
} else {
(build as any)[thing] = thing;
(build as any)[key] = thing;
}
}
console.log("middle");
@ -1444,41 +1768,59 @@ class Form implements OptionsElement<object> {
return;
}
if (this.fetchURL !== "") {
fetch(this.fetchURL, {
method: this.method,
body: JSON.stringify(build),
headers: this.headers,
})
.then((_) => {
return _.text();
})
.then((_) => {
if (_ === "") return {};
return JSON.parse(_);
})
.then(async (json) => {
if (json.errors) {
if (this.errors(json)) {
return;
}
}
try {
await this.onSubmit(json, build);
} catch (e) {
console.error(e);
if (e instanceof FormError) {
this.onFormError(e);
const elm = this.options.html.get(e.elem);
if (elm) {
const html = elm.deref();
if (html) {
this.makeError(html, e.message);
}
const onSubmit = async (json: any) => {
try {
await this.onSubmit(json, build);
} catch (e) {
console.error(e);
if (e instanceof FormError) {
this.onFormError(e);
const elm = this.options.html.get(e.elem);
if (elm) {
const html = elm.deref();
if (html) {
this.makeError(html, e.message);
}
}
return;
}
});
return;
}
};
const doFetch = async () => {
fetch(this.fetchURL, {
method: this.method,
body: JSON.stringify(build),
headers: this.headers,
})
.then((_) => {
return _.text();
})
.then((_) => {
if (_ === "") return {};
return JSON.parse(_);
})
.then(async (json) => {
if (await handleCaptcha(json, build, this.captcha)) {
return await doFetch();
}
const match = this.fetchURL.match(/https?:\/\/[^\/]*\/api\/v9/gm);
if (match) {
const tried = await handle2fa(json, match[0]);
if (tried) {
return await onSubmit(tried);
}
}
if (json.ticket) {
}
if (json.errors) {
if (this.errors(json)) {
return;
}
}
onSubmit(json);
});
};
doFetch();
} else {
try {
await this.onSubmit(build, build);

View file

@ -782,6 +782,9 @@ span.instanceStatus {
#verify {
color: var(--primary-text-soft);
}
.verify {
color: var(--primary-text-soft);
}
#TOS {
vertical-align: middle;
margin-bottom: 4px;
@ -790,6 +793,15 @@ span.instanceStatus {
margin: 16px 0;
text-align: center;
}
.createAccount {
width: 94%;
padding: 8px;
margin-bottom: 16px;
font-size: 1.15rem;
font-weight: bold;
text-align: center;
box-sizing: border-box;
}
#logindiv button {
width: 100%;
padding: 8px;
@ -2216,9 +2228,9 @@ img.bigembedimg {
}
.nonimagecenter,
.accountSwitcher {
max-height: 80svh;
max-height: 95svh;
width: 500px;
padding: 12px;
padding: 6px;
margin: 0;
background: var(--secondary-bg);
border: none;
@ -2227,7 +2239,6 @@ img.bigembedimg {
0 0 24px var(--shadow),
0 0 1.5px var(--primary-text);
box-sizing: border-box;
gap: 8px;
overflow-y: auto;
}
.nonimagecenter & .flexttb,
@ -2484,6 +2495,11 @@ fieldset input[type="radio"] {
max-width: 4in;
height: 2in;
}
.vsmaller {
.optionElement {
margin-top: 8px;
}
}
.optionElement,
.FormSettings > button {
margin: 16px 16px 0 16px;
@ -2501,6 +2517,7 @@ fieldset input[type="radio"] {
}
.optionElement:has(.optionElement) {
margin: 0;
position: relative;
}
.optionElement:has(.Buttons) {
height: 100%;
@ -3056,3 +3073,7 @@ fieldset input[type="radio"] {
.stickerMArea {
padding-left: 48px;
}
.solidBackground {
background: var(--secondary-bg);
opacity: 1;
}

View file

@ -1,5 +1,24 @@
import {I18n} from "../i18n.js";
import {Dialog} from "../settings.js";
let instances:
| {
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: {alltime: number; daytime: number; weektime: number};
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
| null = null;
setTheme();
export function setTheme() {
let name = localStorage.getItem("theme");
@ -172,6 +191,51 @@ export class Specialuser {
localStorage.setItem("userinfos", JSON.stringify(info));
}
}
//this currently does not work, and need to be implemented better at some time.
if (!localStorage.getItem("SWMode")) {
localStorage.setItem("SWMode", "SWOn");
}
export function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
}
wellknown = (user.id || user.email) + "@" + wellknown;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
} else {
delete json.users[thing];
}
} else {
map.set(wellknown, [thing, user]);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
export function adduser(user: typeof Specialuser.prototype.json) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
sessionStorage.setItem("currentuser", user.uid);
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
class Directory {
static home = this.createHome();
handle: FileSystemDirectoryHandle;
@ -257,28 +321,10 @@ export {Directory};
const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const iOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
export {mobile, iOS};
let instances:
| {
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: {alltime: number; daytime: number; weektime: number};
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
| null;
const datalist = document.getElementById("instances");
console.warn(datalist);
const instancefetch = fetch("/instances.json")
export const instancefetch = fetch("/instances.json")
.then((res) => res.json())
.then(
async (
@ -302,48 +348,6 @@ const instancefetch = fetch("/instances.json")
) => {
await I18n.done;
instances = json;
if (datalist) {
console.warn(json);
const instancein = document.getElementById("instancein") as HTMLInputElement;
if (
instancein &&
instancein.value === "" &&
!new URLSearchParams(window.location.search).get("instance")
) {
instancein.value = json[0].name;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
} else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
} else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
} else {
option.label = instance.name;
}
datalist.append(option);
}
if (
json.length !== 0 &&
!localStorage.getItem("instanceinfo") &&
!new URLSearchParams(window.location.search).get("instance")
) {
checkInstance(json[0].name);
}
}
},
);
const stringURLMap = new Map<string, string>();
@ -646,6 +650,14 @@ export function createImg(
},
});
}
export interface instanceinfo {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
value: string;
}
/**
*
* This function takes in a string and checks if the string is a valid instance
@ -653,24 +665,19 @@ export function createImg(
* the alt property is something you may fire on success.
*/
const checkInstance = Object.assign(
async function (instance: string) {
await instancefetch;
const verify = document.getElementById("verify");
const loginButton = (document.getElementById("loginButton") ||
async function (
instance: string,
verify = document.getElementById("verify"),
loginButton = (document.getElementById("loginButton") ||
document.getElementById("createAccount") ||
document.createElement("button")) as HTMLButtonElement;
document.createElement("button")) as HTMLButtonElement,
) {
await instancefetch;
try {
loginButton.disabled = true;
verify!.textContent = I18n.getTranslation("login.checking");
const instanceValue = instance;
const instanceinfo = (await getapiurls(instanceValue)) as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
value: string;
};
const instanceinfo = (await getapiurls(instanceValue)) as instanceinfo;
if (instanceinfo) {
instanceinfo.value = instanceValue;
localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo));
@ -683,14 +690,17 @@ const checkInstance = Object.assign(
console.log(verify!.textContent);
verify!.textContent = "";
}, 3000);
return instanceinfo;
} else {
verify!.textContent = I18n.getTranslation("login.invalid");
loginButton.disabled = true;
return;
}
} catch {
console.log("catch");
verify!.textContent = I18n.getTranslation("login.invalid");
loginButton.disabled = true;
return;
}
},
{} as {
@ -705,9 +715,7 @@ const checkInstance = Object.assign(
},
);
export {checkInstance};
export function getInstances() {
return instances;
}
export class SW {
static worker: undefined | ServiceWorker;
static setMode(mode: "false" | "offlineOnly" | "true") {
@ -727,7 +735,14 @@ export class SW {
}
}
}
let installPrompt: Event | undefined = undefined;
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
installPrompt = event;
});
export function installPGet() {
return installPrompt;
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service.js", {
@ -755,3 +770,9 @@ if ("serviceWorker" in navigator) {
}
});
}
export function getInstances() {
return instances;
}
export function getStringURLMapPair() {
return [stringURLMap, stringURLsMap] as const;
}