Mist, jetzt vielleicht

This commit is contained in:
rxf
2026-03-11 20:33:19 +01:00
parent bc235e4e32
commit a949ebcdc8
28 changed files with 1666 additions and 74 deletions

36
lib/auth.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* Authentifizierungsbibliothek
* Benutzer via Umgebungsvariable konfigurieren:
* AUTH_USERS=user1:passwort1,user2:passwort2
*/
export interface User {
username: string;
password: string;
}
export function getUsers(): User[] {
const usersString = process.env.AUTH_USERS || '';
if (!usersString) {
console.warn('AUTH_USERS nicht in .env konfiguriert');
return [];
}
return usersString
.split(',')
.map((userPair) => {
const [username, password] = userPair.trim().split(':');
return { username: username?.trim(), password: password?.trim() };
})
.filter((user) => user.username && user.password);
}
export function verifyCredentials(username: string, password: string): boolean {
const users = getUsers();
const user = users.find(u => u.username === username);
if (!user) return false;
return user.password === password;
}
export function isAuthEnabled(): boolean {
return !!process.env.AUTH_USERS;
}

26
lib/checkAblauf.ts Normal file
View File

@@ -0,0 +1,26 @@
import moment from 'moment';
import { TabletteRaw } from '@/types/tablette';
const VORLAUF = parseInt(process.env.VORLAUF || '14', 10);
export interface AblaufResult {
akt: number;
until: Date;
rtage: number;
warn: boolean;
}
export function checkAblauf(item: Pick<TabletteRaw, 'cnt' | 'pday' | 'at'>): AblaufResult {
const now = moment();
const atday = moment(item.at);
const days = moment.duration(now.diff(atday)).asDays();
const aktAnzahl = item.cnt - Math.floor(days) * item.pday;
const reichtTage = Math.floor(aktAnzahl / item.pday);
const rbis = now.add(reichtTage, 'day').startOf('day');
return {
akt: aktAnzahl,
until: rbis.toDate(),
rtage: reichtTage,
warn: reichtTage <= VORLAUF,
};
}

49
lib/mailService.ts Normal file
View File

@@ -0,0 +1,49 @@
import nodemailer from 'nodemailer';
import moment from 'moment';
import type { RowDataPacket } from 'mysql2';
import pool from './mysql';
import { checkAblauf } from './checkAblauf';
// SMTP-Konfiguration via Umgebungsvariablen (Passwort niemals hart codieren!)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.1und1.de',
port: parseInt(process.env.SMTP_PORT || '587', 10),
secure: false,
auth: {
user: process.env.SMTP_USER || '',
pass: process.env.SMTP_PASS || '',
},
});
export async function doCheckAndMail(): Promise<string> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT tab, pday, cnt, at FROM tabletten'
);
let body = '';
for (const item of rows) {
if (item.pday !== 0) {
const updates = checkAblauf({ cnt: item.cnt, pday: item.pday, at: item.at });
await pool.execute(
'UPDATE tabletten SET akt = ?, until = ?, warn = ? WHERE tab = ?',
[updates.akt, moment(updates.until).format('YYYY-MM-DD'), updates.warn ? 1 : 0, item.tab]
);
if (updates.warn) {
const name = item.tab.split(' ')[0];
body += `"${name}" wird am ${moment(updates.until).format('YYYY-MM-DD')} (in ${updates.rtage} Tagen) zu Ende sein\n`;
}
}
}
if (body) {
await transporter.sendMail({
from: `"Tabletten" <${process.env.SMTP_USER}>`,
to: process.env.MAIL_TO || '',
subject: 'Tabletten gehen zu Ende',
text: body,
});
}
return body || 'Kein Warn-Eintrag.';
}

36
lib/mongodb.ts Normal file
View File

@@ -0,0 +1,36 @@
import { MongoClient, MongoClientOptions } from 'mongodb';
const MONGOHOST = process.env.MONGOHOST || 'localhost';
const MONGOPORT = process.env.MONGOPORT || '27017';
const MONGOAUTH = process.env.MONGOAUTH === 'true';
const MONGOUSRP = process.env.MONGOUSRP || '';
export const MONGOBASE = process.env.MONGOBASE || 'medizin';
const MONGO_URL = MONGOAUTH
? `mongodb://${MONGOUSRP}@${MONGOHOST}:${MONGOPORT}/?authSource=admin`
: `mongodb://${MONGOHOST}:${MONGOPORT}`;
console.log("auth:", MONGOAUTH, "url:", MONGO_URL)
const options: MongoClientOptions = {};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
declare global {
// eslint-disable-next-line no-var
var _mongoClientPromise: Promise<MongoClient> | undefined;
}
if (process.env.NODE_ENV === 'development') {
// In development, use a global variable to preserve the connection across HMR reloads
if (!global._mongoClientPromise) {
client = new MongoClient(MONGO_URL, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
client = new MongoClient(MONGO_URL, options);
clientPromise = client.connect();
}
export default clientPromise;

13
lib/mysql.ts Normal file
View File

@@ -0,0 +1,13 @@
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASS || '',
database: 'medizin',
waitForConnections: true,
connectionLimit: 10,
});
export default pool;

70
lib/session.ts Normal file
View File

@@ -0,0 +1,70 @@
import { cookies } from 'next/headers';
import { SignJWT, jwtVerify } from 'jose';
const SESSION_COOKIE_NAME = 'auth_session';
const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 Tage
const secretKey = process.env.AUTH_SECRET || 'default-secret-change-in-production';
const key = new TextEncoder().encode(secretKey);
export interface SessionData {
username: string;
isAuthenticated: boolean;
expiresAt: number;
}
async function encrypt(payload: SessionData): Promise<string> {
return await 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 {
username: payload.username as string,
isAuthenticated: payload.isAuthenticated as boolean,
expiresAt: payload.expiresAt as number,
};
} catch {
return null;
}
}
export async function createSession(username: string): Promise<void> {
const expiresAt = Date.now() + SESSION_DURATION;
const session: SessionData = { username, isAuthenticated: true, expiresAt };
const encryptedSession = await encrypt(session);
const cookieStore = await cookies();
cookieStore.set(SESSION_COOKIE_NAME, encryptedSession, {
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);
}
export async function isAuthenticated(): Promise<boolean> {
const session = await getSession();
return session?.isAuthenticated ?? false;
}