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
212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
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 };
|
|
});
|