chore: Projektgeruest mit Next.js 16, Prisma 7 und pnpm (#001)
Konfiguration aus Laufzettel uebernommen, damit alle Projekte gleich gebaut und geprueft werden. Entwicklungsdatenbank auf 5436, weil 5433 und 5434 von ProxLedger und Laufzettel belegt sind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
|
# #001 Projekt-Setup
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Meilenstein:** v0.1.0
|
**Meilenstein:** v0.1.0
|
||||||
**Labels:** infrastruktur
|
**Labels:** infrastruktur
|
||||||
**Geschätzt:** 3 h **Tatsächlich:** –
|
**Geschätzt:** 3 h **Tatsächlich:** 0 h 15 min
|
||||||
|
|
||||||
## Ziel
|
## 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.
|
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
|
## Aufgaben
|
||||||
- [ ] Next.js 16 (App Router), React 19, TypeScript strict
|
- [x] Next.js 16 (App Router), React 19, TypeScript strict
|
||||||
- [ ] pnpm 12, gepinnte Versionen, `.nvmrc`
|
- [x] pnpm 12, gepinnte Versionen, `.nvmrc`
|
||||||
- [ ] ESLint 9, Prettier, Vitest, commitlint + husky
|
- [x] ESLint 9, Prettier, Vitest, commitlint + husky
|
||||||
- [ ] `docker-compose.dev.yml` mit PostgreSQL
|
- [x] `docker-compose.dev.yml` mit PostgreSQL
|
||||||
- [ ] `.env.example` mit Präfix `SSHKV_`
|
- [x] `.env.example` mit Präfix `SSHKV_`
|
||||||
- [ ] `CLAUDE.md` mit Konventionen
|
- [x] `CLAUDE.md` mit Konventionen
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
- [ ] `pnpm lint && pnpm typecheck && pnpm test && pnpm build` grün
|
- [x] `pnpm lint && pnpm typecheck && pnpm test && pnpm build` grün
|
||||||
|
|
||||||
## Abhängigkeiten
|
## Abhängigkeiten
|
||||||
–
|
–
|
||||||
|
|
||||||
## Notizen / Entscheidungen
|
## 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