chore: Projektbasis und PostgreSQL-Migrationen einrichten
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
10af6159b4
14 files changed
+2214
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
DATABASE_URL=postgresql://kibu:CHANGE_ME@127.0.0.1:55432/kibu
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
|
APP_TIMEZONE=Europe/Berlin
|
||||||
|
SESSION_SECURE=false
|
||||||
|
# Optional for db:setup; otherwise a random initial password is printed.
|
||||||
|
BOOTSTRAP_EMAIL=admin@kibu.local
|
||||||
|
# BOOTSTRAP_PASSWORD=at-least-12-characters
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.tsbuildinfo
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: kibu
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: kibu
|
||||||
|
POSTGRES_USER: kibu
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Run npm run configure first}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:55432:5432"
|
||||||
|
volumes:
|
||||||
|
- kibu_postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U kibu -d kibu"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
kibu_postgres:
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/types/routes.d.ts";
|
||||||
|
import "./.next/types/root-params.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
Generated
+1788
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "kibu",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --hostname 127.0.0.1",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start --hostname 127.0.0.1",
|
||||||
|
"configure": "node scripts/configure.mjs",
|
||||||
|
"db:setup": "tsx scripts/setup.ts",
|
||||||
|
"db:seed": "tsx scripts/seed-demo.ts",
|
||||||
|
"test": "tsx --test tests/unit.test.ts",
|
||||||
|
"test:db": "tsx --test tests/database.test.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.2.0",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"next": "^16.0.0",
|
||||||
|
"pg": "^8.16.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"zod": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
|
"@types/pg": "^8.15.0",
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"tsx": "^4.20.0",
|
||||||
|
"typescript": "^5.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { existsSync, writeFileSync } from "node:fs";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
if (existsSync(".env")) {
|
||||||
|
console.log(".env existiert bereits und wurde nicht veraendert.");
|
||||||
|
} else {
|
||||||
|
const password = randomBytes(24).toString("hex");
|
||||||
|
writeFileSync(".env", [
|
||||||
|
`DATABASE_URL=postgresql://kibu:${password}@127.0.0.1:55432/kibu`,
|
||||||
|
`POSTGRES_PASSWORD=${password}`,
|
||||||
|
"APP_TIMEZONE=Europe/Berlin",
|
||||||
|
"SESSION_SECURE=false",
|
||||||
|
"",
|
||||||
|
].join("\n"), { flag: "wx" });
|
||||||
|
console.log(".env mit zufaelligem lokalen Datenbankpasswort erstellt.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
CREATE TABLE pickup_contacts (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
relationship text NOT NULL,
|
||||||
|
phone text NOT NULL DEFAULT '',
|
||||||
|
authorized boolean NOT NULL DEFAULT true,
|
||||||
|
notes text NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
CREATE INDEX pickup_contacts_child ON pickup_contacts(child_id);
|
||||||
|
|
||||||
|
CREATE TABLE alone_rules (
|
||||||
|
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
|
||||||
|
weekday integer NOT NULL CHECK (weekday BETWEEN 1 AND 7),
|
||||||
|
earliest time NOT NULL,
|
||||||
|
PRIMARY KEY(child_id, weekday)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE attendance DROP CONSTRAINT attendance_pickup_kind_check;
|
||||||
|
ALTER TABLE attendance ADD CONSTRAINT attendance_pickup_kind_check
|
||||||
|
CHECK (pickup_kind IN ('planned', 'parents', 'alone'));
|
||||||
|
ALTER TABLE attendance ADD CONSTRAINT attendance_chronology
|
||||||
|
CHECK (picked_up_at IS NULL OR picked_up_at >= arrived_at);
|
||||||
|
|
||||||
|
CREATE SEQUENCE attendance_revision;
|
||||||
|
ALTER TABLE attendance ADD COLUMN version bigint NOT NULL DEFAULT nextval('attendance_revision');
|
||||||
|
CREATE FUNCTION bump_attendance_revision() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.version := nextval('attendance_revision');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
CREATE TRIGGER attendance_revision_trigger BEFORE UPDATE ON attendance
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION bump_attendance_revision();
|
||||||
|
|
||||||
|
CREATE TABLE audit_log (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
child_id uuid NOT NULL,
|
||||||
|
entity text NOT NULL,
|
||||||
|
entity_id text,
|
||||||
|
day date,
|
||||||
|
operation text NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
|
||||||
|
actor_id uuid,
|
||||||
|
actor_name text NOT NULL,
|
||||||
|
reason text NOT NULL,
|
||||||
|
before_data jsonb,
|
||||||
|
after_data jsonb,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||||
|
);
|
||||||
|
CREATE INDEX audit_log_child ON audit_log(child_id, id DESC);
|
||||||
|
|
||||||
|
CREATE FUNCTION record_child_audit() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
|
DECLARE
|
||||||
|
old_data jsonb;
|
||||||
|
new_data jsonb;
|
||||||
|
row_data jsonb;
|
||||||
|
actor uuid;
|
||||||
|
actor_label text;
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP <> 'INSERT' THEN old_data := to_jsonb(OLD); END IF;
|
||||||
|
IF TG_OP <> 'DELETE' THEN new_data := to_jsonb(NEW); END IF;
|
||||||
|
IF TG_OP = 'UPDATE' AND old_data - 'version' = new_data - 'version' THEN RETURN NEW; END IF;
|
||||||
|
row_data := COALESCE(new_data, old_data);
|
||||||
|
actor := NULLIF(current_setting('kibu.actor_id', true), '')::uuid;
|
||||||
|
SELECT name || ' (' || email || ')' INTO actor_label FROM staff WHERE id = actor;
|
||||||
|
INSERT INTO audit_log(child_id, entity, entity_id, day, operation, actor_id, actor_name,
|
||||||
|
reason, before_data, after_data)
|
||||||
|
VALUES (
|
||||||
|
CASE WHEN TG_TABLE_NAME = 'children' THEN (row_data->>'id')::uuid ELSE (row_data->>'child_id')::uuid END,
|
||||||
|
TG_TABLE_NAME, row_data->>'id', (row_data->>'day')::date, TG_OP, actor,
|
||||||
|
COALESCE(actor_label, 'Datenbank / Einrichtung'),
|
||||||
|
COALESCE(NULLIF(current_setting('kibu.reason', true), ''), 'Direkte Datenbankänderung'),
|
||||||
|
old_data, new_data
|
||||||
|
);
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER children_audit AFTER INSERT OR UPDATE OR DELETE ON children FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
CREATE TRIGGER weekly_audit AFTER INSERT OR UPDATE OR DELETE ON weekly_schedules FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
CREATE TRIGGER override_audit AFTER INSERT OR UPDATE OR DELETE ON day_overrides FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
CREATE TRIGGER attendance_audit AFTER INSERT OR UPDATE OR DELETE ON attendance FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
CREATE TRIGGER contacts_audit AFTER INSERT OR UPDATE OR DELETE ON pickup_contacts FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
CREATE TRIGGER alone_audit AFTER INSERT OR UPDATE OR DELETE ON alone_rules FOR EACH ROW EXECUTE FUNCTION record_child_audit();
|
||||||
|
|
||||||
|
CREATE FUNCTION protect_audit_log() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
|
BEGIN
|
||||||
|
RAISE EXCEPTION 'Das Audit-Log ist unveraenderlich.';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
CREATE TRIGGER audit_immutable BEFORE UPDATE OR DELETE OR TRUNCATE ON audit_log
|
||||||
|
FOR EACH STATEMENT EXECUTE FUNCTION protect_audit_log();
|
||||||
|
|
||||||
|
INSERT INTO audit_log(child_id, entity, day, operation, actor_name, reason, after_data)
|
||||||
|
SELECT child_id, 'attendance', day, 'INSERT', 'Datenbank / Einrichtung',
|
||||||
|
'Bestandsübernahme bei Einführung des Audit-Logs; ursprünglicher Änderungsverlauf nicht verfügbar.',
|
||||||
|
to_jsonb(a) FROM attendance a;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
CREATE TABLE pickup_slots (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
starts_at time NOT NULL,
|
||||||
|
ends_at time NOT NULL,
|
||||||
|
active boolean NOT NULL DEFAULT true,
|
||||||
|
CHECK (starts_at < ends_at),
|
||||||
|
UNIQUE(starts_at, ends_at)
|
||||||
|
);
|
||||||
|
INSERT INTO pickup_slots(starts_at, ends_at) VALUES
|
||||||
|
('14:00', '14:15'), ('15:00', '15:15'), ('16:00', '16:15'), ('17:00', '17:15');
|
||||||
|
ALTER TABLE weekly_schedules ADD COLUMN pickup_end time;
|
||||||
|
ALTER TABLE weekly_schedules ADD COLUMN pickup_slot_id uuid REFERENCES pickup_slots(id);
|
||||||
|
ALTER TABLE weekly_schedules ADD CONSTRAINT weekly_pickup_window CHECK(pickup_end IS NULL OR pickup_end > pickup);
|
||||||
|
ALTER TABLE day_overrides ADD COLUMN pickup_end time;
|
||||||
|
ALTER TABLE day_overrides ADD COLUMN pickup_slot_id uuid REFERENCES pickup_slots(id);
|
||||||
|
ALTER TABLE day_overrides ADD CONSTRAINT override_pickup_window CHECK(pickup_end IS NULL OR (attending AND pickup_end > pickup));
|
||||||
|
UPDATE weekly_schedules w SET pickup_slot_id = s.id, pickup_end = s.ends_at FROM pickup_slots s WHERE w.pickup = s.starts_at;
|
||||||
|
UPDATE day_overrides o SET pickup_slot_id = s.id, pickup_end = s.ends_at FROM pickup_slots s WHERE o.pickup = s.starts_at;
|
||||||
|
|
||||||
|
CREATE TABLE admission_rounds (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
code_hash text NOT NULL UNIQUE,
|
||||||
|
expires_on date NOT NULL,
|
||||||
|
status text NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'closed', 'drawn')),
|
||||||
|
created_by uuid NOT NULL REFERENCES staff(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
drawn_by uuid REFERENCES staff(id),
|
||||||
|
drawn_at timestamptz,
|
||||||
|
places integer CHECK(places > 0),
|
||||||
|
CHECK ((status = 'drawn' AND drawn_at IS NOT NULL AND drawn_by IS NOT NULL AND places IS NOT NULL)
|
||||||
|
OR (status <> 'drawn' AND drawn_at IS NULL AND drawn_by IS NULL AND places IS NULL))
|
||||||
|
);
|
||||||
|
CREATE TABLE applications (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
round_id uuid NOT NULL REFERENCES admission_rounds(id),
|
||||||
|
first_name text NOT NULL,
|
||||||
|
last_name text NOT NULL,
|
||||||
|
class_name text NOT NULL,
|
||||||
|
parent_name text NOT NULL,
|
||||||
|
email text NOT NULL,
|
||||||
|
notes text NOT NULL DEFAULT '',
|
||||||
|
status text NOT NULL DEFAULT 'waiting' CHECK(status IN ('waiting', 'selected', 'waitlisted', 'accepted')),
|
||||||
|
child_id uuid UNIQUE REFERENCES children(id),
|
||||||
|
submitted_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
accepted_at timestamptz,
|
||||||
|
accepted_by uuid REFERENCES staff(id),
|
||||||
|
CHECK ((status = 'accepted' AND child_id IS NOT NULL AND accepted_at IS NOT NULL AND accepted_by IS NOT NULL)
|
||||||
|
OR (status <> 'accepted' AND child_id IS NULL AND accepted_at IS NULL AND accepted_by IS NULL))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX application_unique_child ON applications(round_id, lower(first_name), lower(last_name), lower(email));
|
||||||
|
CREATE INDEX applications_round ON applications(round_id, submitted_at, id);
|
||||||
|
CREATE TABLE application_limits (
|
||||||
|
key_hash text PRIMARY KEY,
|
||||||
|
attempts integer NOT NULL,
|
||||||
|
window_start timestamptz NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS staff (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email text NOT NULL UNIQUE,
|
||||||
|
name text NOT NULL,
|
||||||
|
password_hash text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token_hash text PRIMARY KEY,
|
||||||
|
staff_id uuid NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||||
|
expires_at timestamptz NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS login_attempts (
|
||||||
|
email text PRIMARY KEY,
|
||||||
|
attempts integer NOT NULL,
|
||||||
|
window_start timestamptz NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS children (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
first_name text NOT NULL,
|
||||||
|
last_name text NOT NULL,
|
||||||
|
class_name text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS weekly_schedules (
|
||||||
|
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
|
||||||
|
weekday integer NOT NULL CHECK (weekday BETWEEN 1 AND 7),
|
||||||
|
arrival time NOT NULL,
|
||||||
|
pickup time NOT NULL,
|
||||||
|
PRIMARY KEY (child_id, weekday),
|
||||||
|
CHECK (arrival < pickup)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS day_overrides (
|
||||||
|
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
|
||||||
|
day date NOT NULL,
|
||||||
|
attending boolean NOT NULL,
|
||||||
|
arrival time,
|
||||||
|
pickup time,
|
||||||
|
PRIMARY KEY (child_id, day),
|
||||||
|
CHECK ((attending AND arrival IS NOT NULL AND pickup IS NOT NULL AND arrival < pickup)
|
||||||
|
OR (NOT attending AND arrival IS NULL AND pickup IS NULL))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS attendance (
|
||||||
|
child_id uuid NOT NULL REFERENCES children(id) ON DELETE CASCADE,
|
||||||
|
day date NOT NULL,
|
||||||
|
status text NOT NULL CHECK (status IN ('arrived', 'sick', 'picked_up')),
|
||||||
|
arrived_at timestamptz,
|
||||||
|
picked_up_at timestamptz,
|
||||||
|
pickup_kind text CHECK (pickup_kind IN ('planned', 'parents')),
|
||||||
|
updated_by uuid NOT NULL REFERENCES staff(id),
|
||||||
|
PRIMARY KEY (child_id, day),
|
||||||
|
CHECK (
|
||||||
|
(status = 'sick' AND arrived_at IS NULL AND picked_up_at IS NULL AND pickup_kind IS NULL)
|
||||||
|
OR (status = 'arrived' AND arrived_at IS NOT NULL AND picked_up_at IS NULL AND pickup_kind IS NULL)
|
||||||
|
OR (status = 'picked_up' AND arrived_at IS NOT NULL AND picked_up_at IS NOT NULL AND pickup_kind IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS sessions_expiry ON sessions(expires_at);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { readFile, readdir } from "node:fs/promises";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { db } from "../src/lib/db";
|
||||||
|
import { hashPassword } from "../src/lib/password";
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
if (!process.env.DATABASE_URL) throw new Error("Zuerst npm run configure ausfuehren.");
|
||||||
|
await db.query(await readFile(new URL("./schema.sql", import.meta.url), "utf8"));
|
||||||
|
const client = await db.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
await client.query("SELECT pg_advisory_xact_lock(724139002)");
|
||||||
|
await client.query("CREATE TABLE IF NOT EXISTS schema_migrations(name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())");
|
||||||
|
const directory = new URL("./migrations/", import.meta.url);
|
||||||
|
for (const name of (await readdir(directory)).filter((file) => file.endsWith(".sql")).sort()) {
|
||||||
|
const applied = await client.query("SELECT name FROM schema_migrations WHERE name = $1", [name]);
|
||||||
|
if (applied.rowCount) continue;
|
||||||
|
await client.query(await readFile(new URL(name, directory), "utf8"));
|
||||||
|
await client.query("INSERT INTO schema_migrations(name) VALUES ($1)", [name]);
|
||||||
|
console.log(`Migration angewendet: ${name}`);
|
||||||
|
}
|
||||||
|
await client.query("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
const email = (process.env.BOOTSTRAP_EMAIL ?? "admin@kibu.local").trim().toLowerCase();
|
||||||
|
const existing = await db.query("SELECT id FROM staff WHERE email = $1", [email]);
|
||||||
|
if (existing.rowCount) {
|
||||||
|
console.log("Schema bereit. Bestehender Zugang bleibt unveraendert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const password = process.env.BOOTSTRAP_PASSWORD ?? randomBytes(18).toString("base64url");
|
||||||
|
if (password.length < 12) throw new Error("BOOTSTRAP_PASSWORD muss mindestens 12 Zeichen haben.");
|
||||||
|
await db.query(
|
||||||
|
"INSERT INTO staff(email, name, password_hash) VALUES ($1, $2, $3)",
|
||||||
|
[email, "Betreuung", await hashPassword(password)],
|
||||||
|
);
|
||||||
|
console.log(`Erster Zugang erstellt.\nE-Mail: ${email}\nPasswort: ${password}\nBitte sicher aufbewahren.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup().catch((error: unknown) => {
|
||||||
|
console.error("Datenbank-Einrichtung fehlgeschlagen:", error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}).finally(() => db.end());
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
const globalDb = globalThis as typeof globalThis & { kibuPool?: Pool };
|
||||||
|
|
||||||
|
function createPool() {
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
max: 10,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
});
|
||||||
|
pool.on("error", (error) => console.error("PostgreSQL-Verbindung im Pool unterbrochen:", error));
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = globalDb.kibuPool ?? createPool();
|
||||||
|
if (process.env.NODE_ENV !== "production") globalDb.kibuPool = db;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const scrypt = promisify(scryptCallback);
|
||||||
|
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = randomBytes(16).toString("hex");
|
||||||
|
const hash = await scrypt(password, salt, 64) as Buffer;
|
||||||
|
return `${salt}:${hash.toString("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
|
||||||
|
const [salt, hex] = stored.split(":");
|
||||||
|
if (!salt || !hex) return false;
|
||||||
|
const expected = Buffer.from(hex, "hex");
|
||||||
|
const actual = await scrypt(password, salt, 64) as Buffer;
|
||||||
|
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user