Files
objects/wco/apxauth/apxauthgeminicli.js

347 lines
13 KiB
JavaScript

/* eslint-env browser */
/* eslint-disable no-alert, no-console */
/**
* @file apxauth.js (previously authnew.js)
* @description Modern, class-based implementation for handling authentication (apxauth) components.
* @version 2.1
* @author support@ndda.fr
*/
// Establish the global namespace
window.apx = window.apx || {};
/**
* @class ApxAuth
* Manages authentication flows, including sign-in, sign-up, logout, and key management.
*/
class ApxAuth {
constructor() {
if (typeof apx.main === 'undefined') {
throw new Error("ApxAuth requires a global 'apx.main' (ApxManager) instance.");
}
}
async loadwco(id, ctx) {
console.log(`[apxauth] loadwco triggered for id: ${id} with context:`, ctx);
const componentRoot = document.getElementById(id);
if (!componentRoot) return;
const data = this._getData(id, ctx);
if (componentRoot.innerHTML.trim() === "") {
componentRoot.innerHTML = Mustache.render(apx.main.data.tpl.apxauthmain, data);
}
const screenContainer = componentRoot.querySelector('.screenaction');
if (screenContainer) {
const screenTemplate = apx.main.data.tpl[`apxauthscreen${ctx.link}`];
if (screenTemplate) {
screenContainer.innerHTML = Mustache.render(screenTemplate, data);
}
}
const msgInfo = componentRoot.querySelector('.msginfo');
if (msgInfo) msgInfo.innerHTML = "";
}
_getData(id, ctx) {
const tpldataname = `${apx.main.data.pagename}_${id}_apxauth`;
const data = JSON.parse(JSON.stringify(apx.main.data.tpldata[tpldataname] || {}));
data.id = id;
data.xalias = apx.main.data.headers.xalias;
data.xtribe = apx.main.data.headers.xtribe;
data.emailsupport = apx.main.data.appdata?.emailsupport;
if (ctx.link === 'logout') {
data.profils = apx.main.data.headers.xprofils
.filter(p => !['anonymous', 'pagans', 'persons'].includes(p))
.map(p => apx.main.data.options.profil.itms[p]?.title);
data.noprofils = data.profils.length === 0;
data.member = apx.main.data.headers.xprofils.includes('persons');
data.websites = apx.main.data.appdata?.websites;
}
return data;
}
redirectWithAuth(url, tribe, webapp, newWindow = false, windowName = '_blank') {
const { xlang, xalias, xdays, xhash, xprofils, xtrkversion, xuuid } = apx.main.data.headers;
let authUrl = url.replace(/_[a-z]{2}\.html/, `_${xlang}.html`);
const params = new URLSearchParams({
xtribe: tribe,
xapp: webapp,
xalias,
xdays,
xhash,
xprofils: xprofils.join(','),
xtrkversion,
xuuid
});
authUrl += `?${params.toString()}`;
if (newWindow) {
try {
const newWin = window.open(authUrl, windowName);
if (!newWin) {
alert("Popup blocked. Please allow popups for this site.");
return null;
}
newWin.focus();
return newWin;
} catch (error) {
console.error("Error opening new window:", error);
return null;
}
} else {
window.location.href = authUrl;
return null;
}
}
async logout() {
try {
await axios.get('/api/apxtri/pagans/logout', { headers: apx.main.data.headers });
} catch (err) {
console.error("Logout API call failed:", err);
}
apx.main.data = window.apxtri;
apx.main.saveState();
if (apx.main.pageContext.hash.url) {
window.location.href = apx.main.pageContext.hash.url;
} else {
location.reload();
}
}
async _setAuthHeaders(alias, passphrase, publickey, privatekey, rememberme) {
if (alias.length < 3 || publickey.length < 200) {
return { status: 406, ref: "Pagans", msg: "aliasorprivkeytooshort" };
}
if (rememberme) {
apx.main.data.auth = { alias, publickey, privatekey, passphrase: passphrase || "" };
} else {
delete apx.main.data.auth;
}
apx.main.data.headers.xalias = alias;
apx.main.data.headers.xdays = dayjs().valueOf();
const message = `${alias}_${apx.main.data.headers.xdays}`;
try {
apx.main.data.headers.xhash = await this._clearMsgSignature(publickey, privatekey, passphrase, message);
apx.main.saveState();
return { status: 200 };
} catch (err) {
return { status: 500, ref: "Middlewares", msg: "unconsistentpgp", data: { err: err.message } };
}
}
async authenticate(id, alias, passphrase, privatekey, rememberme) {
const msgContainer = `#${id} .msginfo`;
apx.main.notify(msgContainer, {}, true);
if (alias.length < 3 || privatekey.length < 200) {
apx.main.notify(msgContainer, { status: 406, ref: "Pagans", msg: "aliasorprivkeytooshort" });
return;
}
try {
const { data: { data: paganData } } = await axios.get(`/api/apxtri/pagans/alias/${alias}`, { headers: apx.main.data.headers });
const headersResult = await this._setAuthHeaders(alias, passphrase, paganData.publickey, privatekey, rememberme);
if (headersResult.status !== 200) {
apx.main.notify(msgContainer, headersResult);
return;
}
const { data: { data: authData } } = await axios.get('/api/apxtri/pagans/isauth', { headers: apx.main.data.headers, withCredentials: true });
apx.main.data.headers.xprofils = authData.xprofils;
apx.main.saveState();
if (apx.main.pageContext.hash.url) {
window.location.href = apx.main.pageContext.hash.url;
} else {
const parentWco = document.getElementById(id).closest('[wco-name]');
if (parentWco) parentWco.setAttribute('wco-link', 'mytribes');
}
} catch (err) {
console.error("Authentication failed:", err);
delete apx.main.data.auth;
apx.main.saveState();
const parentWco = document.getElementById(id).closest('[wco-name]');
if (parentWco) parentWco.setAttribute('wco-link', 'signin');
apx.main.notify(msgContainer, err.response?.data || { status: 500, ref: "Middlewares", msg: "errrequest" });
}
}
async recoverKey(id, aliasOrEmail) {
const msgContainer = `#${id} .msginfo`;
apx.main.notify(msgContainer, {}, true);
if (aliasOrEmail.length < 3) {
apx.main.notify(msgContainer, { status: 406, ref: "Pagans", msg: "recoveryemailnotfound", data: { search: aliasOrEmail } });
return;
}
const recoveryData = {
tribe: apx.main.data.headers.xtribe,
search: aliasOrEmail,
emailalias: Checkjson.testformat(aliasOrEmail, "email") ? "email" : "alias",
};
try {
const { data: response } = await axios.post('/api/apxtri/pagans/keyrecovery', recoveryData, { headers: apx.main.data.headers });
response.data.search = aliasOrEmail;
apx.main.notify(msgContainer, response, true);
} catch (err) {
const errorData = err.response?.data || { status: 500, ref: "Pagans", msg: "checkconsole" };
errorData.data = { ...errorData.data, search: aliasOrEmail };
apx.main.notify(msgContainer, errorData, true);
}
}
async _generateKey(alias, passphrase) {
const { privateKey, publicKey } = await openpgp.generateKey({
type: "ecc",
curve: "curve25519",
userIDs: [{ alias }],
passphrase,
format: "armored",
});
return { alias, privatekey: privateKey, publickey: publicKey };
}
async createIdentity(id, alias, recoveryEmail, passphrase = "") {
const msgContainer = `#${id} .msginfo`;
apx.main.notify(msgContainer, {}, true);
const aliasRegex = /^[a-z0-9]{4,}$/;
if (!aliasRegex.test(alias)) {
apx.main.notify(msgContainer, { status: 406, ref: "Pagans", msg: "invalidalias" }, true);
return;
}
if (recoveryEmail && !Checkjson.testformat(recoveryEmail, "email")) {
apx.main.notify(msgContainer, { status: 406, ref: "Pagans", msg: "invalidemail" }, true);
return;
}
try {
await axios.get(`/api/apxtri/pagans/alias/${alias}`, { headers: apx.main.data.headers });
apx.main.notify(msgContainer, { ref: "Pagans", msg: "aliasexist", data: { alias } }, true);
} catch (err) {
if (err.response?.status === 404) {
const keys = await this._generateKey(alias, passphrase);
apx.main.data.tmpauth = { keys, recoveryEmail, passphrase };
this._showKeyDownloadUI(id, keys);
} else {
apx.main.notify(msgContainer, { ref: "Middlewares", msg: "errrequest" }, true);
}
}
}
_showKeyDownloadUI(id, keys) {
const componentRoot = document.getElementById(id);
['publickey', 'privatekey'].forEach(keyType => {
const btn = componentRoot.querySelector(`button.signup${keyType}`);
if(btn) {
btn.onclick = () => {
const blob = new Blob([keys[keyType]], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${keys.alias}_${keyType}.txt`;
a.click();
URL.revokeObjectURL(url);
};
}
});
componentRoot.querySelectorAll('.signupalias, .signupemailrecovery, .signuppassphrase').forEach(el => el.disabled = true);
componentRoot.querySelector('.getmykeys')?.classList.remove('hidden');
componentRoot.querySelector('.btncreatekey')?.classList.add('hidden');
}
async registerIdentity(id, isTrustedTribe) {
const msgContainer = `#${id} .msginfo`;
const { keys, recoveryEmail, passphrase } = apx.main.data.tmpauth;
const headersResult = await this._setAuthHeaders(keys.alias, passphrase, keys.publickey, keys.privatekey, false);
if (headersResult.status !== 200) {
apx.main.notify(msgContainer, headersResult);
return;
}
const registrationData = {
alias: keys.alias,
publickey: keys.publickey,
trustedtribe: isTrustedTribe,
};
if (recoveryEmail && Checkjson.testformat(recoveryEmail, "email")) {
registrationData.email = recoveryEmail;
registrationData.passphrase = passphrase;
registrationData.privatekey = keys.privatekey;
}
try {
const { data: response } = await axios.post('/api/apxtri/pagans', registrationData, { headers: apx.main.data.headers });
apx.main.notify(msgContainer, response);
document.querySelector(`#${id} .btncreateidentity`)?.classList.add('hidden');
document.querySelector(`#${id} .signupbtnreload`)?.classList.remove('hidden');
} catch (err) {
apx.main.notify(msgContainer, err.response?.data || { status: 500, ref: "Pagans", msg: "errcreate" });
}
}
async joinTribe(id) {
const msgContainer = `#${id} .msginfo`;
const personData = {
alias: apx.main.data.headers.xalias,
profils: [...new Set([...apx.main.data.headers.xprofils, 'persons'])],
};
try {
const { data: response } = await axios.put(`/api/apxtri/pagans/person/${apx.main.data.headers.xtribe}`, personData, { headers: apx.main.data.headers });
apx.main.notify(msgContainer, response);
await this.logout();
} catch (err) {
apx.main.notify(msgContainer, err.response?.data || { status: 500, ref: "Pagans", msg: "errcreate" });
}
}
async _clearMsgSignature(pubK, privK, passphrase, message) {
const publickey = await openpgp.readKey({ armoredKey: pubK });
const privatekey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privK }),
passphrase,
});
const cleartextMessage = await openpgp.sign({
message: await openpgp.createCleartextMessage({ text: message }),
signingKeys: privatekey,
});
const { signatures: [{ verified }] } = await openpgp.verify({
message: await openpgp.readCleartextMessage({ cleartextMessage }),
verificationKeys: publickey,
});
if (!(await verified)) {
throw new Error("Signature verification failed.");
}
return btoa(cleartextMessage);
}
}
// Attach an instance to the global namespace
apx.apxauth = new ApxAuth();