v4.1
This commit is contained in:
parent
cc7a77f21b
commit
c7fd939f41
19 changed files with 155 additions and 6999 deletions
|
|
@ -11,13 +11,9 @@ NEXTAUTH_SECRET=your-generated-secret-key
|
|||
# Data directory for persistent storage (config, backups, database)
|
||||
DATA_DIR=./data
|
||||
|
||||
# Admin username for login
|
||||
ADMIN_USERNAME=admin
|
||||
|
||||
# Admin password hash (generated with bcryptjs)
|
||||
# To generate a hash, run: node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"
|
||||
# Example hash for password 'admin': $2a$10$your-bcrypt-hash-here
|
||||
ADMIN_PASSWORD_HASH=$2a$10$your-bcrypt-hash-here
|
||||
# SQLite database file (should be under DATA_DIR)
|
||||
DATABASE_FILE=database.db
|
||||
|
||||
# Optional: Polling interval for status checks (in milliseconds)
|
||||
# STATUS_POLL_INTERVAL=30000
|
||||
53
Dockerfile
53
Dockerfile
|
|
@ -1,68 +1,49 @@
|
|||
# Multi-stage build for Next.js standalone output
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Verwende ein robustes Debian-basiertes Node-Image für native Module
|
||||
FROM node:20-slim AS base
|
||||
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN \
|
||||
if [ -f package-lock.json ]; then npm ci; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
|
||||
# Build source code
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Kopiere gebaute App und node_modules
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/src ./src
|
||||
COPY --from=builder /app/data ./data
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Create data directory for persistent storage
|
||||
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
# Erstelle Datenverzeichnis für SQLite, falls nicht vorhanden
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["npx", "next", "start"]
|
||||
49
README.md
49
README.md
|
|
@ -46,17 +46,11 @@ Ein modernes, responsives Dashboard für die Verwaltung von Homelab-Diensten, ge
|
|||
npm install
|
||||
```
|
||||
|
||||
### Schritt 1.1: Prisma Client generieren
|
||||
|
||||
```bash
|
||||
npm run prisma:generate
|
||||
```
|
||||
### Schritt 1.1: Datenbank wird automatisch erstellt
|
||||
|
||||
### Schritt 1.2: Datenbank-Schema anwenden
|
||||
Es ist **keine** externe Datenbank und kein Setup-Script nötig. Die App verwendet eine lokale SQLite-Datenbank (`better-sqlite3`). Die Datenbankdatei wird beim ersten Start automatisch unter dem in `.env` definierten `DATA_DIR` angelegt (Standard: `./data/database.db`).
|
||||
|
||||
```bash
|
||||
npm run prisma:db-push
|
||||
```
|
||||
|
||||
### Schritt 2: Umgebungsvariablen konfigurieren
|
||||
|
||||
|
|
@ -71,29 +65,15 @@ Bearbeite `.env` und setze die erforderlichen Variablen:
|
|||
```env
|
||||
PORT=3000
|
||||
HOSTNAME=0.0.0.0
|
||||
|
||||
# NextAuth Secret (generieren mit: openssl rand -base64 32)
|
||||
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
|
||||
|
||||
# SQLite database URL für persistenten Benutzerzugriff
|
||||
DATABASE_URL=file:./dev.db
|
||||
|
||||
# Admin-Anmeldedaten (optional, solange noch kein erster Benutzer angelegt wurde)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD_HASH=<siehe unten>
|
||||
DATA_DIR=./data
|
||||
DATABASE_FILE=database.db
|
||||
```
|
||||
|
||||
### Schritt 3: Admin-Passwort-Hash generieren
|
||||
|
||||
Um den Passwort-Hash zu generieren, führe aus:
|
||||
### Schritt 3: First-Run-Setup (Admin-Benutzer anlegen)
|
||||
|
||||
```bash
|
||||
node scripts/generate-password-hash.js
|
||||
```
|
||||
|
||||
Das Script fordert dich auf, ein Passwort einzugeben und gibt einen bcryptjs-Hash aus.
|
||||
|
||||
Kopiere den Hash in deine `.env` Datei als `ADMIN_PASSWORD_HASH`.
|
||||
Beim ersten Start ist `/setup` verfügbar, solange noch kein Benutzer existiert. Dort kannst du den ersten Admin-Benutzer anlegen. Danach ist `/setup` gesperrt und du nutzt `/login`.
|
||||
|
||||
### Schritt 4: Entwicklungsserver starten
|
||||
|
||||
|
|
@ -103,12 +83,16 @@ npm run dev
|
|||
|
||||
Der Server läuft unter `http://localhost:3000`
|
||||
|
||||
|
||||
## Login & Admin-Zugang
|
||||
|
||||
1. Navigiere zu `http://localhost:3000/setup`, wenn noch kein Administrator angelegt wurde.
|
||||
2. Erstelle den ersten Admin-Benutzer.
|
||||
1. Navigiere zu `http://localhost:3000/setup`, wenn noch **kein** Benutzer existiert.
|
||||
2. Lege den ersten Admin-Benutzer an (Benutzername und Passwort werden in der Datenbank gespeichert, Passwort wird mit bcryptjs gehasht).
|
||||
3. Danach melde dich unter `http://localhost:3000/login` an.
|
||||
4. Nach erfolgreichem Login wird zu `/admin` weitergeleitet
|
||||
4. Nach erfolgreichem Login wirst du zu `/admin` weitergeleitet.
|
||||
## Speicherort der Datenbank
|
||||
|
||||
Die SQLite-Datenbankdatei liegt standardmäßig unter `./data/database.db` (relativ zum Projektverzeichnis oder `/app/data/database.db` im Docker-Container). Der Pfad kann über `DATA_DIR` und `DATABASE_FILE` in der `.env` angepasst werden.
|
||||
|
||||
## Admin-Interface
|
||||
|
||||
|
|
@ -168,9 +152,6 @@ npm run lint # ESLint ausführen
|
|||
| `PORT` | Port für den Server | `3000` |
|
||||
| `HOSTNAME` | Server-Hostname | `0.0.0.0` |
|
||||
| `NEXTAUTH_SECRET` | Secret für JWT-Signing | `openssl rand -base64 32` |
|
||||
| `DATABASE_URL` | SQLite-Verbindungs-URL für Prisma | `file:./dev.db` |
|
||||
| `ADMIN_USERNAME` | Admin-Benutzername | `admin` |
|
||||
| `ADMIN_PASSWORD_HASH` | Bcryptjs-Hash des Admin-Passworts | `$2a$10$...` |
|
||||
|
||||
## Sicherheit
|
||||
|
||||
|
|
@ -220,7 +201,6 @@ docker-compose up --build
|
|||
docker build -t homelab-dashboard .
|
||||
docker run -p 3000:3000 \
|
||||
-e NEXTAUTH_SECRET=your-secret \
|
||||
-e DATABASE_URL=file:/app/data/database.db \
|
||||
-e DATA_DIR=/app/data \
|
||||
-v $(pwd)/data:/app/data \
|
||||
homelab-dashboard
|
||||
|
|
@ -257,7 +237,6 @@ docker-compose down
|
|||
|
||||
5. **Environment Variables**:
|
||||
- **NEXTAUTH_SECRET**: Dein generierter Secret (openssl rand -base64 32)
|
||||
- **DATABASE_URL**: file:/app/data/database.db
|
||||
- **DATA_DIR**: /app/data
|
||||
- **NODE_ENV**: production
|
||||
|
||||
|
|
@ -287,7 +266,6 @@ services:
|
|||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
- NEXTAUTH_SECRET=your-nextauth-secret
|
||||
- DATABASE_URL=file:/app/data/database.db
|
||||
- DATA_DIR=/app/data
|
||||
volumes:
|
||||
- /mnt/user/appdata/homelab-dashboard/data:/app/data
|
||||
|
|
@ -298,7 +276,6 @@ services:
|
|||
- **Container startet nicht**: Logs prüfen mit `docker-compose logs`
|
||||
- **Daten nicht persistent**: Volume-Mapping überprüfen
|
||||
- **Authentifizierung fehlt**: NEXTAUTH_SECRET setzen
|
||||
- **Datenbank-Fehler**: DATABASE_URL und DATA_DIR prüfen
|
||||
|
||||
## Konfiguration
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export const runtime = "nodejs";
|
||||
import NextAuth from 'next-auth';
|
||||
import { authOptions } from '@/auth';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export const runtime = "nodejs";
|
||||
import { auth } from '@/auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createBackup, listBackups, restoreBackup } from '@/src/lib/config/backup-config';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const runtime = "nodejs";
|
||||
import { NextResponse } from 'next/server';
|
||||
import { hash } from 'bcryptjs';
|
||||
import { z } from 'zod';
|
||||
import { createInitialAdmin, hasAnyUser } from '@/src/lib/db/user';
|
||||
|
||||
|
|
@ -42,10 +42,8 @@ export async function POST(request: Request) {
|
|||
);
|
||||
}
|
||||
|
||||
const passwordHash = await hash(result.data.password, 10);
|
||||
|
||||
try {
|
||||
await createInitialAdmin(result.data.username, passwordHash);
|
||||
await createInitialAdmin(result.data.username, result.data.password);
|
||||
return NextResponse.json({ message: 'Administrator wurde erfolgreich angelegt.' }, { status: 201 });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'INITIAL_ADMIN_ALREADY_EXISTS') {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export const runtime = "nodejs";
|
||||
import { redirect } from 'next/navigation';
|
||||
import { hasAnyUser } from '@/src/lib/db/user';
|
||||
import LoginForm from '@/src/components/LoginForm';
|
||||
|
|
@ -5,17 +6,8 @@ import LoginForm from '@/src/components/LoginForm';
|
|||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function LoginPage() {
|
||||
let hasUser = false;
|
||||
|
||||
try {
|
||||
hasUser = await hasAnyUser();
|
||||
} catch (error) {
|
||||
console.warn('Failed to determine user setup status on /login:', error);
|
||||
}
|
||||
|
||||
if (!hasUser && !process.env.ADMIN_PASSWORD_HASH) {
|
||||
if (!hasAnyUser()) {
|
||||
redirect('/setup');
|
||||
}
|
||||
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export const runtime = "nodejs";
|
||||
import { redirect } from 'next/navigation';
|
||||
import { hasAnyUser } from '@/src/lib/db/user';
|
||||
import SetupForm from '@/src/components/SetupForm';
|
||||
|
|
@ -5,15 +6,8 @@ import SetupForm from '@/src/components/SetupForm';
|
|||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function SetupPage() {
|
||||
try {
|
||||
const hasUser = await hasAnyUser();
|
||||
|
||||
if (hasUser) {
|
||||
redirect('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to determine user setup status on /setup:', error);
|
||||
if (hasAnyUser()) {
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
return <SetupForm />;
|
||||
}
|
||||
|
|
|
|||
59
auth.ts
59
auth.ts
|
|
@ -14,58 +14,27 @@ export const authOptions: NextAuthOptions = {
|
|||
if (!credentials?.username || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = credentials.username.trim();
|
||||
const password = credentials.password;
|
||||
if (process.env.DATABASE_URL) {
|
||||
try {
|
||||
const dbUser = await getUserByUsername(username);
|
||||
|
||||
if (dbUser) {
|
||||
const isPasswordValid = await compare(password, dbUser.passwordHash);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(dbUser.id),
|
||||
name: username,
|
||||
email: `${username}@homelab.local`,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
if (await hasAnyUser()) {
|
||||
try {
|
||||
const dbUser = getUserByUsername(username);
|
||||
if (dbUser) {
|
||||
const isPasswordValid = await compare(password, dbUser.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database authentication error:', error);
|
||||
return {
|
||||
id: String(dbUser.id),
|
||||
name: username,
|
||||
email: `${username}@homelab.local`,
|
||||
username,
|
||||
};
|
||||
}
|
||||
if (hasAnyUser()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
const adminPasswordHash = process.env.ADMIN_PASSWORD_HASH;
|
||||
|
||||
if (!adminPasswordHash || username !== adminUsername) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const isPasswordValid = await compare(password, adminPasswordHash);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: '1',
|
||||
name: 'Admin',
|
||||
email: 'admin@homelab.local',
|
||||
username: adminUsername,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Password comparison error:', error);
|
||||
console.error('Database authentication error:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
}
|
||||
const nextConfig = {};
|
||||
|
||||
module.exports = nextConfig
|
||||
|
|
|
|||
6788
package-lock.json
generated
6788
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -2,16 +2,17 @@
|
|||
"name": "homelab-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src lib --ext .ts,.tsx",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:db-push": "prisma db push"
|
||||
"lint": "eslint app src lib --ext .ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
"better-sqlite3": "^9.0.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"next": "^16.2.3",
|
||||
"next-auth": "^4.24.14",
|
||||
|
|
@ -29,7 +30,6 @@
|
|||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"postcss": "^8.4.32",
|
||||
"prisma": "^5.22.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
passwordHash String
|
||||
role String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
|
|
|||
1
src/lib/db-init.ts
Normal file
1
src/lib/db-init.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
import './db'; // just import to ensure DB is initialized
|
||||
26
src/lib/db.ts
Normal file
26
src/lib/db.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { DATA_DIR } from './config/paths';
|
||||
|
||||
const DB_FILE = path.join(DATA_DIR, 'database.db');
|
||||
|
||||
// Ensure DATA_DIR exists
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Open or create the database
|
||||
export const db = new Database(DB_FILE);
|
||||
|
||||
// Initialize users table if not exists
|
||||
const init = db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'admin',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
init.run();
|
||||
|
|
@ -1,12 +1 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalThis.prisma = prisma;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,34 @@
|
|||
import { prisma } from './prisma';
|
||||
import { db } from '../db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function getUserByUsername(username: string) {
|
||||
return prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export async function createUser(username: string, passwordHash: string, role = 'admin') {
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
passwordHash,
|
||||
role,
|
||||
},
|
||||
});
|
||||
export function getUserByUsername(username: string): User | undefined {
|
||||
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
}
|
||||
|
||||
export async function hasAnyUser() {
|
||||
const count = await prisma.user.count();
|
||||
return count > 0;
|
||||
export function hasAnyUser(): boolean {
|
||||
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
||||
return row.count > 0;
|
||||
}
|
||||
|
||||
export async function createInitialAdmin(username: string, passwordHash: string) {
|
||||
return prisma.$transaction(async (tx: any) => {
|
||||
const existingUsers = await tx.user.count();
|
||||
|
||||
if (existingUsers > 0) {
|
||||
throw new Error('INITIAL_ADMIN_ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
return tx.user.create({
|
||||
data: {
|
||||
username,
|
||||
passwordHash,
|
||||
role: 'admin',
|
||||
},
|
||||
});
|
||||
});
|
||||
export async function createUser(username: string, password: string, role = 'admin'): Promise<User> {
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
const stmt = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)');
|
||||
stmt.run(username, password_hash, role);
|
||||
return getUserByUsername(username)!;
|
||||
}
|
||||
|
||||
export async function createInitialAdmin(username: string, password: string): Promise<User> {
|
||||
if (hasAnyUser()) {
|
||||
throw new Error('INITIAL_ADMIN_ALREADY_EXISTS');
|
||||
}
|
||||
return createUser(username, password, 'admin');
|
||||
}
|
||||
|
||||
|
|
|
|||
33
src/lib/users.ts
Normal file
33
src/lib/users.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { db } from './db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function getUserByUsername(username: string): User | undefined {
|
||||
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
}
|
||||
|
||||
export function hasAnyUser(): boolean {
|
||||
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
||||
return row.count > 0;
|
||||
}
|
||||
|
||||
export async function createUser(username: string, password: string, role = 'admin'): Promise<User> {
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
const stmt = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)');
|
||||
const info = stmt.run(username, password_hash, role);
|
||||
return getUserByUsername(username)!;
|
||||
}
|
||||
|
||||
export async function verifyUser(username: string, password: string): Promise<User | null> {
|
||||
const user = getUserByUsername(username);
|
||||
if (!user) return null;
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
return valid ? user : null;
|
||||
}
|
||||
7
types/user.d.ts
vendored
Normal file
7
types/user.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: string;
|
||||
created_at: string;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue