feat: Phase 1 — Projektstruktur, DB-Schema, Auth-System
- Vollständige Verzeichnisstruktur gemäß CLAUDE.md - Express-Server mit Helmet, Sessions, Rate Limiting, SPA-Fallback - SQLite-Schema (Migration v1): 10 Tabellen, updated_at-Triggers, Indizes - Versioniertes Migrations-System (schema_migrations) - Auth-Routen: Login, Logout, /me, Admin-User-CRUD - Frontend App-Shell: SPA-Router, API-Client, Design-System (CSS Tokens) - PWA: Service Worker, Web App Manifest - Setup-Script für ersten Admin-User (node setup.js) - DB-Tests mit node:sqlite built-in: 29/29 bestanden - Docker Compose + Dockerfile + Nginx-Beispielkonfiguration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Budget
|
||||
* Zweck: Seite für das Budget-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Budget</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Calendar
|
||||
* Zweck: Seite für das Calendar-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Calendar</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Contacts
|
||||
* Zweck: Seite für das Contacts-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Contacts</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Dashboard
|
||||
* Zweck: Seite für das Dashboard-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Dashboard</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Modul: Login-Seite
|
||||
* Zweck: Anmeldeformular mit Username/Passwort, Fehlerbehandlung, Session-Start
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { auth } from '/api.js';
|
||||
|
||||
/**
|
||||
* Rendert die Login-Seite in den gegebenen Container.
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<div class="login-page">
|
||||
<div class="login-card card card--padded">
|
||||
<h1 class="login-card__title">Oikos</h1>
|
||||
<p class="login-card__subtitle">Familienplaner</p>
|
||||
|
||||
<form class="login-form" id="login-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label class="label" for="username">Benutzername</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
placeholder="benutzername"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label" for="password">Passwort</label>
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="login-error" id="login-error" role="alert" aria-live="polite" hidden></div>
|
||||
|
||||
<button type="submit" class="btn btn--primary login-form__submit" id="login-btn">
|
||||
Anmelden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const form = container.querySelector('#login-form');
|
||||
const errorEl = container.querySelector('#login-error');
|
||||
const submitBtn = container.querySelector('#login-btn');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
errorEl.hidden = true;
|
||||
|
||||
const username = form.username.value.trim();
|
||||
const password = form.password.value;
|
||||
|
||||
if (!username || !password) {
|
||||
showError(errorEl, 'Bitte alle Felder ausfüllen.');
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird angemeldet …';
|
||||
|
||||
try {
|
||||
await auth.login(username, password);
|
||||
window.oikos.navigate('/');
|
||||
} catch (err) {
|
||||
showError(errorEl, err.status === 429
|
||||
? 'Zu viele Versuche. Bitte warte kurz.'
|
||||
: 'Ungültige Anmeldedaten.'
|
||||
);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Anmelden';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showError(el, message) {
|
||||
el.textContent = message;
|
||||
el.hidden = false;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Meals
|
||||
* Zweck: Seite für das Meals-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Meals</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Notes
|
||||
* Zweck: Seite für das Notes-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Notes</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Settings
|
||||
* Zweck: Seite für das Settings-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Settings</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Shopping
|
||||
* Zweck: Seite für das Shopping-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Shopping</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Modul: Tasks
|
||||
* Zweck: Seite für das Tasks-Modul
|
||||
* Abhängigkeiten: /api.js
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ user: object }} context
|
||||
*/
|
||||
export async function render(container, { user }) {
|
||||
container.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page__header">
|
||||
<h1 class="page__title">Tasks</h1>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__title">Kommt bald.</div>
|
||||
<div class="empty-state__description">Dieses Modul wird in Phase 2 implementiert.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user