From 3136c81533c4c05fd6763d3746c5877911003ef4 Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 26 Feb 2026 23:15:05 +0100 Subject: [PATCH] V.1.2 - Improved Secrurity, Better Ticket Handling --- Dockerfile | 4 +- categories.json | 10 + docker-compose.yml | 5 +- package.json | 6 +- public/admin-script.js | 184 ++++++-- public/admin.html | 78 +-- public/index.html | 60 +-- public/ticketview/ticket-admin.js | 210 +++------ public/ticketview/ticket-shared.js | 23 +- public/ticketview/ticket-user.js | 98 +--- public/ticketview/ticket-view-admin.html | 29 +- public/ticketview/ticket-view-user.html | 3 +- public/user-script.js | 272 ++++------- public/user.css | 50 ++ public/version.js | 9 + server.js | 576 ++++++++++++++++------- 16 files changed, 879 insertions(+), 738 deletions(-) create mode 100644 categories.json create mode 100644 public/version.js diff --git a/Dockerfile b/Dockerfile index 7a36630..b882b26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,17 +2,15 @@ FROM node:20-alpine WORKDIR /app -# Copy package files first for better layer caching COPY package.json package-lock.json* ./ RUN npm ci --omit=dev -# Copy the rest of the application COPY server.js ./ +COPY categories.json ./ COPY public/ ./public/ COPY assets/ ./assets/ -# Create data directory for the database RUN mkdir -p /app/data EXPOSE 3000 diff --git a/categories.json b/categories.json new file mode 100644 index 0000000..696dfdf --- /dev/null +++ b/categories.json @@ -0,0 +1,10 @@ +{ + "categories": [ + "HELP", + "HELP MEE NOOOW", + "IT-Support", + "Suggestion...", + "Add Book pls", + "Honeyycloud!" + ] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 81d6228..d74c90b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,8 @@ services: ports: - "3000:3000" volumes: - - ./data:/app/data # Bind mount for the SQLite database - - ./assets:/app/assets # Bind mount so you can update avatars etc. + - ./data:/app/data + - ./assets:/app/assets + - ./categories.json:/app/categories.json env_file: - .env \ No newline at end of file diff --git a/package.json b/package.json index 2af8724..f6f7682 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ticketsystem", - "version": "1.2", + "version": "1.2.0", "description": "", "main": "server.js", "scripts": { @@ -14,12 +14,10 @@ "dependencies": { "body-parser": "^2.2.0", "cors": "^2.8.5", - "date-fns-tz": "^3.2.0", "dotenv": "^17.2.1", "express": "^5.1.0", - "luxon": "^3.7.1", "moment-timezone": "^0.6.0", "multer": "^2.0.2", "sqlite3": "^5.1.7" } -} +} \ No newline at end of file diff --git a/public/admin-script.js b/public/admin-script.js index 0f6b212..5a837cd 100644 --- a/public/admin-script.js +++ b/public/admin-script.js @@ -9,21 +9,130 @@ document.addEventListener('DOMContentLoaded', () => { const statOpen = document.getElementById('open-tickets'); const statInProgress = document.getElementById('in-progress-tickets'); const statClosed = document.getElementById('closed-tickets'); - const statTotalTime = document.getElementById('time-tickets'); // Neu: Gesamtzeit Anzeige + const statTotalTime = document.getElementById('time-tickets'); + + // Accordion elements + const closedToggle = document.getElementById('closed-toggle'); + const closedAccordion = document.getElementById('closed-accordion'); + const closedCount = document.getElementById('closed-count'); + + // PFP elements + const updatePfpBtn = document.getElementById('update-pfp-btn'); + const pfpModal = document.getElementById('update-pfp-modal'); + const dropZone = document.getElementById('drop-zone'); + const fileInput = document.getElementById('pfp-file-input'); + const uploadPfpBtn = document.getElementById('upload-pfp-btn'); + + let selectedPfpFile = null; + let accordionOpen = false; searchInput.addEventListener('input', refreshTickets); statusFilter.addEventListener('change', refreshTickets); priorityFilter.addEventListener('change', refreshTickets); categoryFilter.addEventListener('change', refreshTickets); - // Helper: Status/Priority zu "Title-Case-With-Dash" - function toTitleCaseWithDash(str) { - return str - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join('-'); + // --- XSS Protection --- + function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; } + function toTitleCaseWithDash(str) { + return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('-'); + } + + // --- Dynamic Categories --- + async function loadCategories() { + try { + const res = await fetch('/api/categories'); + const categories = await res.json(); + if (categoryFilter) { + categoryFilter.innerHTML = '' + + categories.map(c => ``).join(''); + } + } catch (err) { + console.error('Failed to load categories:', err); + } + } + + // --- Accordion --- + if (closedToggle) { + closedToggle.addEventListener('click', () => { + accordionOpen = !accordionOpen; + closedAccordion.style.display = accordionOpen ? 'block' : 'none'; + closedToggle.classList.toggle('open', accordionOpen); + }); + } + + // --- PFP Modal --- + function openPfpModal() { + if (pfpModal) { + const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; + pfpModal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; + if (scrollBarWidth > 0) document.body.style.paddingRight = scrollBarWidth + 'px'; + } + } + function closePfpModal() { + if (pfpModal) { + pfpModal.style.display = 'none'; + document.body.style.overflow = ''; + document.documentElement.style.overflow = ''; + document.body.style.paddingRight = ''; + } + } + if (updatePfpBtn) updatePfpBtn.addEventListener('click', openPfpModal); + if (pfpModal) { + window.addEventListener('click', (e) => { if (e.target === pfpModal) closePfpModal(); }); + window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && pfpModal.style.display === 'flex') closePfpModal(); }); + } + + // --- Drag & Drop --- + if (dropZone) { + dropZone.addEventListener('click', () => fileInput.click()); + dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#10b981'; }); + dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#4b5563'; }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.style.borderColor = '#4b5563'; + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + selectedPfpFile = file; + dropZone.querySelector('p').textContent = file.name; + } + }); + } + if (fileInput) { + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file && file.type.startsWith('image/')) { + selectedPfpFile = file; + dropZone.querySelector('p').textContent = file.name; + } + }); + } + if (uploadPfpBtn) { + uploadPfpBtn.addEventListener('click', async () => { + if (!selectedPfpFile) return alert('Please select an image first.'); + const formData = new FormData(); + formData.append('pfp', selectedPfpFile, 'admin.jpg'); + try { + const res = await fetch('/api/upload-admin-pfp', { method: 'POST', body: formData }); + if (!res.ok) throw new Error('Upload failed'); + alert('Admin profile picture updated!'); + closePfpModal(); + window.location.reload(); + } catch (err) { + console.error(err); + alert('Error uploading profile picture'); + } + }); + } + + // --- API --- async function fetchTickets() { try { const res = await fetch('/api/tickets'); @@ -39,7 +148,7 @@ document.addEventListener('DOMContentLoaded', () => { const res = await fetch(`/api/tickets/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ actor: 'Admin' }) + body: JSON.stringify({ actor: 'Admin' }), }); return await res.json(); } catch (err) { @@ -59,33 +168,33 @@ document.addEventListener('DOMContentLoaded', () => { const category = categoryFilter.value; const searchTerm = searchInput.value.toLowerCase(); - const filtered = tickets.filter(t => - (status === 'all' || t.status === status) && - (priority === 'all' || t.priority === priority) && - (category === 'all' || t.category === category) && - (t.title.toLowerCase().includes(searchTerm) || - t.description.toLowerCase().includes(searchTerm) || - t.requester.toLowerCase().includes(searchTerm)) - ).sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + const filtered = tickets + .filter(t => + (status === 'all' || t.status === status) && + (priority === 'all' || t.priority === priority) && + (category === 'all' || t.category === category) && + (t.title.toLowerCase().includes(searchTerm) || + t.description.toLowerCase().includes(searchTerm) || + t.requester.toLowerCase().includes(searchTerm)) + ) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); const openRows = []; const closedRows = []; filtered.forEach(ticket => { const shortTitle = ticket.title.length > 50 ? ticket.title.slice(0, 50) + '...' : ticket.title; - - // Wandle Status und Priority korrekt in Klassen um const statusClass = toTitleCaseWithDash(ticket.status || 'Unknown'); const priorityClass = toTitleCaseWithDash(ticket.priority || 'None'); const rowHTML = ` ${ticket.id} - ${shortTitle} - ${ticket.requester} - ${ticket.category || '-'} - ${ticket.status || 'Unknown'} - ${ticket.priority || 'None'} + ${escapeHtml(shortTitle)} + ${escapeHtml(ticket.requester)} + ${escapeHtml(ticket.category) || '-'} + ${escapeHtml(ticket.status) || 'Unknown'} + ${escapeHtml(ticket.priority) || 'None'} ${formatDate(ticket.created_at)} ${ticket.total_time} min @@ -94,31 +203,28 @@ document.addEventListener('DOMContentLoaded', () => { `; - if (ticket.status === 'Closed') { - closedRows.push(rowHTML); - } else { - openRows.push(rowHTML); - } + if (ticket.status === 'Closed') closedRows.push(rowHTML); + else openRows.push(rowHTML); }); ticketsBody.innerHTML = openRows.length ? openRows.join('') - : `No open tickets found`; + : `No open tickets found`; if (closedTicketsBody) { closedTicketsBody.innerHTML = closedRows.length ? closedRows.join('') - : `No closed tickets`; + : `No closed tickets`; } - // Event listeners an Buttons hängen - [...document.querySelectorAll('.view-btn')].forEach(btn => + if (closedCount) closedCount.textContent = closedRows.length; + + document.querySelectorAll('.view-btn').forEach(btn => btn.addEventListener('click', () => { window.location.href = `/ticketview/ticket-view-admin.html?id=${btn.dataset.id}`; }) ); - - [...document.querySelectorAll('.delete-btn')].forEach(btn => + document.querySelectorAll('.delete-btn').forEach(btn => btn.addEventListener('click', () => { if (confirm('Are you sure you want to delete this ticket?')) { deleteTicket(btn.dataset.id).then(refreshTickets); @@ -132,19 +238,15 @@ document.addEventListener('DOMContentLoaded', () => { statOpen.textContent = tickets.filter(t => t.status === 'Open').length; statInProgress.textContent = tickets.filter(t => t.status === 'In Progress').length; statClosed.textContent = tickets.filter(t => t.status === 'Closed').length; - - // Gesamtzeit aller Tickets summieren (total_time in Minuten) - const totalTime = tickets.reduce((sum, ticket) => sum + (ticket.total_time || 0), 0); + const totalTime = tickets.reduce((sum, t) => sum + (t.total_time || 0), 0); statTotalTime.textContent = `${totalTime} min`; } function formatDate(date) { return new Date(date).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric' + year: 'numeric', month: 'short', day: 'numeric', }); } - refreshTickets(); + loadCategories().then(() => refreshTickets()); }); \ No newline at end of file diff --git a/public/admin.html b/public/admin.html index de3792b..2316493 100644 --- a/public/admin.html +++ b/public/admin.html @@ -13,12 +13,30 @@

Admin Dashboard

-
+
+ + +
@@ -64,10 +82,6 @@
@@ -92,36 +106,44 @@ +
-
-

Closed Tickets

+
+ +
-
- - - - - - - - - - - - - - - -
IDTitleRequesterCategoryStatusPriorityCreatedTotal TimeActions
-
-
- + + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 94f3a7c..a6d6369 100644 --- a/public/index.html +++ b/public/index.html @@ -6,10 +6,7 @@ Laila Dashboard - Ticket System! - +
@@ -30,9 +27,7 @@