/* 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();