diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..831444b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install --omit=dev + +COPY . . + +ENV NODE_ENV=production +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1 + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 7f7a5ac..c176c05 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,123 @@ -# zeitkonto +# Zeitkonto -Erfassung der Arbeitszeit für die Arbeit \ No newline at end of file +Eine kleine Web-App zur Arbeitszeiterfassung. Du trägst pro Tag Start-, +End- und Pausenzeit ein, die App berechnet daraus automatisch deine +Über- bzw. Minusstunden (dein "Zeitkonto") — pro Tag, pro Monat und als +laufenden Gesamtsaldo. + +**Stack:** Node.js / Express / EJS, Daten werden in MongoDB gespeichert. +Login ist über einen einzelnen Benutzer geschützt (Zugangsdaten per +Umgebungsvariable). + +## Funktionen + +- Tägliche Buchung von Start-, Endzeit und Pause → automatische Berechnung + der geleisteten Stunden +- Soll-Stunden pro Tag und Arbeitstage frei einstellbar (z. B. 8 h, Mo–Fr) +- Lauf­ender Gesamtsaldo (Überstunden/Minusstunden), inkl. optionalem + Start-Guthaben für bereits bestehende Überstunden +- Monatsübersicht +- Bearbeiten und Löschen einzelner Buchungen +- Einfacher Login-Schutz (ein Benutzer, Zugangsdaten per Env-Variable) + +## Lokal testen + +Voraussetzung: Docker und Docker Compose. + +```bash +cp .env.example .env +# .env nach Bedarf anpassen (APP_PASSWORD, SESSION_SECRET ...) + +docker compose up --build +``` + +Die App ist danach unter `http://localhost:3000` erreichbar. Login mit den +Werten aus `APP_USERNAME` / `APP_PASSWORD` (Standard: `admin` / siehe +`.env.example`). + +## Deployment in Coolify + +Die App bringt ein fertiges `docker-compose.yml` mit (App + MongoDB inkl. +Volume für die Datenbank), das sich als ein einziges Coolify-Projekt +deployen lässt. + +1. **Code in ein Git-Repository pushen** (GitHub, GitLab, Gitea, Bitbucket + — egal, Coolify unterstützt alle gängigen Anbieter, auch selbst + gehostete). +2. In Coolify: **New Resource → Docker Compose** und das Repository + auswählen bzw. die URL eingeben. Coolify erkennt die `docker-compose.yml` + automatisch (Build Pack: *Docker Compose*). +3. **Umgebungsvariablen setzen** (Tab *Environment Variables* der + Resource): mindestens + - `APP_USERNAME` + - `APP_PASSWORD` + - `SESSION_SECRET` (langer zufälliger String) + + `MONGODB_URI` und `PORT` sind in der `docker-compose.yml` bereits fest + auf die interne Mongo-Verbindung gesetzt und müssen nicht verändert + werden. +4. **Domain für die App vergeben.** Dafür zwei Möglichkeiten: + - Im Coolify-UI bei der Resource den Service `app` öffnen und unter + *Domains* eine Domain (oder Coolifys generierte `sslip.io`-Domain) + eintragen, **oder** + - in der `docker-compose.yml` die Zeile `# - SERVICE_FQDN_APP_3000` + einkommentieren — Coolify generiert dann automatisch eine Domain auf + Basis deiner Wildcard-Domain. +5. **Deploy** klicken. Coolify baut das Image, startet MongoDB und die App + im selben internen Docker-Netzwerk und richtet automatisch HTTPS + (Let's Encrypt) für die vergebene Domain ein. + +Die MongoDB ist dabei **nicht** öffentlich erreichbar — sie läuft nur im +internen Netzwerk und ist für die App über den Service-Namen `mongo` +ansprechbar. Die Daten liegen in einem persistenten Volume (`mongo_data`) +und bleiben bei Redeploys erhalten. + +### Alternative: Coolifys eigene Mongo-Datenbank nutzen + +Statt MongoDB im `docker-compose.yml` mitlaufen zu lassen, kannst du in +Coolify auch **New Resource → Database → MongoDB** als eigene, separat +verwaltete Datenbank anlegen und die App stattdessen per `Dockerfile` als +normale *Application*-Resource deployen. Trag in diesem Fall die von +Coolify angezeigte Connection-String als `MONGODB_URI` in den +Umgebungsvariablen der App ein. Das `docker-compose.yml` brauchst du dann +nicht. + +## Einstellungen nach dem ersten Login + +Unter **Einstellungen** legst du fest: + +- Soll-Arbeitszeit pro Arbeitstag (z. B. 8 Stunden) +- An welchen Wochentagen überhaupt gearbeitet wird +- Ein optionales Start-Guthaben in Stunden, falls du schon vor der + Nutzung dieser App ein Über- oder Minusstunden-Konto hattest + +Diese Werte wirken sich nur auf **neue** Buchungen aus — bereits +gespeicherte Tage behalten den Saldo, der zum Zeitpunkt ihrer Erfassung +berechnet wurde. + +## Projektstruktur + +``` +zeitkonto/ +├── server.js Einstiegspunkt (Express-App) +├── src/ +│ ├── db.js MongoDB-Verbindung (mit Retry) +│ ├── middleware/auth.js Login-Schutz +│ ├── models/ Mongoose-Schemas (TimeEntry, Settings) +│ ├── routes/ Login, Dashboard/Buchungen, Einstellungen +│ └── utils/time.js Zeit-/Saldoberechnungen +├── views/ EJS-Templates +├── public/css/style.css Styling +├── Dockerfile +├── docker-compose.yml App + MongoDB für Coolify/lokal +└── .env.example +``` + +## Sicherheitshinweis + +Der Login schützt mit einem einzelnen Benutzernamen/Passwort-Paar aus den +Umgebungsvariablen. Das reicht für ein persönliches Tool hinter HTTPS +(Coolify richtet das automatisch ein), ersetzt aber kein vollwertiges +Mehrbenutzer-Auth-System. Setze in jedem Fall ein eigenes, starkes +`APP_PASSWORD` und einen zufälligen `SESSION_SECRET`, bevor du die App +öffentlich erreichbar machst. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e3161ca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + app: + build: . + restart: unless-stopped + depends_on: + mongo: + condition: service_healthy + environment: + - MONGODB_URI=mongodb://mongo:27017/zeitkonto + - SESSION_SECRET=${SESSION_SECRET:-bitte-langen-zufaelligen-string-einsetzen} + - APP_USERNAME=${APP_USERNAME:-admin} + - APP_PASSWORD=${APP_PASSWORD:-bitte-aendern} + - PORT=3000 + # Optional: Coolify kann hierüber automatisch eine Domain für diesen + # Service vergeben. Dazu einfach die nächste Zeile einkommentieren + # (siehe README, Abschnitt "Deployment in Coolify"). + # - SERVICE_FQDN_APP_3000 + ports: + - '3000:3000' + healthcheck: + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000/healthz'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + mongo: + image: mongo:7 + restart: unless-stopped + volumes: + - mongo_data:/data/db + healthcheck: + test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + mongo_data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..533ff73 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "zeitkonto", + "version": "1.0.0", + "private": true, + "description": "Arbeitszeiterfassung mit automatischer Überstundenberechnung", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "dotenv": "^16.4.5", + "ejs": "^3.1.10", + "express": "^4.19.2", + "express-session": "^1.18.0", + "mongoose": "^8.5.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..d19d75b --- /dev/null +++ b/server.js @@ -0,0 +1,62 @@ +require('dotenv').config(); + +const path = require('path'); +const express = require('express'); +const session = require('express-session'); + +const { connectDB } = require('./src/db'); +const authRoutes = require('./src/routes/auth'); +const dashboardRoutes = require('./src/routes/dashboard'); +const settingsRoutes = require('./src/routes/settings'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use( + session({ + secret: process.env.SESSION_SECRET || 'bitte-secret-setzen', + resave: false, + saveUninitialized: false, + cookie: { + maxAge: 1000 * 60 * 60 * 24 * 30, // 30 Tage + }, + }) +); + +// Healthcheck für Docker/Coolify +app.get('/healthz', (req, res) => res.status(200).send('ok')); + +app.use(authRoutes); +app.use(dashboardRoutes); +app.use(settingsRoutes); + +app.use((req, res) => { + res.status(404).render('error', { title: 'Nicht gefunden', message: 'Seite nicht gefunden.' }); +}); + +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + console.error(err); + res.status(500).render('error', { + title: 'Fehler', + message: 'Es ist ein unerwarteter Fehler aufgetreten.', + }); +}); + +async function start() { + await connectDB(); + app.listen(PORT, () => { + console.log(`Zeitkonto läuft auf Port ${PORT}`); + }); +} + +start().catch((err) => { + console.error('Start fehlgeschlagen:', err); + process.exit(1); +});