This commit is contained in:
ProgrammGamer
2026-07-26 12:03:57 +02:00
commit edf9514bc3
50 changed files with 9918 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
import { DatabaseSync } from "node:sqlite";
import { hashSync } from "bcryptjs";
import { getDatabasePath, getUploadDir } from "./env";
declare global {
var __mopedDatabase: DatabaseSync | undefined;
var __mopedDatabaseInitialized: boolean | undefined;
}
export const db =
globalThis.__mopedDatabase ??
new DatabaseSync(getDatabasePath(), {
enableForeignKeyConstraints: true,
});
globalThis.__mopedDatabase = db;
function ensureSchema() {
db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL COLLATE NOCASE UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS motorcycles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT NOT NULL,
brand TEXT NOT NULL,
model TEXT NOT NULL,
year INTEGER,
vin TEXT,
plate TEXT,
current_km INTEGER NOT NULL DEFAULT 0 CHECK (current_km >= 0),
notes TEXT,
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS maintenance_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
type TEXT NOT NULL CHECK (type IN ('reparatur', 'tuev_hu', 'inspektion', 'reifenwechsel', 'oelwechsel', 'sonstiges')),
title TEXT NOT NULL,
description TEXT,
event_date TEXT NOT NULL,
km INTEGER CHECK (km IS NULL OR km >= 0),
cost_cents INTEGER CHECK (cost_cents IS NULL OR cost_cents >= 0),
due_date TEXT,
due_km INTEGER CHECK (due_km IS NULL OR due_km >= 0),
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS trips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
trip_date TEXT NOT NULL,
driver_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
start_km INTEGER NOT NULL CHECK (start_km >= 0),
end_km INTEGER NOT NULL CHECK (end_km >= start_km),
purpose TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
maintenance_event_id INTEGER NOT NULL UNIQUE REFERENCES maintenance_events(id) ON DELETE CASCADE,
stored_name TEXT NOT NULL UNIQUE,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE INDEX IF NOT EXISTS idx_maintenance_motorcycle
ON maintenance_events(motorcycle_id, event_date DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_maintenance_due
ON maintenance_events(motorcycle_id, type, event_date DESC);
CREATE INDEX IF NOT EXISTS idx_trips_motorcycle
ON trips(motorcycle_id, trip_date DESC, id DESC);
`);
}
function ensureInitialAdmin() {
const row = db.prepare("SELECT COUNT(*) AS count FROM users").get() as
| { count: number }
| undefined;
if ((row?.count ?? 0) > 0) {
return;
}
const email = process.env.ADMIN_EMAIL?.trim().toLowerCase();
const password = process.env.ADMIN_PASSWORD;
const name = process.env.ADMIN_NAME?.trim();
if (!email || !password || !name) {
throw new Error(
"Keine Benutzer vorhanden. ADMIN_EMAIL, ADMIN_PASSWORD und ADMIN_NAME müssen gesetzt sein.",
);
}
if (password.length < 10) {
throw new Error("ADMIN_PASSWORD muss mindestens 10 Zeichen lang sein.");
}
db.prepare(
"INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, 'admin')",
).run(name, email, hashSync(password, 12));
}
export function initDatabase() {
if (globalThis.__mopedDatabaseInitialized) {
return;
}
getUploadDir();
ensureSchema();
ensureInitialAdmin();
globalThis.__mopedDatabaseInitialized = true;
}
export function transaction<T>(work: () => T): T {
db.exec("BEGIN IMMEDIATE");
try {
const result = work();
db.exec("COMMIT");
return result;
} catch (error) {
db.exec("ROLLBACK");
throw error;
}
}