fast fertig

This commit is contained in:
2026-07-26 20:27:24 +02:00
parent 1370ad4ff7
commit c2fbb1b86f
19 changed files with 836 additions and 395 deletions
+149 -8
View File
@@ -1,4 +1,5 @@
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
import { hashSync } from "bcryptjs";
import { getDatabasePath, getUploadDir } from "./env";
@@ -27,6 +28,8 @@ function ensureSchema() {
email TEXT NOT NULL COLLATE NOCASE UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
deleted_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
@@ -39,11 +42,32 @@ function ensureSchema() {
vin TEXT,
plate TEXT,
current_km INTEGER NOT NULL DEFAULT 0 CHECK (current_km >= 0),
tire_pressure_front_bar REAL CHECK (tire_pressure_front_bar IS NULL OR tire_pressure_front_bar > 0),
tire_pressure_rear_bar REAL CHECK (tire_pressure_rear_bar IS NULL OR tire_pressure_rear_bar > 0),
chain_tension_mm INTEGER CHECK (chain_tension_mm IS NULL OR chain_tension_mm >= 0),
chain_tension_min_mm INTEGER CHECK (chain_tension_min_mm IS NULL OR chain_tension_min_mm >= 0),
chain_tension_max_mm INTEGER CHECK (chain_tension_max_mm IS NULL OR chain_tension_max_mm >= 0),
oil_type TEXT,
oil_capacity_liters REAL CHECK (oil_capacity_liters IS NULL OR oil_capacity_liters > 0),
oil_check_temp TEXT CHECK (oil_check_temp IS NULL OR oil_check_temp IN ('warm', 'cold')),
oil_check_run_minutes INTEGER CHECK (oil_check_run_minutes IS NULL OR oil_check_run_minutes >= 0),
oil_check_wait_min_minutes INTEGER CHECK (oil_check_wait_min_minutes IS NULL OR oil_check_wait_min_minutes >= 0),
oil_check_wait_max_minutes INTEGER CHECK (oil_check_wait_max_minutes IS NULL OR oil_check_wait_max_minutes >= 0),
oil_check_wait_minutes INTEGER CHECK (oil_check_wait_minutes IS NULL OR oil_check_wait_minutes >= 0),
oil_check_method TEXT CHECK (oil_check_method IS NULL OR oil_check_method IN ('dipstick', 'sight_glass')),
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 motorcycle_shares (
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (motorcycle_id, user_id)
) STRICT;
CREATE TABLE IF NOT EXISTS maintenance_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
@@ -87,11 +111,95 @@ function ensureSchema() {
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);
CREATE INDEX IF NOT EXISTS idx_motorcycle_shares_user
ON motorcycle_shares(user_id, motorcycle_id);
`);
const userColumns = db.prepare("PRAGMA table_info(users)").all() as { name: string }[];
if (!userColumns.some((column) => column.name === "active")) {
db.exec("ALTER TABLE users ADD COLUMN active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1))");
}
if (!userColumns.some((column) => column.name === "deleted_at")) {
db.exec("ALTER TABLE users ADD COLUMN deleted_at TEXT");
}
const motorcycleColumns = db.prepare("PRAGMA table_info(motorcycles)").all() as { name: string }[];
if (!motorcycleColumns.some((column) => column.name === "tire_pressure_front_bar")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN tire_pressure_front_bar REAL");
}
if (!motorcycleColumns.some((column) => column.name === "tire_pressure_rear_bar")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN tire_pressure_rear_bar REAL");
}
if (!motorcycleColumns.some((column) => column.name === "chain_tension_mm")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN chain_tension_mm INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "chain_tension_min_mm")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN chain_tension_min_mm INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "chain_tension_max_mm")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN chain_tension_max_mm INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "oil_type")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_type TEXT");
}
if (!motorcycleColumns.some((column) => column.name === "oil_capacity_liters")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_capacity_liters REAL");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_temp")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_temp TEXT");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_run_minutes")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_run_minutes INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_wait_min_minutes")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_wait_min_minutes INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_wait_max_minutes")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_wait_max_minutes INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_wait_minutes")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_wait_minutes INTEGER");
}
if (!motorcycleColumns.some((column) => column.name === "oil_check_method")) {
db.exec("ALTER TABLE motorcycles ADD COLUMN oil_check_method TEXT");
}
const motorcycleColumnsAfter = db.prepare("PRAGMA table_info(motorcycles)").all() as { name: string }[];
if (
motorcycleColumnsAfter.some((column) => column.name === "chain_tension_mm") &&
motorcycleColumnsAfter.some((column) => column.name === "chain_tension_min_mm") &&
motorcycleColumnsAfter.some((column) => column.name === "chain_tension_max_mm")
) {
db.exec(`
UPDATE motorcycles
SET chain_tension_min_mm = chain_tension_mm,
chain_tension_max_mm = chain_tension_mm
WHERE chain_tension_mm IS NOT NULL
AND chain_tension_min_mm IS NULL
AND chain_tension_max_mm IS NULL
`);
}
if (
motorcycleColumnsAfter.some((column) => column.name === "oil_check_wait_minutes") &&
motorcycleColumnsAfter.some((column) => column.name === "oil_check_wait_min_minutes") &&
motorcycleColumnsAfter.some((column) => column.name === "oil_check_wait_max_minutes")
) {
db.exec(`
UPDATE motorcycles
SET oil_check_wait_min_minutes = oil_check_wait_minutes,
oil_check_wait_max_minutes = oil_check_wait_minutes
WHERE oil_check_wait_minutes IS NOT NULL
AND oil_check_wait_min_minutes IS NULL
AND oil_check_wait_max_minutes IS NULL
`);
}
db.exec("CREATE INDEX IF NOT EXISTS idx_users_active ON users(active, name COLLATE NOCASE)");
}
function ensureInitialAdmin() {
const row = db.prepare("SELECT COUNT(*) AS count FROM users").get() as
const row = db.prepare("SELECT COUNT(*) AS count FROM users WHERE active = 1").get() as
| { count: number }
| undefined;
@@ -99,13 +207,12 @@ function ensureInitialAdmin() {
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) {
if (!password || !name) {
throw new Error(
"Keine Benutzer vorhanden. ADMIN_EMAIL, ADMIN_PASSWORD und ADMIN_NAME müssen gesetzt sein.",
"Keine Benutzer vorhanden. ADMIN_PASSWORD und ADMIN_NAME müssen gesetzt sein.",
);
}
@@ -113,18 +220,52 @@ function ensureInitialAdmin() {
throw new Error("ADMIN_PASSWORD muss mindestens 10 Zeichen lang sein.");
}
const adminSlug =
name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "konto";
db.prepare(
"INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, 'admin')",
).run(name, email, hashSync(password, 12));
).run(name, `admin-${adminSlug}@local.invalid`, hashSync(password, 12));
}
function ensureReferencedUsersExist() {
const missingUserIds = db
.prepare(
`SELECT DISTINCT refs.user_id AS id
FROM (
SELECT created_by AS user_id FROM motorcycles
UNION
SELECT created_by AS user_id FROM maintenance_events
UNION
SELECT driver_user_id AS user_id FROM trips
) refs
LEFT JOIN users u ON u.id = refs.user_id
WHERE refs.user_id IS NOT NULL AND u.id IS NULL
ORDER BY refs.user_id`,
)
.all() as { id: number }[];
const insertMissingUser = db.prepare(
`INSERT INTO users (id, name, email, password_hash, role, active, deleted_at)
VALUES (?, ?, ?, ?, 'member', 0, CURRENT_TIMESTAMP)`,
);
for (const missing of missingUserIds) {
insertMissingUser.run(
missing.id,
`Gelöschter Benutzer #${missing.id}`,
`deleted-${missing.id}-${randomUUID()}@invalid.local`,
hashSync(randomUUID(), 12),
);
}
}
export function initDatabase() {
getUploadDir();
ensureSchema();
if (globalThis.__mopedDatabaseInitialized) {
return;
}
getUploadDir();
ensureSchema();
ensureReferencedUsersExist();
ensureInitialAdmin();
globalThis.__mopedDatabaseInitialized = true;
}