Merge branch 'phase/001-projekt-setup'
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# SSHKeyVault – Konfiguration. Kopieren nach .env und Werte eintragen.
|
||||
# Alle eigenen Variablen tragen das Praefix SSHKV_ (Ausnahme: DATABASE_URL fuer Prisma).
|
||||
|
||||
# --- Datenbank -----------------------------------------------------------------
|
||||
DATABASE_URL=postgresql://sshkv:sshkv@localhost:5436/sshkv?schema=public
|
||||
# Nur fuer Tests (wird von Vitest geleert!)
|
||||
TEST_DATABASE_URL=postgresql://sshkv:sshkv@localhost:5436/sshkv_test?schema=public
|
||||
|
||||
# --- Web -----------------------------------------------------------------------
|
||||
# Oeffentliche Adresse (hinter dem Reverse Proxy). Bestimmt Secure-Cookie und OIDC-Rueckruf.
|
||||
SSHKV_APP_URL=http://localhost:3400
|
||||
# Signiert Sitzungen. Erzeugen mit: openssl rand -base64 48
|
||||
SSHKV_APP_SECRET=
|
||||
|
||||
# --- Master-Key (nur Worker!) --------------------------------------------------
|
||||
# Datei mit 32 zufaelligen Bytes. Erzeugen mit: head -c 32 /dev/urandom > master.key
|
||||
# Ohne diese Datei ist jedes Datenbank-Backup wertlos – getrennt sichern!
|
||||
SSHKV_MASTER_KEY_FILE=./geheim/master.key
|
||||
|
||||
# --- OIDC (authentik) ----------------------------------------------------------
|
||||
SSHKV_OIDC_ISSUER=
|
||||
SSHKV_OIDC_CLIENT_ID=
|
||||
SSHKV_OIDC_CLIENT_SECRET=
|
||||
SSHKV_OIDC_NAME=authentik
|
||||
# Gruppen im Anbieter, die auf Rollen abgebildet werden
|
||||
SSHKV_OIDC_GROUP_ADMIN=sshkv-admins
|
||||
SSHKV_OIDC_GROUP_OPERATOR=sshkv-operators
|
||||
SSHKV_OIDC_GROUP_READER=sshkv-readers
|
||||
|
||||
# --- Notfall-Admin -------------------------------------------------------------
|
||||
# Wird beim ersten Seed angelegt. TOTP wird bei der ersten Anmeldung eingerichtet.
|
||||
SSHKV_BREAKGLASS_USER=notfall
|
||||
SSHKV_BREAKGLASS_PASSWORD=
|
||||
|
||||
# --- Worker --------------------------------------------------------------------
|
||||
# Zeitplan fuer den Abgleich aller Hosts (cron)
|
||||
SSHKV_SYNC_CRON=30 2 * * *
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm exec commitlint --edit "$1"
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
@@ -0,0 +1,8 @@
|
||||
.next
|
||||
node_modules
|
||||
coverage
|
||||
docs
|
||||
*.md
|
||||
pnpm-lock.yaml
|
||||
src/generated
|
||||
prisma/migrations
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 110
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Konventionen für SSHKeyVault (`sshkv`)
|
||||
|
||||
Diese Datei ist verbindlich. Plan und Begründungen: `docs/PLAN.md`. Arbeitsweise wie in
|
||||
Laufzettel und ProxLedger.
|
||||
|
||||
## Arbeitsweise
|
||||
|
||||
- Issues liegen als Dateien in `docs/issues/NNN-slug.md` und sind die Quelle der Wahrheit;
|
||||
`pnpm issues:sync` überträgt sie nach Gitea (#030).
|
||||
- Je Issue ein Branch `phase/NNN-slug`, Merge nach `main` mit `--no-ff`.
|
||||
- Conventional Commits auf Deutsch, Betreff ≤ 72 Zeichen, Umlaute im Betreff gefaltet,
|
||||
`(#NNN)` am Ende. Der Body erklärt das **Warum**.
|
||||
- `DEVLOG.md`: Zeit je Issue und Meilenstein, Sitzungen mit „Erledigt“ und „Entscheidungen“.
|
||||
- `CHANGELOG.md`: Keep a Changelog, deutsch. Tag `vX.Y.Z` je Meilenstein; Version in `package.json`.
|
||||
|
||||
## Sprache
|
||||
|
||||
**Deutsch, überall wo ein Mensch liest oder benennt** – auch Bezeichner (Funktionen, Variablen,
|
||||
Prisma-Modelle und -Felder, Enum-Werte, Dateinamen, Tests). **Umlaute in Bezeichnern werden
|
||||
ASCII-gefaltet** (`Schluessel`, `geaendertAm`). Oberflächentexte, Fehlermeldungen und
|
||||
Protokoll-Aktionen schreiben sich richtig („Schlüssel widerrufen“).
|
||||
|
||||
Englisch bleiben Framework-Namen (`page.tsx`, `route.ts`, `GET`) und Fachjargon
|
||||
(`Host`, `Fingerprint`, `Token`, `OIDC`, `authorized_keys`, `Public Key`, `Private Key`, `Drift`).
|
||||
|
||||
Kommentare erklären **warum**, nicht was, und tragen wo hilfreich die Issue-Nummer `(#007)`.
|
||||
|
||||
## Namensabbildung zum Plan
|
||||
|
||||
| Plan | Modell | Plan | Modell |
|
||||
|---|---|---|---|
|
||||
| User | `Benutzer` | PublicKeyUsage | `PublicVerwendung` |
|
||||
| Person | `Person` | PrivateKeyUsage | `PrivateVerwendung` |
|
||||
| Customer | `Kunde` | ManagedAccount | `Zielkonto` |
|
||||
| Host | `Host` | Template | `Vorlage` |
|
||||
| HostGroup | `Hostgruppe` | Observation | `Beobachtung` |
|
||||
| HostKey | `HostSchluessel` | DriftFinding | `Abweichung` |
|
||||
| SshKey | `Schluessel` | Operation | `Vorgang` |
|
||||
| AuditEvent | `Protokoll` | Setting | `Einstellung` |
|
||||
|
||||
## Schichtung
|
||||
|
||||
```
|
||||
src/server/services/* Regeln und Schreiboperationen.
|
||||
src/server/queries/* Lesemodelle. Wenden die Rolle selbst an.
|
||||
src/server/actions/* Dünne "use server"-Einstiege: Recht prüfen, zod, Service rufen.
|
||||
src/server/auth/* Anmeldung, Sitzung, Rechte, Re-Auth.
|
||||
src/lib/* Framework-freie Helfer (kein Next, kein React, kein Prisma) – gut testbar.
|
||||
src/lib/ssh/* OpenSSH-Formate, authorized_keys-Parser, Fingerprints. Rein.
|
||||
src/worker/* Alles, was SSH spricht oder den Master-Key braucht. Läuft nur im Worker.
|
||||
src/components/* Darstellung, keine Geschäftslogik.
|
||||
```
|
||||
|
||||
## Feste Regeln
|
||||
|
||||
- **Private Keys** erscheinen nie in Logs, Fehlermeldungen, Listenantworten, Props von
|
||||
Client-Komponenten oder im Protokoll. Prisma-Abfragen auf `Schluessel` nutzen feste
|
||||
`select`-Listen; `privatVerschluesselt` liest nur `src/worker/geheimnisse.ts`.
|
||||
- **Master-Key nur im Worker.** Der Webprozess importiert nichts aus `src/worker/*`.
|
||||
- **Kein Klartext-Passwort** (Bootstrap) in Datenbank, Job-Payload, Log oder Protokoll.
|
||||
- **Host-Keys werden strikt geprüft.** Es gibt keinen Codepfad, der einen geänderten Host-Key
|
||||
automatisch annimmt.
|
||||
- **Rechte** nur serverseitig über `darf()`; jede Seite und jeder Route Handler prüft selbst.
|
||||
Rolle wird pro Anfrage aus der Datenbank gelesen, nicht aus dem Token.
|
||||
- **Protokoll** ist append-only (Trigger) und über eine Hash-Kette verbunden.
|
||||
- Zod immer über `@/lib/validierung` importieren (deutsche Meldungen).
|
||||
|
||||
## Werkzeuge und Versionen
|
||||
|
||||
Node 24 (`.nvmrc`), pnpm 12. Exakt gepinnt: `next@16.3.6`, `react@19.3.0`,
|
||||
`prisma`/`@prisma/client`/`@prisma/adapter-pg` `7.10.0`, `vitest@5.0.1`, `vite@8.3.0`,
|
||||
`@playwright/test@1.63.0`. `eslint` bleibt auf 9.x, `typescript` auf `~6.0.3`
|
||||
(Gründe siehe Laufzettel/CLAUDE.md).
|
||||
|
||||
**Next 16:** `middleware.ts` heißt `proxy.ts`. `cookies()`, `headers()`, `params`,
|
||||
`searchParams` sind async.
|
||||
|
||||
**Prisma 7:** Generator `prisma-client` nach `src/generated/prisma`, Zugriff über
|
||||
`@prisma/adapter-pg`, Lazy-Proxy in `src/lib/db.ts`. Typen aus `@/generated/prisma/client`,
|
||||
Enum-Werte aus `@/generated/prisma/enums`.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
pnpm db:up # PostgreSQL auf 127.0.0.1:5436
|
||||
pnpm db:migrate && pnpm db:seed
|
||||
pnpm dev # http://localhost:3400
|
||||
pnpm worker # zweites Terminal
|
||||
```
|
||||
|
||||
Oberflächen immer gegen den **gebauten Stand** prüfen (`pnpm build && pnpm start`) – siehe
|
||||
Laufzettel: `next dev` hydriert in dieser Umgebung nicht zuverlässig.
|
||||
|
||||
## Vor jedem Commit
|
||||
|
||||
```bash
|
||||
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test
|
||||
```
|
||||
|
||||
Vor jedem Merge zusätzlich `pnpm test:integration` (braucht Docker).
|
||||
@@ -0,0 +1,11 @@
|
||||
// Conventional Commits. Betreff auf Deutsch, hoechstens 72 Zeichen (siehe CLAUDE.md).
|
||||
const konfiguration = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
"header-max-length": [2, "always", 72],
|
||||
"subject-case": [0],
|
||||
"body-max-line-length": [0],
|
||||
},
|
||||
};
|
||||
|
||||
export default konfiguration;
|
||||
@@ -0,0 +1,26 @@
|
||||
# Nur fuer die Entwicklung: PostgreSQL auf eigenem Port (andere Projekte belegen
|
||||
# 5433/5434). Diese Datei niemals im Betrieb verwenden.
|
||||
|
||||
name: sshkv-dev
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: sshkv-dev-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: sshkv
|
||||
POSTGRES_PASSWORD: sshkv
|
||||
POSTGRES_DB: sshkv
|
||||
ports:
|
||||
- "127.0.0.1:${DEV_DB_PORT:-5436}:5432"
|
||||
volumes:
|
||||
- datenbank-dev:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U sshkv"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
datenbank-dev:
|
||||
@@ -1,24 +1,27 @@
|
||||
# #001 Projekt-Setup
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.1.0
|
||||
**Labels:** infrastruktur
|
||||
**Geschätzt:** 3 h **Tatsächlich:** –
|
||||
**Geschätzt:** 3 h **Tatsächlich:** 0 h 15 min
|
||||
|
||||
## Ziel
|
||||
Lauffähiges Grundgerüst: Next.js 16, TypeScript strict, Prisma 7, pnpm, ESLint/Prettier/Vitest, Commit-Prüfung, Docker-Compose für die Entwicklungsdatenbank.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Next.js 16 (App Router), React 19, TypeScript strict
|
||||
- [ ] pnpm 12, gepinnte Versionen, `.nvmrc`
|
||||
- [ ] ESLint 9, Prettier, Vitest, commitlint + husky
|
||||
- [ ] `docker-compose.dev.yml` mit PostgreSQL
|
||||
- [ ] `.env.example` mit Präfix `SSHKV_`
|
||||
- [ ] `CLAUDE.md` mit Konventionen
|
||||
- [x] Next.js 16 (App Router), React 19, TypeScript strict
|
||||
- [x] pnpm 12, gepinnte Versionen, `.nvmrc`
|
||||
- [x] ESLint 9, Prettier, Vitest, commitlint + husky
|
||||
- [x] `docker-compose.dev.yml` mit PostgreSQL
|
||||
- [x] `.env.example` mit Präfix `SSHKV_`
|
||||
- [x] `CLAUDE.md` mit Konventionen
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] `pnpm lint && pnpm typecheck && pnpm test && pnpm build` grün
|
||||
- [x] `pnpm lint && pnpm typecheck && pnpm test && pnpm build` grün
|
||||
|
||||
## Abhängigkeiten
|
||||
–
|
||||
|
||||
## Notizen / Entscheidungen
|
||||
- Konfiguration (tsconfig, ESLint, Prettier, commitlint, husky) aus Laufzettel übernommen.
|
||||
- Entwicklungsdatenbank auf Port 5436 (5433/5434 belegen ProxLedger und Laufzettel), Dev-Server auf 3400.
|
||||
- `ssh2` ohne native Build-Skripte (`allowBuilds: false`): die reine JS-Fassung reicht, und es muss kein Compiler ins Abbild.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import prettier from "eslint-config-prettier/flat";
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
prettier,
|
||||
{
|
||||
rules: {
|
||||
// Zod wird in src/lib/validierung.ts auf deutsche Meldungen eingestellt.
|
||||
// Ein direkter Import aus "zod" umginge das, und die englischen
|
||||
// Standardsaetze landeten in der Oberflaeche.
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "zod",
|
||||
message:
|
||||
'Bitte `import { z } from "@/lib/validierung"` benutzen – dort sind die deutschen Meldungen eingestellt.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Die Datei, die Zod einstellt, muss es selbst importieren duerfen.
|
||||
files: ["src/lib/validierung.ts"],
|
||||
rules: { "no-restricted-imports": "off" },
|
||||
},
|
||||
globalIgnores([
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"coverage/**",
|
||||
"next-env.d.ts",
|
||||
// Erzeugter Prisma-Client.
|
||||
"src/generated/**",
|
||||
"test-results/**",
|
||||
"playwright-report/**",
|
||||
]),
|
||||
]);
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
/**
|
||||
* Sicherheitskopfzeilen fuer alle Antworten. Die CSP selbst wird pro Anfrage in
|
||||
* src/proxy.ts mit einer Nonce gesetzt, weil sie dort dynamisch sein muss.
|
||||
*/
|
||||
const SICHERHEITS_KOPFZEILEN = [
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
// sshkv laeuft im internen Netz; Pfade wie /schluessel/<id> sollen trotzdem nie
|
||||
// als Referrer an fremde Seiten gehen.
|
||||
{ key: "Referrer-Policy", value: "no-referrer" },
|
||||
{ key: "Permissions-Policy", value: "geolocation=(), microphone=(), camera=()" },
|
||||
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
|
||||
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
||||
];
|
||||
|
||||
const konfiguration: NextConfig = {
|
||||
// Fuer das schlanke Laufzeit-Abbild im Dockerfile.
|
||||
output: "standalone",
|
||||
poweredByHeader: false,
|
||||
// ssh2 bringt optionale native Teile mit, die nicht gebuendelt werden duerfen.
|
||||
serverExternalPackages: ["ssh2", "@node-rs/argon2", "pg-boss"],
|
||||
async headers() {
|
||||
return [{ source: "/:pfad*", headers: SICHERHEITS_KOPFZEILEN }];
|
||||
},
|
||||
};
|
||||
|
||||
export default konfiguration;
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "sshkv",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "SSHKeyVault – zentrale Verwaltung von SSH-Schluesseln mit Weboberflaeche",
|
||||
"packageManager": "pnpm@12.6.0",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3400",
|
||||
"build": "next build",
|
||||
"build:worker": "esbuild src/worker/index.ts --bundle --platform=node --target=node24 --format=esm --outfile=dist/worker.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --external:@prisma/client --external:@node-rs/argon2 --external:pg-native --external:ssh2 --external:cpu-features",
|
||||
"start": "next start -p 3400",
|
||||
"worker": "tsx src/worker/index.ts",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format:pruefen": "prettier --check .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:integration": "vitest run --config vitest.integration.config.mts",
|
||||
"db:up": "docker compose -f docker-compose.dev.yml up -d --wait",
|
||||
"db:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:seed": "prisma db seed",
|
||||
"db:generate": "prisma generate",
|
||||
"issues:sync": "tsx scripts/gitea-issues.ts",
|
||||
"icons": "node scripts/icons.mjs",
|
||||
"postinstall": "prisma generate",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,mts,mjs}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,css,yml,yaml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/ibm-plex-sans": "^5.3.0",
|
||||
"@fontsource/jetbrains-mono": "^5.3.0",
|
||||
"@fontsource/space-grotesk": "^5.3.0",
|
||||
"@node-rs/argon2": "^2.2.2",
|
||||
"@prisma/adapter-pg": "7.10.0",
|
||||
"@prisma/client": "7.10.0",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "^6.2.12",
|
||||
"lucide-react": "1.47.0",
|
||||
"next": "16.3.6",
|
||||
"openid-client": "^6.8.8",
|
||||
"pg": "^8.23.1",
|
||||
"pg-boss": "^12.37.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"ssh2": "^1.17.0",
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^21.2.3",
|
||||
"@commitlint/config-conventional": "^21.2.3",
|
||||
"@playwright/test": "1.63.0",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@types/node": "^24.19.2",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/ssh2": "^1.15.6",
|
||||
"dotenv": "^18.0.7",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "16.3.6",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^17.6.0",
|
||||
"prettier": "^3.9.10",
|
||||
"prisma": "7.10.0",
|
||||
"sharp": "^0.35.5",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "8.3.0",
|
||||
"vitest": "5.0.1"
|
||||
}
|
||||
}
|
||||
Generated
+7534
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
# pnpm 12 fuehrt Build-Skripte von Abhaengigkeiten nur nach ausdruecklicher Freigabe aus.
|
||||
allowBuilds:
|
||||
"@prisma/engines": true
|
||||
esbuild: true
|
||||
prisma: true
|
||||
unrs-resolver: true
|
||||
sharp: true
|
||||
ssh2: false
|
||||
cpu-features: false
|
||||
@@ -0,0 +1,6 @@
|
||||
// Tailwind 4 wird ueber sein PostCSS-Plugin eingebunden; eine tailwind.config.js gibt es nicht mehr.
|
||||
const konfiguration = {
|
||||
plugins: { "@tailwindcss/postcss": {} },
|
||||
};
|
||||
|
||||
export default konfiguration;
|
||||
@@ -0,0 +1,11 @@
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "tsx prisma/seed.ts",
|
||||
},
|
||||
datasource: { url: process.env["DATABASE_URL"] },
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
// SSHKeyVault – Datenmodell. Die vollstaendigen Modelle entstehen in #003.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Einstellung {
|
||||
schluessel String @id
|
||||
wert Json
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { VERSION } from "@/lib/version";
|
||||
|
||||
/** Healthcheck fuer Docker und Reverse Proxy. Verraet nichts ausser der Version. */
|
||||
export function GET() {
|
||||
return NextResponse.json({ status: "ok", version: VERSION });
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: { default: "SSHKeyVault", template: "%s · SSHKeyVault" },
|
||||
description: "Zentrale Verwaltung von SSH-Schlüsseln",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Startseite() {
|
||||
return <main>SSHKeyVault</main>;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
/**
|
||||
* Zugriff auf die Datenbank.
|
||||
*
|
||||
* Der Client wird erst beim ersten Zugriff gebaut. Dadurch laeuft `next build`
|
||||
* ohne gesetzte DATABASE_URL durch, obwohl Module den Import auf oberster Ebene
|
||||
* haben. In der Entwicklung liegt die Instanz an globalThis, damit Hot Reload
|
||||
* nicht bei jedem Speichern eine neue Verbindung aufbaut.
|
||||
*/
|
||||
function clientBauen(): PrismaClient {
|
||||
const url = process.env["DATABASE_URL"];
|
||||
if (!url) {
|
||||
throw new Error("DATABASE_URL ist nicht gesetzt. Siehe .env.example.");
|
||||
}
|
||||
return new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) });
|
||||
}
|
||||
|
||||
const globalerZwischenspeicher = globalThis as typeof globalThis & {
|
||||
sshkvPrisma?: PrismaClient;
|
||||
};
|
||||
|
||||
let instanz: PrismaClient | undefined;
|
||||
|
||||
function instanzHolen(): PrismaClient {
|
||||
if (process.env["NODE_ENV"] !== "production") {
|
||||
globalerZwischenspeicher.sshkvPrisma ??= clientBauen();
|
||||
return globalerZwischenspeicher.sshkvPrisma;
|
||||
}
|
||||
instanz ??= clientBauen();
|
||||
return instanz;
|
||||
}
|
||||
|
||||
export const prisma = new Proxy({} as PrismaClient, {
|
||||
get(_ziel, eigenschaft, empfaenger) {
|
||||
return Reflect.get(instanzHolen(), eigenschaft, empfaenger);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Zod mit deutschen Standardmeldungen (#001, uebernommen aus Laufzettel).
|
||||
*
|
||||
* Ohne diese Einstellung kommen bei fehlenden oder falsch typisierten Feldern
|
||||
* englische Saetze heraus („Invalid input: expected string“), und die landen
|
||||
* ueber die Server Actions unmittelbar in der Oberflaeche. Eigene `message`-Texte
|
||||
* helfen nur dort, wo man an sie gedacht hat — die Lokalisierung deckt auch alles
|
||||
* ab, woran niemand gedacht hat.
|
||||
*
|
||||
* Deshalb wird `z` **hier** eingestellt und von hier weitergereicht: ein Import
|
||||
* direkt aus "zod" umginge die Einstellung. Die ESLint-Regel
|
||||
* `no-restricted-imports` sorgt dafuer, dass das auffaellt.
|
||||
*/
|
||||
z.config(z.locales.de());
|
||||
|
||||
export { z };
|
||||
|
||||
// Was sonst noch aus Zod gebraucht wird, laeuft ebenfalls ueber diesen Pfad –
|
||||
// sonst muesste doch wieder jemand direkt aus "zod" importieren.
|
||||
export { ZodError } from "zod";
|
||||
export type { ZodIssue, ZodType } from "zod";
|
||||
@@ -0,0 +1,6 @@
|
||||
import paket from "../../package.json" with { type: "json" };
|
||||
|
||||
/** Version aus package.json – erscheint in der Seitenleiste und im Healthcheck. */
|
||||
export const VERSION: string = paket.version;
|
||||
export const KURZNAME = "sshkv";
|
||||
export const PRODUKTNAME = "SSHKeyVault";
|
||||
@@ -0,0 +1,7 @@
|
||||
import "dotenv/config";
|
||||
|
||||
// Tests laufen nie gegen die Entwicklungsdatenbank: TEST_DATABASE_URL ersetzt DATABASE_URL.
|
||||
if (process.env["TEST_DATABASE_URL"]) {
|
||||
process.env["DATABASE_URL"] = process.env["TEST_DATABASE_URL"];
|
||||
}
|
||||
process.env["SSHKV_APP_SECRET"] ??= "test-geheimnis-test-geheimnis-test-geheimnis";
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { KURZNAME, PRODUKTNAME, VERSION } from "@/lib/version";
|
||||
|
||||
describe("Version", () => {
|
||||
it("folgt SemVer", () => {
|
||||
expect(VERSION).toMatch(/^\d+\.\d+\.\d+(-[\w.]+)?$/);
|
||||
});
|
||||
it("traegt die verbindlichen Namen", () => {
|
||||
expect(PRODUKTNAME).toBe("SSHKeyVault");
|
||||
expect(KURZNAME).toBe("sshkv");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"forceConsistentCasingInFileNames": 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",
|
||||
"**/*.mts",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
|
||||
},
|
||||
test: {
|
||||
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx", "src/**/*.test.ts"],
|
||||
exclude: ["tests/integration/**", "node_modules/**"],
|
||||
environment: "node",
|
||||
setupFiles: ["tests/setup-env.ts"],
|
||||
// Datenbanktests teilen sich eine Datenbank und duerfen sich nicht ueberholen.
|
||||
fileParallelism: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
/** Integrationstests gegen echte SSH-Ziele in Containern (#011). */
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
|
||||
},
|
||||
test: {
|
||||
include: ["tests/integration/**/*.test.ts"],
|
||||
environment: "node",
|
||||
setupFiles: ["tests/setup-env.ts"],
|
||||
fileParallelism: false,
|
||||
testTimeout: 60_000,
|
||||
hookTimeout: 120_000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user