347 lines
13 KiB
JavaScript
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(); |