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