chore(docker): .dockerignore angepasst; lokale Build-Schritte in Rebuild-Skripten; Doku/README zu production vs production-prebuilt aktualisiert

This commit is contained in:
2025-10-06 18:59:17 +02:00
parent 7a84130aec
commit 1124b1f40b
24 changed files with 1149 additions and 270 deletions

View File

@@ -2,7 +2,8 @@ import { call, os } from "@orpc/server";
import { z } from "zod";
import { randomUUID } from "crypto";
import { createKV } from "../lib/create-kv.js";
import { assertOwner } from "../lib/auth.js";
import { assertOwner, getSessionFromCookies } from "../lib/auth.js";
import { checkAdminRateLimit, getClientIP, enforceAdminRateLimit } from "../lib/rate-limiter.js";
// Datenmodelle
const RecurringRuleSchema = z.object({
id: z.string(),
@@ -67,14 +68,22 @@ function detectOverlappingRules(newRule, existingRules) {
// CRUD-Endpoints für Recurring Rules
const createRule = os
.input(z.object({
sessionId: z.string(),
dayOfWeek: z.number().int().min(0).max(6),
startTime: z.string().regex(/^\d{2}:\d{2}$/),
endTime: z.string().regex(/^\d{2}:\d{2}$/),
}).passthrough())
.handler(async ({ input }) => {
.handler(async ({ input, context }) => {
try {
await assertOwner(input.sessionId);
await assertOwner(context);
// Admin Rate Limiting
const ip = getClientIP(context.req.raw.headers);
const session = await getSessionFromCookies(context);
if (session) {
const result = checkAdminRateLimit({ ip, userId: session.userId });
if (!result.allowed) {
throw new Error(`Zu viele Admin-Anfragen. Bitte versuche es in ${result.retryAfterSeconds} Sekunden erneut.`);
}
}
// Validierung: startTime < endTime
const startMinutes = parseTime(input.startTime);
const endMinutes = parseTime(input.endTime);
@@ -106,9 +115,18 @@ const createRule = os
}
});
const updateRule = os
.input(RecurringRuleSchema.extend({ sessionId: z.string() }).passthrough())
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(RecurringRuleSchema.passthrough())
.handler(async ({ input, context }) => {
await assertOwner(context);
// Admin Rate Limiting
const ip = getClientIP(context.req.raw.headers);
const session = await getSessionFromCookies(context);
if (session) {
const result = checkAdminRateLimit({ ip, userId: session.userId });
if (!result.allowed) {
throw new Error(`Zu viele Admin-Anfragen. Bitte versuche es in ${result.retryAfterSeconds} Sekunden erneut.`);
}
}
// Validierung: startTime < endTime
const startMinutes = parseTime(input.startTime);
const endMinutes = parseTime(input.endTime);
@@ -122,20 +140,38 @@ const updateRule = os
const overlappingTimes = overlappingRules.map(rule => `${rule.startTime}-${rule.endTime}`).join(", ");
throw new Error(`Überlappung mit bestehenden Regeln erkannt: ${overlappingTimes}. Bitte Zeitfenster anpassen.`);
}
const { sessionId, ...rule } = input;
const rule = input;
await recurringRulesKV.setItem(rule.id, rule);
return rule;
});
const deleteRule = os
.input(z.object({ sessionId: z.string(), id: z.string() }))
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(z.object({ id: z.string() }))
.handler(async ({ input, context }) => {
await assertOwner(context);
// Admin Rate Limiting
const ip = getClientIP(context.req.raw.headers);
const session = await getSessionFromCookies(context);
if (session) {
const result = checkAdminRateLimit({ ip, userId: session.userId });
if (!result.allowed) {
throw new Error(`Zu viele Admin-Anfragen. Bitte versuche es in ${result.retryAfterSeconds} Sekunden erneut.`);
}
}
await recurringRulesKV.removeItem(input.id);
});
const toggleRuleActive = os
.input(z.object({ sessionId: z.string(), id: z.string() }))
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(z.object({ id: z.string() }))
.handler(async ({ input, context }) => {
await assertOwner(context);
// Admin Rate Limiting
const ip = getClientIP(context.req.raw.headers);
const session = await getSessionFromCookies(context);
if (session) {
const result = checkAdminRateLimit({ ip, userId: session.userId });
if (!result.allowed) {
throw new Error(`Zu viele Admin-Anfragen. Bitte versuche es in ${result.retryAfterSeconds} Sekunden erneut.`);
}
}
const rule = await recurringRulesKV.getItem(input.id);
if (!rule)
throw new Error("Regel nicht gefunden.");
@@ -152,9 +188,9 @@ const listRules = os.handler(async () => {
});
});
const adminListRules = os
.input(z.object({ sessionId: z.string() }))
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(z.object({}))
.handler(async ({ context }) => {
await assertOwner(context);
const allRules = await recurringRulesKV.getAllItems();
return allRules.sort((a, b) => {
if (a.dayOfWeek !== b.dayOfWeek)
@@ -165,14 +201,15 @@ const adminListRules = os
// CRUD-Endpoints für Time-Off Periods
const createTimeOff = os
.input(z.object({
sessionId: z.string(),
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
reason: z.string(),
}))
.handler(async ({ input }) => {
.handler(async ({ input, context }) => {
try {
await assertOwner(input.sessionId);
await assertOwner(context);
// Admin Rate Limiting direkt nach Owner-Check
await enforceAdminRateLimit(context);
// Validierung: startDate <= endDate
if (input.startDate > input.endDate) {
throw new Error("Startdatum muss vor oder am Enddatum liegen.");
@@ -194,21 +231,25 @@ const createTimeOff = os
}
});
const updateTimeOff = os
.input(TimeOffPeriodSchema.extend({ sessionId: z.string() }).passthrough())
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(TimeOffPeriodSchema.passthrough())
.handler(async ({ input, context }) => {
await assertOwner(context);
// Admin Rate Limiting direkt nach Owner-Check
await enforceAdminRateLimit(context);
// Validierung: startDate <= endDate
if (input.startDate > input.endDate) {
throw new Error("Startdatum muss vor oder am Enddatum liegen.");
}
const { sessionId, ...timeOff } = input;
const timeOff = input;
await timeOffPeriodsKV.setItem(timeOff.id, timeOff);
return timeOff;
});
const deleteTimeOff = os
.input(z.object({ sessionId: z.string(), id: z.string() }))
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(z.object({ id: z.string() }))
.handler(async ({ input, context }) => {
await assertOwner(context);
// Admin Rate Limiting direkt nach Owner-Check
await enforceAdminRateLimit(context);
await timeOffPeriodsKV.removeItem(input.id);
});
const listTimeOff = os.handler(async () => {
@@ -216,9 +257,9 @@ const listTimeOff = os.handler(async () => {
return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
});
const adminListTimeOff = os
.input(z.object({ sessionId: z.string() }))
.handler(async ({ input }) => {
await assertOwner(input.sessionId);
.input(z.object({}))
.handler(async ({ context }) => {
await assertOwner(context);
const allTimeOff = await timeOffPeriodsKV.getAllItems();
return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
});
@@ -341,9 +382,9 @@ const live = {
}
}),
adminListRules: os
.input(z.object({ sessionId: z.string() }))
.handler(async function* ({ input, signal }) {
await assertOwner(input.sessionId);
.input(z.object({}))
.handler(async function* ({ context, signal }) {
await assertOwner(context);
const allRules = await recurringRulesKV.getAllItems();
const sortedRules = allRules.sort((a, b) => {
if (a.dayOfWeek !== b.dayOfWeek)
@@ -362,9 +403,9 @@ const live = {
}
}),
adminListTimeOff: os
.input(z.object({ sessionId: z.string() }))
.handler(async function* ({ input, signal }) {
await assertOwner(input.sessionId);
.input(z.object({}))
.handler(async function* ({ context, signal }) {
await assertOwner(context);
const allTimeOff = await timeOffPeriodsKV.getAllItems();
const sortedTimeOff = allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
yield sortedTimeOff;