diff --git a/wco/apx/apx.js b/wco/apx/apx.js index 2c52fe3..10acf94 100644 --- a/wco/apx/apx.js +++ b/wco/apx/apx.js @@ -484,7 +484,7 @@ apx.update = async () => { //try again in 30 seconds setTimeout(apx.update, 30000); } - if (initset.data.msg == "datamodelupdate") { + if (initset.data.msg == "data_model_update") { // mise à jour local /*if (initset.data.data.wco) { diff --git a/wco/apx/apxgeminicli.js b/wco/apx/apxgeminicli.js new file mode 100644 index 0000000..4bf1a34 --- /dev/null +++ b/wco/apx/apxgeminicli.js @@ -0,0 +1,334 @@ +/* eslint-env browser */ +/* eslint-disable no-alert, no-console */ + +/** + * @file apx.js (previously apxnew.js) + * @description Modern, class-based implementation to manage data and interactions with an apxtri instance from a webpage. + * @version 2.1 + * @author support@ndda.fr + */ + +// Establish the global namespace +window.apx = window.apx || {}; + +/** + * @class ApxManager + * Manages the core application state and lifecycle. + */ +class ApxManager { + constructor() { + this.data = {}; + this.pageContext = { search: {}, hash: {} }; + this.afterUpdateCallbacks = []; + this.wcoProxies = {}; + // Capture the observer flag immediately from the initial global config. + this.useWcoObserver = window.apxtri?.wcoobserver || false; + } + + ready(callback) { + if (typeof callback !== 'function') { + alert("Apx.ready(callback) requires a valid function."); + return; + } + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } + } + + registerUpdateCallback(callback) { + this.afterUpdateCallbacks.push(callback); + } + + lazyLoad() { + const { xalias, xuuid, xtrkversion, xlang } = this.data.headers; + const consentCookie = localStorage.getItem('consentcookie'); + + document.querySelectorAll('img[data-lazysrc]').forEach(img => { + let src = img.dataset.lazysrc; + if (img.dataset.trksrckey) { + src = `/trk/${src}?alias=${xalias}&uuid=${xuuid}&srckey=${img.dataset.trksrckey}&version=${xtrkversion}&consentcookie=${consentCookie}&lg=${xlang}`; + } + img.setAttribute('src', src); + img.removeAttribute('data-lazysrc'); + }); + + document.querySelectorAll('[data-lazybgsrc]').forEach(el => { + el.style.backgroundImage = `url(${el.dataset.lazybgsrc})`; + el.removeAttribute('data-lazybgsrc'); + }); + + document.querySelectorAll('a[data-trksrckey]').forEach(a => { + let urlDest = a.getAttribute('href'); + if (!urlDest.startsWith('http') && !urlDest.startsWith('/')) { + urlDest = `/${urlDest}`; + } + const trackedHref = `/trk/redirect?alias=${xalias}&uuid=${xuuid}&srckey=${a.dataset.trksrckey}&version=${xtrkversion}&consentcookie=${consentCookie}&lg=${xlang}&url=${encodeURIComponent(urlDest)}`; + a.setAttribute('href', trackedHref); + a.removeAttribute('data-trksrckey'); + }); + } + + notify(selector, responseData, clearBefore = false) { + const el = document.querySelector(selector); + if (!el) { + console.warn(`Notification selector not found: ${selector}`); + return; + } + if (clearBefore) { + el.innerHTML = ''; + } + + const messages = responseData.multimsg || [responseData]; + messages.forEach(info => { + const template = this.data.ref?.[info.ref]?.[info.msg]; + if (!template) { + console.warn(`Notification template not found for ref: ${info.ref}, msg: ${info.msg}`); + return; + } + el.innerHTML += Mustache.render(template, info.data); + }); + + if (responseData.status === 200) { + el.classList.remove('text-red'); + el.classList.add('text-green'); + } else { + el.classList.add('text-red'); + el.classList.remove('text-green'); + } + } + + listenWcoData() { + if (!this.data.wco || Object.keys(this.data.wco).length === 0) return; + + const updateElement = (element, value) => { + if (value.innerHTML !== undefined) element.innerHTML = value.innerHTML; + if (value.textContent !== undefined) element.textContent = value.textContent; + ['src', 'alt', 'placeholder', 'class', 'href'].forEach(attr => { + if (value[attr] !== undefined) element.setAttribute(attr, value[attr]); + }); + }; + + for (const prop in this.data.wco) { + if (Object.hasOwnProperty.call(this.data.wco, prop) && !this.wcoProxies[prop]) { + const elements = document.querySelectorAll(`[data-wco='${prop}']`); + elements.forEach(el => updateElement(el, this.data.wco[prop])); + + this.wcoProxies[prop] = new Proxy(this.data.wco[prop], { + set: (target, key, value) => { + target[key] = value; + elements.forEach(el => updateElement(el, target)); + return true; + }, + }); + } + } + this.wco = new Proxy(this.data.wco, { + set: (target, prop, value) => { + target[prop] = value; + this.listenWcoData(); + return true; + } + }); + } + + startWcoObserver() { + console.log('[APX] Starting WCO observer and triggering initial load...'); + + const processElement = (element) => { + if (element.nodeType !== 1 || !element.hasAttribute('wco-name') || !element.id) { + return; + } + + const wcoName = element.getAttribute('wco-name'); + const wcoModule = window.apx?.[wcoName]; + + if (wcoModule && typeof wcoModule.loadwco === 'function') { + console.log(`[APX] Observer is processing component: ${wcoName} on #${element.id}`); + const ctx = {}; + for (const attr of element.attributes) { + if (attr.name.startsWith('wco-')) { + ctx[attr.name.slice(4)] = attr.value; + } + } + wcoModule.loadwco(element.id, ctx); + } else { + console.warn(`[APX] WCO handler not found for component: apx.${wcoName}`); + } + }; + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach(processElement); + } + if (mutation.type === 'attributes' && mutation.attributeName.startsWith('wco-')) { + processElement(mutation.target); + } + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + }); + + // This is the key part from your original logic: + // Manually trigger the observer for components already in the DOM. + document.querySelectorAll('div[wco-name]').forEach((element) => { + const wcoName = element.getAttribute('wco-name'); + console.log(`[APX] Manually triggering initial load for: ${wcoName}`); + element.setAttribute('wco-name', wcoName); + }); + } + + saveState() { + localStorage.setItem(this.data.headers.xapp, JSON.stringify(this.data)); + } + + parseUrl() { + const parse = (str) => str.slice(1).split('&').reduce((acc, kv) => { + if (kv) { + const [key, value] = kv.split('='); + acc[key] = decodeURIComponent(value); + } + return acc; + }, {}); + + this.pageContext.search = parse(window.location.search); + this.pageContext.hash = parse(window.location.hash); + } + + loadState() { + if (typeof apxtri === 'undefined') { + throw new Error('Global `apxtri` configuration object is missing.'); + } + + const storedState = localStorage.getItem(apxtri.headers.xapp); + if (storedState) { + this.data = JSON.parse(storedState); + // Invalidate cache if key configuration has changed + if ( + this.data.headers.xtribe !== apxtri.headers.xtribe || + this.data.headers.xlang !== apxtri.headers.xlang || + this.data.headers.xtrkversion !== apxtri.headers.xtrkversion + ) { + localStorage.removeItem(apxtri.headers.xapp); + // Create a deep copy to prevent modifying the global apxtri object + this.data = JSON.parse(JSON.stringify(apxtri)); + } + } else { + // Create a deep copy to prevent modifying the global apxtri object + this.data = JSON.parse(JSON.stringify(apxtri)); + } + // Always update with current page info from the original config + this.data.pagename = apxtri.pagename; + if (apxtri.pageauth) this.data.pageauth = apxtri.pageauth; + } + + handleAuthRedirect() { + const { xhash, xdays, xprofils, xtribe, url } = this.pageContext.hash; + if (xhash && xdays && xprofils && xtribe && dayjs(xdays).isValid() && dayjs(xdays).diff(dayjs(), 'hours') < 25) { + const headerKeys = ['xalias', 'xhash', 'xdays', 'xprofils', 'xtribe', 'xlang']; + const newHeaders = {}; + let isValid = true; + + headerKeys.forEach(key => { + if (this.pageContext.hash[key]) { + newHeaders[key] = (key === 'xprofils') ? this.pageContext.hash[key].split(',') : this.pageContext.hash[key]; + } else { + isValid = false; + } + }); + + if (isValid) { + this.data.headers = { ...this.data.headers, ...newHeaders }; + this.saveState(); + if (url) { + window.location.href = url; + return true; + } + } + } + return false; + } + + checkAccess() { + const { allowedprofils, pagename, pageauth, headers } = this.data; + if (allowedprofils && !allowedprofils.includes('anonymous') && pagename !== pageauth) { + const hasAccess = allowedprofils.some(p => headers.xprofils?.includes(p)); + + if (!hasAccess) { + alert(this.data.ref?.Middlewares?.notallowtoaccess || 'Access denied.'); + return false; + } + if (dayjs().valueOf() - headers.xdays > 86400000) { + const redirectUrl = `/${pageauth}_${headers.xlang}.html#url=${pagename}_${headers.xlang}.html`; + document.location.href = redirectUrl; + return false; + } + } + return true; + } + + async fetchAndUpdateDataModel() { + this.data.version = 0; + + const { xalias, xtribe, xapp } = this.data.headers; + const ano = (xalias === 'anonymous') ? 'anonymous' : ''; + const url = `/api/apxtri/wwws/updatelocaldb${ano}/${xtribe}/${xapp}/${this.data.pagename}/${this.data.version}`; + + try { + const response = await axios.get(url, { headers: this.data.headers, timeout: 2000 }); + + if (response.data.msg === 'datamodelupdate') { + Object.keys(response.data.data).forEach(key => { + if (key !== 'headers') { + this.data[key] = response.data.data[key]; + } + }); + this.saveState(); + console.log('Local data model updated.'); + } else if (response.data.msg === 'forbidenaccess') { + alert(this.data.ref?.Middlewares?.notallowtoaccess || 'Access denied by API.'); + return false; + } + } catch (error) { + console.error('API is unavailable. Retrying in 30 seconds.', error); + setTimeout(() => this.init(), 30000); + return false; + } + return true; + } + + async init() { + this.loadState(); + this.parseUrl(); + + if (this.handleAuthRedirect()) return; + if (!this.checkAccess()) return; + + if (await this.fetchAndUpdateDataModel()) { + this.listenWcoData(); + + // This is the correct place to check and start the observer. + if (window.apxtri?.wcoobserver) { + this.startWcoObserver(); + } else { + console.log('[APX] wcoobserver is not enabled.'); + } + + this.afterUpdateCallbacks.forEach(cb => cb()); + this.lazyLoad(); + this.saveState(); + } + } +} + +// Instantiate the manager and attach it to the global namespace +apx.main = new ApxManager(); + +// Start the application lifecycle +apx.main.ready(() => apx.main.init()); \ No newline at end of file diff --git a/wco/apxauth/apxauth.js b/wco/apxauth/apxauth.js index 4741c8c..7870a39 100644 --- a/wco/apxauth/apxauth.js +++ b/wco/apxauth/apxauth.js @@ -6,15 +6,16 @@ apx.apxauth.loadwco = async (id, ctx) => { // if (dayjs(apx.data.headers.xdays).diff(dayjs(), "hours") >= 24) apx.apxauth.checkisauth(); //load main.mustache of the component //when wco-xxx change it run this function - console.log(`Load wconame:apxauth apx.apxauth.loadwco with id:${id} and ctx: ${JSON.stringify(ctx)}`); + console.log( + `Load wconame:apxauth apx.apxauth.loadwco with id:${id} and ctx: ${JSON.stringify( + ctx + )}` + ); const tpldataname = `${apx.data.pagename}_${id}_apxauth`; - const apxauthid = document.getElementById(id) + const apxauthid = document.getElementById(id); const data = apx.apxauth.getdata(id, ctx); if (apxauthid.innerHTML.trim() === "") { - apxauthid.innerHTML = Mustache.render( - apx.data.tpl.apxauthmain, - data - ); + apxauthid.innerHTML = Mustache.render(apx.data.tpl.apxauthmain, data); } apxauthid.querySelector(`.screenaction`).innerHTML = Mustache.render( apx.data.tpl[`apxauthscreen${ctx.link}`], @@ -29,7 +30,10 @@ apx.apxauth.getdata = (id, ctx) => { data.id = id; data.xalias = apx.data.headers.xalias; data.xtribe = apx.data.headers.xtribe; - data.emailssuport = apx.data.appdata.emailsupport; + + data.emailssuport = apx.data.appdata.emailsupport + ? apx.data.appdata.emailsupport + : ""; switch (ctx.link) { case "logout": if (!data.profils) data.profils = []; @@ -55,34 +59,46 @@ apx.apxauth.getdata = (id, ctx) => { break; } console.log("data for tpl:", data); - return data + return data; }; -apx.apxauth.redirecturlwithauth = (url, tribe, webapp, newwindow, windowname = '_blank') => { - url = url.replace(/_[a-zA-Z0-9]{2}\.html/, `_${apx.data.headers.xlang}.html`) - url += `?xtribe=${tribe}&xapp=${webapp}&xalias=${apx.data.headers.xalias}` - url += `&xdays=${apx.data.headers.xdays}&xhash=${apx.data.headers.xhash}` - url += `&xprofils=${apx.data.headers.xprofils.join(',')}` - url += `&xtrkversion=${apx.data.headers.xtrkversion}&xuuid=${apx.data.headers.xuuid}` +apx.apxauth.redirecturlwithauth = ( + url, + tribe, + webapp, + newwindow, + windowname = "_blank" +) => { + url = url.replace(/_[a-zA-Z0-9]{2}\.html/, `_${apx.data.headers.xlang}.html`); + url += `?xtribe=${tribe}&xapp=${webapp}&xalias=${apx.data.headers.xalias}`; + url += `&xdays=${apx.data.headers.xdays}&xhash=${apx.data.headers.xhash}`; + url += `&xprofils=${apx.data.headers.xprofils.join(",")}`; + url += `&xtrkversion=${apx.data.headers.xtrkversion}&xuuid=${apx.data.headers.xuuid}`; if (newwindow) { try { - const newwin = window.open(url, windowname) - if (newwin === null || typeof newwin === 'undefined') { - console.warn("L'ouverture de la fenêtre a été bloquée par un bloqueur de pop-up."); + const newwin = window.open(url, windowname); + if (newwin === null || typeof newwin === "undefined") { + console.warn( + "L'ouverture de la fenêtre a été bloquée par un bloqueur de pop-up." + ); // Vous pouvez informer l'utilisateur ici qu'il doit désactiver son bloqueur de pop-up - alert("Votre navigateur a bloqué l'ouverture d'un nouvel onglet. Veuillez autoriser les pop-ups pour ce site."); + alert( + "Votre navigateur a bloqué l'ouverture d'un nouvel onglet. Veuillez autoriser les pop-ups pour ce site." + ); } else { // Optionnel: Mettre le focus sur la nouvelle fenêtre/onglet newwin.focus(); } return newwin; } catch (error) { - console.error("Une erreur est survenue lors de l'ouverture de l'onglet :", error); + console.error( + "Une erreur est survenue lors de l'ouverture de l'onglet :", + error + ); return null; } } -} - +}; /** * logout @@ -148,7 +164,7 @@ apx.apxauth.setheadersauth = async ( apx.data.headers.xalias = alias; apx.data.headers.xdays = dayjs().valueOf(); const msg = `${alias}_${apx.data.headers.xdays}`; - //console.log("pvk", privatekey); + try { apx.data.headers.xhash = await apx.apxauth.clearmsgSignature( publickey, @@ -184,8 +200,11 @@ apx.apxauth.authentifyme = async ( //console.log(alias, passphrase); //console.log(privatekey); //clean previous answer if exist - - const idparent=document.getElementById(id).parentElement?.closest('[wco-name]').getAttribute('id') + + const idparent = document + .getElementById(id) + .parentElement?.closest("[wco-name]") + .getAttribute("id"); document.querySelector(`#${id} .msginfo`).innerHTML = ""; if (alias.length < 3 || privatekey.length < 200) { apx.notification(`#${id} .msginfo`, { @@ -218,10 +237,13 @@ apx.apxauth.authentifyme = async ( axios .get(`/api/apxtri/pagans/isauth`, { headers: apx.data.headers, + withCredentials: true, }) .then((rep) => { // Authenticate then store profils in header + // remove xhash for security this xhaskl is stored from the server as cookie http only. apx.data.headers.xprofils = rep.data.data.xprofils; + delete apx.data.headers.xhash; apx.save(); // if this page is call with apxid_fr.html?url=httpsxxx then it redirect to this page. //alert(`${window.location.href.includes("/src/")?"/src/":""}${apx.pagecontext.hash.url}`) @@ -229,14 +251,18 @@ apx.apxauth.authentifyme = async ( window.location.href = `${apx.pagecontext.hash.url}`; } else { //location.reload(); - document.getElementById(idparent).setAttribute('wco-link','mytribes'); + document + .getElementById(idparent) + .setAttribute("wco-link", "mytribes"); } }) .catch((err) => { console.log("Not authentify:", err); delete apx.data.auth; apx.save(); - document.getElementById(idparent).setAttribute("wco-link", "signin") + document + .getElementById(idparent) + .setAttribute("wco-link", "signin"); if (err.response) { apx.notification(`#${id} .msginfo`, err.response.data); } else if (err.request) { @@ -486,13 +512,8 @@ apx.apxauth.authenticatedetachedSignature = async ( return false; } }; -apx.apxauth.createIdentity = async ( - id, - alias, - recoemail, - passphrase = "" -) => { - document.querySelector(`#${id} .msginfo`).innerHTML = "" +apx.apxauth.createIdentity = async (id, alias, recoemail, passphrase = "") => { + document.querySelector(`#${id} .msginfo`).innerHTML = ""; const aliasregex = /^[a-z0-9]*$/; //console.log(aliasregex.test(alias)); if (!(alias && alias.length > 3 && aliasregex.test(alias))) { @@ -542,9 +563,7 @@ apx.apxauth.createIdentity = async ( //console.log(apx.data.tmpauth); ["publickey", "privatekey"].forEach((k) => { console.log(`${id} button.signup${k}`); - const btn = document.querySelector( - `#${id} button.signup${k}` - ); + const btn = document.querySelector(`#${id} button.signup${k}`); btn.addEventListener("click", () => { const blob = new Blob([keys[k]], { type: "text/plain" }); const url = URL.createObjectURL(blob); @@ -561,12 +580,8 @@ apx.apxauth.createIdentity = async ( `#${id} .signupalias, #${id} .signupemailrecovery, #${id} .signuppassphrase` ) .forEach((e) => e.setAttribute("disabled", "disabled")); - document - .querySelector(`#${id} .getmykeys`) - .classList.remove("hidden"); - document - .querySelector(`#${id} .btncreatekey`) - .classList.add("hidden"); + document.querySelector(`#${id} .getmykeys`).classList.remove("hidden"); + document.querySelector(`#${id} .btncreatekey`).classList.add("hidden"); } else { apx.notification( `#${id} .msginfo`, @@ -617,8 +632,14 @@ apx.apxauth.registerIdentity = async (id, trustedtribe) => { const data = {}; data.alias = apx.data.tmpauth.keys.alias; data.publickey = apx.data.tmpauth.keys.publickey; - console.log(apx.data.tmpauth.recoemail, Checkjson.testformat(apx.data.tmpauth.recoemail, "email")) - if (apx.data.tmpauth.recoemail && Checkjson.testformat(apx.data.tmpauth.recoemail, "email")) { + console.log( + apx.data.tmpauth.recoemail, + Checkjson.testformat(apx.data.tmpauth.recoemail, "email") + ); + if ( + apx.data.tmpauth.recoemail && + Checkjson.testformat(apx.data.tmpauth.recoemail, "email") + ) { data.passphrase = apx.data.tmpauth.keyspassphrase; data.privatekey = apx.data.tmpauth.keysprivatekey; data.email = apx.data.tmpauth.recoemail; @@ -629,10 +650,8 @@ apx.apxauth.registerIdentity = async (id, trustedtribe) => { .then((reppagan) => { //console.log(reppagan.data); apx.notification(`#${id} .msginfo`, reppagan.data); - authid.querySelector(`.btncreateidentity`) - .classList.add("hidden"); - authid.querySelector(`.signupbtnreload`) - .classList.remove("hidden"); + authid.querySelector(`.btncreateidentity`).classList.add("hidden"); + authid.querySelector(`.signupbtnreload`).classList.remove("hidden"); //remove tmp cause create phc change to keep tplauth in memory and avoid asking again the pasword //delete apx.data.tmpauth; //apx.save(); diff --git a/wco/apxauth/apxauthgeminicli.js b/wco/apxauth/apxauthgeminicli.js new file mode 100644 index 0000000..349af4f --- /dev/null +++ b/wco/apxauth/apxauthgeminicli.js @@ -0,0 +1,347 @@ +/* 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(); \ No newline at end of file diff --git a/wco/apxauthrefactor/apxauthrefactor.js b/wco/apxauthrefactor/apxauthrefactor.js new file mode 100644 index 0000000..095d02d --- /dev/null +++ b/wco/apxauthrefactor/apxauthrefactor.js @@ -0,0 +1,228 @@ +/** + * @file apxauthrefactor.js + * @description Refactored WCO component for user authentication and identity management. + * @version 2.0 + */ + +((window) => { + 'use strict'; + + // --- Component Definition --- + const apxauth = {}; + + // --- Private State & Configuration --- + const _state = { + container: null, // The main DOM element for this component + config: {}, // Initial configuration + templates: {}, // To cache loaded Mustache templates + currentUser: { + alias: 'anonymous', + profils: ['anonymous'], + // ... other user data + }, + headers: { // To be sent with API requests + xalias: 'anonymous', + xdays: null, + xhash: null, + xprofils: ['anonymous'], + xtribe: null, + } + }; + + // --- Private Helper Modules --- + + /** + * Logger utility for consistent console output. + */ + const _log = (level, ...args) => { + console[level]('[apxauth]', ...args); + }; + + /** + * Handles all interactions with the backend API. + */ + const _api = { + /** + * Generic method to perform an API request. + * @param {string} method - 'get', 'post', 'put', etc. + * @param {string} endpoint - The API endpoint URL. + * @param {object} [data=null] - The request payload for POST/PUT. + * @returns {Promise} - The response data. + */ + async request(method, endpoint, data = null) { + try { + const options = { + method: method.toUpperCase(), + headers: { ..._state.headers, 'Content-Type': 'application/json' }, + }; + if (data) { + options.body = JSON.stringify(data); + } + const response = await fetch(endpoint, options); + const responseData = await response.json(); + if (!response.ok) { + _log('error', `API Error on ${endpoint}:`, responseData); + throw responseData; // Throw error object from backend + } + return responseData; + } catch (error) { + _log('error', `Request failed for ${endpoint}:`, error); + throw error; // Re-throw to be caught by the caller + } + }, + + getAliasInfo: (alias) => _api.request('get', `/api/apxtri/pagans/alias/${alias}`), + checkAuth: () => _api.request('get', '/api/apxtri/pagans/isauth'), + logout: () => _api.request('get', '/api/apxtri/pagans/logout'), + recoverKey: (data) => _api.request('post', '/api/apxtri/pagans/keyrecovery', data), + registerIdentity: (data) => _api.request('post', '/api/apxtri/pagans', data), + joinTribe: (tribe, data) => _api.request('put', `/api/apxtri/pagans/person/${tribe}`, data), + }; + + /** + * Encapsulates all OpenPGP-related functions. + */ + const _pgp = { + async generateKey(alias, passphrase) { + return openpgp.generateKey({ + type: 'ecc', + curve: 'curve25519', + userIDs: [{ alias }], + passphrase, + format: 'armored', + }); + }, + + async createClearSignature(privateKeyArmored, passphrase, message) { + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase, + }); + const cleartextMessage = await openpgp.sign({ + message: await openpgp.createCleartextMessage({ text: message }), + signingKeys: privateKey, + }); + return btoa(cleartextMessage); // Base64 encode for transport + } + }; + + /** + * Handles rendering of Mustache templates. + */ + const _render = { + /** + * Renders a screen template into a specific area of the component. + * @param {string} screenName - The name of the screen to render (e.g., 'signin', 'logout'). + * @param {object} [data={}] - Additional data to pass to the template. + */ + async screen(screenName, data = {}) { + const target = _state.container.querySelector('.screenaction'); + if (!target) { + _log('error', 'Render target ".screenaction" not found.'); + return; + } + const templateName = `apxauthscreen${screenName}`; + if (!_state.templates[templateName]) { + _log('error', `Template ${templateName} not found or loaded.`); + return; + } + const renderData = { ..._state.currentUser, ..._state.config, ...data }; + target.innerHTML = Mustache.render(_state.templates[templateName], renderData); + _log('log', `Rendered screen: ${screenName}`); + }, + + /** + * Displays a notification message. + * @param {string} message - The message to display. + * @param {'success'|'error'|'info'} type - The type of message. + */ + notification(message, type = 'error') { + const target = _state.container.querySelector('.msginfo'); + if(target) { + target.innerHTML = `
${message}
`; + } + } + }; + + // --- Public API & Business Logic --- + + /** + * Sets the authentication headers required for API calls. + * @private + */ + async function _setAuthHeaders(alias, publicKey, privateKey, passphrase) { + _state.headers.xalias = alias; + _state.headers.xdays = dayjs().valueOf(); + const message = `${alias}_${_state.headers.xdays}`; + try { + _state.headers.xhash = await _pgp.createClearSignature(privateKey, passphrase, message); + return true; + } catch (error) { + _log('error', 'Failed to create signature:', error); + _render.notification('Could not create a valid signature. Is the passphrase correct?', 'error'); + return false; + } + } + + /** + * Main authentication flow. + */ + apxauth.login = async (alias, passphrase, privateKey) => { + try { + const { data: { publickey } } = await _api.getAliasInfo(alias); + + const headersSet = await _setAuthHeaders(alias, publickey, privateKey, passphrase); + if (!headersSet) return; + + const { data: authData } = await _api.checkAuth(); + + // Successfully authenticated + _state.currentUser.alias = alias; + _state.currentUser.profils = authData.xprofils; + _state.headers.xprofils = authData.xprofils; + + _log('log', 'Authentication successful. User:', _state.currentUser); + + // TODO: Redirect or render the 'logout' or 'mytribes' screen + _render.screen('logout'); // Example: render logout screen + + } catch (error) { + _log('error', 'Login failed:', error); + _render.notification(error.msg || 'Login failed. Please check credentials.', 'error'); + } + }; + + /** + * Initializes the component. + * @param {object} config - The component configuration. + * @param {string} config.containerId - The ID of the DOM element to render into. + * @param {object} config.templates - An object containing the pre-loaded Mustache templates. + * @param {object} [config.initialData={}] - Any initial data to populate state. + */ + apxauth.init = (config) => { + _state.container = document.getElementById(config.containerId); + if (!_state.container) { + _log('error', `Container with ID "${config.containerId}" not found.`); + return; + } + + _state.config = config.initialData || {}; + _state.templates = config.templates || {}; + + // Render the main component layout + _state.container.innerHTML = Mustache.render(_state.templates.apxauthmain, _state.config); + + // Render the initial screen (e.g., 'signin' or 'logout' based on auth status) + // For now, we default to 'signin' + _render.screen('signin'); + + // TODO: Add event listeners using event delegation + // _state.container.addEventListener('click', _handleEvents); + + _log('log', 'apxauth component initialized.'); + }; + + // Expose the component to the global window object + window.apxauth = apxauth; + +})(window); diff --git a/wco/apxauthrefactor/main_fr.mustache b/wco/apxauthrefactor/main_fr.mustache new file mode 100644 index 0000000..cbb6f7d --- /dev/null +++ b/wco/apxauthrefactor/main_fr.mustache @@ -0,0 +1,7 @@ + +
+
+ +
+

+
\ No newline at end of file diff --git a/wco/apxauthrefactor/screenforgetkey_fr.mustache b/wco/apxauthrefactor/screenforgetkey_fr.mustache new file mode 100644 index 0000000..77510c8 --- /dev/null +++ b/wco/apxauthrefactor/screenforgetkey_fr.mustache @@ -0,0 +1,44 @@ +
+ + +
+
+

+ Si vous avez fait confiance à ce domaine pour garder vos clés, un email va être envoyé avec vos clés. +

+
+
+ +
diff --git a/wco/apxauthrefactor/screeninformation_fr.mustache b/wco/apxauthrefactor/screeninformation_fr.mustache new file mode 100644 index 0000000..b7faaa0 --- /dev/null +++ b/wco/apxauthrefactor/screeninformation_fr.mustache @@ -0,0 +1,41 @@ +
+

Qu'est-ce qu'une identité numérique décentralisée?

+

+ C'est un moyen de s'identifier en prouvant qu'on est le propriétaire + d'un alias ou d'une clé publique. Cette clé publique est accessible à tous et utilisée dans le + monde numérique pour informer, payer, échanger,... et porte une + réputation publique. +

+

+ Concrètement, c'est une paire de fichiers texte appelée clé publique + et clé privée. La clé publique ne porte pas d'information + personnelle autre que celles que vous avez bien voulu y associer. +

+

+ Une fonction mathématique permet au propriétaire de la clé privée de + signer un message. Le destinataire dispose d'une autre fonction qui + permet de vérifier que la signature a été faite avec la clé privée. +

+

+ Cette interface permet de créer une identité et de l'utiliser pour + s'authentifier pour 24 heures. Elle n'envoie que le couple alias/clé + publique sur internet, la clé privée est + votre propriété et ne doit jamais être communiquée. Si vous + la perdez, vous ne pourrez plus récupérer les informations + associées. Sauf si vous + avez fait confiance à ce nom de domaine, vous pourrez lui + demander d'envoyer un email avec ces clés. +

+

+ Vous pouvez avoir autant d'identités que vous voulez, vous pouvez + créer une identité pour des objets uniques. La seule limite est qu'à + partir du moment où vous associez des informations personnelles à + cette clé, le destinataire de ces informations peut les relier aux + activités de cette identité inscrite dans la blockchain apxtri. +

+

+ Pour auditer le code js, utiliser l'outil de développement de votre + navigateur. Pour toute remarque, question ou détection de failles : + {{supportemail}} +

+
diff --git a/wco/apxauthrefactor/screenlogout_fr.mustache b/wco/apxauthrefactor/screenlogout_fr.mustache new file mode 100644 index 0000000..ce2648f --- /dev/null +++ b/wco/apxauthrefactor/screenlogout_fr.mustache @@ -0,0 +1,39 @@ +
+
+

+ Bonjour {{xalias}}, +

+

+ Si cet appareil ne vous appartiens pas et que vous n'utilisez pas l'application, vous devriez vous deconnecter. +

+

+ Nettoyer mes traces de cet appareil? + Se deconnecter +

+
+
+

+ Voir mes échanges? + Mon activité +

+ {{#member}} +

+ Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):
{{#profils}} {{.}}
{{/profils}}
{{/noprofils}} +

+ {{/member}} + {{^member}} +

Vous n'êtes pas encore membre de {{xtribe}}

+

+ Envie d'jouter cette tribut {{xtribe}}? + Rejoindre {{xtribe}} +

+ {{/member}} +

Les applications ou pages web de {{xtribe}} à visiter:
+ {{#websites}}{{{name}}}
{{/websites}} +

+ +
+
\ No newline at end of file diff --git a/wco/apxauthrefactor/screenmytribes_fr.mustache b/wco/apxauthrefactor/screenmytribes_fr.mustache new file mode 100644 index 0000000..9128c94 --- /dev/null +++ b/wco/apxauthrefactor/screenmytribes_fr.mustache @@ -0,0 +1,23 @@ +
+
+

+ Bonjour {{xalias}}, +

+
+
+

+ Redirige vers + Redirige vers recruiter.smatchit.io/offer_fr.html&xhash.... +

+ {{#member}} +

+ Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):
{{#profils}} {{.}}
{{/profils}}
{{/noprofils}} +

+ {{/member}} +

Les applications ou pages web de {{xtribe}} à visiter:
+ {{#websites}}{{{name}}}
{{/websites}} +

+ +
+
\ No newline at end of file diff --git a/wco/apxauthrefactor/screensignin_fr.mustache b/wco/apxauthrefactor/screensignin_fr.mustache new file mode 100644 index 0000000..da7adb4 --- /dev/null +++ b/wco/apxauthrefactor/screensignin_fr.mustache @@ -0,0 +1,69 @@ +

+ {{{signintitle}}} +

+
+ + +
+
+ +
+
+ +
+
+
+ +
+
+

{{{remembermetext}}}

+
+
+
+ +
diff --git a/wco/apxauthrefactor/screensignup_fr.mustache b/wco/apxauthrefactor/screensignup_fr.mustache new file mode 100644 index 0000000..4526129 --- /dev/null +++ b/wco/apxauthrefactor/screensignup_fr.mustache @@ -0,0 +1,121 @@ +

+ {{{signuptitle}}} +

+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + diff --git a/wco/itm/adminskull.json b/wco/itm/adminskull.json index d074a92..9609376 100644 --- a/wco/itm/adminskull.json +++ b/wco/itm/adminskull.json @@ -25,9 +25,9 @@ "profil": "{{tribe}}/objects/options/profil" }, "ref": { - "Odmdb": "apxtri/objects/tplstrings/Odmdb", - "Pagans": "apxtri/objects/tplstrings/Pagans", - "Persons": "apxtri/objects/tplstrings/Persons" + "Odmdb": "apxtri/models/tplstrings/Odmdb", + "Pagans": "apxtri/models//tplstrings/Pagans", + "Persons": "apxtri/models/tplstrings/Persons" }, "schema": [ "apxtri/objects/pagans", diff --git a/wco/itm/apx.json b/wco/itm/apx.json index 414dfde..15dfe37 100644 --- a/wco/itm/apx.json +++ b/wco/itm/apx.json @@ -11,7 +11,7 @@ "tpl": {}, "tpldata": {}, "ref": { - "Checkjson": "apxtri/objects/tplstrings/Checkjson", - "Notification": "apxtri/objects/tplstrings/Notifications", "Middlewares": "apxtri/objects/tplstrings/middlewares" + "Checkjson": "apxtri/models/tplstrings/Checkjson", + "Notification": "apxtri/models/tplstrings/Notifications", "Middlewares": "apxtri/models/tplstrings/Middlewares" } } \ No newline at end of file diff --git a/wco/itm/apxauth.json b/wco/itm/apxauth.json index cc0d236..1b4e78f 100644 --- a/wco/itm/apxauth.json +++ b/wco/itm/apxauth.json @@ -23,9 +23,9 @@ "profil": "{{tribeId}}/objects/options/profil" }, "ref": { - "Odmdb": "apxtri/objects/tplstrings/Odmdb", - "Pagans": "apxtri/objects/tplstrings/Pagans", - "Persons": "apxtri/objects/tplstrings/Persons" + "Odmdb": "apxtri/models/tplstrings/Odmdb", + "Pagans": "apxtri/models/tplstrings/Pagans", + "Persons": "apxtri/models/tplstrings/Persons" }, "schema": ["apxtri/objects/pagans", "{{tribe}}/objects/persons"] } diff --git a/wco/privatri/main_fr.mustache b/wco/privatri/main_fr.mustache new file mode 100644 index 0000000..1a6a152 --- /dev/null +++ b/wco/privatri/main_fr.mustache @@ -0,0 +1,8 @@ +
+ +

Messages Privés (privatri)

+

Ce composant gère le stockage sécurisé des messages dans le navigateur.

+
+ +
+
diff --git a/wco/privatri/privatri.js b/wco/privatri/privatri.js new file mode 100644 index 0000000..f5fadd3 --- /dev/null +++ b/wco/privatri/privatri.js @@ -0,0 +1,194 @@ +/** + * @file privatri.js + * @description WCO component for managing private, thread-based messages in IndexedDB. + * @version 1.0 + */ + +((window) => { + 'use strict'; + + // --- Component Definition --- + const privatri = {}; + + // --- Private State --- + let _db = null; // Holds the single, persistent IndexedDB connection + const DB_NAME = 'privatriDB'; + const DB_VERSION = 1; + + /** + * Logs messages to the console with a consistent prefix. + * @param {string} level - The log level (e.g., 'log', 'error', 'warn'). + * @param {...any} args - The messages to log. + */ + const _log = (level, ...args) => { + console[level]('[privatri]', ...args); + }; + + /** + * Opens and initializes the IndexedDB database. + * This function is called by init() and handles the connection and schema upgrades. + * @param {string[]} initialStoreNames - An array of store names (thread IDs) to ensure exist. + * @returns {Promise} A promise that resolves with the database connection. + */ + const _openDatabase = (initialStoreNames = []) => { + return new Promise((resolve, reject) => { + _log('log', `Opening database "${DB_NAME}" with version ${DB_VERSION}.`); + + const request = indexedDB.open(DB_NAME, DB_VERSION); + + // This event is only triggered for new databases or version changes. + request.onupgradeneeded = (event) => { + const db = event.target.result; + _log('log', 'Database upgrade needed. Current stores:', [...db.objectStoreNames]); + + initialStoreNames.forEach(storeName => { + if (!db.objectStoreNames.contains(storeName)) { + _log('log', `Creating new object store: "${storeName}"`); + db.createObjectStore(storeName, { keyPath: 'key' }); + } + }); + }; + + request.onsuccess = (event) => { + _log('log', 'Database opened successfully.'); + _db = event.target.result; + + // Generic error handler for the connection + _db.onerror = (event) => { + _log('error', 'Database error:', event.target.error); + }; + + resolve(_db); + }; + + request.onerror = (event) => { + _log('error', 'Failed to open database:', event.target.error); + reject(event.target.error); + }; + }); + }; + + // --- Public API --- + + /** + * Initializes the privatri component. + * Opens the database connection and ensures initial object stores are created. + * @param {object} config - Configuration object. + * @param {string[]} [config.threads=[]] - An array of initial thread IDs (store names) to create. + * @returns {Promise} + */ + privatri.init = async (config = {}) => { + if (_db) { + _log('warn', 'privatri component already initialized.'); + return; + } + const threads = config.threads || []; + await _openDatabase(threads); + }; + + /** + * Retrieves a value from a specific store (thread). + * @param {string} storeName - The name of the store (thread ID). + * @param {string} key - The key of the item to retrieve. + * @returns {Promise} A promise that resolves with the value or null if not found. + */ + privatri.getValue = (storeName, key) => { + return new Promise((resolve, reject) => { + if (!_db || !_db.objectStoreNames.contains(storeName)) { + _log('warn', `Store "${storeName}" does not exist.`); + return resolve(null); + } + const transaction = _db.transaction(storeName, 'readonly'); + const store = transaction.objectStore(storeName); + const request = store.get(key); + + request.onsuccess = () => resolve(request.result ? request.result.value : null); + request.onerror = (e) => { + _log('error', `Error getting value for key "${key}" from store "${storeName}":`, e.target.error); + reject(e.target.error); + }; + }); + }; + + /** + * Adds or updates a key-value pair in a specific store (thread). + * @param {string} storeName - The name of the store (thread ID). + * @param {string} key - The key of the item to set. + * @param {any} value - The value to store. + * @returns {Promise} + */ + privatri.setValue = (storeName, key, value) => { + return new Promise((resolve, reject) => { + if (!_db || !_db.objectStoreNames.contains(storeName)) { + _log('error', `Cannot set value. Store "${storeName}" does not exist.`); + return reject(new Error(`Store "${storeName}" not found.`)); + } + const transaction = _db.transaction(storeName, 'readwrite'); + const store = transaction.objectStore(storeName); + const request = store.put({ key: key, value: value }); + + request.onsuccess = () => resolve(); + request.onerror = (e) => { + _log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error); + reject(e.target.error); + }; + }); + }; + + /** + * Removes a key-value pair from a specific store (thread). + * @param {string} storeName - The name of the store (thread ID). + * @param {string} key - The key of the item to remove. + * @returns {Promise} + */ + privatri.removeKey = (storeName, key) => { + return new Promise((resolve, reject) => { + if (!_db || !_db.objectStoreNames.contains(storeName)) { + _log('warn', `Cannot remove key. Store "${storeName}" does not exist.`); + return resolve(); // Resolve peacefully if store doesn't exist + } + const transaction = _db.transaction(storeName, 'readwrite'); + const store = transaction.objectStore(storeName); + const request = store.delete(key); + + request.onsuccess = () => resolve(); + request.onerror = (e) => { + _log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error); + reject(e.target.error); + }; + }); + }; + + /** + * A utility function to save a batch of messages, demonstrating how to use the component. + * @param {object} threadsObj - An object where keys are thread IDs and values are message objects. + * @example + * const messages = { + * "thread-123": { "1678886400": { text: "Hello" } }, + * "thread-456": { "1678886401": { text: "Hi there" } } + * }; + * await privatri.storeMessages(messages); + */ + privatri.storeMessages = async (threadsObj = {}) => { + if (!_db) { + _log('error', 'Database not initialized. Please call privatri.init() first.'); + return; + } + _log('log', 'Storing messages in IndexedDB...'); + for (const [uuid, threadObj] of Object.entries(threadsObj)) { + // Ensure the object store exists before trying to write to it. + if (!_db.objectStoreNames.contains(uuid)) { + _log('warn', `Store "${uuid}" not found during message storage. You may need to re-init with this thread.`); + continue; + } + for (const [timestamp, messageObj] of Object.entries(threadObj)) { + await privatri.setValue(uuid, timestamp, messageObj); + } + } + _log('log', 'Finished storing messages.'); + }; + + // Expose the component to the global window object + window.privatri = privatri; + +})(window); diff --git a/wco/simplemobnav/simplemobnavgeminicli.js b/wco/simplemobnav/simplemobnavgeminicli.js new file mode 100644 index 0000000..4b7302d --- /dev/null +++ b/wco/simplemobnav/simplemobnavgeminicli.js @@ -0,0 +1,131 @@ +/* eslint-env browser */ +/* eslint-disable no-alert, no-console */ + +/** + * @file simplemobnav.js (previously simplemobnavnew.js) + * @description Modern, class-based implementation for a simple mobile navigation component. + * @version 2.1 + * @author support@ndda.fr + */ + +// Establish the global namespace +window.apx = window.apx || {}; + +/** + * @class SimpleMobNav + * Manages a mobile navigation menu, dynamically rendering links based on user profiles. + */ +class SimpleMobNav { + constructor() { + if (typeof apx.main === 'undefined') { + throw new Error("SimpleMobNav requires a global 'apx.main' (ApxManager) instance."); + } + } + + loadwco(id, ctx) { + console.log(`[simplemobnav] loadwco triggered for id: ${id} with context:`, ctx); + const componentRoot = document.getElementById(id); + if (!componentRoot) { + console.error(`SimpleMobNav: Element with id "${id}" not found.`); + return; + } + + const tpldataname = `${apx.main.data.pagename}_${id}_simplemobnav`; + if (!this._validateTplData(tpldataname)) return; + + const tplData = apx.main.data.tpldata[tpldataname]; + let currentLink = ctx.link; + + if (componentRoot.innerHTML.trim() === "") { + currentLink = this._getInitialLink(tplData); + tplData.contentscreen = currentLink; + componentRoot.innerHTML = Mustache.render(apx.main.data.tpl.simplemobnavmain, tplData); + } else { + componentRoot.querySelectorAll('[wco-name]').forEach(el => { + el.setAttribute('wco-link', currentLink); + }); + } + + this._renderNavLinks(id, currentLink, tplData); + + const loadingIndicator = document.getElementById("loading"); + if (loadingIndicator) { + loadingIndicator.classList.add("hidden"); + } + console.log(`SimpleMobNav: Screen "${currentLink}" loaded.`); + } + + action(id, link, action, wconame) { + if (action === "navigation") { + document.getElementById(id)?.setAttribute("wco-link", link); + return; + } + + const wco = window.apx[wconame]; + if (!wco) { + console.warn(`Action target WCO "${wconame}" does not exist.`); + return; + } + if (typeof wco[action] !== 'function') { + console.warn(`Action "${action}" does not exist on WCO "${wconame}".`); + return; + } + wco[action](); + } + + reload() { + location.reload(); + } + + _renderNavLinks(id, currentLink, tplData) { + const componentRoot = document.getElementById(id); + const navlinkContainer = componentRoot?.querySelector('.navlink'); + if (!navlinkContainer) return; + + const currentScreenConfig = tplData.links.find(m => m.link === currentLink); + if (!currentScreenConfig) return; + + const visibleLinks = tplData.links + .filter(m => + currentScreenConfig.next.includes(m.link) && + m.allowedprofil.some(p => apx.main.data.headers.xprofils.includes(p)) + ) + .map(m => ({ + ...m, + classnavbutton: m.classnavbutton || tplData.classnavbutton, + classnavlist: m.classnavlist || tplData.classnavlist, + })); + + const navTemplate = apx.main.data.tpl[`simplemobnav${tplData.navtpl}`]; + navlinkContainer.innerHTML = Mustache.render(navTemplate, { id, links: visibleLinks }); + } + + _getInitialLink(tplData) { + for (const menu of tplData.profilmenu) { + if (apx.main.data.headers.xprofils.includes(menu.mainprofil)) { + return menu.link; + } + } + return null; + } + + _validateTplData(tpldataname) { + const tplData = apx.main.data.tpldata[tpldataname]; + if (!tplData) { + console.error(`Template data "${tpldataname}" not found in apx.main.data.tpldata.`); + return false; + } + + const mandatoryProps = ["contentwconame", "contentid", "profilmenu", "links"]; + const missingProps = mandatoryProps.filter(p => !tplData[p]); + + if (missingProps.length > 0) { + console.error(`Missing properties in ${tpldataname}: ${missingProps.join(', ')}`); + return false; + } + return true; + } +} + +// Attach an instance to the global namespace +apx.simplemobnav = new SimpleMobNav(); \ No newline at end of file diff --git a/wwws/admin/src/apxid_fr.html b/wwws/admin/src/apxid_fr.html index cb2a240..11fff18 100644 --- a/wwws/admin/src/apxid_fr.html +++ b/wwws/admin/src/apxid_fr.html @@ -44,6 +44,7 @@ + diff --git a/wwws/admin/src/static/css/sourcetw.css b/wwws/admin/src/static/css/sourcetw.css index 7aeca7e..97be250 100644 --- a/wwws/admin/src/static/css/sourcetw.css +++ b/wwws/admin/src/static/css/sourcetw.css @@ -1,8 +1,3 @@ -@source "../../../../../../../apxtri/objects/wco/apx/*.{html,js,mustache}"; -@source "../../../../../../../apxtri/objects/wwws/admin/src/**/*.{html,js,mustache}"; -@source "../../../../../../../apxtri/objects/wco/simplemobnav/*.{html,js,mustache}"; -@source "../../../../../../../apxtri/objects/wco/apxauth/*.{html,js,mustache}"; -@source "../../../../../../../apxtri/objects/wco/adminskull/*.{html,js,mustache}"; @source "/media/phil/usbfarm/apxtowns/data/apxtri/objects/wco/apx/*.{html,js,mustache}"; @source "/media/phil/usbfarm/apxtowns/data/apxtri/objects/wwws/admin/src/**/*.{html,js,mustache}"; @source "/media/phil/usbfarm/apxtowns/data/apxtri/objects/wco/simplemobnav/*.{html,js,mustache}"; diff --git a/wwws/cdn/lib/symlink.json b/wwws/cdn/lib/symlink.json index 391dd26..2344e5b 100644 --- a/wwws/cdn/lib/symlink.json +++ b/wwws/cdn/lib/symlink.json @@ -1,22 +1,26 @@ [ { - "pathsrc":"nodePath/apxtri/models/Checkjson.js", - "pathdest":"dataPath/apxtri/objects/wwws/cdn/lib/checkjson.js" + "pathsrc": "nodePath/apxtri/models/Checkjson.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/checkjson.js" }, { - "pathsrc":"nodePath/apxtri/node_modules/axios/dist/axios.min.js", - "pathdest":"dataPath/apxtri/objects/wwws/cdn/lib/axios/dist/axios.min.js" + "pathsrc": "nodePath/apxtri/node_modules/axios/dist/axios.min.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/axios/dist/axios.min.js" }, - { - "pathsrc":"nodePath/apxtri/node_modules/dayjs/dayjs.min.js", - "pathdest":"dataPath/apxtri/objects/wwws/cdn/lib/dayjs/dayjs.min.js" + { + "pathsrc": "nodePath/apxtri/node_modules/dayjs/dayjs.min.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/dayjs/dayjs.min.js" }, - { - "pathsrc":"nodePath/apxtri/node_modules/mustache/mustache.min.js", - "pathdest":"dataPath/apxtri/objects/wwws/cdn/lib/mustache/mustache.min.js" + { + "pathsrc": "nodePath/apxtri/node_modules/mustache/mustache.min.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/mustache/mustache.min.js" }, - { - "pathsrc":"nodePath/apxtri/node_modules/openpgp/dist/openpgp.min.js", - "pathdest":"dataPath/apxtri/objects/wwws/cdn/lib/openpgp/dist/openpgp.min.js" + { + "pathsrc": "nodePath/apxtri/node_modules/openpgp/dist/openpgp.min.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/openpgp/dist/openpgp.min.js" + }, + { + "pathsrc": "nodePath/apxtri/node_modules/qr-code-styling/lib/qr-code-styling.js", + "pathdest": "dataPath/apxtri/objects/wwws/cdn/lib/qr-code-styling/lib/qr-code-styling.js" } ] \ No newline at end of file diff --git a/wwws/itm/admin.json b/wwws/itm/admin.json index b4dacf3..ffad8d9 100644 --- a/wwws/itm/admin.json +++ b/wwws/itm/admin.json @@ -45,11 +45,11 @@ "tpldata": {}, "itms": {}, "ref": { - "Checkjson": "apxtri/objects/tplstrings/Checkjson", - "Notification": "apxtri/objects/tplstrings/Notifications", - "Odmdb": "apxtri/objects/tplstrings/Odmdb", - "Pagans": "apxtri/objects/tplstrings/Pagans", - "Middlewares": "apxtri/objects/tplstrings/middlewares" + "Checkjson": "apxtri/models/tplstrings/Checkjson", + "Notification": "apxtri/models/tplstrings/Notifications", + "Odmdb": "apxtri/models/tplstrings/Odmdb", + "Pagans": "apxtri/models/tplstrings/Pagans", + "Middlewares": "apxtri/models/tplstrings/middlewares" }, "schema": [ "apxtri/objects/pagans", @@ -70,7 +70,9 @@ "textContent": "L'Unique et sa propriété" } }, - "appdata": {} + "appdata": { + "emailsupport": "support@need-data.com" + } }, "apxid": { "version": 1, @@ -101,19 +103,19 @@ }, "itms": {}, "ref": { - "Checkjson": "apxtri/objects/tplstrings/Checkjson", - "Notification": "apxtri/objects/tplstrings/Notifications", - "Odmdb": "apxtri/objects/tplstrings/Odmdb", - "Pagans": "apxtri/objects/tplstrings/Pagans", - "Middlewares": "apxtri/objects/tplstrings/middlewares", - "Persons": "apxtri/objects/tplstrings/Persons" + "Checkjson": "apxtri/models/tplstrings/Checkjson", + "Middlewares": "apxtri/models/tplstrings/Middlewares", + "Notification": "apxtri/models/tplstrings/Notifications", + "Odmdb": "apxtri/models/tplstrings/Odmdb", + "Pagans": "apxtri/models/tplstrings/Pagans", + "Persons": "apxtri/models/tplstrings/Persons" }, "schema": [ "apxtri/objects/pagans", "apxtri/objects/persons" ], "options": { - "profil": "apxtri/objects/options/profil" + "profil": "apxtri/objects/options/itm/profil" }, "wcodata": { "favicon": { @@ -168,12 +170,12 @@ }, "itms": {}, "ref": { - "Checkjson": "apxtri/objects/tplstrings/Checkjson", - "Notification": "apxtri/objects/tplstrings/Notifications", - "Odmdb": "apxtri/objects/tplstrings/Odmdb", - "Pagans": "apxtri/objects/tplstrings/Pagans", - "Middlewares": "apxtri/objects/tplstrings/middlewares", - "Persons": "apxtri/objects/tplstrings/Persons" + "Checkjson": "apxtri/models/tplstrings/Checkjson", + "Notification": "apxtri/models/tplstrings/Notifications", + "Odmdb": "apxtri/models/tplstrings/Odmdb", + "Pagans": "apxtri/models/tplstrings/Pagans", + "Middlewares": "apxtri/models/tplstrings/middlewares", + "Persons": "apxtri/models/tplstrings/Persons" }, "schema": [ "apxtri/objects/pagans", diff --git a/wwws/recruiter/src/index_fr.html b/wwws/recruiter/src/index_fr.html new file mode 100644 index 0000000..3dd6cf1 --- /dev/null +++ b/wwws/recruiter/src/index_fr.html @@ -0,0 +1 @@ +

Test

\ No newline at end of file