feat: configurable trip reminders, admin full access, and enhanced audit logging

- Add configurable trip reminder days (1, 3, 9 or custom up to 30) settable by trip owner
- Grant administrators full access to edit, archive, delete, view and list all trips
- Show trip owner email in audit logs and docker logs when admin edits/deletes another user's trip
- Show target user email in audit logs when admin edits or deletes a user account
- Use email instead of username in all notifications (Discord/Slack/email) to avoid ambiguity
- Grey out notification event toggles when no SMTP/webhook is configured
- Grey out trip reminder selector when notifications are disabled
- Skip local admin account creation when OIDC_ONLY=true with OIDC configured
- Conditional scheduler logging: show disabled reason or active reminder count
- Log per-owner reminder creation/update in docker logs
- Demote 401/403 HTTP errors to DEBUG log level to reduce noise
- Hide edit/archive/delete buttons for non-owner invited users on trip cards
- Fix literal "0" rendering on trip cards from SQLite numeric is_owner field
- Add missing translation keys across all 14 language files

Made-with: Cursor
This commit is contained in:
Andrei Brebene
2026-03-31 16:42:37 +03:00
parent 9b2f083e4b
commit 7522f396e7
31 changed files with 378 additions and 83 deletions
+3
View File
@@ -433,6 +433,9 @@ function runMigrations(db: Database.Database): void {
() => {
try { db.exec('ALTER TABLE users ADD COLUMN must_change_password INTEGER DEFAULT 0'); } catch {}
},
() => {
try { db.exec('ALTER TABLE trips ADD COLUMN reminder_days INTEGER DEFAULT 3'); } catch {}
},
];
if (currentVersion < migrations.length) {
+1
View File
@@ -41,6 +41,7 @@ function createTables(db: Database.Database): void {
currency TEXT DEFAULT 'EUR',
cover_image TEXT,
is_archived INTEGER DEFAULT 0,
reminder_days INTEGER DEFAULT 3,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
+15
View File
@@ -1,11 +1,26 @@
import Database from 'better-sqlite3';
import crypto from 'crypto';
function isOidcOnlyConfigured(): boolean {
if (process.env.OIDC_ONLY !== 'true') return false;
return !!(process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID);
}
function seedAdminAccount(db: Database.Database): void {
try {
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
if (userCount > 0) return;
if (isOidcOnlyConfigured()) {
console.log('');
console.log('╔══════════════════════════════════════════════╗');
console.log('║ TREK — OIDC-Only Mode ║');
console.log('║ First SSO login will become admin. ║');
console.log('╚══════════════════════════════════════════════╝');
console.log('');
return;
}
const bcrypt = require('bcryptjs');
const password = crypto.randomBytes(12).toString('base64url');
const hash = bcrypt.hashSync(password, 12);