v1.6.1: Sicherheit – Rate Limiting, Default-PW via Env, AUTH_SECRET Pflicht, Bcrypt 12
This commit is contained in:
@@ -9,8 +9,8 @@ export interface BeoUser {
|
||||
kürzel: string | null;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
pw: string | null;
|
||||
role: string | null;
|
||||
hasPw: boolean;
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<BeoUser[]> {
|
||||
@@ -18,10 +18,10 @@ export async function listUsers(): Promise<BeoUser[]> {
|
||||
if (!session || !session.role?.includes('admin')) redirect('/');
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, `kürzel`, name, vorname, pw, role FROM beos ORDER BY name, vorname',
|
||||
'SELECT id, `kürzel`, name, vorname, role, (pw IS NOT NULL) AS hasPw FROM beos ORDER BY name, vorname',
|
||||
[]
|
||||
) as BeoUser[];
|
||||
return rows;
|
||||
) as (Omit<BeoUser, 'hasPw'> & { hasPw: number })[];
|
||||
return rows.map(r => ({ ...r, hasPw: r.hasPw === 1 }));
|
||||
}
|
||||
|
||||
export async function resetPassword(
|
||||
|
||||
+3
-3
@@ -41,10 +41,10 @@ export default async function AdminPage() {
|
||||
<td className="px-4 py-3">{user.vorname ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.role ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.pw == null ? (
|
||||
<span className="text-amber-600 font-medium">Standard</span>
|
||||
) : (
|
||||
{user.hasPw ? (
|
||||
<span className="text-green-700">gesetzt</span>
|
||||
) : (
|
||||
<span className="text-amber-600 font-medium">Standard</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
|
||||
@@ -11,6 +11,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const logbuchId = parseInt(id);
|
||||
|
||||
try {
|
||||
// Zugriffskontrolle: Nur Ersteller oder Admin dürfen ändern
|
||||
const existingRows = await query('SELECT created_by FROM logbuch WHERE ID = ?', [logbuchId]) as { created_by: number }[];
|
||||
if (existingRows.length === 0) {
|
||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isAdmin = session.role?.includes('admin');
|
||||
const isCreator = existingRows[0].created_by === session.beoId;
|
||||
|
||||
if (!isAdmin && !isCreator) {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body;
|
||||
|
||||
@@ -67,9 +80,23 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const logbuchId = parseInt(id);
|
||||
|
||||
try {
|
||||
await query('DELETE FROM logbuch WHERE ID = ?', [parseInt(id)]);
|
||||
// Zugriffskontrolle: Nur Ersteller oder Admin dürfen löschen
|
||||
const existingRows = await query('SELECT created_by FROM logbuch WHERE ID = ?', [logbuchId]) as { created_by: number }[];
|
||||
if (existingRows.length === 0) {
|
||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isAdmin = session.role?.includes('admin');
|
||||
const isCreator = existingRows[0].created_by === session.beoId;
|
||||
|
||||
if (!isAdmin && !isCreator) {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 });
|
||||
}
|
||||
|
||||
await query('DELETE FROM logbuch WHERE ID = ?', [logbuchId]);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/logbuch/[id]:', error);
|
||||
|
||||
@@ -23,6 +23,8 @@ const LIST_SQL =
|
||||
' ORDER BY l.Beginn DESC';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kuppel = searchParams.get('kuppel') || 'West';
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '10'), 100);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kuppel = searchParams.get('kuppel') || 'West';
|
||||
const year = parseInt(searchParams.get('year') || String(new Date().getFullYear()), 10);
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function changePassword(
|
||||
return { error: 'Das Passwort muss mindestens 6 Zeichen lang sein.' };
|
||||
}
|
||||
|
||||
if (newPassword === 'welzheim') {
|
||||
if (newPassword === (process.env.DEFAULT_PASSWORD ?? 'welzheim')) {
|
||||
return { error: 'Das Standard-Passwort darf nicht als neues Passwort verwendet werden.' };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
'use server';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { verifyCredentials, getBeoDisplayName } from '@/lib/auth';
|
||||
import { createSession } from '@/lib/session';
|
||||
import { checkRateLimit } from '@/lib/ratelimit';
|
||||
|
||||
export async function login(
|
||||
_prevState: { error: string } | undefined,
|
||||
formData: FormData
|
||||
): Promise<{ error: string }> {
|
||||
const headersList = await headers();
|
||||
const ip =
|
||||
headersList.get('x-forwarded-for')?.split(',')[0].trim() ??
|
||||
headersList.get('x-real-ip') ??
|
||||
'unknown';
|
||||
|
||||
const { allowed, remainingMs } = checkRateLimit(ip);
|
||||
if (!allowed) {
|
||||
const minutes = Math.ceil(remainingMs / 60000);
|
||||
return { error: `Zu viele Anmeldeversuche. Bitte ${minutes} Minute${minutes !== 1 ? 'n' : ''} warten.` };
|
||||
}
|
||||
|
||||
const login = (formData.get('username') as string)?.trim();
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user