diff --git a/app.js b/app.js index 400b533..015d42e 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,18 @@ +require("dotenv").config(); + const express = require("express"); const session = require("express-session"); const helmet = require("helmet"); -const sessionStore = require("./config/session"); -require("dotenv").config(); +const mysql = require("mysql2/promise"); +// ✅ Verschlüsselte Config +const { configExists, saveConfig } = require("./config-manager"); + +// ✅ Reset-Funktionen (Soft-Restart) +const db = require("./db"); +const { getSessionStore, resetSessionStore } = require("./config/session"); + +// ✅ Deine Routes (unverändert) const adminRoutes = require("./routes/admin.routes"); const dashboardRoutes = require("./routes/dashboard.routes"); const patientRoutes = require("./routes/patient.routes"); @@ -19,20 +28,73 @@ const authRoutes = require("./routes/auth.routes"); const app = express(); +/* =============================== + SETUP HTML +================================ */ +function setupHtml(error = "") { + return ` + + + + + + Praxissoftware Setup + + + +
+

🔧 Datenbank Einrichtung

+ ${error ? `
❌ ${error}
` : ""} + +
+ + + + + + + + + + + + + +
+ +
+ Die Daten werden verschlüsselt gespeichert (config.enc).
+ Danach wirst du automatisch auf die Loginseite weitergeleitet. +
+
+ + +`; +} + /* =============================== MIDDLEWARE ================================ */ app.use(express.urlencoded({ extended: true })); +app.use(express.json()); app.use(helmet()); +// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore) app.use( session({ name: "praxis.sid", secret: process.env.SESSION_SECRET, - store: sessionStore, + store: getSessionStore(), resave: false, saveUninitialized: false, - }) + }), ); const flashMiddleware = require("./middleware/flash.middleware"); @@ -41,63 +103,91 @@ app.use(flashMiddleware); app.use(express.static("public")); app.use("/uploads", express.static("uploads")); app.set("view engine", "ejs"); +app.use((req, res, next) => { + res.locals.user = req.session.user || null; + next(); +}); /* =============================== - COMPANY SETTINGS + SETUP ROUTES ================================ */ + +// Setup-Seite +app.get("/setup", (req, res) => { + if (configExists()) return res.redirect("/"); + return res.status(200).send(setupHtml()); +}); + +// Setup speichern + DB testen + Soft-Restart + Login redirect +app.post("/setup", async (req, res) => { + try { + const { host, user, password, name } = req.body; + + if (!host || !user || !password || !name) { + return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen.")); + } + + // ✅ DB Verbindung testen + const conn = await mysql.createConnection({ + host, + user, + password, + database: name, + }); + + await conn.query("SELECT 1"); + await conn.end(); + + // ✅ verschlüsselt speichern + saveConfig({ + db: { host, user, password, name }, + }); + + // ✅ Soft-Restart (DB Pool + SessionStore neu laden) + if (typeof db.resetPool === "function") { + db.resetPool(); + } + resetSessionStore(); + + // ✅ automatisch zurück zur Loginseite + return res.redirect("/"); + } catch (err) { + return res + .status(500) + .send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message)); + } +}); + +// Wenn keine config.enc → alles außer /setup auf Setup umleiten +app.use((req, res, next) => { + if (!configExists() && req.path !== "/setup") { + return res.redirect("/setup"); + } + next(); +}); + +/* =============================== + DEINE LOGIK (unverändert) +================================ */ + app.use(companySettingsRoutes); - -/* =============================== - AUTH / LOGIN -================================ */ app.use("/", authRoutes); - -/* =============================== - DASHBOARD -================================ */ app.use("/dashboard", dashboardRoutes); - -/* =============================== - ADMIN -================================ */ app.use("/admin", adminRoutes); -/* =============================== - PATIENTEN -================================ */ app.use("/patients", patientRoutes); app.use("/patients", patientMedicationRoutes); app.use("/patients", patientServiceRoutes); -/* =============================== - MEDIKAMENTE -================================ */ app.use("/medications", medicationRoutes); console.log("🧪 /medications Router mounted"); -/* =============================== - LEISTUNGEN -================================ */ app.use("/services", serviceRoutes); -/* =============================== - DATEIEN -================================ */ app.use("/", patientFileRoutes); - -/* =============================== - WARTEZIMMER -================================ */ app.use("/", waitingRoomRoutes); - -/* =============================== - RECHNUNGEN -================================ */ app.use("/", invoiceRoutes); -/* =============================== - LOGOUT -================================ */ app.get("/logout", (req, res) => { req.session.destroy(() => res.redirect("/")); }); @@ -113,7 +203,7 @@ app.use((err, req, res, next) => { /* =============================== SERVER ================================ */ -const PORT = 51777; +const PORT = process.env.PORT || 51777; const HOST = "127.0.0.1"; app.listen(PORT, HOST, () => { diff --git a/config-manager.js b/config-manager.js new file mode 100644 index 0000000..4d46fcf --- /dev/null +++ b/config-manager.js @@ -0,0 +1,71 @@ +const fs = require("fs"); +const crypto = require("crypto"); +const path = require("path"); + +const CONFIG_FILE = path.join(__dirname, "config.enc"); + +function getKey() { + const key = process.env.CONFIG_KEY; + if (!key) throw new Error("CONFIG_KEY fehlt in .env"); + + // stabil auf 32 bytes + return crypto.createHash("sha256").update(key).digest(); +} + +function encryptConfig(obj) { + const key = getKey(); + const iv = crypto.randomBytes(12); + + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + const json = JSON.stringify(obj); + + const encrypted = Buffer.concat([ + cipher.update(json, "utf8"), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + + return Buffer.concat([iv, tag, encrypted]).toString("base64"); +} + +function decryptConfig(str) { + const raw = Buffer.from(str, "base64"); + + const iv = raw.subarray(0, 12); + const tag = raw.subarray(12, 28); + const encrypted = raw.subarray(28); + + const key = getKey(); + + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(tag); + + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final(), + ]); + return JSON.parse(decrypted.toString("utf8")); +} + +function configExists() { + return fs.existsSync(CONFIG_FILE); +} + +function loadConfig() { + if (!configExists()) return null; + const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim(); + if (!enc) return null; + return decryptConfig(enc); +} + +function saveConfig(obj) { + const enc = encryptConfig(obj); + fs.writeFileSync(CONFIG_FILE, enc, "utf8"); + return true; +} + +module.exports = { + configExists, + loadConfig, + saveConfig, +}; diff --git a/config.enc b/config.enc new file mode 100644 index 0000000..5c52f6f --- /dev/null +++ b/config.enc @@ -0,0 +1 @@ +G/kDLEJ/LddnnNnginIGYSM4Ax0g5pJaF0lrdOXke51cz3jSTrZxP7rjTXRlqLcoUJhPaVLvjb/DcyNYB/C339a+PFWyIdWYjSb6G4aPkD8J21yFWDDLpc08bXvoAx2PeE+Fc9v5mJUGDVv2wQoDvkHqIpN8ewrfRZ6+JF3OfQ== \ No newline at end of file diff --git a/config/session.js b/config/session.js index bfc3210..fcb81b9 100644 --- a/config/session.js +++ b/config/session.js @@ -1,6 +1,31 @@ -const MySQLStore = require("express-mysql-session")(require("express-session")); -const db = require("../db"); +const session = require("express-session"); +const { configExists } = require("../config-manager"); -const sessionStore = new MySQLStore({}, db); +let store = null; -module.exports = sessionStore; +function getSessionStore() { + if (store) return store; + + // ✅ Setup-Modus (keine DB) + if (!configExists()) { + console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore"); + store = new session.MemoryStore(); + return store; + } + + // ✅ Normalbetrieb (mit DB) + const MySQLStore = require("express-mysql-session")(session); + const db = require("../db"); + + store = new MySQLStore({}, db); + return store; +} + +function resetSessionStore() { + store = null; +} + +module.exports = { + getSessionStore, + resetSessionStore, +}; diff --git a/db.js b/db.js index d64fac8..60d7d1f 100644 --- a/db.js +++ b/db.js @@ -1,24 +1,62 @@ const mysql = require("mysql2"); +const { loadConfig } = require("./config-manager"); -const pool = mysql.createPool({ - host: "85.215.63.122", - user: "praxisuser", - password: "praxisuser", - database: "praxissoftware", - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0 -}); +let pool = null; -// Optionaler Test beim Start -pool.getConnection((err, connection) => { - if (err) { - console.error("❌ MySQL Pool Fehler:", err); - process.exit(1); - } - console.log("✅ MySQL Pool verbunden"); - connection.release(); -}); +function initPool() { + const config = loadConfig(); -module.exports = pool; + // ✅ Setup-Modus: noch keine config.enc → kein Pool + if (!config || !config.db) return null; + return mysql.createPool({ + host: config.db.host, + user: config.db.user, + password: config.db.password, + database: config.db.name, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + }); +} + +function getPool() { + if (!pool) pool = initPool(); + return pool; +} + +function resetPool() { + pool = null; +} + +/** + * ✅ Proxy damit alter Code weitergeht: + * const db = require("../db"); + * await db.query(...) + */ +const dbProxy = new Proxy( + {}, + { + get(target, prop) { + const p = getPool(); + + if (!p) { + throw new Error( + "❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup", + ); + } + + const value = p[prop]; + + if (typeof value === "function") { + return value.bind(p); + } + + return value; + }, + }, +); + +module.exports = dbProxy; +module.exports.getPool = getPool; +module.exports.resetPool = resetPool; diff --git a/middleware/auth.middleware.js b/middleware/auth.middleware.js index 4e766c7..5cedc7b 100644 --- a/middleware/auth.middleware.js +++ b/middleware/auth.middleware.js @@ -3,30 +3,52 @@ function requireLogin(req, res, next) { return res.redirect("/"); } - // optional, aber sauber req.user = req.session.user; - next(); } +// ✅ NEU: Arzt-only (das war früher dein requireAdmin) +function requireArzt(req, res, next) { + console.log("ARZT CHECK:", req.session.user); + + if (!req.session.user) { + return res.redirect("/"); + } + + if (req.session.user.role !== "arzt") { + return res + .status(403) + .send( + "⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role, + ); + } + + req.user = req.session.user; + next(); +} + +// ✅ NEU: Admin-only function requireAdmin(req, res, next) { console.log("ADMIN CHECK:", req.session.user); if (!req.session.user) { - return res.send("NICHT EINGELOGGT"); + return res.redirect("/"); } - if (req.session.user.role !== "arzt") { - return res.send("KEIN ARZT: " + req.session.user.role); + if (req.session.user.role !== "admin") { + return res + .status(403) + .send( + "⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role, + ); } - // 🔑 DAS HAT GEFEHLT req.user = req.session.user; - next(); } module.exports = { requireLogin, - requireAdmin + requireArzt, + requireAdmin, }; diff --git a/routes/admin.routes.js b/routes/admin.routes.js index ed2e1e2..98fcf46 100644 --- a/routes/admin.routes.js +++ b/routes/admin.routes.js @@ -13,8 +13,11 @@ const { updateUser, } = require("../controllers/admin.controller"); -const { requireAdmin } = require("../middleware/auth.middleware"); +const { requireArzt, requireAdmin } = require("../middleware/auth.middleware"); +/* ========================== + ✅ VERWALTUNG (NUR ADMIN) +========================== */ router.get("/users", requireAdmin, listUsers); router.get("/create-user", requireAdmin, showCreateUser); router.post("/create-user", requireAdmin, postCreateUser); @@ -23,7 +26,18 @@ router.post("/users/change-role/:id", requireAdmin, changeUserRole); router.post("/users/reset-password/:id", requireAdmin, resetUserPassword); router.post("/users/activate/:id", requireAdmin, activateUser); router.post("/users/deactivate/:id", requireAdmin, deactivateUser); -router.get("/invoices", requireAdmin, showInvoiceOverview); router.post("/users/update/:id", requireAdmin, updateUser); +/* ✅ Admin-Tools: Datenbankverwaltung */ +router.get("/database", requireAdmin, (req, res) => { + res.render("admin/database", { + user: req.session.user, + }); +}); + +/* ========================== + ✅ ABRECHNUNG (NUR ARZT) +========================== */ +router.get("/invoices", requireArzt, showInvoiceOverview); + module.exports = router; diff --git a/routes/companySettings.routes.js b/routes/companySettings.routes.js index fa249b6..d7c79e6 100644 --- a/routes/companySettings.routes.js +++ b/routes/companySettings.routes.js @@ -1,23 +1,19 @@ const express = require("express"); const router = express.Router(); -const { requireAdmin } = require("../middleware/auth.middleware"); +const { requireArzt } = require("../middleware/auth.middleware"); const uploadLogo = require("../middleware/uploadLogo"); const { getCompanySettings, - saveCompanySettings + saveCompanySettings, } = require("../controllers/companySettings.controller"); -router.get( - "/admin/company-settings", - requireAdmin, - getCompanySettings -); +router.get("/admin/company-settings", requireArzt, getCompanySettings); router.post( "/admin/company-settings", - requireAdmin, + requireArzt, uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN - saveCompanySettings + saveCompanySettings, ); module.exports = router; diff --git a/routes/invoice.routes.js b/routes/invoice.routes.js index 2d19a16..e25c12a 100644 --- a/routes/invoice.routes.js +++ b/routes/invoice.routes.js @@ -1,14 +1,8 @@ const express = require("express"); const router = express.Router(); -const {requireAdmin } = require("../middleware/auth.middleware"); -const {createInvoicePdf} = require("../controllers/invoicePdf.controller"); +const { requireArzt } = require("../middleware/auth.middleware"); +const { createInvoicePdf } = require("../controllers/invoicePdf.controller"); - -router.post( - "/patients/:id/create-invoice", - requireAdmin, - createInvoicePdf -); +router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf); module.exports = router; - diff --git a/routes/patient.routes.js b/routes/patient.routes.js index 7641a87..e7ab1d9 100644 --- a/routes/patient.routes.js +++ b/routes/patient.routes.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const { requireLogin, requireAdmin } = require("../middleware/auth.middleware"); +const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); const { listPatients, @@ -31,7 +31,7 @@ router.get("/:id/medications", requireLogin, showPatientMedications); router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom); router.get("/:id/overview", requireLogin, showPatientOverview); router.post("/:id/notes", requireLogin, addPatientNote); -router.post("/waiting-room/call/:id", requireAdmin, callFromWaitingRoom); +router.post("/waiting-room/call/:id", requireArzt, callFromWaitingRoom); router.post("/:id/discharge", requireLogin, dischargePatient); router.get("/:id/plan", requireLogin, showMedicationPlan); router.post("/deactivate/:id", requireLogin, deactivatePatient); diff --git a/routes/patientMedication.routes.js b/routes/patientMedication.routes.js index 0953855..e1b380e 100644 --- a/routes/patientMedication.routes.js +++ b/routes/patientMedication.routes.js @@ -1,15 +1,15 @@ const express = require("express"); const router = express.Router(); -const { requireAdmin } = require("../middleware/auth.middleware"); +const { requireArzt } = require("../middleware/auth.middleware"); const { addMedication, endMedication, - deleteMedication + deleteMedication, } = require("../controllers/patientMedication.controller"); -router.post("/:id/medications", requireAdmin, addMedication); -router.post("/patient-medications/end/:id", requireAdmin, endMedication); -router.post("/patient-medications/delete/:id", requireAdmin, deleteMedication); +router.post("/:id/medications", requireArzt, addMedication); +router.post("/patient-medications/end/:id", requireArzt, endMedication); +router.post("/patient-medications/delete/:id", requireArzt, deleteMedication); module.exports = router; diff --git a/routes/patientService.routes.js b/routes/patientService.routes.js index 722aeb7..7e3d534 100644 --- a/routes/patientService.routes.js +++ b/routes/patientService.routes.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const { requireLogin, requireAdmin } = require("../middleware/auth.middleware"); +const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); const { addPatientService, deletePatientService, @@ -10,11 +10,11 @@ const { } = require("../controllers/patientService.controller"); router.post("/:id/services", requireLogin, addPatientService); -router.post("/services/delete/:id", requireAdmin, deletePatientService); +router.post("/services/delete/:id", requireArzt, deletePatientService); router.post( "/services/update-price/:id", - requireAdmin, - updatePatientServicePrice + requireArzt, + updatePatientServicePrice, ); router.post("/services/update-quantity/:id", updatePatientServiceQuantity); diff --git a/routes/service.routes.js b/routes/service.routes.js index c21d2a6..9844a39 100644 --- a/routes/service.routes.js +++ b/routes/service.routes.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const { requireLogin, requireAdmin } = require("../middleware/auth.middleware"); +const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); const { listServices, showCreateService, @@ -10,17 +10,16 @@ const { toggleService, listOpenServices, showServiceLogs, - listServicesAdmin + listServicesAdmin, } = require("../controllers/service.controller"); router.get("/", requireLogin, listServicesAdmin); -router.get("/", requireAdmin, listServices); -router.get("/create", requireAdmin, showCreateService); -router.post("/create", requireAdmin, createService); -router.post("/:id/update-price", requireAdmin, updateServicePrice); -router.post("/:id/toggle", requireAdmin, toggleService); +router.get("/", requireArzt, listServices); +router.get("/create", requireArzt, showCreateService); +router.post("/create", requireArzt, createService); +router.post("/:id/update-price", requireArzt, updateServicePrice); +router.post("/:id/toggle", requireArzt, toggleService); router.get("/open", requireLogin, listOpenServices); -router.get("/logs", requireAdmin, showServiceLogs); - +router.get("/logs", requireArzt, showServiceLogs); module.exports = router; diff --git a/views/admin/database.ejs b/views/admin/database.ejs new file mode 100644 index 0000000..4bba9f7 --- /dev/null +++ b/views/admin/database.ejs @@ -0,0 +1,172 @@ + + + + + Datenbankverwaltung + + + + + + + + + + + + +
+ + +<%- include("../partials/admin-sidebar", { user, active: "database" }) %> + + +
+ + + +
+ <%- include("../partials/flash") %> + +
+
+ +

Datenbank Tools

+ +
+ Hinweis: Diese Funktionen sind nur für Admins sichtbar und sollten mit Vorsicht benutzt werden. +
+ +
+ +
+
+
+
📦 Backup
+

+ Hier kannst du später ein Datenbank-Backup erstellen. +

+ +
+
+
+ +
+
+
+
♻️ Restore
+

+ Hier kannst du später ein Backup wiederherstellen. +

+ +
+
+
+ +
+
+
+
🔍 Systeminfo
+

+ In diesem Bereich kannst du später z.B. DB Version, Tabellenstatus, Speicherplatz etc. anzeigen. +

+
+
+
+ +
+ +
+
+
+ +
+
+ + + diff --git a/views/admin_users.ejs b/views/admin_users.ejs index 68d50b2..62e981f 100644 --- a/views/admin_users.ejs +++ b/views/admin_users.ejs @@ -6,7 +6,7 @@ - + @@ -14,6 +14,79 @@ - + - + + <%- include("partials/admin-sidebar", { active: "users" }) %> -
- <%- include("partials/flash") %> + +
-
-
- -

Benutzerübersicht

- - -
- - + Neuen Benutzer anlegen - + - <% if (query?.q) { %> - Reset - <% } %> - +
+ <%- include("partials/flash") %> -
- +
+
-
- - - - - - - - - - - +

Benutzerübersicht

- - <% users.forEach(u => { %> + + - + + + + - - + <% if (query?.q) { %> + Reset + <% } %> + - +
+
IDTitelVornameNachnameUsernameRolleStatusAktionen
<%= u.id %>
- + + + + + + + + + + + + - + + <% users.forEach(u => { %> - + - + + - + - + + + + + + + + + + + + - + - + <% }) %> + - +
- -
IDTitelVornameNachnameUsernameRolleStatusAktionen
- -
- -
- -
- - <%= u.id %> - <% if (u.active === 0) { %> - Inaktiv - <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %> - Gesperrt + + + + + + + + + + + + <% if (u.active === 0) { %> + Inaktiv + <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %> + Gesperrt + <% } else { %> + Aktiv + <% } %> + + + + + + + + + + <% if (u.id !== currentUser.id) { %> +
/<%= u.id %>"> + +
<% } else { %> - Aktiv + + Du selbst + <% } %> -
+
+
- - - - <% if (u.id !== currentUser.id) { %> -
/<%= u.id %>"> - -
- <% } else { %> - - Du selbst - - <% } %> - - - - - <% }) %> - - - +
-
+
diff --git a/views/dashboard.ejs b/views/dashboard.ejs index b85e78e..5d06917 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -102,7 +102,7 @@ .waiting-grid { display: grid; grid-template-columns: repeat(7, 1fr); - grid-template-rows: repeat(3, 80px); + grid-auto-rows: 80px; gap: 12px; width: 100%; } @@ -117,7 +117,9 @@ align-items: center; justify-content: center; - overflow: hidden; /* 🔥 DAS fehlt */ + overflow: hidden; + text-decoration: none; + color: inherit; } .waiting-slot.occupied { @@ -125,22 +127,11 @@ background: #eefdf5; } - .waiting-grid { - grid-auto-flow: row dense; - } - - .chair-icon { - width: 40px; - height: 40px; - object-fit: contain; - opacity: 0.25; - } - .patient-text { display: flex; flex-direction: column; align-items: center; - gap: 4px; /* 🔥 Abstand zwischen Name und Datum */ + gap: 4px; } .waiting-slot.clickable { @@ -153,43 +144,21 @@ box-shadow: 0 0 0 2px #2563eb; } + .nav-item.locked { + opacity: 0.5; + cursor: not-allowed; + } + .nav-item.locked:hover { + background: transparent; + color: #cbd5e1; + }
- - + + <%- include("partials/sidebar", { user, active: "patients" }) %>
@@ -201,18 +170,16 @@ <%- include("partials/flash") %> + WARTEZIMMER MONITOR + ========================= -->
🪑 Wartezimmer-Monitor
- <% const maxSlots = 21; - for (let i = 0; i < maxSlots; i++) { - const p = waitingPatients && waitingPatients[i]; - %> + <% if (waitingPatients && waitingPatients.length > 0) { %> + + <% waitingPatients.forEach(p => { %> - <% if (p) { %> <% if (user.role === 'arzt') { %>
@@ -232,12 +199,14 @@
<% } %> - <% } %> + <% }) %> + + <% } else { %> +
Keine Patienten im Wartezimmer.
<% } %>
-
diff --git a/views/login.ejs b/views/login.ejs index 717e44e..b3e7a30 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -27,10 +27,6 @@ - -
- Registrieren -
diff --git a/views/partials/admin-sidebar.ejs b/views/partials/admin-sidebar.ejs new file mode 100644 index 0000000..9dd9bdf --- /dev/null +++ b/views/partials/admin-sidebar.ejs @@ -0,0 +1,50 @@ + diff --git a/views/partials/sidebar.ejs b/views/partials/sidebar.ejs new file mode 100644 index 0000000..0960357 --- /dev/null +++ b/views/partials/sidebar.ejs @@ -0,0 +1,97 @@ +