Initial implementation: Logbuch Sternwarte Welzheim
Vollständige Next.js 16 Webanwendung als Logbuch für die Sternwarte Welzheim. 4 Kuppeln (West/Ost/Süd/Pluto), BEO-basierte Authentifizierung mit erzwungenem Passwort-Wechsel beim Erstlogin, MySQL-Backend, Docker-Deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
38
lib/auth.ts
Normal file
38
lib/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { query } from './db';
|
||||
|
||||
export interface Beo {
|
||||
ID: number;
|
||||
Kuerzel: string;
|
||||
Name: string;
|
||||
Passwort: string | null;
|
||||
MustChangePassword: number;
|
||||
}
|
||||
|
||||
export async function getBeoByKuerzel(kuerzel: string): Promise<Beo | null> {
|
||||
const rows = await query(
|
||||
'SELECT ID, Kuerzel, Name, Passwort, MustChangePassword FROM beos WHERE Kuerzel = ?',
|
||||
[kuerzel]
|
||||
) as Beo[];
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function verifyCredentials(
|
||||
kuerzel: string,
|
||||
password: string
|
||||
): Promise<{ beo: Beo; valid: boolean } | null> {
|
||||
const beo = await getBeoByKuerzel(kuerzel);
|
||||
if (!beo) return null;
|
||||
|
||||
if (!beo.Passwort) {
|
||||
const valid = password === 'logbuch123';
|
||||
return { beo, valid };
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, beo.Passwort);
|
||||
return { beo, valid };
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
27
lib/db.ts
Normal file
27
lib/db.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import type { QueryResult } from 'mysql2/promise';
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mydbase_mysql',
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME || 'logbuch',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
};
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function query(sql: string, params?: (string | number | null)[]): Promise<QueryResult> {
|
||||
const p = getPool();
|
||||
const [rows] = await p.execute(sql, params || []);
|
||||
return rows as QueryResult;
|
||||
}
|
||||
62
lib/session.ts
Normal file
62
lib/session.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
|
||||
const SESSION_COOKIE_NAME = 'logbuch_session';
|
||||
const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const secretKey = process.env.AUTH_SECRET || 'logbuch-secret-change-in-production';
|
||||
const key = new TextEncoder().encode(secretKey);
|
||||
|
||||
export interface SessionData {
|
||||
kuerzel: string;
|
||||
beoId: number;
|
||||
beoName: string;
|
||||
mustChangePassword: boolean;
|
||||
isAuthenticated: boolean;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
async function encrypt(payload: SessionData): Promise<string> {
|
||||
return new SignJWT(payload as unknown as Record<string, unknown>)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(new Date(payload.expiresAt))
|
||||
.sign(key);
|
||||
}
|
||||
|
||||
async function decrypt(token: string): Promise<SessionData | null> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] });
|
||||
return payload as unknown as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSession(data: Omit<SessionData, 'expiresAt'>): Promise<void> {
|
||||
const expiresAt = Date.now() + SESSION_DURATION;
|
||||
const session: SessionData = { ...data, expiresAt };
|
||||
const encrypted = await encrypt(session);
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SESSION_COOKIE_NAME, encrypted, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
expires: expiresAt,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<SessionData | null> {
|
||||
const cookieStore = await cookies();
|
||||
const cookie = cookieStore.get(SESSION_COOKIE_NAME);
|
||||
if (!cookie?.value) return null;
|
||||
const session = await decrypt(cookie.value);
|
||||
if (!session || session.expiresAt < Date.now()) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function deleteSession(): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(SESSION_COOKIE_NAME);
|
||||
}
|
||||
Reference in New Issue
Block a user