new wco wwws compatible with new backeend

This commit is contained in:
2025-07-23 12:36:48 +02:00
parent bc76840707
commit b6ae865332
23 changed files with 1702 additions and 94 deletions

View File

@@ -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) {

334
wco/apx/apxgeminicli.js Normal file
View File

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

View File

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

View File

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

View File

@@ -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<object>} - 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 = `<div class="notification ${type}">${message}</div>`;
}
}
};
// --- 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);

View File

@@ -0,0 +1,7 @@
<!-- screen action-->
<div class="screenaction mt-5 sm:mx-auto sm:w-full sm:max-w-sm">
</div>
<!-- feedback action-->
<div class="my-5">
<p class="msginfo text-center text-info"></p>
</div>

View File

@@ -0,0 +1,44 @@
<div class="mt-1">
<label class="input validator mbt-1">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.89 4.111a5.5 5.5 0 1 0 0 7.778.75.75 0 1 1 1.06 1.061A7 7 0 1 1 15 8a2.5 2.5 0 0 1-4.083 1.935A3.5 3.5 0 1 1 11.5 8a1 1 0 0 0 2 0 5.48 5.48 0 0 0-1.61-3.889ZM10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0Z"></path>
</g>
</svg>
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input id="inputaliasrecovery" type="text" placeholder="mail@site.com | alias" required />
</label>
<div class="validator-hint hidden">
Enter a valid email or an alias (lowercase a-z and 0-9)
</div>
</div>
<div class="my-5">
<p>
Si vous avez fait confiance à ce domaine pour garder vos clés, un email va être envoyé avec vos clés.
</p>
</div>
<div class="my-5">
<button
class="btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="apx.apxauth.recoverykey('{{id}}',document.getElementById('inputaliasrecovery').value);"
>
M'envoyer un email avec mes clés
</button>
</div>

View File

@@ -0,0 +1,41 @@
<div class="space-y-6 text-justify">
<h2>Qu'est-ce qu'une identité numérique décentralisée?</h2>
<p>
C'est <span class="text-secondary">un moyen de s'identifier en prouvant qu'on est le propriétaire
d'un alias ou d'une clé publique</span>. Cette clé publique est accessible à tous et utilisée dans le
monde numérique pour informer, payer, échanger,... et porte une
réputation publique.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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
<span class="text-secondary">votre propriété et ne doit jamais être communiquée</span>. Si vous
la perdez, vous ne pourrez plus récupérer les informations
associées. Sauf si vous
<span class="text-secondary">avez fait confiance à ce nom de domaine</span>, vous pourrez lui
demander d'envoyer un email avec ces clés.
</p>
<p>
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.
</p>
<p>
Pour auditer le code js, utiliser l'outil de développement de votre
navigateur. Pour toute remarque, question ou détection de failles :
{{supportemail}}
</p>
</div>

View File

@@ -0,0 +1,39 @@
<div class="flex flex-col space-y-1 text-center">
<div class="mt-1">
<h1 class="mb-6">
Bonjour {{xalias}},
</h1>
<p>
Si cet appareil ne vous appartiens pas et que vous n'utilisez pas l'application, vous devriez vous deconnecter.
</p>
<p class="text-center text-gray-500">
Nettoyer mes traces de cet appareil?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.logout('{{id}}','logout','logout','apxauth')">Se deconnecter</a>
</p>
</div>
<div class="mt-4">
<p class="text-center text-gray-500">
Voir mes échanges?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
href="https://wall-ants.ndda.fr/apxwallet_fr.html" >Mon activité </a>
</p>
{{#member}}
<p>
Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):<br><span class="text-info"> {{#profils}} {{.}}<br> {{/profils}} </span> {{/noprofils}}
</p>
{{/member}}
{{^member}}
<p> Vous n'êtes pas encore membre de {{xtribe}} </p>
<p class=" mt-1 text-center text-gray-500">
Envie d'jouter cette tribut {{xtribe}}?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.jointribe('{{id}}')">Rejoindre {{xtribe}}</a>
</p>
{{/member}}
<p>Les applications ou pages web de {{xtribe}} à visiter:<br>
{{#websites}}<a class="font-semibold leading-6 text-secondary hover:text-primary" href='{{{href}}}'>{{{name}}}</a><br> {{/websites}}
</p>
<button class="btn btn-primary" onclick="apx.apxauth.runtest()">testbtn</button>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<div class="flex flex-col space-y-1 text-center">
<div class="mt-1">
<h1 class="mb-6">
Bonjour {{xalias}},
</h1>
</div>
<div class="mt-4">
<p class="text-center text-gray-500">
Redirige vers
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.redirecturlwithauth('http://recruiter.smatchit.newdev.ants/src/offer_fr.html','smatchit','recruiter',true);" >Redirige vers recruiter.smatchit.io/offer_fr.html&xhash....</a>
</p>
{{#member}}
<p>
Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):<br><span class="text-info"> {{#profils}} {{.}}<br> {{/profils}} </span> {{/noprofils}}
</p>
{{/member}}
<p>Les applications ou pages web de {{xtribe}} à visiter:<br>
{{#websites}}<a class="font-semibold leading-6 text-secondary hover:text-primary" href='{{{href}}}'>{{{name}}}</a><br> {{/websites}}
</p>
<button class="btn btn-primary" onclick="apx.apxauth.runtest()">testbtn</button>
</div>
</div>

View File

@@ -0,0 +1,69 @@
<p data-wco="createid" class="text-center text-neutral-content">
{{{signintitle}}}
</p>
<div class="mt-2">
<label class="input validator">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input
class="signinalias"
type="input"
required
placeholder="alias"
pattern="[a-z0-9\-]*"
minlength="3"
maxlength="30"
title="{{{aliastitle}}}"
/>
</label>
<p class="validator-hint hidden"> {{{aliasinvalid}}}</p>
</div>
<div class="mt-2">
<label class="input mt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"></path>
</g>
</svg>
<input type="text" class="signinpassphrase" placeholder="passphrase (option)" />
</label>
</div>
<div class="mt-2">
<textarea rows=5 class="mt-2 textarea signinprivatekey" placeholder="{{{privatekeyplaceholder}}}"></textarea>
</div>
<div class="flex m-6">
<div class="w-14 flex-none">
<input type="checkbox" checked="checked" class="checkbox signinrememberme" />
</div>
<div class="flex-1">
<p class="text-sm text-justify" >{{{remembermetext}}}</p>
</div>
</div>
<div class="m-4">
<button
class="btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const loginid= document.getElementById('{{id}}');apx.apxauth.authentifyme(
'{{id}}',
loginid.querySelector('.signinalias').value,
loginid.querySelector('.signinpassphrase').value,
loginid.querySelector('.signinprivatekey').value,
loginid.querySelector('.signinrememberme').checked
)">
{{{authentifyme}}}
</button>
</div>

View File

@@ -0,0 +1,121 @@
<p data-wco="createid" class="text-center text-neutral-content">
{{{signuptitle}}}
</p>
<div class="paramid">
<div class="mt-2">
<label class="input validator mbt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input
class="signupalias"
type="input"
required
placeholder="alias"
pattern="[a-z0-9\-]*"
minlength="3"
maxlength="30"
title="{{{aliastitle}}}"
/>
</label>
<div class="validator-hint hidden">
<p>{{{aliasinvalid}}}</p>
</div>
</div>
<div class="mt-2">
<label class="input validator mt-1">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg>
<input class="signupemailrecovery" type="email" placeholder="mail@site.com" required />
</label>
<div class="validator-hint hidden">
{{{emailinvalid}}}
</div>
</div>
<div class="mt-2">
<label class="input mt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"></path>
</g>
</svg>
<input type="text" class="signuppassphrase" placeholder="passphrase (option)" />
</label>
</div>
<div class="mt-5">
<button
class="btncreatekey btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const authid=document.getElementById('{{id}}');console.log('{{id}}'); apx.apxauth.createIdentity(
'{{id}}',
authid.querySelector('.signupalias').value,
authid.querySelector('.signupemailrecovery').value,
authid.querySelector('.signuppassphrase').value
)"
>
{{{createkey}}}
</button>
</div>
</div>
<div class="getmykeys hidden mt-1">
<div class="flex m-6">
<div class="w-14 flex-none">
<input type="checkbox" checked="checked" class="signuptrustedcheck checkbox checkbox-secondary" />
</div>
<div class="flex-1">
<p class="text-sm text-justify" >{{{trusttext}}}</p>
</div>
</div>
<div class="downloadkeys text-center mt-1">
<button
class="signuppublickey btn btn-outline btn-accent text-white shadow-sm"
>
{{{downloadPuK}}}
</button>
<button
class="signupprivatekey btn btn-outline btn-accent text-white shadow-sm"
>
{{{downloadPrK}}}
</button>
</div>
<div class="mt-2">
<button
class="btncreateidentity btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const authid=document.getElementById('{{id}}');apx.apxauth.registerIdentity(
'{{id}}',
authid.querySelector('.signuptrustedcheck').checked
)"
>{{{saveidentity}}}
</button>
<button
class="signupbtnreload hidden btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="location.reload(true)"
>
{{{nextpage}}}
</button>
</div>
</div>

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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"]
}

View File

@@ -0,0 +1,8 @@
<div id="privatri-container">
<!-- Le contenu du composant privatri sera rendu ici -->
<h2>Messages Privés (privatri)</h2>
<p>Ce composant gère le stockage sécurisé des messages dans le navigateur.</p>
<div id="privatri-threads">
<!-- La liste des fils de discussion pourrait être affichée ici -->
</div>
</div>

194
wco/privatri/privatri.js Normal file
View File

@@ -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<IDBDatabase>} 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<void>}
*/
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<any|null>} 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<void>}
*/
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<void>}
*/
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);

View File

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