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;
}

View file

@ -232,15 +232,20 @@
"box3title": "Contribute to Jank Client",
"box3description": "We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos."
},
"form": {
"captcha": "Wait, are you a human?"
},
"useTemplate": "Use $1 as a template",
"useTemplateButton": "Use Template",
"register": {
"register": "Register",
"passwordError:": "Password: $1",
"usernameError": "Username: $1",
"emailError": "Email: $1",
"DOBError": "Date of Birth: $1",
"agreeTOS": "I agree to the [Terms of Service]($1):",
"noTOS": "This instance has no Terms of Service, accept ToS anyways:"
"noTOS": "This instance has no Terms of Service, accept ToS anyways:",
"tos": "You must agree to the TOS"
},
"leaving": "You're leaving Spacebar",
"goingToURL": "You're going to $1. Are you sure you want to go there?",
@ -330,6 +335,9 @@
"save": "Save changes"
},
"localuser": {
"install": "Install",
"installJank": "Install Jank Client",
"installDesc": "Installing Jank Client will allow you to open it in its own window and act like its own app! You can also just continue to use Jank Client in the web browser like you have been and it'll work the same.",
"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",
@ -538,7 +546,8 @@
"pasteInfo": "Paste the recovery URL here:",
"newPassword": "New password:",
"enterPAgain": "Enter new password again:",
"recovery": "Forgotten Password"
"recovery": "Forgotten Password",
"login": "Login"
},
"member": {
"kick": "Kick $1 from $2",