Some checks failed
Deploy Encrypted Todo App / build-and-push (push) Has been cancelled
380 lines
16 KiB
HTML
380 lines
16 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Encrypted Todo List</title>
|
|
<link rel="stylesheet" href="styles.css" />
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Encrypted Todo List</h1>
|
|
<button id="logoutBtn" style="display: none" onclick="logoutUser()">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
|
|
<div id="user-setup" class="container">
|
|
<h3 class="section-title">Create/Login User</h3>
|
|
<input type="text" id="userId" placeholder="Enter user ID" />
|
|
<button onclick="createUser()">Create User</button>
|
|
</div>
|
|
|
|
<div id="todo-app" style="display: none">
|
|
<div class="container">
|
|
<h3 class="section-title">Add New Todo</h3>
|
|
<div>
|
|
<input
|
|
type="text"
|
|
id="todoText"
|
|
placeholder="Enter todo item"
|
|
/>
|
|
<button onclick="addTodo()">Add Todo</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<h3 class="section-title">Your Encrypted Todos</h3>
|
|
<div class="toggle-container">
|
|
<span>Show Decrypted</span>
|
|
<label class="toggle-switch">
|
|
<input
|
|
type="checkbox"
|
|
id="viewToggle"
|
|
onchange="toggleView()"
|
|
/>
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span>Show Encrypted</span>
|
|
</div>
|
|
|
|
<div id="todos"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// State variables
|
|
let currentUser = null;
|
|
let allTodos = [];
|
|
let showEncrypted = false;
|
|
let allUsers = [];
|
|
let ws = null;
|
|
|
|
// Try to restore user session from localStorage
|
|
window.onload = function () {
|
|
const savedUser = localStorage.getItem("currentUser");
|
|
if (savedUser) {
|
|
currentUser = savedUser;
|
|
document.getElementById("user-setup").style.display =
|
|
"none";
|
|
document.getElementById("todo-app").style.display = "block";
|
|
document.getElementById("logoutBtn").style.display =
|
|
"inline-block";
|
|
connectWebSocket();
|
|
loadTodos();
|
|
}
|
|
};
|
|
|
|
function connectWebSocket() {
|
|
if (!currentUser) return;
|
|
ws = new WebSocket(
|
|
`ws://${window.location.host}?userId=${currentUser}`,
|
|
);
|
|
ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
switch (msg.type) {
|
|
case "users":
|
|
allUsers = msg.data;
|
|
updateUserDropdowns();
|
|
break;
|
|
case "todo_added":
|
|
case "todo_shared":
|
|
loadTodos();
|
|
break;
|
|
case "todo_deleted":
|
|
allTodos = allTodos.filter(
|
|
(todo) => todo.id !== msg.data.id,
|
|
);
|
|
renderTodos();
|
|
break;
|
|
case "todo_updated":
|
|
allTodos = allTodos.map((todo) => {
|
|
if (todo.id === msg.data.id) {
|
|
return {
|
|
...todo,
|
|
completed: msg.data.completed,
|
|
};
|
|
}
|
|
return todo;
|
|
});
|
|
renderTodos();
|
|
break;
|
|
case "todo_share_confirmed":
|
|
// Reload todos to show updated sharing info
|
|
loadTodos();
|
|
break;
|
|
}
|
|
};
|
|
ws.onclose = () => {
|
|
setTimeout(connectWebSocket, 1000);
|
|
};
|
|
}
|
|
|
|
function updateUserDropdowns() {
|
|
document.querySelectorAll(".share-select").forEach((select) => {
|
|
const todoId = select.id.replace("share-", "");
|
|
const todo = allTodos.find((t) => t.id === todoId);
|
|
const availableUsers = allUsers.filter(
|
|
(u) =>
|
|
u !== currentUser &&
|
|
(!todo ||
|
|
!todo.participants ||
|
|
!todo.participants.includes(u)),
|
|
);
|
|
|
|
const prev = select.value;
|
|
select.innerHTML = [
|
|
'<option value="">Add participant...</option>',
|
|
...availableUsers.map(
|
|
(u) => `<option value="${u}">${u}</option>`,
|
|
),
|
|
].join("");
|
|
select.value = prev;
|
|
});
|
|
}
|
|
|
|
async function createUser() {
|
|
const userId = document.getElementById("userId").value.trim();
|
|
if (!userId) return;
|
|
try {
|
|
const res = await fetch("/api/users", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userId }),
|
|
});
|
|
if (res.ok) {
|
|
currentUser = userId;
|
|
localStorage.setItem("currentUser", userId);
|
|
document.getElementById("user-setup").style.display =
|
|
"none";
|
|
document.getElementById("todo-app").style.display =
|
|
"block";
|
|
document.getElementById("logoutBtn").style.display =
|
|
"inline-block";
|
|
connectWebSocket();
|
|
loadTodos();
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error);
|
|
}
|
|
} catch (err) {
|
|
alert("Error creating user: " + err.message);
|
|
}
|
|
}
|
|
|
|
function logoutUser() {
|
|
localStorage.removeItem("currentUser");
|
|
currentUser = null;
|
|
if (ws) ws.close();
|
|
document.getElementById("todo-app").style.display = "none";
|
|
document.getElementById("user-setup").style.display = "block";
|
|
document.getElementById("userId").value = "";
|
|
document.getElementById("logoutBtn").style.display = "none";
|
|
}
|
|
|
|
async function addTodo() {
|
|
const text = document.getElementById("todoText").value.trim();
|
|
if (!text) return;
|
|
try {
|
|
const res = await fetch(`/api/users/${currentUser}/todos`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
if (res.ok) {
|
|
document.getElementById("todoText").value = "";
|
|
loadTodos();
|
|
}
|
|
} catch (err) {
|
|
alert("Error adding todo: " + err.message);
|
|
}
|
|
}
|
|
|
|
async function loadTodos() {
|
|
try {
|
|
const [plainRes, encRes] = await Promise.all([
|
|
fetch(`/api/users/${currentUser}/todos`),
|
|
fetch(`/api/users/${currentUser}/todos/encrypted`),
|
|
]);
|
|
const todos = await plainRes.json();
|
|
const encTodos = await encRes.json();
|
|
const encMap = {};
|
|
encTodos.forEach((t) => {
|
|
encMap[t.id] = t.encrypted;
|
|
});
|
|
allTodos = todos.map((t) => ({
|
|
...t,
|
|
encrypted: encMap[t.id] || {
|
|
body: "Encryption data not available",
|
|
},
|
|
}));
|
|
renderTodos();
|
|
} catch (err) {
|
|
console.error("Error loading todos:", err);
|
|
}
|
|
}
|
|
|
|
function renderTodos() {
|
|
const todosDiv = document.getElementById("todos");
|
|
if (!allTodos.length) {
|
|
todosDiv.innerHTML = "<p>No todos yet. Add one above.</p>";
|
|
return;
|
|
}
|
|
todosDiv.innerHTML = allTodos
|
|
.map(
|
|
(todo) => `
|
|
<div class="todo-item${todo.completed ? " completed" : ""}">
|
|
<div class="todo-content">
|
|
<div class="todo-header">
|
|
<input type="checkbox" class="todo-checkbox" ${todo.completed ? "checked" : ""} onchange="toggleTodoStatus('${todo.id}', this.checked)">
|
|
${
|
|
showEncrypted
|
|
? `<div class="encrypted-data">
|
|
<strong>Type:</strong> ${todo.encrypted.type}<br>
|
|
<strong>Encrypted Data:</strong><br>${todo.encrypted.body}
|
|
</div>`
|
|
: `<div>
|
|
<span class="todo-text">${todo.text}</span>
|
|
<div class="todo-metadata">
|
|
${todo.isCreator ? `<span class="creator-badge">You created this</span>` : `<span class="shared-badge">Created by ${todo.createdBy}</span>`}
|
|
${todo.participants && todo.participants.length > 1 ? `<span class="participants-badge">${todo.participants.length} participant${todo.participants.length > 1 ? "s" : ""}: ${todo.participants.filter((p) => p !== currentUser).join(", ")}</span>` : ""}
|
|
</div>
|
|
</div>`
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="todo-actions">
|
|
${
|
|
todo.isCreator
|
|
? `
|
|
<div class="share-container">
|
|
<select id="share-${todo.id}" class="share-select">
|
|
<option value="">Add participant...</option>
|
|
${allUsers
|
|
.filter(
|
|
(u) =>
|
|
u !== currentUser &&
|
|
!todo.participants.includes(u),
|
|
)
|
|
.map(
|
|
(u) =>
|
|
`<option value="${u}">${u}</option>`,
|
|
)
|
|
.join("")}
|
|
</select>
|
|
<button onclick="shareTodo('${todo.id}')">Add</button>
|
|
</div>
|
|
<button onclick="deleteTodo('${todo.id}')">Delete</button>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function toggleView() {
|
|
showEncrypted = document.getElementById("viewToggle").checked;
|
|
renderTodos();
|
|
}
|
|
|
|
async function shareTodo(todoGroupId) {
|
|
const select = document.getElementById(`share-${todoGroupId}`);
|
|
const recipientId = select.value;
|
|
if (!recipientId) {
|
|
alert("Please select a user to add");
|
|
return;
|
|
}
|
|
try {
|
|
const res = await fetch(
|
|
`/api/users/${currentUser}/todos/${todoGroupId}/share`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ recipientId }),
|
|
},
|
|
);
|
|
if (res.ok) {
|
|
alert(`${recipientId} added to todo`);
|
|
select.value = "";
|
|
loadTodos(); // Reload to show updated participants
|
|
} else {
|
|
const err = await res.json();
|
|
if (err.error.includes("Only the creator")) {
|
|
alert(
|
|
"⚠️ Permission denied: Only the creator can add participants to this todo",
|
|
);
|
|
} else {
|
|
alert(err.error);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
alert("Error adding participant: " + err.message);
|
|
}
|
|
}
|
|
|
|
async function deleteTodo(todoGroupId) {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/users/${currentUser}/todos/${todoGroupId}`,
|
|
{ method: "DELETE" },
|
|
);
|
|
if (res.ok) {
|
|
loadTodos();
|
|
} else {
|
|
const err = await res.json();
|
|
if (err.error.includes("Only the creator")) {
|
|
alert(
|
|
"⚠️ Permission denied: Only the creator can delete this todo",
|
|
);
|
|
} else {
|
|
alert(err.error);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
alert("Error deleting todo: " + err.message);
|
|
}
|
|
}
|
|
|
|
async function toggleTodoStatus(todoGroupId, completed) {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/users/${currentUser}/todos/${todoGroupId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ completed }),
|
|
},
|
|
);
|
|
if (res.ok) {
|
|
// Update local state immediately for better UX
|
|
allTodos = allTodos.map((todo) => {
|
|
if (todo.id === todoGroupId) {
|
|
return { ...todo, completed };
|
|
}
|
|
return todo;
|
|
});
|
|
renderTodos();
|
|
// Note: WebSocket will handle updates for other participants
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error);
|
|
}
|
|
} catch (err) {
|
|
alert("Error updating todo: " + err.message);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|