Compare commits

...

18 Commits

Author SHA1 Message Date
devpotatoes
d0def00967 Fix some issues. 2025-09-06 14:22:03 +02:00
devpotatoes
7dfbc7ced1 Privatri is now responsive 2025-09-06 13:55:49 +02:00
595f026589 simplemobnav change to look like smartphone 2025-09-06 09:53:00 +02:00
5a4771311f modif simplemobnav to look like a smartphone on big screen 2025-09-02 08:57:40 +02:00
2ac5e9ce07 fix creation/auth process 2025-09-01 07:20:17 +02:00
devpotatoes
d218c3b1d0 Privatri works locally. 2025-08-28 09:19:28 +02:00
93842f84a7 set tailwindcss to work with apxtri for any tribes appname 2025-08-26 10:54:37 +02:00
devpotatoes
5d6aed3603 SPA privatri format 2025-08-25 17:47:12 +02:00
1a2b076fe1 test timestamp 2025-08-25 11:42:54 +02:00
5828a6ee48 fix bug generated by gemini 2025-08-25 09:00:02 +02:00
be706a81ab add comment in package.json 2025-08-22 08:47:18 +02:00
42469dcb1d new package to manange version 2025-08-21 16:03:10 +02:00
0ca3dde60f update wco apxauth to align it with the BE 2025-08-21 15:36:52 +02:00
c744f1fb4a modif libsource.json to get lib 2025-08-18 15:32:44 +02:00
c13f5ece7e clean list of lib that are dynamicaly added when needed 2025-08-18 09:41:08 +02:00
a7c1cb3ae9 modifprivatri wco and page 2025-08-14 10:36:30 +02:00
devpotatoes
0ebb4a6419 Add privatri wco 2025-08-11 09:31:24 +02:00
devpotatoes
87aaf3fa19 Add privatri wco 2025-08-11 09:26:06 +02:00
76 changed files with 7223 additions and 842 deletions

4
.gitignore vendored
View File

@@ -5,8 +5,8 @@ wwws/*/dist/**
!wwws/*/src/**
!wwws/itm/
!wwws/itm/**
wwws/cdn/lib
!wwws/cdn/lib/symlink.json
wwws/cdn/lib/*
!wwws/cdn/lib/libsource.json
devices
frenchlocation
nations

4
package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"version":"1.0.0",
"comment":"Object of a tribe"
}

View File

@@ -336,64 +336,7 @@ apx.wcoobserver = () => {
e.setAttribute("wco-name", wconame);
});
};
// State management
apx.save = () => {
localStorage.setItem(apx.data.headers.xapp, JSON.stringify(apx.data));
};
apx.update = async () => {
if (!apxtri) {
console.log(
'Please add to the html page header, this line const apxtri = { headers: { xtrkversion: 1, xtribe: "smatchit", xapp: "pwa", xlang: "fr", xalias: "anonymous", xhash: "anonymous", xdays: 0} ,pagename:"apxid"} '
);
return;
}
//if (apxtri.forcereload){localStorage.setItem("forcereload",true)};
if (document.querySelector("html").getAttribute("lang")) {
apxtri.headers.xlang = document.querySelector("html").getAttribute("lang");
}
//alert(localStorage.getItem(apxtri.headers.xapp))
if (localStorage.getItem(apxtri.headers.xapp)) {
apx.data = JSON.parse(localStorage.getItem(apxtri.headers.xapp));
//update with current pagename and eventualy pageauth
apx.data.pagename = apxtri.pagename;
if (apxtri.pageauth) apx.data.pageauth = apxtri.pageauth;
// check localstorage in line with current webpage
if (
apx.data.headers.xtribe != apxtri.headers.xtribe ||
apx.data.headers.xlang != apxtri.headers.xlang ||
apx.data.headers.xtrkversion != apxtri.headers.xtrkversion
) {
// if an app change of tribe
localStorage.removeItem(apxtri.headers.xapp);
delete apx.data;
}
}
if (!apx.data) {
console.log("init or reinit apx.data");
apx.data = apxtri;
}
apx.pagecontext = { search: {}, hash: {} };
if (window.location.hash != "") {
window.location.hash
.slice(1)
.split("&")
.forEach((kv) => {
const keyval = kv.split("=");
apx.pagecontext.hash[keyval[0]] = keyval[1];
});
}
if (window.location.search != "") {
window.location.search
.slice(1)
.split("&")
.forEach((kv) => {
const keyval = kv.split("=");
apx.pagecontext.hash[keyval[0]] = keyval[1];
});
}
console.log("apx.pagecontext:", apx.pagecontext);
apx.managecontext = () => {
// Set authenticate parameter if in pagecontext and redirect to the requested url
console.log(
apx.pagecontext.hash.xdays,
@@ -413,17 +356,12 @@ apx.update = async () => {
) {
//Means this page is called from an external auth app
let headervalid = true;
const headerkey = [
"xalias",
"xhash",
"xdays",
"xprofils",
"xtribe",
"xlang",
];
headerkey.forEach((h) => {
if (apx.pagecontext.hash[h]) {
apx.data.headers[h] = (h==="xprofils")? apx.pagecontext.hash[h].split(","):apx.pagecontext.hash[h];
apx.data.headers[h] =
h === "xprofils"
? apx.pagecontext.hash[h].split(",")
: apx.pagecontext.hash[h];
} else {
headervalid = false;
}
@@ -438,6 +376,85 @@ apx.update = async () => {
console.log("Your try to access a page failled with ", apx.pagecontext);
}
}
};
// State management
apx.save = () => {
localStorage.setItem(apx.data.headers.xapp, JSON.stringify(apx.data));
};
apx.update = async () => {
if (!apxtri || !apxtri.headers || !apxtri.headers.xapp) {
console.log(
'Please add to the html page header, this line const apxtri = { headers: { xtrkversion: 1, xtribe: "smatchit", xapp: "pwa", xlang: "fr", xalias: "anonymous", xhash: "anonymous", xdays: 0} ,pagename:"apxid"} '
);
return;
}
const headerkey = ["xalias", "xhash", "xdays", "xprofils", "xtribe", "xlang"];
if (document.querySelector("html").getAttribute("lang")) {
apxtri.headers.xlang = document.querySelector("html").getAttribute("lang");
}
apxtri.headers.xlang = apxtri.headers?.xlang || "en";
//Get if exist in localstorage a key apxtri.headers.xapp
if (apxtri.headers.xapp && localStorage.getItem(apxtri.headers.xapp)) {
apx.data = JSON.parse(localStorage.getItem(apxtri.headers.xapp));
//update with current pagename and eventualy pageauth
apx.data.pagename = apxtri.pagename;
if (apxtri.pageauth) apx.data.pageauth = apxtri.pageauth;
const isauth = await axios.get('/api/apxtri/pagans/isauth', { headers: apx.data.headers, withCredentials:true,validateStatus: (status) => true});
if (
(isauth.status!==200 && apx.data.headers.alias!=="anonymous") ||
apx.data.headers.xtribe != apxtri.headers.xtribe ||
apx.data.headers.xlang != apxtri.headers.xlang ||
apx.data.headers.xtrkversion != apxtri.headers.xtrkversion
) {
// if no more authenticated or change of tribe...
localStorage.removeItem(apxtri.headers.xapp);
delete apx.data;
}
}
//reinit if apx.data does not exist
apx.data = apx?.data || apxtri;
// get context from url search ?q=x&s=z or hash #q=x&s=z
// search comme from a reloading page instead of hash is just a passive link
apx.pagecontext = { search: {}, hash: {} };
["hash", "search"].forEach((type) => {
const paramString = window.location[type];
if (paramString) {
paramString
.slice(1)
.split("&")
.forEach((kv) => {
const [key, value] = kv.split("=");
apx.pagecontext.hash[key] = value;
});
}
});
console.log("apx.pagecontext:", apx.pagecontext);
// check is authenticated
if (
apx.data.headers.xalias === "anonymous" ||
(apx.pagecontext.hash.xdays &&
dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25)
) {
// reset authentification data
apx.data.headers.xalias = "anonymous";
apx.data.headers.xdays = 0;
apx.data.headers.xprofils = ["anonymous"];
delete apx.data.headers.xhash;
}
//apx.managecontext(); (todo when all work target is to use context to redirect if key word used in contexte url, ....)
if (
apx.data.headers.xalias === "anonymous" &&
(apx.data.auth || !apx.data.allowedprofils.includes("anonymous"))
) {
// Means user is not authenticat and has a rememberMe previously selected (auth) OR this page is not accessible to an anonymous.
// Redirect to apxid page if auth is ok then redirect to this page
// notauth then expected to have alias privatekey
//document.location.href = `/apxtriadmin/${apx.data.pageauth}_${apx.data.headers.xlang}.html#url=${apx.data.
document.location.href = `/apxtriadmin/apxid_${apx.data.headers.xlang}.html#url=${apx.data.pagename}_${apx.data.headers.xlang}.html`;
}
// check accessright to the page
if (
apx.data.allowedprofils &&
!apx.data.allowedprofils.includes("anonymous") &&
@@ -449,13 +466,9 @@ apx.update = async () => {
console.log("profils authorized:", profilintersect);
if (profilintersect.length == 0) {
alert(apx.data.ref.Middlewares.notallowtoaccess);
document.location.href = `/apxtriadmin/apxid_${apx.data.headers.xlang}.html`
return false;
}
if (dayjs().valueOf() - apx.data.headers.xdays > 86400000) {
// need to refresh authentification if possible by opening the pageauth with url context
// the pageauth redirect to this current page after authentification, if not then wait credential
document.location.href = `/${apx.data.pageauth}_${apx.data.headers.xlang}.html#url=${apx.data.pagename}_${apx.data.headers.xlang}.html`;
}
}
console.log("authorized to access");
/* à voir si utile redirect to authentification page pageauth with a redirection if authentify to the pagename (check if /src/ then add it)
@@ -465,6 +478,11 @@ apx.update = async () => {
apxtri.pagename
}_${apxtri.headers.xlang}.html`;
*/
/// axios setting //
axios.defaults.withCredentials = true; // force to send cookie http only for authentification
axios.defaults.headers.common=apx.data.headers;
////////////////////
////////////////////////////////////////////
apx.data.version = 0; //this force an update to be removed in production
///////////////////////////////////////////
@@ -473,8 +491,7 @@ apx.update = async () => {
let initset = {};
try {
initset = await axios.get(initdb, {
headers: apx.data.headers,
timeout: 2000,
timeout: 2000
});
} catch (err) {
console.log(err);
@@ -491,21 +508,11 @@ apx.update = async () => {
setTimeout(apx.update, 30000);
}
if (initset.data.msg == "data_model_update") {
// mise à jour local
/*if (initset.data.data.wco) {
console.log("WARNING!!, local apxtri.wco was erase by updatelocaldb.wco");
}*/
Object.keys(initset.data.data).forEach((k) => {
if (k !== "headers") {
apx.data[k] = initset.data.data[k];
}
});
/* if (apx.data.confpage.wco && !apx.data.wco){
console.log("update apx.data.wco with localdb cause does not exist")
apx.data.wco=apx.data.confpage.wco;
}
*/
console.log("local update done");
apx.save();
}
@@ -529,11 +536,11 @@ apx.indexedDB.set = async (db, storeName, value) => {
if (!db.objectStoreNames.contains("threads")) {
db.createObjectStore("threads", { keyPath: "uuid" });
};
}
if (!db.objectStoreNames.contains("messages")) {
db.createObjectStore("messages", { keyPath: "privatriid" });
};
}
};
request.onsuccess = (event) => {
@@ -541,7 +548,7 @@ apx.indexedDB.set = async (db, storeName, value) => {
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
}
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
@@ -589,7 +596,7 @@ apx.indexedDB.del = async (db, storeName, key) => {
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
}
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
@@ -602,3 +609,28 @@ apx.indexedDB.del = async (db, storeName, key) => {
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.getAllKeys = async (db, storeName) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const keysRequest = store.getAllKeys();
keysRequest.onsuccess = (event) => {
resolve(event.target.result);
};
keysRequest.onerror = (event) => {
reject(event);
};
};
request.onerror = (error) => reject(error);
});
}

View File

@@ -11,6 +11,8 @@ apx.apxauth.loadwco = async (id, ctx) => {
ctx
)}`
);
// Check that in localdb tpl exist if not means it is not authenticated
if (!apx.data.tpl[`apxauthscreen${ctx.link}`]) ctx.link="signin";
const tpldataname = `${apx.data.pagename}_${id}_apxauth`;
const apxauthid = document.getElementById(id);
const data = apx.apxauth.getdata(id, ctx);
@@ -31,7 +33,7 @@ apx.apxauth.getdata = (id, ctx) => {
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) {
@@ -44,7 +46,7 @@ apx.apxauth.getdata = (id, ctx) => {
});
data.noprofils = data.profils.length == 0;
data.member = apx.data.headers.xprofils.includes("persons");
data.websites = apx.data.appdata.websites;
data.websites = apx.data?.appdata?.websites || [];
data.optionlinks=apx.data.tpldata[tpldataname].optionlinks
data.town=apx.data.town
data.nation=apx.data.nation
@@ -52,10 +54,13 @@ apx.apxauth.getdata = (id, ctx) => {
apx.data.tpldata[tpldataname].optionlinksmajor.forEach(o=>data.optionlinks.push(o))
apx.save()
}
if (!apx.data.itms) apx.data.itms={}
if (!apx.data.wco) apx.data.wco={}
// get towns list
axios
.get(`/api/apxtri/odmdb/idx/apxtri/towns/towns`, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then((rep) => {
console.log(rep)
@@ -72,7 +77,7 @@ apx.apxauth.getdata = (id, ctx) => {
});
axios
.get(`/api/apxtri/odmdb/idx/apxtri/pagans/lst_alias`, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then((rep) => {
if (rep.status==200){
@@ -139,7 +144,7 @@ apx.apxauth.redirecturlwithauth = (
apx.apxauth.logout = () => {
axios
.get(`/api/apxtri/pagans/logout`, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then((rep) => {
console.log("logout", rep);
@@ -250,7 +255,7 @@ apx.apxauth.authentifyme = async (
console.log(`get /api/apxtri/pagans/alias/${alias}`);
axios
.get(`/api/apxtri/pagans/alias/${alias}`, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then(async (rep) => {
//console.log(rep.data);
@@ -351,13 +356,14 @@ apx.apxauth.recoverykey = (id, aliasoremail) => {
return false;
}
const recodata = { tribe: apx.data.headers.xtribe, search: aliasoremail };
recodata.emailalias = Checkjson.testformat(aliasoremail, "email")
const validator= new Checkjson({properties:{email:{type:"string",format:"email"}}});
recodata.emailalias = validator.verify({email:aliasoremail})
? "email"
: "alias";
document.querySelector(`#${id} .msginfo`).innerHTML = "";
axios
.post(`/api/apxtri/pagans/keyrecovery`, recodata, {
headers: apx.data.headers,
headers: apx.data.headers
})
.then((rep) => {
rep.data.data.search = aliasoremail;
@@ -561,7 +567,8 @@ apx.apxauth.createIdentity = async (id, alias, recoemail, passphrase = "") => {
);
return false;
}
if (recoemail.length > 0 && !Checkjson.testformat(recoemail, "email")) {
const validator= new Checkjson({properties:{email:{type:"string",format:"email"}}});
if (recoemail.length > 0 && !validator.verify({email:recoemail})) {
apx.notification(`#${id} .msginfo`, {
status: 406,
ref: "Pagans",
@@ -591,8 +598,9 @@ apx.apxauth.createIdentity = async (id, alias, recoemail, passphrase = "") => {
if (err.response && err.response.status == 404) {
// alias does not exist create it is possible
const keys = await apx.apxauth.generateKey(alias, passphrase);
apx.data.tmpauth = { keys, recoemail, passphrase };
//console.log(apx.data.tmpauth);
apx.data.tmpauth = { ...keys, recoemail, passphrase };
apx.save();
console.log(apx.data.tmpauth);
["publickey", "privatekey"].forEach((k) => {
console.log(`${id} button.signup${k}`);
const btn = document.querySelector(`#${id} button.signup${k}`);
@@ -647,13 +655,16 @@ apx.apxauth.test = () => {
};
apx.apxauth.registerIdentity = async (id, trustedtribe) => {
const authid = document.getElementById(id);
console.log("trustedtribe",trustedtribe)
// trustedtribe boolean
//previously store in apx.data.tmpauth={keys:{alias,privatekey,publickey},recoemail,passphrase}
console.log("tmpauth:",apx.data.tmpauth)
const setheaders = await apx.apxauth.setheadersauth(
apx.data.tmpauth.keys.alias,
apx.data.tmpauth.alias,
apx.data.tmpauth.passphrase,
apx.data.tmpauth.keys.publickey,
apx.data.tmpauth.keys.privatekey,
apx.data.tmpauth.publickey,
apx.data.tmpauth.privatekey,
false
);
if (setheaders.status != 200) {
@@ -662,31 +673,24 @@ apx.apxauth.registerIdentity = async (id, trustedtribe) => {
// add withpublickeyforcreate to check isAuthenticated alias does not already exist
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")
) {
data.passphrase = apx.data.tmpauth.keyspassphrase;
data.privatekey = apx.data.tmpauth.keysprivatekey;
data.alias = apx.data.tmpauth.alias;
data.publickey = apx.data.tmpauth.publickey;
if (apx.data.tmpauth.recoemail) {
data.passphrase = apx.data.tmpauth.passphrase;
data.privatekey = apx.data.tmpauth.privatekey;
data.email = apx.data.tmpauth.recoemail;
}
data.trustedtribe = trustedtribe;
console.log(data)
axios
.post(`/api/apxtri/pagans`, data, { headers: apx.data.headers })
.post(`/api/apxtri/pagans`, data, { headers: apx.data.headers, withCredentials:true})
.then((reppagan) => {
//console.log(reppagan.data);
apx.notification(`#${id} .msginfo`, reppagan.data);
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();
apx.data.headers.xprofils=reppagan.data.profils;
apx.data.save();
})
.catch((err) => {
console.log("error:", err);
@@ -713,13 +717,13 @@ apx.apxauth.jointribe = (id) => {
};
axios
.put(`/api/apxtri/pagans/person/${apx.data.headers.xtribe}`, data, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then((rep) => {
apx.notification(`#${id} .msginfo`, rep.data);
axios
.get(`/api/apxtri/pagans/logout`, {
headers: apx.data.headers,
headers: apx.data.headers, withCredentials:true
})
.then((rep) => {
console.log("logout", rep);

View File

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

View File

@@ -1,5 +1,5 @@
<div class="mt-1">
<label class="input validator mbt-1">
<div class="mt-1 w-full">
<label class="input validator mbt- w-full max-w-full">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -23,7 +23,7 @@
<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 />
<input id="inputaliasrecovery" class="w-full max-w-full" 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)

View File

@@ -1,4 +1,4 @@
<div class="space-y-6 text-justify">
<div class="mx-3 my-auto 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

View File

@@ -1,8 +1,9 @@
<div class="w-full">
<p data-wco="createid" class="text-center text-neutral-content">
{{{signintitle}}}
</p>
<div class="mt-2">
<label class="input validator">
<label class="input validator w-full">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -15,7 +16,7 @@
</g>
</svg>
<input
class="signinalias"
class="signinalias w-full"
type="input"
required
placeholder="alias"
@@ -26,9 +27,9 @@
/>
</label>
<p class="validator-hint hidden"> {{{aliasinvalid}}}</p>
</div>
<div class="mt-2">
<label class="input mt-1">
</div>
<div class="mt-2">
<label class="input mt-1 w-full">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -42,19 +43,19 @@
</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>
<div class="mt-2">
<textarea rows=5 class="mt-2 textarea signinprivatekey w-full" 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">
</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(
@@ -66,4 +67,5 @@
)">
{{{authentifyme}}}
</button>
</div>
</div>

View File

@@ -1,9 +1,10 @@
<p data-wco="createid" class="text-center text-neutral-content">
<div class="w-full">
<p data-wco="createid" class="text-center text-neutral-content w-full">
{{{signuptitle}}}
</p>
<div class="paramid">
</p>
<div class="paramid space-y-3 w-full">
<div class="mt-2">
<label class="input validator mbt-1">
<label class="input validator mbt-1 w-full">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -16,7 +17,7 @@
</g>
</svg>
<input
class="signupalias"
class="signupalias w-full"
type="input"
required
placeholder="alias"
@@ -31,7 +32,7 @@
</div>
</div>
<div class="mt-2">
<label class="input validator mt-1">
<label class="input validator mt-1 w-full">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -44,14 +45,14 @@
<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 />
<input class="signupemailrecovery w-full" 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">
<label class="input mt-1 w-full">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
@@ -63,7 +64,7 @@
<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)" />
<input type="text" class="signuppassphrase w-full" placeholder="passphrase (option)" />
</label>
</div>
<div class="mt-5">
@@ -79,13 +80,13 @@
{{{createkey}}}
</button>
</div>
</div>
<div class="getmykeys hidden mt-1">
</div>
<div class="getmykeys hidden mt-1 w-full">
<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">
<div class="flex-1 w-full">
<p class="text-sm text-justify" >{{{trusttext}}}</p>
</div>
</div>
@@ -117,5 +118,5 @@
{{{nextpage}}}
</button>
</div>
</div>
</div>

View File

@@ -43,6 +43,7 @@ apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => {
decryptionKeys: privateKey,
});
};
apx.crypto.isSignedby = async (
alias,
publicKey,
@@ -69,4 +70,69 @@ apx.crypto.isSignedby = async (
return false;
}
};
export default apx;
apx.crypto.sign = async (message, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
return await openpgp.sign(
{
message: await openpgp.createMessage(
{
text: message
}
),
signingKeys: privateKey
}
);
};
apx.crypto.verifySignature = async (message, signature, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
const verified = await openpgp.verify(
{
message: await openpgp.createMessage(
{
text: message
}
),
signature: await openpgp.readSignature(
{
armoredSignature: signature
}
),
verificationKeys: publicKey
}
);
if (await verified.signatures[0].verified) {
return true;
} else {
return false;
};
};
apx.crypto.genUUID = () => {
const uuidTemplate = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
return uuidTemplate.replace(/[xy]/g, (char) => {
const random = Math.random() * 16 | 0;
let value;
if (char === "x") {
value = random;
} else {
value = (random & 0x3) | 0x8;
};
return value.toString(16);
});
};

26
wco/itm/privatri.json Normal file
View File

@@ -0,0 +1,26 @@
{
"wconame": "privatri",
"owner": "philc",
"price": 0,
"aliascode": [],
"codehash": "123",
"thumbnail": "",
"title": "Gestion des accès privés",
"description": "Composant pour gérer les accès et les données privées.",
"lang": ["fr"],
"tpl": {
"privatrimain": "apxtri/objects/wco/privatri/main.mustache",
"privatriAlias": "apxtri/objects/wco/privatri/alias.mustache",
"privatriCreateThread": "apxtri/objects/wco/privatri/createThread.mustache",
"privatriEditMessage": "apxtri/objects/wco/privatri/editMessage.mustache",
"privatriInviteAlias": "apxtri/objects/wco/privatri/inviteAlias.mustache",
"privatriMessage": "apxtri/objects/wco/privatri/message.mustache",
"privatriThread": "apxtri/objects/wco/privatri/thread.mustache",
"privatriThreadAliasList": "apxtri/objects/wco/privatri/threadAliasList.mustache",
"privatriThreadSettings": "apxtri/objects/wco/privatri/threadSettings.mustache",
"privatriToastAlert": "apxtri/objects/wco/privatri/toastAlert.mustache"
},
"tpldatamodel": { "privatri": "apxtri/objects/wco/privatri/exampleprivatri" },
"schema": [],
"ref": {}
}

View File

@@ -0,0 +1,6 @@
<ul class="list">
<li class="list-row items-center">
<div class="text-xl font-medium">{{alias.decrypted}}</div>
<button class="removeAliasBtn btn btn-neutral w-fit rounded-lg ml-auto" data-alias="{{alias.crypted}}">Remove this alias</button>
</li>
</ul>

View File

@@ -0,0 +1,34 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">New thread</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." value="{threadName}" class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<textarea id="threadDescriptionInput" class="textarea rounded-lg" placeholder="Description..."></textarea>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="createThreadBtn" class="btn w-full rounded-lg mt-16">Create</button>
</div>

View File

@@ -0,0 +1,16 @@
<textarea class="message textarea rounded-lg">{{message}}</textarea>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button class="cancelEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</svg>
</button>
<button class="saveEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,9 @@
{
"title": "Titre du Composant Privatri",
"description": "Ceci est la description par défaut du composant, chargée depuis son modèle de données d'exemple.",
"items": [
"Élément 1 par défaut",
"Élément 2 par défaut",
"Élément 3 par défaut"
]
}

View File

@@ -0,0 +1,12 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit">
<h1 class="text-4xl">Invite an alias into this thread</h1>
<div id="qrCodeCanvas"></div>
<div class="input rounded-lg flex items-center w-full pr-0 gap-x-2 bg-base-100">
<input id="invitationLinkInput" type="text" placeholder="Url" class="flex-1 bg-transparent border-none outline-none shadow-none" readonly/>
<button id="copyBtn" type="button" class="p-2 rounded-lg fill-current text-base-content">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
<path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z" />
</svg>
</button>
</div>
</div>

106
wco/privatri/main.mustache Normal file
View File

@@ -0,0 +1,106 @@
<section class="w-fit md:min-w-110 h-screen max-h-screen bg-base-100 border-r border-solid border-r-base-300 relative flex flex-col">
<div class="w-full h-12 max-h-12 flex items-center justify-evenly mt-4 mb-4 flex-shrink-0">
<div class="dropdown h-full hidden md:block">
<div tabindex="0" role="button" class="h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
{{#appOptions}}
<li>
<a data-template="{{template}}" class="rounded-lg">{{{title}}}</a>
</li>
{{/appOptions}}
</ul>
</div>
<div class="drawer md:hidden">
<input id="my-drawer" type="checkbox" class="drawer-toggle"/>
<div class="drawer-content">
<label for="my-drawer" class="btn btn-ghost drawer-button h-full flex items-center hover:bg-transparent hover:shadow-none border-none">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</label>
</div>
<div class="drawer-side">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
{{#appOptions}}
<li>
<a data-template="{{template}}" class="rounded-lg">{{{title}}}</a>
</li>
{{/appOptions}}
</ul>
</div>
</div>
<label class="hidden md:flex input rounded-lg md-4">
<svg class="h-4 w-4 text-base-content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input id="threadSearchBar" type="search" required placeholder="Search for threads" class="flex-1 bg-transparent outline-none placeholder:opacity-75 placeholder:text-base-content text-base-content" />
</label>
<div class="hidden md:block dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</div>
<ul id="threadFilterOptions" tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li data-filter="date">
<p class="rounded-lg">Sort by date</p>
</li>
<li data-filter="name">
<p class="rounded-lg">Sort by name</p>
</li>
</ul>
</div>
</div>
<div id="threadsContainer" class="flex-1 overflow-y-auto flex flex-col"></div>
<a class="hidden md:flex w-16 h-16 btn bg-base-content rounded-full p-0 shadow-lg items-center justify-center absolute bottom-8 right-8">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-100">
<path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/>
</svg>
</a>
</section>
<section id="threadPage" class="w-full flex-1 bg-base-100 flex flex-col h-screen">
<div class="w-full h-12 max-h-12 mt-4 mb-4 flex items-center justify-center pl-8 pr-8">
<div class="flex-1 flex items-center justify-center">
<span id="threadName" class="text-xl">{threadName}</span>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm">
{{#threadOptions}}
<li>
<a data-template="{{template}}" class="rounded-lg">{{{title}}}</a>
</li>
{{/threadOptions}}
</ul>
</div>
</div>
<div id="messagesContainer" class="w-full flex-1 min-h-0 bg-base-200 overflow-y-auto flex flex-col p-4 gap-y-6"></div>
<div class="flex items-center justify-center w-full h-fit pt-4 pb-4 pl-2 pr-4 md:pl-8 md:pr-12 gap-x-2 md:gap-x-4">
<button id="attachmentsBtn" class="hover:bg-base-200 transition rounded-full p-2 flex items-center justify-center">
<input id="attachmentsInput" type="file" multiple accept="image/png, image/jpeg" class="hidden"/>
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z"/>
</svg>
</button>
<div class="flex items-center rounded-full w-full h-10 bg-base-300">
<input id="messageInput" type="text" placeholder="Write a message..." class="flex-1 bg-transparent border-transparent outline-none px-4" />
<button id="sendMessageBtn" type="button" class="flex justify-center items-center ml-2 p-2 rounded-full hover:bg-base-200 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M120-160v-640l760 320-760 320Zm80-120 474-200-474-200v140l240 60-240 60v140Zm0 0v-400 400Z" />
</svg>
</button>
</div>
</div>
</section>

View File

@@ -1,8 +0,0 @@
<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>

View File

@@ -0,0 +1,23 @@
<div data-timestamp="{{timestamp}}" class="group">
<div class="chat chat-start">
<div class="message chat-bubble rounded-lg text-base">
{{message}}
<div class="attachmentsContainer grid gap-2 mt-2 grid-cols-[repeat(auto-fit,minmax(5rem,1fr))] max-w-[20rem]"></div>
</div>
</div>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="hidden group-hover:flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button onclick="attachDeleteMessageEvent(this)" class="deleteMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/>
</svg>
</button>
<button onclick="attachEditMessageEvent(this)" class="editMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/>
</svg>
</button>
</div>
</div>
</div>

View File

@@ -1,194 +1,800 @@
/**
* @file privatri.js
* @description WCO component for managing private, thread-based messages in IndexedDB.
* @version 1.0
*/
var apx = apx || {};
((window) => {
'use strict';
apx.privatri = {};
apx.privatri.templates = {};
// --- Component Definition ---
const privatri = {};
const apiUrl = "http://admin.apxtri.farm.test/api/apxtri/privatri"
const tribe = "apxtri";
// --- Private State ---
let _db = null; // Holds the single, persistent IndexedDB connection
const DB_NAME = 'privatriDB';
const DB_VERSION = 1;
apx.privatri.loadwco = async (id, dataObj = {}, ctx = null) => {
// check if not authenticate, do nothing cause by default screensignin and wait authentification
// if authenticate, if url xhash then redirect if no url then change wco-link=screenmyworld
// 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:privatri apx.privatri.loadwco with id:${id} and ctx: ${JSON.stringify(
ctx
)}`
);
/**
* 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);
const templatesObj = JSON.parse(localStorage.getItem("admin")).tpl;
const template = Object.entries(templatesObj).find(([key]) => key.toLowerCase() === `privatri${id}`.toLocaleLowerCase())?.[1];
return Mustache.render(template, dataObj);
};
async function getOldestPrivatriids(db, storeName) {
const keysArray = await apx.indexedDB.getAllKeys(db, storeName);
const oldestPerUuid = {};
for (const key of keysArray) {
const [uuid, timestampStr] = key.split("_");
const timestamp = Number(timestampStr);
if (!oldestPerUuid[uuid] || timestamp < oldestPerUuid[uuid].timestamp) {
oldestPerUuid[uuid] = { key, timestamp };
};
};
/**
* 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 oldestKeysArray = Object.values(oldestPerUuid).map(obj => obj.key)
const request = indexedDB.open(DB_NAME, DB_VERSION);
const threadsArray = [];
// 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]);
for (key of oldestKeysArray) {
threadsArray.push(await apx.indexedDB.get(db, storeName, key));
};
initialStoreNames.forEach(storeName => {
if (!db.objectStoreNames.contains(storeName)) {
_log('log', `Creating new object store: "${storeName}"`);
db.createObjectStore(storeName, { keyPath: 'key' });
return threadsArray;
};
apx.privatri.syncronizeBackend = async (alias, lastConnection) => {
const threadsArray = await getOldestPrivatriids("privatri", "messages");
for (const threadObj of threadsArray) {
try {
const response = await fetch(`${apiUrl}/${tribe}/${threadObj.thread}?since=${lastConnection}`);
if (response.ok === false) {
throw new Error("HTTP error");
};
console.log(response);
// for (const message of response) {
// await apx.indexedDB.set("privatri", "messages", message);
// };
} catch (error) {
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
document.querySelector("body").insertAdjacentHTML("beforeend", await displayToastAlert("An error occurred while synchronizing messages. Please try again later."));
};
};
};
apx.privatri.templates.scripts = {
createThread: async () => {
const bodyEl = document.querySelector("body");
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});
document.querySelector("#createThreadBtn").addEventListener("click", async () => {
const { publicKey, privateKey } = await apx.crypto.genKey();
const messageObj = await (async (publicKey) => {
const uuid = apx.crypto.genUUID();
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("admin")).headers.xalias, publicKey);
return {
privatriid: `${uuid}_${timestamp}`,
thread: uuid,
timestamp: timestamp,
owner: alias,
title: await apx.crypto.encryptMessage(document.querySelector("#threadNameInput").value, publicKey),
sender_alias: alias,
publicKey: publicKey,
aliases: [
alias
],
message: await apx.crypto.encryptMessage(document.querySelector("#threadDescriptionInput").value, publicKey),
dt_autodestruction: (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})(),
urgencydeletion: document.querySelector('input[type="checkbox"].toggle').checked
};
})(publicKey);
try {
const response = await fetch(`${apiUrl}/${tribe}/${messageObj.thread}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"xdays": admin.headers.xdays,
"xalias": admin.headers.xalias,
"xlang": admin.headers.xlang,
"xtribe": admin.headers.xtribe,
"xapp": admin.headers.xapp,
"xuuid": admin.headers.xuuid
},
body: JSON.stringify(messageObj)
});
if (response.ok === false) {
throw new Error("HTTP error");
};
await apx.indexedDB.set("privatri", "threads", { uuid: messageObj.thread, privateKey: privateKey });
await apx.indexedDB.set("privatri", "messages", messageObj);
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Thread created successfully."));
} catch (error) {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("An error occurred while creating the thread. Please try again later."));
};
});
},
inviteAlias: async () => {
const url = "https://www.facebook.com/";
const bodyEl = document.querySelector("body");
const qrCodeCanvasEl = document.querySelector("#qrCodeCanvas");
const invitationLinkInputEl = document.querySelector("#invitationLinkInput");
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
const qrCode = new QRCodeStyling({
width: 425,
height: 425,
type: "svg",
data: url,
image: "static/img/icons/privatri.png",
dotsOptions: {
color: "#ffffff",
type: "rounded"
},
backgroundOptions: {
color: getComputedStyle(bodyEl).backgroundColor || "#000000",
},
imageOptions: {
crossOrigin: "anonymous",
margin: 20
}
});
qrCode.append(qrCodeCanvasEl);
invitationLinkInputEl.value = url;
copyBtn.addEventListener("click", async () => {
navigator.clipboard.writeText(invitationLinkInputEl.value);
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Invitation link copied to clipboard."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
});
},
threadAliasList: async () => {
const bodyEl = document.querySelector("body");
const aliasListContainerEl = document.querySelector("#aliasListContainer");
let aliasesArray = [];
const newAlias = async (alias) => {
return await apx.privatri.loadwco("alias", { alias });
};
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
privatriidArray.forEach(async privatriid => {
if (privatriid.thread === uuid) {
messageObj = privatriid;
aliasesArray = messageObj.aliases;
const ownerAlias = messageObj.owner;
for (const alias of aliasesArray) {
aliasListContainerEl.insertAdjacentHTML("beforeend", await newAlias({
decrypted: (await apx.crypto.decryptMessage(alias, privateKey)).data,
crypted: alias
}));
};
document.querySelectorAll("button.removeAliasBtn").forEach(btn => {
btn.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("admin")).headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
if (btn.getAttribute("data-alias") !== ownerAlias) {
const alias = btn.getAttribute("data-alias");
aliasesArray = aliasesArray.filter(a => a !== alias);
btn.parentElement.parentElement.remove();
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
const messageObj = await apx.indexedDB.get("privatri", "messages", privatriid);
messageObj.aliases = aliasesArray;
await apx.indexedDB.set("privatri", "messages", messageObj);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You cannot remove the owner of the thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to remove this alias."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
});
};
});
},
threadSettings: async () => {
const bodyEl = document.querySelector("body");
const threadNameInputEl = document.querySelector("#threadNameInput");
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
const applyModificationsBtnEl = document.querySelector("#applyModificationsBtn");
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
let messageObj = {};
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
let ownerAlias = "";
privatriidArray.forEach(async privatriid => {
if (privatriid.thread === uuid) {
messageObj = privatriid;
ownerAlias = messageObj.owner;
threadNameInputEl.value = (await apx.crypto.decryptMessage(messageObj.title, privateKey)).data;
(Array.from(autoDeletionBtnElArray).find(el => el.getAttribute("data-auto-deletion") === String(messageObj.dt_autodestruction))).classList.add("bg-base-200");
if (messageObj.urgencydeletion === true) {
document.querySelector('input[type="checkbox"].toggle').checked = true;
};
};
});
applyModificationsBtnEl.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("admin")).headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
messageObj.title = await apx.crypto.encryptMessage(threadNameInputEl.value, privateKey);
messageObj.dt_autodestruction = (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})();
messageObj.urgencydeletion = document.querySelector('input[type="checkbox"].toggle').checked;
await apx.indexedDB.set("privatri", "messages", messageObj);
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});
}
};
apx.ready(async () => {
document.querySelector("body").innerHTML = await apx.privatri.loadwco("main", JSON.parse(localStorage.getItem("admin")).tpldata.privatri_main_privatri);
const bodyEl = document.querySelector("body");
const searchInputEl = document.querySelector("#threadSearchBar");
const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li");
const threadsContainerEl = document.querySelector("#threadsContainer");
const threadPageEl = document.querySelector("#threadPage");
const messageInputEl = document.getElementById("messageInput");
const messagesContainerEl = document.querySelector("#messagesContainer");
const attachmentsInputEl = document.querySelector("#attachmentsInput");
await (async () => {
const lastConnection = JSON.parse(localStorage.getItem("lastConnection")) || Date.now();
// await apx.privatri.syncronizeBackend(apx.data.headers.xalias, lastConnection);
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
console.log(privatriidArray);
const thread = async (name, uuid) => {
return await apx.privatri.loadwco("thread", { uuid, name });
};
for (const privatriidObj of privatriidArray) {
const obj = await apx.indexedDB.get("privatri", "messages", privatriidObj.privatriid)
const privateKey = (await apx.indexedDB.get("privatri", "threads", obj.thread)).privateKey;
const name = (await apx.crypto.decryptMessage(obj.title, privateKey)).data;
threadsContainerEl.insertAdjacentHTML("beforeend", await thread(name, obj.thread));
};
Array.from(threadsContainerEl.children).forEach(child => {
child.addEventListener("click", async () => {
const uuid = child.getAttribute("data-uuid");
const messagesObj = await new Promise((resolve, reject) => {
const request = indexedDB.open("privatri", 1);
request.onsuccess = (event) => {
_log('log', 'Database opened successfully.');
_db = event.target.result;
const db = event.target.result;
// Generic error handler for the connection
_db.onerror = (event) => {
_log('error', 'Database error:', event.target.error);
const transaction = db.transaction("messages", "readonly");
const store = transaction.objectStore("messages");
const cursorRequest = store.openCursor();
const result = [];
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const obj = cursor.value;
const keyUuid = obj.privatriid.split("_")[0];
if (keyUuid === uuid) {
result.push(obj);
};
cursor.continue();
} else {
resolve(result);
};
};
resolve(_db);
cursorRequest.onerror = (event) => reject(event);
};
request.onerror = (event) => {
_log('error', 'Failed to open database:', event.target.error);
reject(event.target.error);
request.onerror = (event) => reject(event);
});
threadPageEl.setAttribute("data-uuid", uuid);
threadPageEl.querySelector("#threadName").innerText = (await apx.crypto.decryptMessage(messagesObj[0].title, (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey)).data;
messagesContainerEl.innerHTML = "";
let privateKey = "";
for (const message of messagesObj) {
if (privateKey === "") {
privateKey = (await apx.indexedDB.get("privatri", "threads", message.thread)).privateKey;
};
const decryptedMessage = (await apx.crypto.decryptMessage(message.message, privateKey)).data;
let decryptedAttachments = [];
if (message.attachments !== undefined) {
decryptedAttachments = await (async () => {
const attachmentsArray = [];
if (message.attachments.length > 0) {
for (const attachment of message.attachments) {
attachmentsArray.push({
fileType: attachment.fileType,
filename: attachment.filename,
content: (await apx.crypto.decryptMessage(attachment.content, privateKey)).data
});
};
// --- 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);
return attachmentsArray;
})();
};
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);
messagesContainerEl.insertAdjacentHTML("beforeend", await sendNewMessage(message.timestamp, decryptedMessage, decryptedAttachments));
};
});
});
threadsContainerEl.children[0]?.click();
document.querySelectorAll("a").forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
window.history.replaceState({}, document.title, window.location.pathname);
const templateName = link.getAttribute("data-template");
if (templateName === "threadAliasList" || templateName === "threadSettings") {
window.history.pushState({}, "", `${window.location.pathname}?uuid=${threadPageEl.getAttribute("data-uuid")}`);
};
/**
* 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 });
bodyEl.innerHTML = await apx.privatri.loadwco(templateName);
request.onsuccess = () => resolve();
request.onerror = (e) => {
_log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error);
reject(e.target.error);
await apx.privatri.templates.scripts[templateName]();
});
});
})();
function formatDate(timestamp) {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
searchInputEl.addEventListener("input", () => {
const value = searchInputEl.value.toLowerCase();
Array.from(threadsContainerEl.children).forEach(child => {
if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) {
child.style.display = "block";
} else {
child.style.display = "none";
};
});
};
});
/**
* 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);
threadFilterOptionsElArray.forEach(option => {
option.addEventListener("click", async () => {
const filter = option.getAttribute("data-filter");
request.onsuccess = () => resolve();
request.onerror = (e) => {
_log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error);
reject(e.target.error);
};
if (filter === "date") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > span").getAttribute("data-timestamp") - b.querySelector("li > span").getAttribute("data-timestamp");
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
} else if (filter === "name") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > div").textContent.localeCompare(b.querySelector("li > div").textContent);
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
};
});
});
/**
* 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.');
const sendNewMessage = async (timestamp, message, attachmentsArray) => {
const messageTemplate = await apx.privatri.loadwco("message", { timestamp, message, date: formatDate(timestamp) });
const tempNode = document.createElement("div");
tempNode.innerHTML = messageTemplate;
const messageNode = tempNode.firstElementChild;
attachmentsArray.forEach(attachment => {
messageNode.querySelector("div.attachmentsContainer").insertAdjacentHTML("beforeend", `<img onclick="attachEventDisplayAttachment(this)" class="w-20 h-20 object-cover rounded" src="data:${attachment.fileType};base64,${attachment.content}" alt="${attachment.filename}"/>`);
});
return messageNode.outerHTML;
};
// Expose the component to the global window object
window.privatri = privatri;
const editMessage = async (timestamp, message) => {
return await apx.privatri.loadwco("editMessage", { message, date: formatDate(timestamp) });
};
})(window);
const displayToastAlert = async (message) => {
return await apx.privatri.loadwco("toastAlert", { message });
};
sendMessageBtn.addEventListener("click", async () => {
const message = messageInputEl.value.trim();
if (message !== "") {
const messageObj = await (async (publicKey) => {
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("admin")).headers.xalias, publicKey);
return {
privatriid: `${threadPageEl.getAttribute("data-uuid")}_${timestamp}`,
timestamp: timestamp,
sender_alias: alias,
message: await apx.crypto.encryptMessage(message, publicKey),
attachments: await (async () => {
const attachmentsArray = [];
const localAttachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || [];
if (localAttachmentsArray.length > 0) {
for (const attachment of localAttachmentsArray) {
attachmentsArray.push(
{
fileType: attachment.fileType,
filename: attachment.filename,
content: await apx.crypto.encryptMessage(attachment.content, publicKey)
}
);
};
};
return attachmentsArray;
})()
};
})((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messagesContainerEl.firstElementChild.getAttribute("data-timestamp")}`)).publicKey);
const newMessageHTML = await sendNewMessage(messageObj.timestamp, message, JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []);
sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`);
messagesContainerEl.insertAdjacentHTML("beforeend", newMessageHTML);
messageInputEl.value = "";
try {
const response = await fetch(`${apiUrl}/`);
if (response.ok === false) {
throw new Error("HTTP error");
};
await apx.indexedDB.set("privatri", "messages", messageObj);
} catch (error) {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("An error occurred while sending the message. Please try again later."));
};
};
});
window.addEventListener("beforeunload", () => {
localStorage.setItem("lastConnection", JSON.stringify(Date.now()));
});
window.attachDeleteMessageEvent = async function attachDeleteMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const signatureMessage = `${JSON.parse(localStorage.getItem("admin")).headers.xalias}_${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`;
const signature = await apx.crypto.sign(signatureMessage, privateKey);
let verified = false;
// try {
// verified = await fetch("", {
// method: "GET",
// body: JSON.stringify({
// message: signatureMessage,
// signature: signature,
// uuid: threadPageEl.getAttribute("data-uuid"),
// timestamp: messageEl.getAttribute("data-timestamp"),
// })
// });
// } catch (error) {
// console.error("Error while verifying signature:", error);
// };
const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, privateKey)).data;
if ((JSON.parse(localStorage.getItem("admin")).headers.xalias === authorAlias) && (verified === true)) {
await apx.indexedDB.del("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.remove();
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to delete this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEditMessageEvent = async function attachEditMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey)).data;
if (JSON.parse(localStorage.getItem("admin")).headers.xalias === authorAlias) {
const messageValue = messageEl.querySelector("div.message").innerText;
const attachmentsArray = (() => {
const attachmentsArray = [];
messageEl.querySelector("div.attachmentsContainer").querySelectorAll("img").forEach(img => {
attachmentsArray.push(
{
fileType: img.getAttribute("src").match(/^data:(.*);base64,/)[1],
filename: img.getAttribute("alt"),
content: img.getAttribute("src").split(",")[1]
}
);
});
return attachmentsArray;
})();
messageEl.innerHTML = await editMessage(parseInt(messageEl.getAttribute("data-timestamp"), 10), messageEl.querySelector("div.message").innerText);
messageEl.querySelector("button.cancelEditBtn").addEventListener("click", async () => {
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, messageValue, attachmentsArray);
});
messageEl.querySelector("button.saveEditBtn").addEventListener("click", async () => {
const newMessageValue = messageEl.querySelector("textarea").value.trim();
if (newMessageValue !== "") {
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageObj.message = await apx.crypto.encryptMessage(newMessageValue, privateKey),
await apx.indexedDB.set("privatri", "messages", messageObj);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, newMessageValue, attachmentsArray);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEventDisplayAttachment = function(img) {
let overlay = document.getElementById("imageOverlay");
if (overlay === null) {
overlay = document.createElement('div');
overlay.id = "imageOverlay";
overlay.style.position = "fixed";
overlay.style.top = 0;
overlay.style.left = 0;
overlay.style.width = "100vw";
overlay.style.height = "100vh";
overlay.style.background = "rgba(0, 0, 0, 0.75)";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.zIndex = 999;
overlay.onclick = () => {
overlay.remove();
};
document.body.appendChild(overlay);
} else {
overlay.innerHTML = "";
overlay.style.display = "flex";
};
const fullScreenImage = document.createElement("img");
fullScreenImage.src = img.src;
fullScreenImage.alt = img.alt;
fullScreenImage.style.maxWidth = "90vw";
fullScreenImage.style.maxHeight = "90vh";
overlay.appendChild(fullScreenImage);
};
document.querySelector("#attachmentsBtn").addEventListener("click", () => {
attachmentsInputEl.click();
});
attachmentsInputEl.addEventListener("change", async () => {
const filesArray = Array.from(attachmentsInputEl.files);
const maxFileSize = 5 * 1024 * 1024;
const maxSize = 512;
for (const file of filesArray) {
if (file.size <= maxFileSize) {
const attachmentObj = await new Promise((resolve, reject) => {
const attachmentObj = {
fileType: file.type,
filename: file.name,
content: ""
};
const img = new Image();
img.onload = () => {
if (img.width > maxSize || img.height > maxSize) {
let width = img.width;
let height = img.height;
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
};
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
};
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(blob => {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
}, "image/jpeg", 0.8);
} else {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(file);
};
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
return attachmentObj;
});
if (sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`) === null) {
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify([attachmentObj]));
} else {
const attachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`));
attachmentsArray.push(attachmentObj);
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify(attachmentsArray));
};
};
};
});
});

View File

@@ -0,0 +1,7 @@
<ul data-uuid="{{uuid}}" class="list hover:bg-base-200 transition">
<li class="gap-x-0 md:gap-x-4 list-row items-center">
<img class="size-12 rounded-full bg-base-content" src=""/>
<div class="hidden md:block text-xl font-medium">{{name}}</div>
<span class="hidden md:flex badge h-8 bg-info text-info-content font-semibold border-transparent">17</span>
</li>
</ul>

View File

@@ -0,0 +1,6 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-100 h-full">
<h1 class="text-4xl flex-shrink-0 mt-8">Alias list</h1>
<div id="aliasListContainer" class="overflow-y-auto flex flex-col w-full flex-1 min-h-0">
</div>
</div>

View File

@@ -0,0 +1,33 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">Thread settings</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="applyModificationsBtn" class="btn w-full rounded-lg mt-16">Apply modifications</button>
</div>

View File

@@ -0,0 +1,5 @@
<div class="toast">
<div class="alert alert-info">
<span>{{message}}</span>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<ul class="list">
<li class="list-row items-center">
<div class="text-xl font-medium">{{alias.decrypted}}</div>
<button class="removeAliasBtn btn btn-neutral w-fit rounded-lg ml-auto" data-alias="{{alias.crypted}}">Remove this alias</button>
</li>
</ul>

194
wco/privatri_mathieu/apx.js Normal file
View File

@@ -0,0 +1,194 @@
var apx = apx || {};
apx.crypto = apx.crypto || {};
apx.indexedDB = apx.indexedDB || {};
apx.crypto.genKey = async (uuid) => {
return await openpgp.generateKey(
{
type: "ecc",
curve: "curve25519",
userIDs: [
{
alias: uuid
}
],
passphrase: "",
format: "armored",
}
);
};
apx.crypto.encryptMessage = async (message, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
return await openpgp.encrypt(
{
message: await openpgp.createMessage(
{
text: message
}
),
encryptionKeys: publicKey
}
);
};
apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
const message = await openpgp.readMessage(
{
armoredMessage: encryptedMessage
}
);
return await openpgp.decrypt(
{
message,
decryptionKeys: privateKey
}
);
};
apx.crypto.sign = async (message, privateKey) => {
privateKey = await openpgp.readPrivateKey(
{
armoredKey: privateKey
}
);
return await openpgp.sign(
{
message: await openpgp.createMessage(
{
text: message
}
),
signingKeys: privateKey
}
);
};
apx.crypto.verifySignature = async (message, signature, publicKey) => {
publicKey = await openpgp.readKey(
{
armoredKey: publicKey
}
);
const verified = await openpgp.verify(
{
message: await openpgp.createMessage(
{
text: message
}
),
signature: await openpgp.readSignature(
{
armoredSignature: signature
}
),
verificationKeys: publicKey
}
);
if (await verified.signatures[0].verified) {
return true;
} else {
return false;
};
};
apx.indexedDB.set = async (db, storeName, value) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("threads")) {
db.createObjectStore("threads", { keyPath: "uuid" });
};
if (!db.objectStoreNames.contains("messages")) {
db.createObjectStore("messages", { keyPath: "privatriid" });
};
};
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const putRequest = store.put(value);
putRequest.onsuccess = () => resolve();
putRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.get = async (db, storeName, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve(null);
}
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const getRequest = store.get(key);
getRequest.onsuccess = () => {
resolve(getRequest.result || null);
};
getRequest.onerror = () => resolve(null);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.del = async (db, storeName, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const deleteRequest = store.delete(key);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};
export default apx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,44 @@
import apx from "./apx.js";
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});
document.querySelector("#createThreadBtn").addEventListener("click", async () => {
const { publicKey, privateKey } = await apx.crypto.genKey();
const messageObj = await (async (publicKey) => {
const uuid = crypto.randomUUID();
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey);
return {
privatriid: `${uuid}_${timestamp}`,
thread: uuid,
timestamp: timestamp,
owner: alias,
title: await apx.crypto.encryptMessage(document.querySelector("#threadNameInput").value, publicKey),
sender_alias: alias,
publicKey: publicKey,
aliases: [
alias
],
message: await apx.crypto.encryptMessage(document.querySelector("#threadDescriptionInput").value, publicKey),
dt_autodestruction: (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})(),
urgencydeletion: document.querySelector('input[type="checkbox"].toggle').checked
};
})(publicKey);
// Faire un post sur l'endpoint /privatri
await apx.indexedDB.set("privatri", "threads", { uuid: messageObj.thread, privateKey: privateKey });
await apx.indexedDB.set("privatri", "messages", messageObj);
});

View File

@@ -0,0 +1,34 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">New thread</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." value="{threadName}" class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<textarea id="threadDescriptionInput" class="textarea rounded-lg" placeholder="Description..."></textarea>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="createThreadBtn" class="btn w-full rounded-lg mt-16">Create</button>
</div>

View File

@@ -0,0 +1,16 @@
<textarea class="message textarea rounded-lg">{{message}}</textarea>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button class="cancelEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</svg>
</button>
<button class="saveEditBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,52 @@
const url = "https://www.facebook.com/";
const bodyEl = document.querySelector("body");
const qrCodeCanvasEl = document.querySelector("#qrCodeCanvas");
const invitationLinkInputEl = document.querySelector("#invitationLinkInput");
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
(async () => {
const qrCode = new QRCodeStyling({
width: 425,
height: 425,
type: "svg",
data: url,
image: "./assets/icon.png",
dotsOptions: {
color: "#ffffff",
type: "rounded"
},
backgroundOptions: {
color: getComputedStyle(bodyEl).backgroundColor || "#000000",
},
imageOptions: {
crossOrigin: "anonymous",
margin: 20
}
});
qrCode.append(qrCodeCanvasEl);
invitationLinkInputEl.value = url;
copyBtn.addEventListener("click", async () => {
navigator.clipboard.writeText(invitationLinkInputEl.value);
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Invitation link copied to clipboard."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
});
})();

View File

@@ -0,0 +1,12 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit">
<h1 class="text-4xl">Invite an alias into this thread</h1>
<div id="qrCodeCanvas"></div>
<div class="input rounded-lg flex items-center w-full pr-0 gap-x-2 bg-base-100">
<input id="invitationLinkInput" type="text" placeholder="Url" class="flex-1 bg-transparent border-none outline-none shadow-none" readonly/>
<button id="copyBtn" type="button" class="p-2 rounded-lg fill-current text-base-content">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
<path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z" />
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./output.css">
<script type="module" src="./privatri.js" defer></script>
<script src="https://unpkg.com/mustache@latest/mustache.min.js"></script>
<script src="https://unpkg.com/openpgp@latest/dist/openpgp.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/qr-code-styling@1.5.0/lib/qr-code-styling.js"></script>
<title>Document</title>
</head>
<body class="bg-base-100 flex items-center justify-center h-screen w-screen ">
<section class="w-fit min-w-110 h-screen max-h-screen bg-base-100 border-r border-solid border-r-base-300 relative flex flex-col">
<div class="w-full h-12 max-h-12 flex items-center justify-evenly mt-4 mb-4 flex-shrink-0">
<div class="dropdown h-full">
<div tabindex="0" role="button" class="h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li>
<a class="rounded-lg">Notifications</a>
</li>
<li>
<a class="rounded-lg" data-template="createThread">New thread</a>
</li>
<li>
<a class="rounded-lg">Account</a>
</li>
</ul>
</div>
<label class="input rounded-lg md-4">
<svg class="h-4 w-4 text-base-content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input id="threadSearchBar" type="search" required placeholder="Search for threads" class="flex-1 bg-transparent outline-none placeholder:opacity-75 placeholder:text-base-content text-base-content" />
</label>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</div>
<ul id="threadFilterOptions" tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li data-filter="date">
<p class="rounded-lg">Sort by date</p>
</li>
<li data-filter="name">
<p class="rounded-lg">Sort by name</p>
</li>
</ul>
</div>
</div>
<div id="threadsContainer" class="flex-1 overflow-y-auto flex flex-col"></div>
<a class="w-16 h-16 btn bg-base-content rounded-full p-0 shadow-lg flex items-center justify-center absolute bottom-8 right-8" data-template="createThread">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-100">
<path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/>
</svg>
</a>
</section>
<section id="threadPage" class="w-full flex-1 bg-base-100 flex flex-col h-screen">
<div class="w-full h-12 max-h-12 mt-4 mb-4 flex items-center justify-center pl-8 pr-8">
<div class="flex-1 flex items-center justify-center">
<span id="threadName" class="text-xl">{threadName}</span>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm">
<li>
<a class="rounded-lg" data-template="inviteAlias">Invite an alias</a>
</li>
<li>
<a class="rounded-lg" data-template="threadAliasList">See all alias</a>
</li>
<li>
<a class="rounded-lg" data-template="threadSettings">Settings</a>
</li>
</ul>
</div>
</div>
<div id="messagesContainer" class="w-full flex-1 min-h-0 bg-base-200 overflow-y-auto flex flex-col p-4 gap-y-6"></div>
<div class="flex items-center justify-center w-full h-fit pt-4 pb-4 pl-8 pr-8 gap-x-4">
<button id="attachmentsBtn" class="hover:bg-base-200 transition rounded-full p-2 flex items-center justify-center">
<input id="attachmentsInput" type="file" multiple accept="image/png, image/jpeg" class="hidden"/>
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z"/>
</svg>
</button>
<div class="flex items-center rounded-full w-full h-10 bg-base-300">
<input id="messageInput" type="text" placeholder="Write a message..." class="flex-1 bg-transparent border-transparent outline-none px-4" />
<button id="sendMessageBtn" type="button" class="flex justify-center items-center ml-2 p-2 rounded-full hover:bg-base-200 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M120-160v-640l760 320-760 320Zm80-120 474-200-474-200v140l240 60-240 60v140Zm0 0v-400 400Z" />
</svg>
</button>
</div>
</section>
</body>
</html>

View File

@@ -0,0 +1,93 @@
<section class="w-fit min-w-110 h-screen max-h-screen bg-base-100 border-r border-solid border-r-base-300 relative flex flex-col">
<div class="w-full h-12 max-h-12 flex items-center justify-evenly mt-4 mb-4 flex-shrink-0">
<div class="dropdown h-full">
<div tabindex="0" role="button" class="h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li>
<a href="./notificationsMenu.html" class="rounded-lg">Notifications</a>
</li>
<li>
<a href="./newMenu3.html" class="rounded-lg">New thread</a>
</li>
<li>
<a class="rounded-lg">Account</a>
</li>
</ul>
</div>
<label class="input rounded-lg md-4">
<svg class="h-4 w-4 text-base-content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input id="threadSearchBar" type="search" required placeholder="Search for threads" class="flex-1 bg-transparent outline-none placeholder:opacity-75 placeholder:text-base-content text-base-content" />
</label>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</div>
<ul id="threadFilterOptions" tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm mt-2">
<li data-filter="date">
<p class="rounded-lg">Sort by date</p>
</li>
<li data-filter="name">
<a class="rounded-lg">Sort by name</a>
</li>
</ul>
</div>
</div>
<div id="threadsContainer" class="flex-1 overflow-y-auto flex flex-col"></div>
<a href="./newMenu3.html" class="w-16 h-16 btn bg-base-content rounded-full p-0 shadow-lg flex items-center justify-center absolute bottom-8 right-8">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-100">
<path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/>
</svg>
</a>
</section>
<section id="threadPage" class="w-full flex-1 bg-base-100 flex flex-col h-screen">
<div class="w-full h-12 max-h-12 mt-4 mb-4 flex items-center justify-center pl-8 pr-8">
<div class="flex-1 flex items-center justify-center">
<span id="threadName" class="text-xl">{threadName}</span>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="m-1">
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="h-full fill-current text-base-content">
<path d="M240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-1 w-52 p-2 shadow-sm">
<li>
<a href="./inviteAllias.html" class="rounded-lg">Invite an alias</a>
</li>
<li>
<a id="aliasListHyperlink" class="rounded-lg">See all alias</a>
</li>
<li>
<a id="settingsHyperlink" class="rounded-lg">Settings</a>
</li>
</ul>
</div>
</div>
<div id="messagesContainer" class="w-full flex-1 min-h-0 bg-base-200 overflow-y-auto flex flex-col p-4 gap-y-6"></div>
<div class="flex items-center justify-center w-full h-fit pt-4 pb-4 pl-8 pr-8 gap-x-4">
<button id="attachmentsBtn" class="hover:bg-base-200 transition rounded-full p-2 flex items-center justify-center">
<input id="attachmentsInput" type="file" multiple accept="image/png, image/jpeg" class="hidden"/>
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z"/>
</svg>
</button>
<div class="flex items-center rounded-full w-full h-10 bg-base-300">
<input id="messageInput" type="text" placeholder="Write a message..." class="flex-1 bg-transparent border-transparent outline-none px-4" />
<button id="sendMessageBtn" type="button" class="flex justify-center items-center ml-2 p-2 rounded-full hover:bg-base-200 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="28px" viewBox="0 -960 960 960" width="28px" class="h-full fill-current text-base-content opacity-50">
<path d="M120-160v-640l760 320-760 320Zm80-120 474-200-474-200v140l240 60-240 60v140Zm0 0v-400 400Z" />
</svg>
</button>
</div>
</section>

View File

@@ -0,0 +1,23 @@
<div data-timestamp="{{timestamp}}" class="group">
<div class="chat chat-start">
<div class="message chat-bubble rounded-lg text-base">
{{message}}
<div class="attachmentsContainer grid gap-2 mt-2 grid-cols-[repeat(auto-fit,minmax(5rem,1fr))] max-w-[20rem]"></div>
</div>
</div>
<div class="flex">
<span class="text-base-content opacity-50 text-sm ml-4">{{date}}</span>
<div class="hidden group-hover:flex badge rounded-lg w-20 ml-4 gap-0 p-0">
<button onclick="attachDeleteMessageEvent(this)" class="deleteMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tl-lg rounded-bl-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/>
</svg>
</button>
<button onclick="attachEditMessageEvent(this)" class="editMessageBtn flex justify-center items-center flex-1 pt-1 pb-1 rounded-tr-lg rounded-br-lg h-full hover:bg-base-300 transition">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="h-4 fill-current text-base-content">
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/>
</svg>
</button>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,506 @@
import apx from "./apx.js";
import { getOldestPrivatriids, syncronizeBackend } from "./utils.js";
const bodyEl = document.querySelector("body");
const searchInputEl = document.querySelector("#threadSearchBar");
const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li");
const threadsContainerEl = document.querySelector("#threadsContainer");
const threadPageEl = document.querySelector("#threadPage");
const messageInputEl = document.getElementById("messageInput");
const messagesContainerEl = document.querySelector("#messagesContainer");
const attachmentsInputEl = document.querySelector("#attachmentsInput");
function formatDate(timestamp) {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
searchInputEl.addEventListener("input", () => {
const value = searchInputEl.value.toLowerCase();
Array.from(threadsContainerEl.children).forEach(child => {
if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) {
child.style.display = "block";
} else {
child.style.display = "none";
};
});
});
threadFilterOptionsElArray.forEach(option => {
option.addEventListener("click", async () => {
const filter = option.getAttribute("data-filter");
if (filter === "date") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > span").getAttribute("data-timestamp") - b.querySelector("li > span").getAttribute("data-timestamp");
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
} else if (filter === "name") {
Array.from(threadsContainerEl.children).sort((a, b) => {
return a.querySelector("li > div").textContent.localeCompare(b.querySelector("li > div").textContent);
}).forEach(child => {
threadsContainerEl.appendChild(child);
});
};
});
});
const sendNewMessage = async (timestamp, message, attachmentsArray) => {
let messageTemplate = "";
await fetch("./message.mustache")
.then(res => res.text())
.then(template => {
messageTemplate = template;
});
messageTemplate = Mustache.render(messageTemplate, { timestamp, message, date: formatDate(timestamp) });
const tempNode = document.createElement("div");
tempNode.innerHTML = messageTemplate;
const messageNode = tempNode.firstElementChild;
attachmentsArray.forEach(attachment => {
messageNode.querySelector("div.attachmentsContainer").insertAdjacentHTML("beforeend", `<img onclick="attachEventDisplayAttachment(this)" class="w-20 h-20 object-cover rounded" src="data:${attachment.fileType};base64,${attachment.content}" alt="${attachment.filename}"/>`);
});
return messageNode.outerHTML;
};
const editMessage = async (timestamp, message) => {
let editMessageTemplate = "";
await fetch("./editMessage.mustache")
.then(res => res.text())
.then(template => {
editMessageTemplate = template;
});
return Mustache.render(editMessageTemplate, { message, date: formatDate(timestamp) });
};
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
sendMessageBtn.addEventListener("click", async () => {
const message = messageInputEl.value.trim();
if (message !== "") {
const messageObj = await (async (publicKey) => {
const timestamp = Date.now();
const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey);
return {
privatriid: `${threadPageEl.getAttribute("data-uuid")}_${timestamp}`,
timestamp: timestamp,
sender_alias: alias,
message: await apx.crypto.encryptMessage(message, publicKey),
attachments: await (async () => {
const attachmentsArray = [];
const localAttachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || [];
if (localAttachmentsArray.length > 0) {
for (const attachment of localAttachmentsArray) {
attachmentsArray.push(
{
fileType: attachment.fileType,
filename: attachment.filename,
content: await apx.crypto.encryptMessage(attachment.content, publicKey)
}
);
};
};
return attachmentsArray;
})()
};
})((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messagesContainerEl.firstElementChild.getAttribute("data-timestamp")}`)).publicKey);
const newMessageHTML = await sendNewMessage(messageObj.timestamp, message, JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []);
sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`);
messagesContainerEl.insertAdjacentHTML("beforeend", newMessageHTML);
messageInputEl.value = "";
// Faire un post sur l'endpoint /privatri
await apx.indexedDB.set("privatri", "messages", messageObj);
};
});
document.addEventListener("DOMContentLoaded", async () => {
const lastConnection = JSON.parse(localStorage.getItem("lastConnection")) || Date.now();
await syncronizeBackend(lastConnection);
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
const thread = async (name, uuid) => {
let threadTemplate = "";
await fetch("./thread.mustache")
.then(res => res.text())
.then(template => {
threadTemplate = template;
});
return Mustache.render(threadTemplate, { uuid, name });
};
for (const privatriid of privatriidArray) {
const obj = await apx.indexedDB.get("privatri", "messages", privatriid)
const privateKey = (await apx.indexedDB.get("privatri", "threads", obj.thread)).privateKey;
const name = (await apx.crypto.decryptMessage(obj.title, privateKey)).data;
threadsContainerEl.insertAdjacentHTML("beforeend", await thread(name, obj.thread));
};
Array.from(threadsContainerEl.children).forEach(child => {
child.addEventListener("click", async () => {
const uuid = child.getAttribute("data-uuid");
const messagesObj = await new Promise((resolve, reject) => {
const request = indexedDB.open("privatri", 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction("messages", "readonly");
const store = transaction.objectStore("messages");
const cursorRequest = store.openCursor();
const result = [];
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const obj = cursor.value;
const keyUuid = obj.privatriid.split("_")[0];
if (keyUuid === uuid) {
result.push(obj);
};
cursor.continue();
} else {
resolve(result);
};
};
cursorRequest.onerror = (event) => reject(event);
};
request.onerror = (event) => reject(event);
});
threadPageEl.setAttribute("data-uuid", uuid);
threadPageEl.querySelector("#threadName").innerText = (await apx.crypto.decryptMessage(messagesObj[0].title, (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey)).data;
messagesContainerEl.innerHTML = "";
let privateKey = "";
for (const message of messagesObj) {
if (privateKey === "") {
privateKey = (await apx.indexedDB.get("privatri", "threads", message.thread)).privateKey;
};
const decryptedMessage = (await apx.crypto.decryptMessage(message.message, privateKey)).data;
let decryptedAttachments = [];
if (message.attachments !== undefined) {
decryptedAttachments = await (async () => {
const attachmentsArray = [];
if (message.attachments.length > 0) {
for (const attachment of message.attachments) {
attachmentsArray.push({
fileType: attachment.fileType,
filename: attachment.filename,
content: (await apx.crypto.decryptMessage(attachment.content, privateKey)).data
});
};
};
return attachmentsArray;
})();
};
messagesContainerEl.insertAdjacentHTML("beforeend", await sendNewMessage(message.timestamp, decryptedMessage, decryptedAttachments));
};
});
});
document.querySelectorAll("a").forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
window.history.replaceState({}, document.title, window.location.pathname);
const templateName = link.getAttribute("data-template");
if (templateName === "threadAliasList" || templateName === "threadSettings") {
window.history.pushState({}, "", `${window.location.pathname}?uuid=${threadPageEl.getAttribute("data-uuid")}`);
};
await fetch(`./${templateName}.mustache`)
.then(res => res.text())
.then(template => {
template = Mustache.render(template, {});
bodyEl.innerHTML = template;
});
const script = document.createElement("script");
script.type = "module";
script.src = `./${templateName}.js`;
bodyEl.appendChild(script);
});
});
});
window.addEventListener("beforeunload", () => {
if (apx)
localStorage.setItem("lastConnection", JSON.stringify(Date.now()));
});
window.attachDeleteMessageEvent = async function attachDeleteMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const signatureMessage = `${JSON.parse(localStorage.getItem("apx")).data.headers.xalias}_${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`;
const signature = await apx.crypto.sign(signatureMessage, privateKey);
let verified = false;
try {
verified = await fetch("", {
method: "GET",
body: JSON.stringify({
message: signatureMessage,
signature: signature,
uuid: threadPageEl.getAttribute("data-uuid"),
timestamp: messageEl.getAttribute("data-timestamp"),
})
});
} catch (error) {
console.error("Error while verifying signature:", error);
};
const authorAlias = await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, privateKey).data;
if ((JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) && (verified === true)) {
await apx.indexedDB.del("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.remove();
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to delete this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEditMessageEvent = async function attachEditMessageEvent(btn) {
const messageEl = btn.parentElement.parentElement.parentElement;
const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey)).data;
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) {
const messageValue = messageEl.querySelector("div.message").innerText;
const attachmentsArray = (() => {
const attachmentsArray = [];
messageEl.querySelector("div.attachmentsContainer").querySelectorAll("img").forEach(img => {
attachmentsArray.push(
{
fileType: img.getAttribute("src").match(/^data:(.*);base64,/)[1],
filename: img.getAttribute("alt"),
content: img.getAttribute("src").split(",")[1]
}
);
});
return attachmentsArray;
})();
messageEl.innerHTML = await editMessage(parseInt(messageEl.getAttribute("data-timestamp"), 10), messageEl.querySelector("div.message").innerText);
messageEl.querySelector("button.cancelEditBtn").addEventListener("click", async () => {
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, messageValue, attachmentsArray);
});
messageEl.querySelector("button.saveEditBtn").addEventListener("click", async () => {
const newMessageValue = messageEl.querySelector("textarea").value.trim();
if (newMessageValue !== "") {
const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey;
const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`);
messageObj.message = await apx.crypto.encryptMessage(newMessageValue, privateKey),
await apx.indexedDB.set("privatri", "messages", messageObj);
messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, newMessageValue, attachmentsArray);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this message."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
};
window.attachEventDisplayAttachment = function(img) {
let overlay = document.getElementById("imageOverlay");
if (overlay === null) {
overlay = document.createElement('div');
overlay.id = "imageOverlay";
overlay.style.position = "fixed";
overlay.style.top = 0;
overlay.style.left = 0;
overlay.style.width = "100vw";
overlay.style.height = "100vh";
overlay.style.background = "rgba(0, 0, 0, 0.75)";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.zIndex = 999;
overlay.onclick = () => {
overlay.remove();
};
document.body.appendChild(overlay);
} else {
overlay.innerHTML = "";
overlay.style.display = "flex";
};
const fullScreenImage = document.createElement("img");
fullScreenImage.src = img.src;
fullScreenImage.alt = img.alt;
fullScreenImage.style.maxWidth = "90vw";
fullScreenImage.style.maxHeight = "90vh";
overlay.appendChild(fullScreenImage);
};
document.querySelector("#attachmentsBtn").addEventListener("click", () => {
attachmentsInputEl.click();
});
attachmentsInputEl.addEventListener("change", async () => {
const filesArray = Array.from(attachmentsInputEl.files);
const maxFileSize = 5 * 1024 * 1024;
const maxSize = 512;
for (const file of filesArray) {
if (file.size <= maxFileSize) {
const attachmentObj = await new Promise((resolve, reject) => {
const attachmentObj = {
fileType: file.type,
filename: file.name,
content: ""
};
const img = new Image();
img.onload = () => {
if (img.width > maxSize || img.height > maxSize) {
let width = img.width;
let height = img.height;
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
};
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
};
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(blob => {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
}, "image/jpeg", 0.8);
} else {
const reader = new FileReader();
reader.onload = (event) => {
attachmentObj.content = event.target.result.split(",")[1];
resolve(attachmentObj);
};
reader.onerror = reject;
reader.readAsDataURL(file);
};
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
return attachmentObj;
});
if (sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`) === null) {
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify([attachmentObj]));
} else {
const attachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`));
attachmentsArray.push(attachmentObj);
sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify(attachmentsArray));
};
};
};
});

View File

@@ -0,0 +1,7 @@
<ul data-uuid="{{uuid}}" class="list hover:bg-base-200 transition">
<li class="list-row items-center">
<img class="size-12 rounded-full bg-base-content" src=""/>
<div class="text-xl font-medium">{{name}}</div>
<span class="badge h-8 bg-info text-info-content font-semibold border-transparent">17</span>
</li>
</ul>

View File

@@ -0,0 +1,88 @@
import apx from "./apx.js";
import { getOldestPrivatriids } from "./utils.js";
const bodyEl = document.querySelector("body");
const aliasListContainerEl = document.querySelector("#aliasListContainer");
let aliasesArray = [];
const newAlias = async (alias) => {
let aliasTemplate = "";
await fetch("./alias.mustache")
.then(res => res.text())
.then(template => {
aliasTemplate = template;
});
return Mustache.render(aliasTemplate, { alias });
};
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
(async () => {
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
aliasesArray = (await apx.indexedDB.get("privatri", "messages", privatriid)).aliases;
const ownerAlias = (await apx.indexedDB.get("privatri", "messages", privatriid)).owner;
for (const alias of aliasesArray) {
aliasListContainerEl.insertAdjacentHTML("beforeend", await newAlias({
decrypted: (await apx.crypto.decryptMessage(alias, privateKey)).data,
crypted: alias
}));
};
document.querySelectorAll("button.removeAliasBtn").forEach(btn => {
btn.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
if (btn.getAttribute("data-alias") !== ownerAlias) {
const alias = btn.getAttribute("data-alias");
aliasesArray = aliasesArray.filter(a => a !== alias);
btn.parentElement.parentElement.remove();
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
const messageObj = await apx.indexedDB.get("privatri", "messages", privatriid);
messageObj.aliases = aliasesArray;
await apx.indexedDB.set("privatri", "messages", messageObj);
};
});
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You cannot remove the owner of the thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to remove this alias."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
});
};
});
})();

View File

@@ -0,0 +1,6 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-100 h-full">
<h1 class="text-4xl flex-shrink-0 mt-8">Alias list</h1>
<div id="aliasListContainer" class="overflow-y-auto flex flex-col w-full flex-1 min-h-0">
</div>
</div>

View File

@@ -0,0 +1,72 @@
import apx from "./apx.js";
import { getOldestPrivatriids } from "./utils.js";
const bodyEl = document.querySelector("body");
const threadNameInputEl = document.querySelector("#threadNameInput");
const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn");
const applyModificationsBtnEl = document.querySelector("#applyModificationsBtn");
const displayToastAlert = async (message) => {
let toastAlertTemplate = "";
await fetch("./toastAlert.mustache")
.then(res => res.text())
.then(template => {
toastAlertTemplate = template;
});
return Mustache.render(toastAlertTemplate, { message });
};
let messageObj = {};
(async () => {
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey;
const privatriidArray = await getOldestPrivatriids("privatri", "messages");
let ownerAlias = "";
privatriidArray.forEach(async privatriid => {
if (privatriid.split("_")[0] === uuid) {
messageObj = await apx.indexedDB.get("privatri", "messages", privatriid);
ownerAlias = messageObj.owner;
threadNameInputEl.value = (await apx.crypto.decryptMessage(messageObj.title, privateKey)).data;
(Array.from(autoDeletionBtnElArray).find(el => el.getAttribute("data-auto-deletion") === String(messageObj.dt_autodestruction))).classList.add("bg-base-200");
if (messageObj.urgencydeletion === true) {
document.querySelector('input[type="checkbox"].toggle').checked = true;
};
};
});
applyModificationsBtnEl.addEventListener("click", async () => {
if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) {
messageObj.title = await apx.crypto.encryptMessage(threadNameInputEl.value, privateKey);
messageObj.dt_autodestruction = (() => {
const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200"));
return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10);
})();
messageObj.urgencydeletion = document.querySelector('input[type="checkbox"].toggle').checked;
await apx.indexedDB.set("privatri", "messages", messageObj);
} else {
bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this thread."));
setTimeout(() => {
bodyEl.lastElementChild.remove();
}, 3000);
};
});
})();
autoDeletionBtnElArray.forEach(btn => {
btn.addEventListener("click", () => {
autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200"));
btn.classList.add("bg-base-200");
});
});

View File

@@ -0,0 +1,33 @@
<div class="flex flex-col items-center justify-center gap-y-8 w-fit min-w-xs">
<h1 class="text-4xl">Thread settings</h1>
<div class="input rounded-lg w-full flex items-center gap-x-2 focus-within:outline focus-within:outline-2 focus-within:outline-primary bg-base-100">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" class="fill-current text-base-content">
<path d="M80 0v-160h800V0H80Zm160-320h56l312-311-29-29-28-28-311 312v56Zm-80 80v-170l448-447q11-11 25.5-17t30.5-6q16 0 31 6t27 18l55 56q12 11 17.5 26t5.5 31q0 15-5.5 29.5T777-687L330-240H160Zm560-504-56-56 56 56ZM608-631l-29-29-28-28 57 57Z"/>
</svg>
<input id="threadNameInput" type="text" placeholder="Thread name..." class="flex-1 bg-transparent border-none outline-none shadow-none"/>
</div>
<div class="dropdown dropdown-center w-full">
<div tabindex="0" role="button" class="btn w-full rounded-lg flex items-center justify-between">
Message auto destruction
<svg xmlns="http://www.w3.org/2000/svg" height="32px" viewBox="0 -960 960 960" width="32px" class="fill-current text-base-content">
<path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/>
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box w-full z-1 w-52 p-2 shadow-sm">
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="86400000">
<a class="rounded-lg">Every days</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="604800000">
<a class="rounded-lg">Every weeks</a>
</li>
<li class="autoDeletionBtn rounded-lg" data-auto-deletion="2592000000">
<a class="rounded-lg">Every months</a>
</li>
</ul>
</div>
<div class="w-full flex items-center justify-left gap-x-4">
<input type="checkbox" class="toggle"/>
<p>Urgency deletion</p>
</div>
<button id="applyModificationsBtn" class="btn w-full rounded-lg mt-16">Apply modifications</button>
</div>

View File

@@ -0,0 +1,5 @@
<div class="toast">
<div class="alert alert-info">
<span>{{message}}</span>
</div>
</div>

View File

@@ -0,0 +1,46 @@
async function getOldestPrivatriids(dbName, storeName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const cursorRequest = store.openCursor();
const uuidMap = {};
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const obj = cursor.value;
const [uuid, timestamp] = obj.privatriid.split("_");
if (!uuidMap[uuid] || Number(timestamp) < uuidMap[uuid].timestamp) {
uuidMap[uuid] = { privatriid: obj.privatriid, timestamp: Number(timestamp) };
};
cursor.continue();
} else {
const result = Object.values(uuidMap).map(event => event.privatriid);
resolve(result);
};
};
cursorRequest.onerror = (event) => reject(event);
};
request.onerror = (event) => reject(event);
});
};
async function syncronizeBackend(lastConnection) {
};
export {
getOldestPrivatriids,
syncronizeBackend
};

View File

@@ -1,6 +1,8 @@
<div class="py-1">
<div id="pageheader" class="py-5 w-full max-w-sm mx-auto">
<div class="flex flex-col items-center h-sm:flex-row h-sm:text-start h-sm:items-start h-sm:gap-4">
<div class="h-sm:w-1/3">
<img
class="mx-auto w-auto block dark:hidden"
class="mx-auto w-auto block dark:hidden h-sm:max-h-10"
data-wco="logo"
src="{{logobglight.src}}"
alt="{{logobglight.alt}}"
@@ -8,22 +10,26 @@
alt="{{logobgdark.alt}}"
/>
<img
class="mx-auto w-auto hidden dark:block"
class="mx-auto w-auto hidden dark:block h-sm:max-h-10"
data-wco="logo"
src="{{logobgdark.src}}"
alt="{{logobgdark.alt}}"
/>
</div>
<div class="h-sm:w-2/3">
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight"
class="mt-10 h-sm:mt-0 h-sm:text-lg text-2xl font-bold leading-9 tracking-tight"
data-wco="claim"
>
{{claim.textContent}}
</h2>
</div>
</div>
</div>
<div id="loading" class="flex min-h-full flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-sm py-4 text-center">
<span class="loading loading-spinner loading-lg text-secondary"></span>
</div>
</div>
<div id="{{contentid}}" wco-name="{{contentwconame}}" wco-link="{{contentscreen}}" class="mt-5 sm:mx-auto sm:w-full sm:max-w-sm"></div>
<div class="navlink"></div>
<div id="{{contentid}}" wco-name="{{contentwconame}}" wco-link="{{contentscreen}}" class="py-5 flex-grow flex flex-col overflow-y-auto items-start"></div>
<div class="navlink flex justify-around border-t bg-base-200 p-2"></div>

View File

@@ -0,0 +1,29 @@
<div class="py-1">
<img
class="mx-auto w-auto block dark:hidden"
data-wco="logo"
src="{{logobglight.src}}"
alt="{{logobglight.alt}}"
src="{{logobgdark.src}}"
alt="{{logobgdark.alt}}"
/>
<img
class="mx-auto w-auto hidden dark:block"
data-wco="logo"
src="{{logobgdark.src}}"
alt="{{logobgdark.alt}}"
/>
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight"
data-wco="claim"
>
{{claim.textContent}}
</h2>
</div>
<div id="loading" class="flex min-h-full flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-sm py-4 text-center">
<span class="loading loading-spinner loading-lg text-secondary"></span>
</div>
</div>
<div id="{{contentid}}" wco-name="{{contentwconame}}" wco-link="{{contentscreen}}" class="mt-5 sm:mx-auto sm:w-full sm:max-w-sm"></div>
<div class="navlink"></div>

View File

@@ -1,11 +1,8 @@
<div class="flex justify-center gap-2 p-4">
{{#links}}
<button class="btn {{classnavbutton}} flex-col gap-1" onclick="apx.simplemobnav.action('{{id}}','{{link}}','{{action}}','{{wconame}}');">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 box-content">
<path stroke-linecap="round" stroke-linejoin="round" d="{{d}}" />
</svg>
<span class="text-sm">{{{shortlabel}}}</span>
<button class="{{classnavbutton}} btn btn-ghost btn-sm flex flex-col items-center" onclick="apx.simplemobnav.action('{{id}}','{{link}}','{{action}}','{{wconame}}');">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{d}}"/></svg>
<span class="text-xs">{{{shortlabel}}}</span>
</button>
{{/links}}
</div>

View File

@@ -0,0 +1,11 @@
<div class="flex justify-center gap-2 p-4">
{{#links}}
<button class="btn {{classnavbutton}} flex-col gap-1" onclick="apx.simplemobnav.action('{{id}}','{{link}}','{{action}}','{{wconame}}');">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 box-content">
<path stroke-linecap="round" stroke-linejoin="round" d="{{d}}" />
</svg>
<span class="text-sm">{{{shortlabel}}}</span>
</button>
{{/links}}
</div>

View File

@@ -5,7 +5,9 @@ apx.simplemobnav.loadwco = (id, ctx) => {
const tpldataname = `${apx.data.pagename}_${id}_simplemobnav`;
const simplemobnavid = document.getElementById(id)
console.log("load simplemobnav with tpldataname:", tpldataname, " id:", id, " ctx:", ctx);
// check if authenticate if yes then show myworld instead of signin
//console.log("ggggggggggggg",apx.data.headers.xalias)
//if (apx.data.headers.xalias!="anonymous") ctx.link="myworld";
let initmenu;
if (simplemobnavid.innerHTML.trim() === "") {
// Get 1st menu matching the first profil in profilmenu

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtridark" class="h-full bg-base-200 text-neutral-content ">
<html lang="fr" data-theme="apxtridark">
<head>
<meta charset="utf-8" />
<title>Authentification</title>
@@ -34,7 +34,6 @@
xuuid:0
},
pagename: "apxid",
pageauth: "apxid",
wcoobserver:true,
allowedprofils:["anonymous"],
version:0
@@ -51,16 +50,11 @@
<script src="/api/apxtri/wwws/getwco/simplemobnav.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=authentification"></script>
<script src="/api/apxtri/wwws/getwco/apxauth.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=signature"></script>
</head>
<body class="h-full">
<div class="flex items-center justify-center min-h-screen px-4">
<body class="bg-base-100 flex items-center justify-center min-h-screen">
<div
id="authentification"
wco-name="simplemobnav"
class="bg-base-100 min-h-screen w-full p-4 text-center">
</div>
<!--div wco-name="chatroom" class="hidden min-h-full flex-col justify-center px-6 py-12 lg:px-8">
</div-->
class="w-full h-screen sm:w-[600px] bg-base-100 rounded-none sm:rounded-2xl shadow-xl flex flex-col overflow-hidden">
</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtridark" class="h-full bg-base-200 text-neutral-content ">
<head>
<meta charset="utf-8" />
<title>Accès Privé</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="gestion, accès, privé, apxtri"
name="keywords"
/>
<meta
content="Page de gestion des accès privés."
name="description"
/>
<link data-wco="favicon" href="static/img/icons/iconbgdark.png" rel="icon" />
<link href="static/css/output.css" rel="stylesheet" />
<script>
/**
* Read apx.js to know more
*/
const apxtri = {
headers: {
xtrkversion: 1,
xtribe: "apxtri",
xapp: "admin",
xlang: "fr",
xalias: "anonymous",
xhash: "anonymous",
xprofils:["anonymous"],
xdays: 0,
xuuid:0
},
pagename: "privatri",
pageauth: "apxid",
wcoobserver:true,
allowedprofils:["pagans"],
version:0
};
</script>
<script src="/apxtrilib/axios/dist/axios.min.js"></script>
<script src="/apxtrilib/dayjs/dayjs.min.js"></script>
<script src="/apxtrilib/openpgp/dist/openpgp.min.js"></script>
<script src="/apxtrilib/mustache/mustache.min.js"></script>
<script src="/apxtrilib/qr-code-styling/lib/qr-code-styling.js"></script>
<script src="/apxtrilib/checkjson.js"></script>
<script src="/api/apxtri/wwws/getwco/apx.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=privatri&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/crypto.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=privatri&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/privatri.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=privatri&code=enjoy&tagid=main"></script>
</head>
<body class="bg-base-100 flex items-center justify-center h-screen w-screen ">
<div
id="main"
wco-name="privatri"
>
</div>
</body>
</html>

View File

@@ -19,10 +19,17 @@
/*@import "tailwindcss" source("../../.{html,js,mustache}");
*/
@import "tailwindcss" source("/media/phil/usbfarm/apxtowns/data/apxtri/objects/wwws/admin/src/**/*.{html,js,mustache}");
/*
@import "tailwindcss" source("/var/lib/apxtowns/data/apxtri/objects/wwws/admin/src / * * / *.{html,js,mustache}");
@import "./sourcetw.css";
@plugin "daisyui";
*/
@import "/opt/apxtowns/farm-test/apxtri/node_modules/tailwindcss";
@plugin "/opt/apxtowns/farm-test/apxtri/node_modules/daisyui";
@import "./sourcetw.css";
/*
add h-sm a vertical breakpoint to check screen height that doies not exist in tw or daisyui
*/
@config "./tailwind.config.json";
@theme {
--font-display: "Questrial", "sans-serif";
@@ -43,7 +50,7 @@
}
}
@plugin "daisyui/theme" {
@plugin "/opt/apxtowns/farm-test/apxtri/node_modules/daisyui/theme" {
name: "apxtridark";
default: true;
prefersdark: true;
@@ -79,7 +86,7 @@
}
@plugin "daisyui/theme" {
@plugin "/opt/apxtowns/farm-test/apxtri/node_modules/daisyui/theme" {
name: "apxtrilight";
default: false;
prefersdark: false;
@@ -114,17 +121,17 @@
--noise: 0;
}
::-webkit-scrollbar {
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
}
::-webkit-scrollbar-track {
background: #2d3748;
}
::-webkit-scrollbar-thumb {
}
::-webkit-scrollbar-thumb {
background: #4a5568;
border-radius: 4px;
}
}
.transition-width {
.transition-width {
transition: width 0.3s ease;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
@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}";
@source "/media/phil/usbfarm/apxtowns/data/apxtri/objects/wco/apxauth/*.{html,js,mustache}";
@source "../../**/*.{html,js,mustache}";
@source "../../../../../wco/apx/*.{html,js,mustache}";
@source "../../../../../wco/simplemobnav/*.{html,js,mustache}";
@source "../../../../../wco/apxauth/*.{html,js,mustache}";
@source "../../../../../wco/privatri/*.{html,js,mustache}";

View File

@@ -0,0 +1,9 @@
{
"theme": {
"extend": {
"screens": {
"h-sm": { "raw": "(max-height: 700px)" }
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

0
wwws/admin/src/static/lib/dayjs/plugin/arraySupport.d.ts vendored Executable file → Normal file
View File

0
wwws/admin/src/static/lib/dayjs/plugin/objectSupport.d.ts vendored Executable file → Normal file
View File

58
wwws/admin/src/test.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App Smartphone Responsive</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- DaisyUI -->
<script>
tailwind.config = {
plugins: [daisyui],
daisyui: { themes: ["light"] }
}
</script>
</head>
<body class="bg-gray-200 flex items-center justify-center min-h-screen">
<!-- Smartphone container -->
<div class="w-full h-screen sm:w-[600px] bg-white rounded-none sm:rounded-2xl shadow-xl flex flex-col overflow-hidden">
<!-- Header -->
<div class="p-4 border-b font-bold text-lg">
Mon Application
</div>
<!-- Contenu scrollable -->
<div class="flex-1 overflow-y-auto flex flex-col justify-center p-4 space-y-4">
<div class="card bg-base-200 p-4">Contenu 1</div>
<div class="card bg-base-200 p-4">Contenu 2</div>
<div class="card bg-base-200 p-4">Contenu 3</div>
<div class="card bg-base-200 p-4">Contenu 4</div>
<div class="card bg-base-200 p-4">Contenu 5</div>
<div class="card bg-base-200 p-4">Contenu 6</div>
<div class="card bg-base-200 p-4">Contenu 7</div>
<div class="card bg-base-200 p-4">Contenu 8</div>
<div class="card bg-base-200 p-4">Contenu 9</div>
</div>
<!-- Dock en bas -->
<div class="flex justify-around border-t bg-base-100 p-2">
<button class="btn btn-ghost btn-sm flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0h6"/></svg>
<span class="text-xs">Home</span>
</button>
<button class="btn btn-ghost btn-sm flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-6h13M9 5v6h13"/></svg>
<span class="text-xs">Search</span>
</button>
<button class="btn btn-ghost btn-sm flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 7.292m0 0a4 4 0 100 7.292m0-14.584v14.584"/></svg>
<span class="text-xs">Profile</span>
</button>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtridark" >
<head>
<meta charset="utf-8" />
<title>Authentification</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="L'unique et sa propriété, authentification, apxtri, cle public, cle privée"
name="keywords"
/>
<meta
content="Porte d'entrée dans l'univers libre d'apXtri, là où vous pouvez être l'Unique et sa propriété."
name="description"
/>
<link data-wco="favicon" href="static/img/icons/iconbgdark.png" rel="icon" />
<!--script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script-->
<link href="static/css/output.css" rel="stylesheet" />
<script>
/**
* Read apx.js to know more
*/
const apxtri = {
headers: {
xtrkversion: 1,
xtribe: "apxtri",
xapp: "admin",
xlang: "fr",
xalias: "anonymous",
xhash: "anonymous",
xprofils:["anonymous"],
xdays: 0,
xuuid:0
},
pagename: "test",
wcoobserver:true,
allowedprofils:["anonymous"],
version:0
};
</script>
<script src="/apxtrilib/axios/dist/axios.min.js"></script>
<script src="/apxtrilib/dayjs/dayjs.min.js"></script>
<script src="/apxtrilib/openpgp/dist/openpgp.min.js"></script>
<script src="/apxtrilib/mustache/mustache.min.js"></script>
<script src="/apxtrilib/qr-code-styling/lib/qr-code-styling.js"></script>
<script src="/apxtrilib/checkjson.js"></script>
<script src="/api/apxtri/wwws/getwco/apx.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/simplemobnav.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=authentification"></script>
<script src="/api/apxtri/wwws/getwco/apxauth.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=signature"></script>
</head>
<body class="bg-base-100 flex items-center justify-center min-h-screen">
<div
id="authentification"
wco-name="simplemobnav"
class="w-full h-screen sm:w-[600px] bg-base-100 rounded-none sm:rounded-2xl shadow-xl flex flex-col overflow-hidden">
</div>
</body>
</html>

View File

@@ -22,7 +22,7 @@
},
{
"mainprofil": "pagans",
"link": "logout"
"link": "myworld"
},
{
"mainprofil": "anonymous",

View File

@@ -0,0 +1,41 @@
{
"appOptions": [
{
"onclick": "onclick='apx.privatri.notificationsMenu();'",
"title": "Notifications"
},
{
"title": "New thread",
"template": "createThread"
},
{
"title": "Account",
"template": ""
}
],
"searchplaceholder": "Recherche parmis les fils",
"sortoptions": [
{
"title": "Tri par date",
"onclick": "onclick='apx.privatri.sortby('bydate');'"
},
{
"title": "Tri par NOM DE FIL",
"onclick": "onclick='apx.privatri.sortby('bytitlethread');'"
}
],
"threadOptions": [
{
"title": "Invite an alias",
"template": "inviteAlias"
},
{
"title": "See all alias",
"template": "threadAliasList"
},
{
"title": "Settings",
"template": "threadSettings"
}
]
}

View File

@@ -0,0 +1,120 @@
{
"contentwconame": "apxauth",
"contentid": "signature",
"logobgdark": {
"src": "static/img/logo/logobgdark.png",
"alt": "apxtri"
},
"logobglight": {
"src": "static/img/logo/logobglight.png",
"alt": "apxtri"
},
"claim": {
"textContent": "L'Unique et sa Propriété"
},
"navtpl":"navbuttonh",
"classnavbutton":"btn-primary hover:bg-secondary",
"classnavlist":{"p":"text-sm text-gray-500","a":"text-secondary hover:text-primary"},
"profilmenu": [
{
"mainprofil": "persons",
"link": "myworld"
},
{
"mainprofil": "pagans",
"link": "myworld"
},
{
"mainprofil": "anonymous",
"link": "signin"
}
],
"links": [
{
"link": "signup",
"d":"M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z",
"shortlabel":"Créer",
"label": "Pas encore d'identité apxtri ?",
"textlink": "Créer mon identité",
"allowedprofil":["anonymous"],
"action":"navigation",
"next": [
"signin",
"forgetkey",
"information"
]
},
{
"link": "signin",
"d":"M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z",
"shortlabel": "S'identifier",
"label": "S'identifier ?",
"textlink": "Accédez à vos données",
"allowedprofil":["anonymous"],
"action":"navigation",
"next": [
"signup",
"forgetkey",
"information"
]
},
{
"link": "forgetkey",
"d":"M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z",
"shortlabel": "Clé oublié",
"label": "Clé oubliée ?",
"textlink": "Récupérez par email",
"allowedprofil":["anonymous"],
"action":"navigation",
"next": [
"signin",
"signup",
"information"
]
},
{
"link": "information",
"d":"m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z",
"shortlabel": "Info",
"label": " C'est quoi une identité apxtri ?",
"textlink": "En savoir plus",
"allowedprofil":["anonymous"],
"action":"navigation",
"next": [
"back"
]
},
{
"link": "back",
"d":"M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3",
"shortlabel":"Retour",
"label": "Retour au menu ",
"allowedprofil":["anonymous"],
"action":"reload",
"wconame":"simplemobnav",
"textlink": "Retour",
"next": []
},
{
"link": "logout",
"d":"M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15",
"shortlabel": "Logout",
"label": "Nettoyer mes traces",
"allowedprofil":["pagans"],
"label":"Se deconnecter",
"textlink": "Se déconnecter",
"action":"logout",
"wconame":"apxauth",
"next": []
},
{
"link": "myworld",
"shortlabel": "Ma chose",
"label": "Ma propriété",
"allowedprofil":["pagans"],
"textlink": "Voir ",
"action":"navigation",
"next": ["logout"]
}
]
}

View File

@@ -0,0 +1,113 @@
{
"contentwconame": "apxauth",
"contentid": "signature",
"logo": {
"src": "/src/static/img/logo/logobgdark.png",
"alt": "smatchit"
},
"claim": {
"textContent": "Never miss an opportunity"
},
"textlist": true,
"commentmenutype": "textlist: vertical list of menu with texte, buttonlist: horizontal btn",
"profilmenu": [
{
"mainprofil": "persons",
"link": "mytribes"
},
{
"mainprofil": "pagans",
"link": "logout"
},
{
"mainprofil": "anonymous",
"link": "signin"
}
],
"links": [
{
"link": "signup",
"label": "Pas encore d'identité apxtri ?",
"textlink": "Créer mon identité",
"tpl": "apxauthscreensignup",
"allowedprofil": [
"anonymous"
],
"next": [
"signin",
"forgetkey",
"information"
]
},
{
"link": "signin",
"label": "S'identifier ?",
"textlink": "Accédez à vos données",
"tpl": "apxauthscreensignin",
"allowedprofil": [
"anonymous"
],
"next": [
"signup",
"forgetkey",
"information"
]
},
{
"link": "forgetkey",
"label": "Clé oubliée ?",
"textlink": "Récupérez par email",
"tpl": "apxauthscreenforgetkey",
"allowedprofil": [
"anonymous"
],
"next": [
"signin",
"signup",
"information"
]
},
{
"link": "information",
"label": " C'est quoi une identité apxtri ?",
"textlink": "En savoir plus",
"allowedprofil": [
"anonymous"
],
"tpl": "apxauthscreeninformation",
"next": [
"back"
]
},
{
"link": "back",
"label": "Retour au menu ",
"allowedprofil": [
"anonymous"
],
"tpl": "sc",
"textlink": "Retour",
"next": []
},
{
"link": "logout",
"label": " ",
"allowedprofil": [
"pagans"
],
"tpl": "apxauthscreenlogout",
"textlink": "",
"next": []
},
{
"link": "mytribes",
"label": " ",
"tpl": "apxauthscreenmytribes",
"allowedprofil": [
"persons"
],
"textlink": "",
"next": []
}
]
}

View File

@@ -0,0 +1,46 @@
{
"idparent": "authentification",
"signintitle": "Qui êtes-vous?",
"signuptitle": "Creer un compte anonyme",
"aliastitle": "Votre alias",
"aliasinvalid": "Combinaison de 3 à 150 caractères <br /> composée de minuscules (a à z) et/ou de chiffres (0 à 9)",
"aliastitle": "Uniquement minuscules ou chiffres",
"emailinvalid": "Vérifier votre email",
"privatekeyplaceholder": "Votre clé privée",
"remembermetext": "Stocker mon identité sur ce navigateur plus de 24 heures.Pour supprimer la clé de ce navigateur, il suffit de se déconnecter.",
"authentifyme": "S'identifier",
"createkey": "Créer mes clés en local",
"trusttext": "Si vous faites plus confiance à ce domaine qu'à vous même pour garder vos clés, cochez cette case. Les administrateurs de ce domaine auront accès à vos clés et pourront donc agir en votre nom. L'email ci-dessus sera utilisé pour vous renvoyer vos clés par le domaine de confiance. Dans tous les cas, il est conseillé de telecharger vos clés et de les stocker sur un support fiable non connecté (papier, clé usb)",
"downloadPuK": " Download <br />Publickey",
"downloadPrK": " Download <br />Privatekey",
"saveidentity": "Sauvegarde cette identité",
"nextpage": "Page suivante",
"optionlinksmajor":[ {
"notification_count": 5,
"typebadge": "success",
"svg": "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='size-6'> <path stroke-linecap='round' stroke-linejoin='round' d='M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75' /></svg>",
"title": "Accéder à l'administration de la ville",
"onclick": " href='admindata_fr.html' "
}],
"optionlinks": [
{
"notification_count": 1,
"typebadge": "success",
"svg":"<svg class='w-10 h-10 text-base-content' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='currentColor' ><path d='M97.6 214.8h2.3a2159.5 2159.5 0 0 1 24.2 0 2976.5 2976.5 0 0 0 38-.2h2.2c4.4 0 7.2.7 10.7 3.4 2.4 3.7 2.3 5.4 2.3 9.7v9.1a5200 5200 0 0 0 0 18.5v11.2a13324.6 13324.6 0 0 1 .2 77.7 3051.3 3051.3 0 0 0 0 38.8v3.8c-.6 3.8-1.7 5.5-4.5 8.2-3.4 1-3.4 1-7 1a54.1 54.1 0 0 1-11.2-9l-1.5-1.4a1543.7 1543.7 0 0 1-17.7-17.7l-9.8-9.9a20275.3 20275.3 0 0 1-74.5-74.6l-4.6-4.6-2.6-2.6c-2.6-2.8-4-4.6-4.7-8.4.6-4 1-4.6 3.6-7.4l1.9-2.2c3.7-4 7.6-7.8 11.4-11.7l2.7-2.6a5470.5 5470.5 0 0 1 19.2-19.3l2.5-2.6 2.4-2.3 2-2.1c3.8-3.2 7.9-2.8 12.5-2.8ZM216.6 345.9a238272208 238272208 0 0 0 62-.1 12532.8 12532.8 0 0 0 85.8-.2 628.2 628.2 0 0 1 10.4 0c3.5.4 5.4 1.3 8.2 3.4 1.9 2.5 2 3.7 1.8 6.8-1 4-2.6 5.7-5.6 8.6l-1.5 1.5-5 5a2949 2949 0 0 0-13.6 13.5l-10.3 10.3a9057.4 9057.4 0 0 1-41.8 41.6 27411 27411 0 0 1-41.1 41l-2.7 2.7a11.1 11.1 0 0 1-9.1 3.6c-10-2.8-21-18.3-28.1-25.4a3170 3170 0 0 0-12.3-12.3l-2.2-2.2-2.1-2-1.9-2c-3.7-4.2-3.8-8-3.8-13.6v-2.4a1220.2 1220.2 0 0 1 0-24.8 2068.7 2068.7 0 0 0-.2-31.3v-9.8c0-4.9 1-7.4 4.5-10.8 2.9-1.4 5.4-1.1 8.6-1.1ZM259 51c6.4 4.6 12 10.8 17.5 16.4l2.6 2.6A5839.7 5839.7 0 0 1 301 92l2.3 2.3 2.1 2c3.2 3.6 2.7 7 2.7 11.5v2.4a2459.5 2459.5 0 0 1 0 24.9 4177 4177 0 0 0 .2 41.3c0 6.8 0 6.8-2.3 9.7-3.6 2.3-6.7 2.3-11 2.3h-9a5189.6 5189.6 0 0 0-18.6 0h-11.3a13928.8 13928.8 0 0 1-78.2 0 4265 4265 0 0 0-39 0h-3.8c-3.4-.3-4.7-.9-7.1-3.3-1-3.3-1-3.3-1-7 2.5-4.3 5.5-7.8 9-11.3l1.5-1.5a1601.7 1601.7 0 0 1 18-18l10.1-10a21005.6 21005.6 0 0 1 76-75.9l4.6-4.7L249 54c3.4-3.2 5.5-3.7 10.1-3ZM348 139c2.7 2 2.7 2 5.4 4.8l1.6 1.5a682.9 682.9 0 0 1 8.7 8.7l10 10 8.2 8.2a8423.8 8423.8 0 0 1 39.7 39.6 5758 5758 0 0 1 48.5 48.4c2.4 3.5 2.4 5.6 1.9 9.8-1.2 2.1-1.2 2.1-3 3.8l-1.9 2-2.1 2.2-2.2 2.2c-2.3 2.5-4.7 4.9-7.2 7.3l-2.6 2.6A5839.7 5839.7 0 0 1 431 312l-2.3 2.3-2 2.1c-3.6 3.2-7 2.8-11.6 2.8H402a6035.4 6035.4 0 0 1-54 .3h-2.3c-3.5 0-5.3-.2-8.1-2.4-2.8-3.7-2.7-6-2.7-10.7v-2.1a2222.5 2222.5 0 0 1-.2-34 21694.8 21694.8 0 0 1-.2-94.5 1746.5 1746.5 0 0 1 0-24.8v-3.8c.6-3.8 1.7-5.5 4.5-8.2 3.6-1.2 5.3-1 9 0ZM218.6 214.9h2.7a3102.4 3102.4 0 0 1 28 0 5270.3 5270.3 0 0 0 43.8-.1h2.6c7.3 0 7.3 0 10.3 2.2 2.6 4 2.3 7.3 2.3 11.9v2.7a1551.6 1551.6 0 0 1 .1 28.5 2352.9 2352.9 0 0 0 .2 36v11.3c0 4.1-.3 6.1-2.6 9.6-4.1 2.7-7.3 2.7-12.1 2.6H291a694.7 694.7 0 0 1-15.2 0 1867.7 1867.7 0 0 1-30 0 1797.5 1797.5 0 0 1-27.8 0h-2.6c-4 0-6-.3-9.5-2.6-2.7-4.1-2.7-7.3-2.6-12.1V302a708.4 708.4 0 0 1 0-15.3 1897.8 1897.8 0 0 1 0-30 2184 2184 0 0 1 0-27.8v-2.6c.1-5.8.1-5.8 2.5-8.9 4.1-3 7.8-2.6 12.7-2.6Z'/></svg>",
"title": "Accéder à vos messages chiffrés",
"onclick": " href='privatri_fr.html' "
},
{
"notification_count": 1,
"typebadge": "success",
"svg": "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='size-6'> <path stroke-linecap='round' stroke-linejoin='round' d='m21 7.5-2.25-1.313M21 7.5v2.25m0-2.25-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3 2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75 2.25-1.313M12 21.75V19.5m0 2.25-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25' /></svg>",
"title": "Accéder à mon wallet",
"onclick": " href='wallet_fr.html' "
},
{
"svg": "<svg class='w-10 h-10 text-base-content' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor'><path stroke-linecap= 'round' stroke-linejoin='round' d='M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.33 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z' /></svg>",
"title": "Accéder à mon profil",
"onclick": " href='myprofil_fr.html' "
}
]
}

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtridark" class="h-full bg-base-200 text-neutral-content ">
<head>
<meta charset="utf-8" />
<title>Mes transactions</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="L'unique et sa propriété, authentification, apxtri, cle public, cle privée"
name="keywords"
/>
<meta
content="Porte d'entrée dans l'univers libre d'apXtri, là où vous pouvez être l'Unique et sa propriété."
name="description"
/>
<link data-wco="favicon" href="static/img/icons/iconbgdark.png" rel="icon" />
<link href="static/css/output.css" rel="stylesheet" />
<script>
/**
* Read apx.js to know more
*/
const apxtri = {
headers: {
xtrkversion: 1,
xtribe: "apxtri",
xapp: "admin",
xlang: "fr",
xalias: "anonymous",
xhash: "anonymous",
xprofils:["anonymous"],
xdays: 0,
xuuid:0
},
pagename: "wallet",
wcoobserver:true,
allowedprofils:["pagans"],
version:0
};
</script>
<script src="/apxtrilib/axios/dist/axios.min.js"></script>
<script src="/apxtrilib/dayjs/dayjs.min.js"></script>
<script src="/apxtrilib/openpgp/dist/openpgp.min.js"></script>
<script src="/apxtrilib/mustache/mustache.min.js"></script>
<script src="/apxtrilib/qr-code-styling/lib/qr-code-styling.js"></script>
<script src="/apxtrilib/checkjson.js"></script>
<script src="/api/apxtri/wwws/getwco/apx.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/simplemobnav.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=walletmanager"></script>
<script src="/api/apxtri/wwws/getwco/apxwallet.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=wallet&code=enjoy&tagid=signature"></script>
</head>
<body class="h-full">
<div class="flex items-center justify-center min-h-screen px-4">
<div
id="walletmanager"
wco-name="simplemobnav"
class="bg-base-100 min-h-screen w-full p-4 text-center">
</div>
</div>
</body>
</html>

0
wwws/apx/src/static/lib/dayjs/plugin/arraySupport.d.ts vendored Executable file → Normal file
View File

0
wwws/apx/src/static/lib/dayjs/plugin/objectSupport.d.ts vendored Executable file → Normal file
View File

View File

@@ -1 +0,0 @@
/media/phil/usbfarm/apxtowns/farm-ants/apxtri/node_modules/axios/dist/axios.min.js

View File

@@ -1 +0,0 @@
/media/phil/usbfarm/apxtowns/farm-ants/apxtri/models/Checkjson.js

View File

@@ -1 +0,0 @@
/media/phil/usbfarm/apxtowns/farm-ants/apxtri/node_modules/dayjs/dayjs.min.js

View File

@@ -0,0 +1,26 @@
[
{
"pathsrc": "models/Checkjson.js",
"pathdest": "checkjson.js"
},
{
"pathsrc": "node_modules/axios/dist/axios.min.js",
"pathdest": "axios/dist/axios.min.js"
},
{
"pathsrc": "node_modules/dayjs/dayjs.min.js",
"pathdest": "dayjs/dayjs.min.js"
},
{
"pathsrc": "node_modules/mustache/mustache.min.js",
"pathdest": "mustache/mustache.min.js"
},
{
"pathsrc": "node_modules/openpgp/dist/openpgp.min.js",
"pathdest": "openpgp/dist/openpgp.min.js"
},
{
"pathsrc": "node_modules/qr-code-styling/lib/qr-code-styling.js",
"pathdest": "qr-code-styling/lib/qr-code-styling.js"
}
]

View File

@@ -1 +0,0 @@
/media/phil/usbfarm/apxtowns/farm-ants/apxtri/node_modules/mustache/mustache.min.js

View File

@@ -1 +0,0 @@
/media/phil/usbfarm/apxtowns/farm-ants/apxtri/node_modules/openpgp/dist/openpgp.min.js

View File

@@ -1,26 +0,0 @@
[
{
"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/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/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"
}
]

View File

@@ -1,78 +1,27 @@
{
"pages": {
"apxid": {
"privatri": {
"version": 1,
"profils": [
"anonymous"
],
"tpl": {
"apxauthmain": "apxtri/objects/wco/apxauth/main",
"apxauthscreensignup": "apxtri/objects/wco/apxauth/screensignup",
"apxauthscreensignin": "apxtri/objects/wco/apxauth/screensignin",
"apxauthscreenlogout": "apxtri/objects/wco/apxauth/screenlogout",
"apxauthscreenmyworld": "apxtri/objects/wco/apxauth/screenmyworld",
"apxauthscreeninformation": "apxtri/objects/wco/apxauth/screeninformation",
"apxauthscreenforgetkey": "apxtri/objects/wco/apxauth/screenforgetkey",
"simplemobnavnavbuttonh": "apxtri/objects/wco/simplemobnav/navbuttonh.mustache",
"simplemobnavnavlist": "apxtri/objects/wco/simplemobnav/navlist.mustache",
"simplemobnavnavbutton": "apxtri/objects/wco/simplemobnav/navbuttonh.mustache",
"simplemobnavmain": "apxtri/objects/wco/simplemobnav/main.mustache"
"privatrimain": "apxtri/objects/wco/privatri/main.mustache",
"privatriAlias": "apxtri/objects/wco/privatri/alias.mustache",
"privatriCreateThread": "apxtri/objects/wco/privatri/createThread.mustache",
"privatriEditMessage": "apxtri/objects/wco/privatri/editMessage.mustache",
"privatriInviteAlias": "apxtri/objects/wco/privatri/inviteAlias.mustache",
"privatriMessage": "apxtri/objects/wco/privatri/message.mustache",
"privatriThread": "apxtri/objects/wco/privatri/thread.mustache",
"privatriThreadAliasList": "apxtri/objects/wco/privatri/threadAliasList.mustache",
"privatriThreadSettings": "apxtri/objects/wco/privatri/threadSettings.mustache",
"privatriToastAlert": "apxtri/objects/wco/privatri/toastAlert.mustache"
},
"tpldata": {
"apxid_signature_apxauth": "apxtri/objects/wwws/admin/src/tpldata/apxid_signature_apxauth",
"apxid_authentification_simplemobnav": "apxtri/objects/wwws/admin/src/tpldata/apxid_authentification_simplemobnav",
"apxid_mydata_simplemobnav": "apxtri/objects/wwws/admin/src/tpldata/apxid_mydata_simplemobnav"
"privatri_main_privatri": "apxtri/objects/wwws/admin/src/tpldata/privatri_main_privatri"
},
"schema": [
"apxtri/objects/pagans",
"apxtri/objects/persons"
],
"ref": {
"Odmdb": "apxtri/models/tplstrings/Odmdb",
"Pagans": "apxtri/models/tplstrings/Pagans",
"Persons": "apxtri/models/tplstrings/Persons",
"Checkjson": "apxtri/models/tplstrings/Checkjson",
"Notification": "apxtri/models/tplstrings/Notifications",
"Middlewares": "apxtri/models/tplstrings/Middlewares"
},
"wco": {
"favicon": {
"href": "static/img/icons/iconbglight.png"
},
"logo": {
"src": "static/img/logo/logobgdark.png",
"alt": "apXtri"
},
"claim": {
"textContent": "L'Unique et sa propriété"
}
},
"appdata": {}
},
"admindata": {
"version": 1,
"profils": [
"anonymous"
],
"tpl": {
"adminskullverticalnav": "apxtri/objects/wco/adminskull/verticalnav",
"adminskullresult": "apxtri/objects/wco/adminskull/result",
"adminskullmain": "apxtri/objects/wco/adminskull/main",
"adminskullheadnav": "apxtri/objects/wco/adminskull/headnav"
},
"tpldata": {},
"schema": [
"apxtri/objects/pagans",
"apxtri/objects/persons"
],
"ref": {
"Checkjson": "apxtri/models/tplstrings/Checkjson",
"Notification": "apxtri/models/tplstrings/Notifications",
"Middlewares": "apxtri/models/tplstrings/Middlewares",
"Odmdb": "apxtri/models/tplstrings/Odmdb",
"Pagans": "apxtri/models//tplstrings/Pagans",
"Persons": "apxtri/models/tplstrings/Persons"
}
"schema": [],
"ref": {}
}
}
}