TeaWeb/shared/js/profiles/identities/teaspeak-forum.ts

366 lines
13 KiB
TypeScript

interface Window {
grecaptcha: GReCaptcha;
}
interface GReCaptcha {
render(container: string | HTMLElement, parameters: {
sitekey: string;
theme?: "dark" | "light";
size?: "compact" | "normal";
tabindex?: number;
callback?: (token: string) => any;
"expired-callback"?: () => any;
"error-callback"?: (error: any) => any;
}) : string; /* widget_id */
reset(widget_id?: string);
}
namespace forum {
export namespace gcaptcha {
export async function initialize() {
if(typeof(window.grecaptcha) === "undefined") {
let script = document.createElement("script");
script.async = true;
let timeout;
const callback_name = "captcha_callback_" + Math.random().toString().replace(".", "");
try {
await new Promise((resolve, reject) => {
script.onerror = reject;
window[callback_name] = resolve;
script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit";
document.body.append(script);
timeout = setTimeout(() => reject("timeout"), 15000);
});
} catch(error) {
script.remove();
script = undefined;
console.error(tr("Failed to fetch recaptcha javascript source: %o"), error);
throw tr("failed to download source");
} finally {
if(script)
script.onerror = undefined;
delete window[callback_name];
clearTimeout(timeout);
}
}
if(typeof(window.grecaptcha) === "undefined")
throw tr("failed to load recaptcha");
}
export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) {
try {
await initialize();
} catch(error) {
console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error);
throw tr("initialisation failed");
}
if(container.attr("captcha-uuid"))
window.grecaptcha.reset(container.attr("captcha-uuid"));
else {
container.attr("captcha-uuid", window.grecaptcha.render(container[0], {
"sitekey": key,
callback: callback_data
}));
}
}
}
function api_url() {
return settings.static_global(Settings.KEY_TEAFORO_URL);
}
export class Data {
readonly auth_key: string;
readonly raw: string;
readonly sign: string;
parsed: {
user_id: number;
user_name: string;
data_age: number;
user_group_id: number;
is_staff: boolean;
user_groups: number[];
};
constructor(auth: string, raw: string, sign: string) {
this.auth_key = auth;
this.raw = raw;
this.sign = sign;
this.parsed = JSON.parse(raw);
}
data_json() : string { return this.raw; }
data_sign() : string { return this.sign; }
name() : string { return this.parsed.user_name; }
user_id() { return this.parsed.user_id; }
user_group() { return this.parsed.user_group_id; }
is_stuff() : boolean { return this.parsed.is_staff; }
is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; }
data_age() : Date { return new Date(this.parsed.data_age); }
is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); }
should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */
}
let _data: Data | undefined;
export function logged_in() : boolean {
return !!_data && !_data.is_expired();
}
export function data() : Data { return _data; }
export interface LoginResult {
status: "success" | "captcha" | "error";
error_message?: string;
captcha?: {
type: "gre-captcha" | "unknown";
data: any; /* in case of gre-captcha it would be the side key */
};
}
export async function login(username: string, password: string, captcha?: any) : Promise<LoginResult> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/login",
type: "POST",
cache: false,
data: {
username: username,
password: password,
remember: true,
"g-recaptcha-response": captcha
},
crossDomain: true,
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Login request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
return {
status: "error",
error_message: tr("failed to send login request")
};
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
return {
status: "error",
error_message: (response["errors"] || [])[0] || tr("Unknown error")
};
}
if(!response["success"]) {
console.error(tr("Login failed. Response %o"), response);
let message = tr("failed to login");
let captcha;
/* user/password wrong | and maybe captcha required */
if(response["code"] == 1 || response["code"] == 3)
message = tr("Invalid username or password");
if(response["code"] == 2 || response["code"] == 3) {
captcha = {
type: response["captcha"]["type"],
data: response["captcha"]["siteKey"] //TODO: Why so static here?
};
if(response["code"] == 2)
message = tr("captcha required");
}
return {
status: typeof(captcha) !== "undefined" ? "captcha" : "error",
error_message: message,
captcha: captcha
};
}
//document.cookie = "user_data=" + response["data"] + ";path=/";
//document.cookie = "user_sign=" + response["sign"] + ";path=/";
try {
_data = new Data(response["auth-key"], response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
localStorage.setItem("teaspeak-forum-auth", response["auth-key"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
return {
status: "error",
error_message: tr("Failed to parse response data")
}
}
return {
status: "success"
};
}
export async function renew_data() : Promise<"success" | "login-required"> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/renew-data",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Renew request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send renew request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
if(response["code"] == 1) {
return "login-required";
}
throw "invalid error code (" + response["code"] + ")";
}
if(!response["data"] || !response["sign"])
throw tr("response missing data");
console.debug(tr("Renew succeeded. Parsing data."));
try {
_data = new Data(_data.auth_key, response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
throw tr("failed to parse data");
}
return "success";
}
export async function logout() : Promise<void> {
if(!logged_in())
return;
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/logout",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Logout request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send logout request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
/* code 1 means not logged in, its an success */
if(response["code"] != 1) {
throw "invalid error code (" + response["code"] + ")";
}
}
_data = undefined;
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
profiles.identities.update_forum();
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "TeaForo initialize",
priority: 10,
function: async () => {
const raw_data = localStorage.getItem("teaspeak-forum-data");
const raw_sign = localStorage.getItem("teaspeak-forum-sign");
const forum_auth = localStorage.getItem("teaspeak-forum-auth");
if(!raw_data || !raw_sign || !forum_auth) {
console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected"));
return;
}
try {
_data = new Data(forum_auth, raw_data, raw_sign);
} catch(error) {
console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error);
return;
}
if(_data.should_renew()) {
console.info(tr("TeaForo data should be renewed. Executing renew."));
renew_data().then(status => {
if(status === "success") {
console.info(tr("TeaForo data has been successfully renewed."));
} else {
console.warn(tr("Failed to renew TeaForo data. New login required."));
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
}
}).catch(error => {
console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error);
});
return;
}
if(_data && _data.is_expired()) {
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
}
}
})
}