fix: Session-Store, DOM-Timing und API-Pfad-Fehler beheben
- connect-sqlite3 durch eigenen BetterSQLiteStore ersetzt (sessions-Tabelle
in der bestehenden DB, keine native Kompilierung nötig)
- db.init() vor require('./auth') gezogen damit BetterSQLiteStore-Konstruktor
db.get() erfolgreich aufrufen kann
- router.js: App-Shell und pageWrapper vor module.render() in DOM einfügen
damit document.getElementById() in Seiten-Modulen funktioniert
- Seiten-Module (meals, notes, contacts, calendar, budget): _container-Referenz
eingeführt, alle document.getElementById() auf _container.querySelector() bzw.
document.querySelector() für body-Elemente umgestellt
- login.js: User-Objekt nach erfolgreichem Login an navigate() übergeben
damit auth.me()-Roundtrip entfällt
- calendar.js: /users → /auth/users korrigiert (404-Fix)
- SW-Cache v8 (erzwingt Reload aller gecachten Seiten-Module)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-2
@@ -19,8 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"better-sqlite3": "^9.6.0",
|
"better-sqlite3": "^9.6.0",
|
||||||
"connect-sqlite3": "^0.9.15",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv": "^16.4.7",
|
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
|||||||
+10
-8
@@ -28,6 +28,7 @@ let state = {
|
|||||||
entries: [],
|
entries: [],
|
||||||
summary: null,
|
summary: null,
|
||||||
};
|
};
|
||||||
|
let _container = null;
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Formatierung
|
// Formatierung
|
||||||
@@ -67,6 +68,7 @@ async function loadMonth(month) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
_container = container;
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
state.month = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
state.month = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
@@ -103,17 +105,17 @@ export async function render(container, { user }) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function wireNav() {
|
function wireNav() {
|
||||||
document.getElementById('budget-prev').addEventListener('click', async () => {
|
_container.querySelector('#budget-prev').addEventListener('click', async () => {
|
||||||
await loadMonth(addMonths(state.month, -1));
|
await loadMonth(addMonths(state.month, -1));
|
||||||
renderBody();
|
renderBody();
|
||||||
updateLabel();
|
updateLabel();
|
||||||
});
|
});
|
||||||
document.getElementById('budget-next').addEventListener('click', async () => {
|
_container.querySelector('#budget-next').addEventListener('click', async () => {
|
||||||
await loadMonth(addMonths(state.month, 1));
|
await loadMonth(addMonths(state.month, 1));
|
||||||
renderBody();
|
renderBody();
|
||||||
updateLabel();
|
updateLabel();
|
||||||
});
|
});
|
||||||
document.getElementById('budget-today').addEventListener('click', async () => {
|
_container.querySelector('#budget-today').addEventListener('click', async () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const m = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
const m = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
||||||
if (m === state.month) return;
|
if (m === state.month) return;
|
||||||
@@ -121,12 +123,12 @@ function wireNav() {
|
|||||||
renderBody();
|
renderBody();
|
||||||
updateLabel();
|
updateLabel();
|
||||||
});
|
});
|
||||||
document.getElementById('budget-add').addEventListener('click', () => openModal({ mode: 'create' }));
|
_container.querySelector('#budget-add').addEventListener('click', () => openModal({ mode: 'create' }));
|
||||||
updateLabel();
|
updateLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLabel() {
|
function updateLabel() {
|
||||||
const lbl = document.getElementById('budget-label');
|
const lbl = _container.querySelector('#budget-label');
|
||||||
if (lbl) lbl.textContent = formatMonthLabel(state.month);
|
if (lbl) lbl.textContent = formatMonthLabel(state.month);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +137,7 @@ function updateLabel() {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderBody() {
|
function renderBody() {
|
||||||
const body = document.getElementById('budget-body');
|
const body = _container.querySelector('#budget-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
updateLabel();
|
updateLabel();
|
||||||
|
|
||||||
@@ -186,7 +188,7 @@ function renderBody() {
|
|||||||
|
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
|
|
||||||
document.getElementById('budget-list')?.addEventListener('click', async (e) => {
|
_container.querySelector('#budget-list')?.addEventListener('click', async (e) => {
|
||||||
const delBtn = e.target.closest('[data-action="delete"]');
|
const delBtn = e.target.closest('[data-action="delete"]');
|
||||||
if (delBtn) { await deleteEntry(parseInt(delBtn.dataset.id, 10)); return; }
|
if (delBtn) { await deleteEntry(parseInt(delBtn.dataset.id, 10)); return; }
|
||||||
|
|
||||||
@@ -262,7 +264,7 @@ function formatEntryDate(dateStr) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function openModal({ mode, entry = null }) {
|
function openModal({ mode, entry = null }) {
|
||||||
document.getElementById('budget-modal-overlay')?.remove();
|
document.querySelector('#budget-modal-overlay')?.remove();
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ let state = {
|
|||||||
rangeFrom: '',
|
rangeFrom: '',
|
||||||
rangeTo: '',
|
rangeTo: '',
|
||||||
};
|
};
|
||||||
|
let _container = null;
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Datumshelfer
|
// Datumshelfer
|
||||||
@@ -130,7 +131,7 @@ async function loadRange(from, to) {
|
|||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/users');
|
const res = await api.get('/auth/users');
|
||||||
state.users = res.data;
|
state.users = res.data;
|
||||||
} catch {
|
} catch {
|
||||||
state.users = [];
|
state.users = [];
|
||||||
@@ -142,6 +143,7 @@ async function loadUsers() {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
_container = container;
|
||||||
state.today = isoDate(new Date());
|
state.today = isoDate(new Date());
|
||||||
state.cursor = state.today;
|
state.cursor = state.today;
|
||||||
state.view = 'month';
|
state.view = 'month';
|
||||||
@@ -165,7 +167,7 @@ export async function render(container, { user }) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderToolbar() {
|
function renderToolbar() {
|
||||||
const bar = document.getElementById('cal-toolbar');
|
const bar = _container.querySelector('#cal-toolbar');
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
|
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
@@ -216,7 +218,7 @@ function renderToolbar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateLabel() {
|
function updateLabel() {
|
||||||
const lbl = document.getElementById('cal-label');
|
const lbl = _container.querySelector('#cal-label');
|
||||||
if (!lbl) return;
|
if (!lbl) return;
|
||||||
const d = new Date(state.cursor + 'T00:00:00');
|
const d = new Date(state.cursor + 'T00:00:00');
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
@@ -273,7 +275,7 @@ async function reloadForView() {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderView() {
|
function renderView() {
|
||||||
const body = document.getElementById('cal-body');
|
const body = _container.querySelector('#cal-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
body.innerHTML = '';
|
body.innerHTML = '';
|
||||||
|
|
||||||
@@ -624,7 +626,7 @@ function renderAgendaEvent(ev) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function showEventPopup(ev, anchor) {
|
function showEventPopup(ev, anchor) {
|
||||||
document.getElementById('event-popup')?.remove();
|
document.querySelector('#event-popup')?.remove();
|
||||||
|
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.id = 'event-popup';
|
popup.id = 'event-popup';
|
||||||
@@ -689,7 +691,7 @@ function showEventPopup(ev, anchor) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function openEventModal({ mode, event = null, date = null }) {
|
function openEventModal({ mode, event = null, date = null }) {
|
||||||
document.getElementById('event-modal-overlay')?.remove();
|
document.querySelector('#event-modal-overlay')?.remove();
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.id = 'event-modal-overlay';
|
overlay.id = 'event-modal-overlay';
|
||||||
@@ -860,15 +862,15 @@ function buildEventModalHTML({ mode, event, date }) {
|
|||||||
// Allday-Toggle: Felder umschalten
|
// Allday-Toggle: Felder umschalten
|
||||||
document.addEventListener('change', (e) => {
|
document.addEventListener('change', (e) => {
|
||||||
if (e.target.id !== 'modal-allday') return;
|
if (e.target.id !== 'modal-allday') return;
|
||||||
const tf = document.getElementById('time-fields');
|
const tf = document.querySelector('#time-fields');
|
||||||
const af = document.getElementById('allday-fields');
|
const af = document.querySelector('#allday-fields');
|
||||||
if (!tf || !af) return;
|
if (!tf || !af) return;
|
||||||
if (e.target.checked) { tf.style.display = 'none'; af.style.display = ''; }
|
if (e.target.checked) { tf.style.display = 'none'; af.style.display = ''; }
|
||||||
else { tf.style.display = ''; af.style.display = 'none'; }
|
else { tf.style.display = ''; af.style.display = 'none'; }
|
||||||
});
|
});
|
||||||
|
|
||||||
function closeEventModal() {
|
function closeEventModal() {
|
||||||
document.getElementById('event-modal-overlay')?.remove();
|
document.querySelector('#event-modal-overlay')?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEvent(overlay, mode, eventId) {
|
async function saveEvent(overlay, mode, eventId) {
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ let state = {
|
|||||||
activeCategory: null,
|
activeCategory: null,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
};
|
};
|
||||||
|
let _container = null;
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Entry Point
|
// Entry Point
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
_container = container;
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="contacts-page">
|
<div class="contacts-page">
|
||||||
<div class="contacts-toolbar">
|
<div class="contacts-toolbar">
|
||||||
@@ -70,7 +72,7 @@ export async function render(container, { user }) {
|
|||||||
|
|
||||||
// Suche
|
// Suche
|
||||||
let searchTimer;
|
let searchTimer;
|
||||||
document.getElementById('contacts-search').addEventListener('input', (e) => {
|
_container.querySelector('#contacts-search').addEventListener('input', (e) => {
|
||||||
clearTimeout(searchTimer);
|
clearTimeout(searchTimer);
|
||||||
searchTimer = setTimeout(() => {
|
searchTimer = setTimeout(() => {
|
||||||
state.searchQuery = e.target.value.trim();
|
state.searchQuery = e.target.value.trim();
|
||||||
@@ -79,10 +81,10 @@ export async function render(container, { user }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Kategorie-Filter
|
// Kategorie-Filter
|
||||||
document.getElementById('contacts-filters').addEventListener('click', (e) => {
|
_container.querySelector('#contacts-filters').addEventListener('click', (e) => {
|
||||||
const chip = e.target.closest('[data-cat]');
|
const chip = e.target.closest('[data-cat]');
|
||||||
if (!chip) return;
|
if (!chip) return;
|
||||||
document.querySelectorAll('.contact-filter-chip').forEach((c) =>
|
_container.querySelectorAll('.contact-filter-chip').forEach((c) =>
|
||||||
c.classList.toggle('contact-filter-chip--active', c === chip)
|
c.classList.toggle('contact-filter-chip--active', c === chip)
|
||||||
);
|
);
|
||||||
state.activeCategory = chip.dataset.cat || null;
|
state.activeCategory = chip.dataset.cat || null;
|
||||||
@@ -90,7 +92,7 @@ export async function render(container, { user }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Neu
|
// Neu
|
||||||
document.getElementById('contacts-add-btn').addEventListener('click', () =>
|
_container.querySelector('#contacts-add-btn').addEventListener('click', () =>
|
||||||
openModal({ mode: 'create' })
|
openModal({ mode: 'create' })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -119,7 +121,7 @@ function filterContacts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
const container = document.getElementById('contacts-list');
|
const container = _container.querySelector('#contacts-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const contacts = filterContacts();
|
const contacts = filterContacts();
|
||||||
@@ -196,7 +198,7 @@ function renderContactItem(c) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function openModal({ mode, contact = null }) {
|
function openModal({ mode, contact = null }) {
|
||||||
document.getElementById('contact-modal-overlay')?.remove();
|
document.querySelector('#contact-modal-overlay')?.remove();
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const v = (field) => escHtml(isEdit && contact[field] ? contact[field] : '');
|
const v = (field) => escHtml(isEdit && contact[field] ? contact[field] : '');
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export async function render(container) {
|
|||||||
submitBtn.textContent = 'Wird angemeldet …';
|
submitBtn.textContent = 'Wird angemeldet …';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await auth.login(username, password);
|
const result = await auth.login(username, password);
|
||||||
window.oikos.navigate('/');
|
window.oikos.navigate('/', result.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(errorEl, err.status === 429
|
showError(errorEl, err.status === 429
|
||||||
? 'Zu viele Versuche. Bitte warte kurz.'
|
? 'Zu viele Versuche. Bitte warte kurz.'
|
||||||
|
|||||||
+11
-7
@@ -30,6 +30,9 @@ let state = {
|
|||||||
modal: null,
|
modal: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Container-Referenz für Hilfsfunktionen (wird in render() gesetzt)
|
||||||
|
let _container = null;
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Datumshelfer
|
// Datumshelfer
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -90,6 +93,7 @@ async function loadLists() {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
_container = container;
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="meals-page">
|
<div class="meals-page">
|
||||||
<div class="week-nav">
|
<div class="week-nav">
|
||||||
@@ -123,10 +127,10 @@ export async function render(container, { user }) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderWeekGrid() {
|
function renderWeekGrid() {
|
||||||
const grid = document.getElementById('week-grid');
|
const grid = _container.querySelector('#week-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
document.getElementById('week-label').textContent =
|
_container.querySelector('#week-label').textContent =
|
||||||
formatWeekLabel(state.currentWeek);
|
formatWeekLabel(state.currentWeek);
|
||||||
|
|
||||||
const days = Array.from({ length: 7 }, (_, i) => addDays(state.currentWeek, i));
|
const days = Array.from({ length: 7 }, (_, i) => addDays(state.currentWeek, i));
|
||||||
@@ -211,17 +215,17 @@ function renderSlot(date, type, mealsForDay) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function wireNav() {
|
function wireNav() {
|
||||||
document.getElementById('week-prev')?.addEventListener('click', async () => {
|
_container.querySelector('#week-prev')?.addEventListener('click', async () => {
|
||||||
await loadWeek(addDays(state.currentWeek, -7));
|
await loadWeek(addDays(state.currentWeek, -7));
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('week-next')?.addEventListener('click', async () => {
|
_container.querySelector('#week-next')?.addEventListener('click', async () => {
|
||||||
await loadWeek(addDays(state.currentWeek, 7));
|
await loadWeek(addDays(state.currentWeek, 7));
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('week-today')?.addEventListener('click', async () => {
|
_container.querySelector('#week-today')?.addEventListener('click', async () => {
|
||||||
const monday = getMondayOf(new Date().toISOString().slice(0, 10));
|
const monday = getMondayOf(new Date().toISOString().slice(0, 10));
|
||||||
if (monday === state.currentWeek) return;
|
if (monday === state.currentWeek) return;
|
||||||
await loadWeek(monday);
|
await loadWeek(monday);
|
||||||
@@ -272,7 +276,7 @@ function wireGrid(grid) {
|
|||||||
|
|
||||||
function openModal(opts) {
|
function openModal(opts) {
|
||||||
state.modal = opts;
|
state.modal = opts;
|
||||||
document.getElementById('meal-modal-overlay')?.remove();
|
document.querySelector('#meal-modal-overlay')?.remove();
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.id = 'meal-modal-overlay';
|
overlay.id = 'meal-modal-overlay';
|
||||||
@@ -465,7 +469,7 @@ function ingredientRowHTML(name, qty, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('meal-modal-overlay')?.remove();
|
document.querySelector('#meal-modal-overlay')?.remove();
|
||||||
state.modal = null;
|
state.modal = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const NOTE_COLORS = [
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
let state = { notes: [], user: null };
|
let state = { notes: [], user: null };
|
||||||
|
let _container = null;
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Markdown-Light Renderer
|
// Markdown-Light Renderer
|
||||||
@@ -39,6 +40,7 @@ function renderMarkdownLight(text) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
_container = container;
|
||||||
state.user = user;
|
state.user = user;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -60,7 +62,7 @@ export async function render(container, { user }) {
|
|||||||
state.notes = res.data;
|
state.notes = res.data;
|
||||||
renderGrid();
|
renderGrid();
|
||||||
|
|
||||||
document.getElementById('notes-add-btn').addEventListener('click', () => openModal({ mode: 'create' }));
|
_container.querySelector('#notes-add-btn').addEventListener('click', () => openModal({ mode: 'create' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -68,7 +70,7 @@ export async function render(container, { user }) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderGrid() {
|
function renderGrid() {
|
||||||
const grid = document.getElementById('notes-grid');
|
const grid = _container.querySelector('#notes-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
if (!state.notes.length) {
|
if (!state.notes.length) {
|
||||||
@@ -140,7 +142,7 @@ function renderNoteCard(note) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function openModal({ mode, note = null }) {
|
function openModal({ mode, note = null }) {
|
||||||
document.getElementById('note-modal-overlay')?.remove();
|
document.querySelector('#note-modal-overlay')?.remove();
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.id = 'note-modal-overlay';
|
overlay.id = 'note-modal-overlay';
|
||||||
|
|||||||
+22
-10
@@ -48,9 +48,18 @@ let currentPath = null;
|
|||||||
/**
|
/**
|
||||||
* Navigiert zu einem Pfad und rendert die entsprechende Seite.
|
* Navigiert zu einem Pfad und rendert die entsprechende Seite.
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
|
* @param {Object|boolean} userOrPushState - Direkt ein User-Objekt nach Login,
|
||||||
|
* oder boolean (pushState) für interne Navigation
|
||||||
* @param {boolean} pushState - false beim initialen Load und popstate
|
* @param {boolean} pushState - false beim initialen Load und popstate
|
||||||
*/
|
*/
|
||||||
async function navigate(path, pushState = true) {
|
async function navigate(path, userOrPushState = true, pushState = true) {
|
||||||
|
// Überlastung: navigate(path, user) nach Login vs navigate(path, false) beim Init
|
||||||
|
if (typeof userOrPushState === 'object' && userOrPushState !== null) {
|
||||||
|
currentUser = userOrPushState;
|
||||||
|
} else {
|
||||||
|
pushState = userOrPushState;
|
||||||
|
}
|
||||||
|
|
||||||
if (path === currentPath) return;
|
if (path === currentPath) return;
|
||||||
currentPath = path;
|
currentPath = path;
|
||||||
|
|
||||||
@@ -100,21 +109,23 @@ async function renderPage(route) {
|
|||||||
throw new Error(`Seite ${route.page} exportiert keine render()-Funktion.`);
|
throw new Error(`Seite ${route.page} exportiert keine render()-Funktion.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seiten-Wrapper erstellen
|
// App-Shell einmalig aufbauen BEVOR render() aufgerufen wird —
|
||||||
const pageWrapper = document.createElement('div');
|
// page-content muss im DOM existieren damit document.getElementById()
|
||||||
pageWrapper.className = 'page-transition';
|
// in Seiten-Modulen funktioniert.
|
||||||
pageWrapper.style.animation = 'page-in 0.2s ease forwards';
|
|
||||||
|
|
||||||
await module.render(pageWrapper, { user: currentUser });
|
|
||||||
|
|
||||||
// Nav + Content einmalig aufbauen (beim ersten Render)
|
|
||||||
if (!document.querySelector('.nav-bottom') && currentUser) {
|
if (!document.querySelector('.nav-bottom') && currentUser) {
|
||||||
renderAppShell(app);
|
renderAppShell(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seiten-Wrapper bereits jetzt in den DOM einfügen, damit
|
||||||
|
// document.getElementById() in render() die richtigen Elemente findet.
|
||||||
|
const pageWrapper = document.createElement('div');
|
||||||
|
pageWrapper.className = 'page-transition';
|
||||||
|
pageWrapper.style.animation = 'page-in 0.2s ease forwards';
|
||||||
const content = document.getElementById('page-content') || app;
|
const content = document.getElementById('page-content') || app;
|
||||||
content.replaceChildren(pageWrapper);
|
content.replaceChildren(pageWrapper);
|
||||||
|
|
||||||
|
await module.render(pageWrapper, { user: currentUser });
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Router] Seiten-Render-Fehler:', err);
|
console.error('[Router] Seiten-Render-Fehler:', err);
|
||||||
renderError(app, err);
|
renderError(app, err);
|
||||||
@@ -199,9 +210,10 @@ function renderError(container, err) {
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state__title">Etwas ist schiefgelaufen.</div>
|
<div class="empty-state__title">Etwas ist schiefgelaufen.</div>
|
||||||
<div class="empty-state__description">${err.message}</div>
|
<div class="empty-state__description">${err.message}</div>
|
||||||
<button class="btn btn--primary" onclick="location.reload()">Neu laden</button>
|
<button class="btn btn--primary" id="error-reload-btn">Neu laden</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
container.querySelector('#error-reload-btn')?.addEventListener('click', () => location.reload());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@
|
|||||||
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v5';
|
const SHELL_CACHE = 'oikos-shell-v8';
|
||||||
const PAGES_CACHE = 'oikos-pages-v5';
|
const PAGES_CACHE = 'oikos-pages-v8';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v5';
|
const ASSETS_CACHE = 'oikos-assets-v8';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
// App-Shell: sofort benötigt für ersten Render
|
// App-Shell: sofort benötigt für ersten Render
|
||||||
|
|||||||
+68
-8
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Modul: Authentifizierung (Auth)
|
* Modul: Authentifizierung (Auth)
|
||||||
* Zweck: Login-Route, Session-Middleware, Auth-Guard für geschützte Routen
|
* Zweck: Login-Route, Session-Middleware, Auth-Guard für geschützte Routen
|
||||||
* Abhängigkeiten: express, bcrypt, express-session, connect-sqlite3, server/db.js
|
* Abhängigkeiten: express, bcrypt, express-session, server/db.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -17,15 +17,75 @@ const { generateToken } = require('./middleware/csrf');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Session-Store (SQLite)
|
// Session-Store (better-sqlite3, gleiche DB-Instanz wie App)
|
||||||
|
// Eigene Implementierung — kein connect-sqlite3 (nutzt sqlite3-Bindings,
|
||||||
|
// die separat kompiliert werden müssten und die Fehlerquelle waren).
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
const SQLiteStore = require('connect-sqlite3')(session);
|
class BetterSQLiteStore extends session.Store {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// Tabelle anlegen falls nicht vorhanden
|
||||||
|
db.get().exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
sid TEXT PRIMARY KEY,
|
||||||
|
sess TEXT NOT NULL,
|
||||||
|
expired_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
// Abgelaufene Sessions regelmäßig aufräumen (alle 15 Minuten)
|
||||||
|
setInterval(() => {
|
||||||
|
db.get().prepare('DELETE FROM sessions WHERE expired_at <= ?').run(Date.now());
|
||||||
|
}, 15 * 60_000).unref();
|
||||||
|
}
|
||||||
|
|
||||||
const sessionStore = new SQLiteStore({
|
get(sid, callback) {
|
||||||
db: 'sessions.db',
|
try {
|
||||||
dir: process.env.DB_PATH ? require('path').dirname(process.env.DB_PATH) : '.',
|
const row = db.get()
|
||||||
ttl: 60 * 60 * 24 * 7, // 7 Tage in Sekunden
|
.prepare('SELECT sess FROM sessions WHERE sid = ? AND expired_at > ?')
|
||||||
});
|
.get(sid, Date.now());
|
||||||
|
callback(null, row ? JSON.parse(row.sess) : null);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(sid, sess, callback) {
|
||||||
|
try {
|
||||||
|
const ttl = sess.cookie?.maxAge ?? 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const expiredAt = Date.now() + ttl;
|
||||||
|
db.get()
|
||||||
|
.prepare('INSERT OR REPLACE INTO sessions (sid, sess, expired_at) VALUES (?, ?, ?)')
|
||||||
|
.run(sid, JSON.stringify(sess), expiredAt);
|
||||||
|
callback(null);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(sid, callback) {
|
||||||
|
try {
|
||||||
|
db.get().prepare('DELETE FROM sessions WHERE sid = ?').run(sid);
|
||||||
|
callback(null);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch(sid, sess, callback) {
|
||||||
|
try {
|
||||||
|
const ttl = sess.cookie?.maxAge ?? 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const expiredAt = Date.now() + ttl;
|
||||||
|
db.get()
|
||||||
|
.prepare('UPDATE sessions SET expired_at = ? WHERE sid = ?')
|
||||||
|
.run(expiredAt, sid);
|
||||||
|
callback(null);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionStore = new BetterSQLiteStore();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session-Middleware konfigurieren.
|
* Session-Middleware konfigurieren.
|
||||||
|
|||||||
+7
-8
@@ -11,20 +11,19 @@ const express = require('express');
|
|||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Datenbank initialisieren (muss vor require('./auth') stehen,
|
||||||
|
// da BetterSQLiteStore im Konstruktor db.get() aufruft)
|
||||||
|
// --------------------------------------------------------
|
||||||
const db = require('./db');
|
const db = require('./db');
|
||||||
|
db.init();
|
||||||
|
|
||||||
const { router: authRouter, sessionMiddleware, requireAuth } = require('./auth');
|
const { router: authRouter, sessionMiddleware, requireAuth } = require('./auth');
|
||||||
const { csrfMiddleware } = require('./middleware/csrf');
|
const { csrfMiddleware } = require('./middleware/csrf');
|
||||||
const googleCalendar = require('./services/google-calendar');
|
const googleCalendar = require('./services/google-calendar');
|
||||||
const appleCalendar = require('./services/apple-calendar');
|
const appleCalendar = require('./services/apple-calendar');
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// Datenbank initialisieren
|
|
||||||
// --------------------------------------------------------
|
|
||||||
db.init();
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Security-Middleware
|
// Security-Middleware
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user