modifprivatri wco and page

This commit is contained in:
2025-08-14 10:36:30 +02:00
parent 0ebb4a6419
commit a7c1cb3ae9
27 changed files with 748 additions and 506 deletions

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

@@ -0,0 +1,17 @@
{
"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"
},
"tpldatamodel": { "privatri": "apxtri/objects/wco/privatri/exampleprivatri" },
"schema": [],
"ref": {}
}

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,88 @@
<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">
{{#menuapp}}
<li>
<a {{onclick}} class="rounded-lg">{{{title}}}</a>
</li>
{{/menuapp}}
</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="{{{seacheplaceholder}}}" 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">
{{#sortoptions}}
<li>
<p class="rounded-lg" {{onclick}} >{{onclick}}</p>
</li>
{{/sortoptions}}
</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

@@ -1,506 +1,25 @@
import apx from "./apx.js"; var apx = apx || {};
import { getOldestPrivatriids, syncronizeBackend } from "./utils.js"; apx.privatri = {};
apx.privatri.loadwco = async (id, ctx) => {
const bodyEl = document.querySelector("body"); // check if not authenticate, do nothing cause by default screensignin and wait authentification
const searchInputEl = document.querySelector("#threadSearchBar"); // if authenticate, if url xhash then redirect if no url then change wco-link=screenmyworld
const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li"); // if (dayjs(apx.data.headers.xdays).diff(dayjs(), "hours") >= 24) apx.apxauth.checkisauth();
const threadsContainerEl = document.querySelector("#threadsContainer"); //load main.mustache of the component
const threadPageEl = document.querySelector("#threadPage"); //when wco-xxx change it run this function
const messageInputEl = document.getElementById("messageInput"); console.log(
const messagesContainerEl = document.querySelector("#messagesContainer"); `Load wconame:privatri apx.privatri.loadwco with id:${id} and ctx: ${JSON.stringify(
const attachmentsInputEl = document.querySelector("#attachmentsInput"); ctx
)}`
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)
}
); );
}; //refresh thread from BE
}; const threadlist = apx.privatri.getthread(apx.data.headers.xalias,1000000)
const tpldataname = `${apx.data.pagename}_${id}_privatri`;
return attachmentsArray; const privatriid = document.getElementById(id);
})() privatriid.innerHTML = Mustache.render(apx.data.tpl.privatrimain, threadlist);
};
})((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")}`)) || []); apx.privatri.getthread=async(alias,timestamp)=>{
//store in indexdb the thread
sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`); return true
}
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

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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,57 @@
<!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/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

@@ -0,0 +1,30 @@
{
"menuapp": [
{
"onclick": "onclick='apx.privatri.notificationsMenu();'",
"title": "Notification",
"comment": "./notificationsMenu.html"
},
{
"onclick": "onclick='apx.privatri.addthread();'",
"title": "Nouveau fil",
"comment": "./newMenu3.html"
},
{
"onclick": "href='apxid_fr.html'",
"title": "Compte",
"comment": ""
}
],
"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');'"
}
]
}

View File

@@ -73,6 +73,22 @@
"Pagans": "apxtri/models//tplstrings/Pagans", "Pagans": "apxtri/models//tplstrings/Pagans",
"Persons": "apxtri/models/tplstrings/Persons" "Persons": "apxtri/models/tplstrings/Persons"
} }
},
"privatri": {
"version": 1,
"profils": [
"anonymous"
],
"tpl": {
"privatrimain": "apxtri/objects/wco/privatri/main.mustache"
},
"tpldata": {
"privatri_main_privatri": "apxtri/objects/wwws/admin/src/tpldata/privatri_main_privatri"
},
"schema": [],
"ref": {},
"wco": {},
"appdata": {}
} }
} }
} }