🚀 Backend komplett implementiert

Features:
- Auth mit JWT + Argon2 (Login, Register, Refresh)
- Rollen-System (Chef/Disponent/Mitarbeiter)
- User Management mit Berechtigungen
- Aufträge mit Zuweisungen
- Verfügbarkeitsplanung
- Stundenzettel mit Foto-Upload Support
- Modulares System mit Config
- Entwickler-Panel Endpoints

Tech:
- Deno + Oak
- PostgreSQL
- CORS enabled
This commit is contained in:
2026-02-20 15:12:06 +00:00
parent a07c2ad858
commit ee19e45171
16 changed files with 2079 additions and 2 deletions

211
src/routes/modules.ts Normal file
View File

@@ -0,0 +1,211 @@
import { Router } from "@oak/oak";
import { query, queryOne, execute } from "../db/postgres.ts";
import { AppError } from "../middleware/error.ts";
import { authMiddleware, requireChef } from "../middleware/auth.ts";
import type { Module, OrganizationModule } from "../types/index.ts";
export const modulesRouter = new Router({ prefix: "/api/modules" });
// Get all available modules
modulesRouter.get("/", authMiddleware, async (ctx) => {
const modules = await query<Module>(
`SELECT * FROM modules ORDER BY is_core DESC, name`
);
ctx.response.body = { modules };
});
// Get organization's module configuration
modulesRouter.get("/org", authMiddleware, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
const modules = await query<Module & { enabled: boolean; config: Record<string, unknown> }>(
`SELECT m.*,
COALESCE(om.enabled, false) as enabled,
COALESCE(om.config, m.default_config) as config
FROM modules m
LEFT JOIN organization_modules om ON m.id = om.module_id AND om.org_id = $1
ORDER BY m.is_core DESC, m.name`,
[orgId]
);
ctx.response.body = { modules };
});
// Enable/disable module for organization (Chef only)
modulesRouter.post("/:moduleId/toggle", requireChef, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
const moduleId = ctx.params.moduleId;
const body = await ctx.request.body.json();
const { enabled } = body;
if (enabled === undefined) {
throw new AppError("enabled field required", 400);
}
// Get module
const module = await queryOne<Module>(
`SELECT * FROM modules WHERE id = $1`,
[moduleId]
);
if (!module) {
throw new AppError("Module not found", 404);
}
// Cannot disable core modules
if (module.is_core && !enabled) {
throw new AppError("Cannot disable core module", 400);
}
// Upsert organization_modules
await execute(
`INSERT INTO organization_modules (org_id, module_id, enabled, config)
VALUES ($1, $2, $3, $4)
ON CONFLICT (org_id, module_id)
DO UPDATE SET enabled = $3, updated_at = NOW()`,
[orgId, moduleId, enabled, module.default_config]
);
ctx.response.body = {
message: `Module ${enabled ? "enabled" : "disabled"}`,
module: module.name,
enabled
};
});
// Update module configuration (Chef only)
modulesRouter.put("/:moduleId/config", requireChef, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
const moduleId = ctx.params.moduleId;
const body = await ctx.request.body.json();
const { config } = body;
if (!config || typeof config !== "object") {
throw new AppError("config object required", 400);
}
// Verify module exists
const module = await queryOne<Module>(
`SELECT * FROM modules WHERE id = $1`,
[moduleId]
);
if (!module) {
throw new AppError("Module not found", 404);
}
// Upsert with new config
await execute(
`INSERT INTO organization_modules (org_id, module_id, enabled, config)
VALUES ($1, $2, true, $3)
ON CONFLICT (org_id, module_id)
DO UPDATE SET config = $3, updated_at = NOW()`,
[orgId, moduleId, JSON.stringify(config)]
);
ctx.response.body = { message: "Module configuration updated" };
});
// Check if specific module is enabled for current org
modulesRouter.get("/check/:moduleName", authMiddleware, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
const moduleName = ctx.params.moduleName;
const result = await queryOne<{ enabled: boolean; config: Record<string, unknown> }>(
`SELECT om.enabled, COALESCE(om.config, m.default_config) as config
FROM modules m
LEFT JOIN organization_modules om ON m.id = om.module_id AND om.org_id = $2
WHERE m.name = $1`,
[moduleName, orgId]
);
if (!result) {
throw new AppError("Module not found", 404);
}
ctx.response.body = {
module: moduleName,
enabled: result.enabled ?? false,
config: result.config
};
});
// ============ DEVELOPER PANEL ENDPOINTS ============
// These require the 'developer' module to be enabled and special permissions
// Get system status
modulesRouter.get("/developer/status", requireChef, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
// Check if developer module is enabled
const devModule = await queryOne<{ enabled: boolean }>(
`SELECT om.enabled FROM organization_modules om
JOIN modules m ON om.module_id = m.id
WHERE m.name = 'developer' AND om.org_id = $1`,
[orgId]
);
if (!devModule?.enabled) {
throw new AppError("Developer module not enabled", 403);
}
// Get stats
const stats = await queryOne<{
user_count: number;
order_count: number;
timesheet_count: number;
enabled_modules: number;
}>(
`SELECT
(SELECT COUNT(*) FROM users WHERE org_id = $1) as user_count,
(SELECT COUNT(*) FROM orders WHERE org_id = $1) as order_count,
(SELECT COUNT(*) FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count,
(SELECT COUNT(*) FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`,
[orgId]
);
ctx.response.body = {
status: "ok",
organization: orgId,
stats,
serverTime: new Date().toISOString(),
};
});
// Get audit logs
modulesRouter.get("/developer/logs", requireChef, async (ctx) => {
const { org_id: orgId } = ctx.state.auth.user;
const limit = parseInt(ctx.request.url.searchParams.get("limit") || "50");
const offset = parseInt(ctx.request.url.searchParams.get("offset") || "0");
// Check developer module
const devModule = await queryOne<{ enabled: boolean }>(
`SELECT om.enabled FROM organization_modules om
JOIN modules m ON om.module_id = m.id
WHERE m.name = 'developer' AND om.org_id = $1`,
[orgId]
);
if (!devModule?.enabled) {
throw new AppError("Developer module not enabled", 403);
}
const logs = await query<{
id: string;
action: string;
entity_type: string;
user_email: string;
created_at: Date;
}>(
`SELECT al.id, al.action, al.entity_type, al.created_at, u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.org_id = $1
ORDER BY al.created_at DESC
LIMIT $2 OFFSET $3`,
[orgId, limit, offset]
);
ctx.response.body = { logs };
});