new features
This commit is contained in:
@@ -101,7 +101,7 @@ import serviceMonitorRoutes from './routes/serviceMonitor.routes';
|
||||
import settingsRoutes from './routes/settings.routes';
|
||||
import bannerRoutes from './routes/banner.routes';
|
||||
import permissionRoutes from './routes/permission.routes';
|
||||
import shopRoutes from './routes/shop.routes';
|
||||
import ausruestungsanfrageRoutes from './routes/ausruestungsanfrage.routes';
|
||||
import issueRoutes from './routes/issue.routes';
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
@@ -126,7 +126,7 @@ app.use('/api/admin/settings', settingsRoutes);
|
||||
app.use('/api/settings', settingsRoutes);
|
||||
app.use('/api/banners', bannerRoutes);
|
||||
app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/shop', shopRoutes);
|
||||
app.use('/api/ausruestungsanfragen', ausruestungsanfrageRoutes);
|
||||
app.use('/api/issues', issueRoutes);
|
||||
|
||||
// Static file serving for uploads (authenticated)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import shopService from '../services/shop.service';
|
||||
import ausruestungsanfrageService from '../services/ausruestungsanfrage.service';
|
||||
import notificationService from '../services/notification.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
class ShopController {
|
||||
class AusruestungsanfrageController {
|
||||
// -------------------------------------------------------------------------
|
||||
// Catalog Items
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -12,10 +12,10 @@ class ShopController {
|
||||
try {
|
||||
const kategorie = req.query.kategorie as string | undefined;
|
||||
const aktiv = req.query.aktiv !== undefined ? req.query.aktiv === 'true' : undefined;
|
||||
const items = await shopService.getItems({ kategorie, aktiv });
|
||||
const items = await ausruestungsanfrageService.getItems({ kategorie, aktiv });
|
||||
res.status(200).json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getItems error', { error });
|
||||
logger.error('AusruestungsanfrageController.getItems error', { error });
|
||||
res.status(500).json({ success: false, message: 'Artikel konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,14 @@ class ShopController {
|
||||
async getItemById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const item = await shopService.getItemById(id);
|
||||
const item = await ausruestungsanfrageService.getItemById(id);
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: 'Artikel nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getItemById error', { error });
|
||||
logger.error('AusruestungsanfrageController.getItemById error', { error });
|
||||
res.status(500).json({ success: false, message: 'Artikel konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,10 @@ class ShopController {
|
||||
res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
const item = await shopService.createItem(req.body, req.user!.id);
|
||||
const item = await ausruestungsanfrageService.createItem(req.body, req.user!.id);
|
||||
res.status(201).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.createItem error', { error });
|
||||
logger.error('AusruestungsanfrageController.createItem error', { error });
|
||||
res.status(500).json({ success: false, message: 'Artikel konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
@@ -53,14 +53,14 @@ class ShopController {
|
||||
async updateItem(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const item = await shopService.updateItem(id, req.body, req.user!.id);
|
||||
const item = await ausruestungsanfrageService.updateItem(id, req.body, req.user!.id);
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: 'Artikel nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.updateItem error', { error });
|
||||
logger.error('AusruestungsanfrageController.updateItem error', { error });
|
||||
res.status(500).json({ success: false, message: 'Artikel konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
@@ -68,20 +68,20 @@ class ShopController {
|
||||
async deleteItem(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await shopService.deleteItem(id);
|
||||
await ausruestungsanfrageService.deleteItem(id);
|
||||
res.status(200).json({ success: true, message: 'Artikel gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.deleteItem error', { error });
|
||||
logger.error('AusruestungsanfrageController.deleteItem error', { error });
|
||||
res.status(500).json({ success: false, message: 'Artikel konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getCategories(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const categories = await shopService.getCategories();
|
||||
const categories = await ausruestungsanfrageService.getCategories();
|
||||
res.status(200).json({ success: true, data: categories });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getCategories error', { error });
|
||||
logger.error('AusruestungsanfrageController.getCategories error', { error });
|
||||
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -94,20 +94,20 @@ class ShopController {
|
||||
try {
|
||||
const status = req.query.status as string | undefined;
|
||||
const anfrager_id = req.query.anfrager_id as string | undefined;
|
||||
const requests = await shopService.getRequests({ status, anfrager_id });
|
||||
const requests = await ausruestungsanfrageService.getRequests({ status, anfrager_id });
|
||||
res.status(200).json({ success: true, data: requests });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getRequests error', { error });
|
||||
logger.error('AusruestungsanfrageController.getRequests error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getMyRequests(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const requests = await shopService.getMyRequests(req.user!.id);
|
||||
const requests = await ausruestungsanfrageService.getMyRequests(req.user!.id);
|
||||
res.status(200).json({ success: true, data: requests });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getMyRequests error', { error });
|
||||
logger.error('AusruestungsanfrageController.getMyRequests error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -115,14 +115,14 @@ class ShopController {
|
||||
async getRequestById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const request = await shopService.getRequestById(id);
|
||||
const request = await ausruestungsanfrageService.getRequestById(id);
|
||||
if (!request) {
|
||||
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: request });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getRequestById error', { error });
|
||||
logger.error('AusruestungsanfrageController.getRequestById error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfrage konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -150,10 +150,10 @@ class ShopController {
|
||||
}
|
||||
}
|
||||
|
||||
const request = await shopService.createRequest(req.user!.id, items, notizen);
|
||||
const request = await ausruestungsanfrageService.createRequest(req.user!.id, items, notizen);
|
||||
res.status(201).json({ success: true, data: request });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.createRequest error', { error });
|
||||
logger.error('AusruestungsanfrageController.createRequest error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfrage konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
@@ -178,13 +178,13 @@ class ShopController {
|
||||
}
|
||||
|
||||
// Fetch request to get anfrager_id for notification
|
||||
const existing = await shopService.getRequestById(id);
|
||||
const existing = await ausruestungsanfrageService.getRequestById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await shopService.updateRequestStatus(id, status, admin_notizen, req.user!.id);
|
||||
const updated = await ausruestungsanfrageService.updateRequestStatus(id, status, admin_notizen, req.user!.id);
|
||||
|
||||
// Notify requester on status changes
|
||||
if (['genehmigt', 'abgelehnt', 'bestellt', 'erledigt'].includes(status)) {
|
||||
@@ -193,19 +193,19 @@ class ShopController {
|
||||
: `#${id}`;
|
||||
await notificationService.createNotification({
|
||||
user_id: existing.anfrager_id,
|
||||
typ: 'shop_anfrage',
|
||||
typ: 'ausruestung_anfrage',
|
||||
titel: status === 'genehmigt' ? 'Anfrage genehmigt' : status === 'abgelehnt' ? 'Anfrage abgelehnt' : `Anfrage ${status}`,
|
||||
nachricht: `Deine Shop-Anfrage ${orderLabel} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
|
||||
nachricht: `Deine Ausrüstungsanfrage ${orderLabel} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
|
||||
schwere: status === 'abgelehnt' ? 'warnung' : 'info',
|
||||
link: '/shop',
|
||||
link: '/ausruestungsanfrage',
|
||||
quell_id: String(id),
|
||||
quell_typ: 'shop_anfrage',
|
||||
quell_typ: 'ausruestung_anfrage',
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, data: updated });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.updateRequestStatus error', { error });
|
||||
logger.error('AusruestungsanfrageController.updateRequestStatus error', { error });
|
||||
res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
@@ -213,10 +213,10 @@ class ShopController {
|
||||
async deleteRequest(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await shopService.deleteRequest(id);
|
||||
await ausruestungsanfrageService.deleteRequest(id);
|
||||
res.status(200).json({ success: true, message: 'Anfrage gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.deleteRequest error', { error });
|
||||
logger.error('AusruestungsanfrageController.deleteRequest error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfrage konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
@@ -227,10 +227,10 @@ class ShopController {
|
||||
|
||||
async getOverview(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const overview = await shopService.getOverview();
|
||||
const overview = await ausruestungsanfrageService.getOverview();
|
||||
res.status(200).json({ success: true, data: overview });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getOverview error', { error });
|
||||
logger.error('AusruestungsanfrageController.getOverview error', { error });
|
||||
res.status(500).json({ success: false, message: 'Übersicht konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
@@ -249,10 +249,10 @@ class ShopController {
|
||||
return;
|
||||
}
|
||||
|
||||
await shopService.linkToOrder(anfrageId, bestellung_id);
|
||||
await ausruestungsanfrageService.linkToOrder(anfrageId, bestellung_id);
|
||||
res.status(200).json({ success: true, message: 'Verknüpfung erstellt' });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.linkToOrder error', { error });
|
||||
logger.error('AusruestungsanfrageController.linkToOrder error', { error });
|
||||
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
@@ -261,13 +261,13 @@ class ShopController {
|
||||
try {
|
||||
const anfrageId = Number(req.params.id);
|
||||
const bestellungId = Number(req.params.bestellungId);
|
||||
await shopService.unlinkFromOrder(anfrageId, bestellungId);
|
||||
await ausruestungsanfrageService.unlinkFromOrder(anfrageId, bestellungId);
|
||||
res.status(200).json({ success: true, message: 'Verknüpfung entfernt' });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.unlinkFromOrder error', { error });
|
||||
logger.error('AusruestungsanfrageController.unlinkFromOrder error', { error });
|
||||
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht entfernt werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ShopController();
|
||||
export default new AusruestungsanfrageController();
|
||||
@@ -67,13 +67,25 @@ class IssueController {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canManage) {
|
||||
|
||||
const existing = await issueService.getIssueById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const isOwner = existing.erstellt_von === userId;
|
||||
if (!canManage && !isOwner) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
const issue = await issueService.updateIssue(id, req.body);
|
||||
|
||||
// Owners without manage permission can only change status
|
||||
const updateData = canManage ? req.body : { status: req.body.status };
|
||||
const issue = await issueService.updateIssue(id, updateData);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
|
||||
@@ -242,6 +242,64 @@ class PermissionController {
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Benutzer' });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* GET /api/permissions/debug/:userId
|
||||
* Returns debug info for a specific user: their groups, resolved permissions,
|
||||
* and maintenance flags. Admin only.
|
||||
*/
|
||||
async debugUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.params.userId as string;
|
||||
|
||||
// Fetch user's Authentik groups from DB
|
||||
const { pool } = await import('../config/database');
|
||||
const userResult = await pool.query(
|
||||
'SELECT authentik_groups, email, name FROM users WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
res.status(404).json({ success: false, message: 'Benutzer nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userResult.rows[0];
|
||||
const groups: string[] = user.authentik_groups ?? [];
|
||||
const isAdmin = groups.includes('dashboard_admin');
|
||||
|
||||
// Resolve permissions for those groups
|
||||
let permissions: string[];
|
||||
if (isAdmin) {
|
||||
const matrix = await permissionService.getMatrix();
|
||||
permissions = matrix.permissions.map(p => p.id);
|
||||
} else {
|
||||
permissions = permissionService.getEffectivePermissions(groups);
|
||||
}
|
||||
|
||||
// Maintenance flags
|
||||
const maintenance = permissionService.getMaintenanceFlags();
|
||||
const maintenanceActive = Object.entries(maintenance)
|
||||
.filter(([, active]) => active)
|
||||
.map(([featureGroup]) => featureGroup);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
authentikGroups: groups,
|
||||
isAdmin,
|
||||
permissions,
|
||||
maintenance,
|
||||
maintenanceActiveFeatureGroups: maintenanceActive,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to debug user permissions', { error, userId: req.params.userId });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Debug-Informationen' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new PermissionController();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
-- Migration 046: Rename Shop → Ausrüstungsanfrage
|
||||
-- Renames all shop_* tables and updates permission references.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Rename tables
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE IF EXISTS shop_artikel RENAME TO ausruestung_artikel;
|
||||
ALTER TABLE IF EXISTS shop_anfragen RENAME TO ausruestung_anfragen;
|
||||
ALTER TABLE IF EXISTS shop_anfrage_positionen RENAME TO ausruestung_anfrage_positionen;
|
||||
ALTER TABLE IF EXISTS shop_anfrage_bestellung RENAME TO ausruestung_anfrage_bestellung;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Rename indexes
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER INDEX IF EXISTS idx_shop_artikel_kategorie RENAME TO idx_ausruestung_artikel_kategorie;
|
||||
ALTER INDEX IF EXISTS idx_shop_artikel_aktiv RENAME TO idx_ausruestung_artikel_aktiv;
|
||||
ALTER INDEX IF EXISTS idx_shop_anfragen_anfrager RENAME TO idx_ausruestung_anfragen_anfrager;
|
||||
ALTER INDEX IF EXISTS idx_shop_anfragen_status RENAME TO idx_ausruestung_anfragen_status;
|
||||
ALTER INDEX IF EXISTS idx_shop_anfrage_positionen_anfrage RENAME TO idx_ausruestung_anfrage_positionen_anfrage;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. Rename triggers
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TRIGGER IF EXISTS trg_shop_artikel_aktualisiert ON ausruestung_artikel RENAME TO trg_ausruestung_artikel_aktualisiert;
|
||||
ALTER TRIGGER IF EXISTS trg_shop_anfragen_aktualisiert ON ausruestung_anfragen RENAME TO trg_ausruestung_anfragen_aktualisiert;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 4. Update feature_groups
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
UPDATE feature_groups SET id = 'ausruestungsanfrage' WHERE id = 'shop';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 5. Update permissions
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
UPDATE permissions SET
|
||||
id = REPLACE(id, 'shop:', 'ausruestungsanfrage:'),
|
||||
feature_group_id = 'ausruestungsanfrage'
|
||||
WHERE feature_group_id = 'shop';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 6. Update group_permissions
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
UPDATE group_permissions SET
|
||||
permission_id = REPLACE(permission_id, 'shop:', 'ausruestungsanfrage:')
|
||||
WHERE permission_id LIKE 'shop:%';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 7. Update notification quell_typ references
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
UPDATE benachrichtigungen SET quell_typ = 'ausruestung_anfrage' WHERE quell_typ = 'shop_anfrage';
|
||||
UPDATE benachrichtigungen SET typ = 'ausruestung_anfrage' WHERE typ = 'shop_anfrage';
|
||||
UPDATE benachrichtigungen SET link = '/ausruestungsanfrage' WHERE link = '/shop';
|
||||
@@ -265,6 +265,41 @@ router.delete(
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reset / Truncate endpoints (no olderThanDays, just ?confirm=true)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ResetTarget = 'reset-bestellungen' | 'reset-ausruestung-anfragen' | 'reset-issues' | 'issues-all';
|
||||
|
||||
const RESET_TARGETS: Record<ResetTarget, (confirm: boolean) => Promise<{ count: number; deleted: boolean }>> = {
|
||||
'reset-bestellungen': (c) => cleanupService.resetBestellungenSequence(c),
|
||||
'reset-ausruestung-anfragen': (c) => cleanupService.resetAusruestungAnfragenSequence(c),
|
||||
'reset-issues': (c) => cleanupService.resetIssuesSequence(c),
|
||||
'issues-all': (c) => cleanupService.resetIssuesSequence(c),
|
||||
};
|
||||
|
||||
router.delete(
|
||||
'/cleanup/:resetTarget(reset-bestellungen|reset-ausruestung-anfragen|reset-issues|issues-all)',
|
||||
authenticate,
|
||||
requirePermission('admin:write'),
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const target = req.params.resetTarget as ResetTarget;
|
||||
const handler = RESET_TARGETS[target];
|
||||
if (!handler) {
|
||||
res.status(400).json({ success: false, message: `Unknown reset target: ${target}` });
|
||||
return;
|
||||
}
|
||||
const confirm = req.query.confirm === 'true';
|
||||
const result = await handler(confirm);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Reset failed', { error, target: req.params.resetTarget });
|
||||
res.status(500).json({ success: false, message: 'Reset failed' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /api/admin/users/:userId/sync-data — selective sync data deletion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
44
backend/src/routes/ausruestungsanfrage.routes.ts
Normal file
44
backend/src/routes/ausruestungsanfrage.routes.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Router } from 'express';
|
||||
import ausruestungsanfrageController from '../controllers/ausruestungsanfrage.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog Items
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/items', authenticate, requirePermission('ausruestungsanfrage:view'), ausruestungsanfrageController.getItems.bind(ausruestungsanfrageController));
|
||||
router.get('/items/:id', authenticate, requirePermission('ausruestungsanfrage:view'), ausruestungsanfrageController.getItemById.bind(ausruestungsanfrageController));
|
||||
router.post('/items', authenticate, requirePermission('ausruestungsanfrage:manage_catalog'), ausruestungsanfrageController.createItem.bind(ausruestungsanfrageController));
|
||||
router.patch('/items/:id', authenticate, requirePermission('ausruestungsanfrage:manage_catalog'), ausruestungsanfrageController.updateItem.bind(ausruestungsanfrageController));
|
||||
router.delete('/items/:id', authenticate, requirePermission('ausruestungsanfrage:manage_catalog'), ausruestungsanfrageController.deleteItem.bind(ausruestungsanfrageController));
|
||||
|
||||
router.get('/categories', authenticate, requirePermission('ausruestungsanfrage:view'), ausruestungsanfrageController.getCategories.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:view_overview'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/requests', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.getRequests.bind(ausruestungsanfrageController));
|
||||
router.get('/requests/my', authenticate, ausruestungsanfrageController.getMyRequests.bind(ausruestungsanfrageController));
|
||||
router.get('/requests/:id', authenticate, ausruestungsanfrageController.getRequestById.bind(ausruestungsanfrageController));
|
||||
router.post('/requests', authenticate, requirePermission('ausruestungsanfrage:create_request'), ausruestungsanfrageController.createRequest.bind(ausruestungsanfrageController));
|
||||
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
|
||||
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linking requests to orders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.post('/requests/:id/link', authenticate, requirePermission('ausruestungsanfrage:link_orders'), ausruestungsanfrageController.linkToOrder.bind(ausruestungsanfrageController));
|
||||
router.delete('/requests/:id/link/:bestellungId', authenticate, requirePermission('ausruestungsanfrage:link_orders'), ausruestungsanfrageController.unlinkFromOrder.bind(ausruestungsanfrageController));
|
||||
|
||||
export default router;
|
||||
@@ -19,5 +19,6 @@ router.put('/admin/group/:groupName', authenticate, requirePermission('admin:wri
|
||||
router.delete('/admin/group/:groupName', authenticate, requirePermission('admin:write'), permissionController.deleteGroup.bind(permissionController));
|
||||
router.put('/admin/bulk', authenticate, requirePermission('admin:write'), permissionController.setBulkPermissions.bind(permissionController));
|
||||
router.put('/admin/maintenance/:featureGroupId', authenticate, requirePermission('admin:write'), permissionController.setMaintenanceFlag.bind(permissionController));
|
||||
router.get('/debug/:userId', authenticate, requirePermission('admin:write'), permissionController.debugUser.bind(permissionController));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Router } from 'express';
|
||||
import shopController from '../controllers/shop.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog Items
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/items', authenticate, requirePermission('shop:view'), shopController.getItems.bind(shopController));
|
||||
router.get('/items/:id', authenticate, requirePermission('shop:view'), shopController.getItemById.bind(shopController));
|
||||
router.post('/items', authenticate, requirePermission('shop:manage_catalog'), shopController.createItem.bind(shopController));
|
||||
router.patch('/items/:id', authenticate, requirePermission('shop:manage_catalog'), shopController.updateItem.bind(shopController));
|
||||
router.delete('/items/:id', authenticate, requirePermission('shop:manage_catalog'), shopController.deleteItem.bind(shopController));
|
||||
|
||||
router.get('/categories', authenticate, requirePermission('shop:view'), shopController.getCategories.bind(shopController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/overview', authenticate, requirePermission('shop:view_overview'), shopController.getOverview.bind(shopController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/requests', authenticate, requirePermission('shop:approve_requests'), shopController.getRequests.bind(shopController));
|
||||
router.get('/requests/my', authenticate, shopController.getMyRequests.bind(shopController));
|
||||
router.get('/requests/:id', authenticate, shopController.getRequestById.bind(shopController));
|
||||
router.post('/requests', authenticate, requirePermission('shop:create_request'), shopController.createRequest.bind(shopController));
|
||||
router.patch('/requests/:id/status', authenticate, requirePermission('shop:approve_requests'), shopController.updateRequestStatus.bind(shopController));
|
||||
router.delete('/requests/:id', authenticate, requirePermission('shop:approve_requests'), shopController.deleteRequest.bind(shopController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linking requests to orders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.post('/requests/:id/link', authenticate, requirePermission('shop:link_orders'), shopController.linkToOrder.bind(shopController));
|
||||
router.delete('/requests/:id/link/:bestellungId', authenticate, requirePermission('shop:link_orders'), shopController.unlinkFromOrder.bind(shopController));
|
||||
|
||||
export default router;
|
||||
@@ -2,7 +2,7 @@ import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog Items (shop_artikel)
|
||||
// Catalog Items (ausruestung_artikel)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getItems(filters?: { kategorie?: string; aktiv?: boolean }) {
|
||||
@@ -20,14 +20,14 @@ async function getItems(filters?: { kategorie?: string; aktiv?: boolean }) {
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM shop_artikel ${where} ORDER BY kategorie, bezeichnung`,
|
||||
`SELECT * FROM ausruestung_artikel ${where} ORDER BY kategorie, bezeichnung`,
|
||||
params,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function getItemById(id: number) {
|
||||
const result = await pool.query('SELECT * FROM shop_artikel WHERE id = $1', [id]);
|
||||
const result = await pool.query('SELECT * FROM ausruestung_artikel WHERE id = $1', [id]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async function createItem(
|
||||
userId: string,
|
||||
) {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO shop_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von)
|
||||
`INSERT INTO ausruestung_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, true), $6)
|
||||
RETURNING *`,
|
||||
[data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.aktiv ?? true, userId],
|
||||
@@ -94,25 +94,25 @@ async function updateItem(
|
||||
|
||||
params.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE shop_artikel SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
|
||||
`UPDATE ausruestung_artikel SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
|
||||
params,
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function deleteItem(id: number) {
|
||||
await pool.query('DELETE FROM shop_artikel WHERE id = $1', [id]);
|
||||
await pool.query('DELETE FROM ausruestung_artikel WHERE id = $1', [id]);
|
||||
}
|
||||
|
||||
async function getCategories() {
|
||||
const result = await pool.query(
|
||||
'SELECT DISTINCT kategorie FROM shop_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie',
|
||||
'SELECT DISTINCT kategorie FROM ausruestung_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie',
|
||||
);
|
||||
return result.rows.map((r: { kategorie: string }) => r.kategorie);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests (shop_anfragen)
|
||||
// Requests (ausruestung_anfragen)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getRequests(filters?: { status?: string; anfrager_id?: string }) {
|
||||
@@ -133,8 +133,8 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
|
||||
`SELECT a.*,
|
||||
u.vorname || ' ' || u.nachname AS anfrager_name,
|
||||
u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name,
|
||||
(SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
||||
FROM shop_anfragen a
|
||||
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
||||
FROM ausruestung_anfragen a
|
||||
LEFT JOIN users u ON u.id = a.anfrager_id
|
||||
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
|
||||
${where}
|
||||
@@ -147,8 +147,8 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
|
||||
async function getMyRequests(userId: string) {
|
||||
const result = await pool.query(
|
||||
`SELECT a.*,
|
||||
(SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
||||
FROM shop_anfragen a
|
||||
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
||||
FROM ausruestung_anfragen a
|
||||
WHERE a.anfrager_id = $1
|
||||
ORDER BY a.erstellt_am DESC`,
|
||||
[userId],
|
||||
@@ -161,7 +161,7 @@ async function getRequestById(id: number) {
|
||||
`SELECT a.*,
|
||||
u.vorname || ' ' || u.nachname AS anfrager_name,
|
||||
u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name
|
||||
FROM shop_anfragen a
|
||||
FROM ausruestung_anfragen a
|
||||
LEFT JOIN users u ON u.id = a.anfrager_id
|
||||
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
|
||||
WHERE a.id = $1`,
|
||||
@@ -171,8 +171,8 @@ async function getRequestById(id: number) {
|
||||
|
||||
const positionen = await pool.query(
|
||||
`SELECT p.*, sa.bezeichnung AS artikel_bezeichnung, sa.kategorie AS artikel_kategorie
|
||||
FROM shop_anfrage_positionen p
|
||||
LEFT JOIN shop_artikel sa ON sa.id = p.artikel_id
|
||||
FROM ausruestung_anfrage_positionen p
|
||||
LEFT JOIN ausruestung_artikel sa ON sa.id = p.artikel_id
|
||||
WHERE p.anfrage_id = $1
|
||||
ORDER BY p.id`,
|
||||
[id],
|
||||
@@ -180,7 +180,7 @@ async function getRequestById(id: number) {
|
||||
|
||||
const bestellungen = await pool.query(
|
||||
`SELECT b.*
|
||||
FROM shop_anfrage_bestellung ab
|
||||
FROM ausruestung_anfrage_bestellung ab
|
||||
JOIN bestellungen b ON b.id = ab.bestellung_id
|
||||
WHERE ab.anfrage_id = $1`,
|
||||
[id],
|
||||
@@ -206,14 +206,14 @@ async function createRequest(
|
||||
const currentYear = new Date().getFullYear();
|
||||
const maxResult = await client.query(
|
||||
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
|
||||
FROM shop_anfragen
|
||||
FROM ausruestung_anfragen
|
||||
WHERE bestell_jahr = $1`,
|
||||
[currentYear],
|
||||
);
|
||||
const nextNr = maxResult.rows[0].next_nr;
|
||||
|
||||
const anfrageResult = await client.query(
|
||||
`INSERT INTO shop_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
|
||||
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *`,
|
||||
[userId, notizen || null, nextNr, currentYear],
|
||||
@@ -226,7 +226,7 @@ async function createRequest(
|
||||
// If artikel_id is provided, copy bezeichnung from catalog
|
||||
if (item.artikel_id) {
|
||||
const artikelResult = await client.query(
|
||||
'SELECT bezeichnung FROM shop_artikel WHERE id = $1',
|
||||
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
|
||||
[item.artikel_id],
|
||||
);
|
||||
if (artikelResult.rows.length > 0) {
|
||||
@@ -235,7 +235,7 @@ async function createRequest(
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO shop_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[anfrage.id, item.artikel_id || null, bezeichnung, item.menge, item.notizen || null],
|
||||
);
|
||||
@@ -245,7 +245,7 @@ async function createRequest(
|
||||
return getRequestById(anfrage.id);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('shopService.createRequest failed', { error });
|
||||
logger.error('ausruestungsanfrageService.createRequest failed', { error });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
@@ -259,7 +259,7 @@ async function updateRequestStatus(
|
||||
bearbeitetVon?: string,
|
||||
) {
|
||||
const result = await pool.query(
|
||||
`UPDATE shop_anfragen
|
||||
`UPDATE ausruestung_anfragen
|
||||
SET status = $1,
|
||||
admin_notizen = COALESCE($2, admin_notizen),
|
||||
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
||||
@@ -272,16 +272,16 @@ async function updateRequestStatus(
|
||||
}
|
||||
|
||||
async function deleteRequest(id: number) {
|
||||
await pool.query('DELETE FROM shop_anfragen WHERE id = $1', [id]);
|
||||
await pool.query('DELETE FROM ausruestung_anfragen WHERE id = $1', [id]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linking (shop_anfrage_bestellung)
|
||||
// Linking (ausruestung_anfrage_bestellung)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function linkToOrder(anfrageId: number, bestellungId: number) {
|
||||
await pool.query(
|
||||
`INSERT INTO shop_anfrage_bestellung (anfrage_id, bestellung_id)
|
||||
`INSERT INTO ausruestung_anfrage_bestellung (anfrage_id, bestellung_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[anfrageId, bestellungId],
|
||||
@@ -290,7 +290,7 @@ async function linkToOrder(anfrageId: number, bestellungId: number) {
|
||||
|
||||
async function unlinkFromOrder(anfrageId: number, bestellungId: number) {
|
||||
await pool.query(
|
||||
'DELETE FROM shop_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2',
|
||||
'DELETE FROM ausruestung_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2',
|
||||
[anfrageId, bestellungId],
|
||||
);
|
||||
}
|
||||
@@ -298,7 +298,7 @@ async function unlinkFromOrder(anfrageId: number, bestellungId: number) {
|
||||
async function getLinkedOrders(anfrageId: number) {
|
||||
const result = await pool.query(
|
||||
`SELECT b.*
|
||||
FROM shop_anfrage_bestellung ab
|
||||
FROM ausruestung_anfrage_bestellung ab
|
||||
JOIN bestellungen b ON b.id = ab.bestellung_id
|
||||
WHERE ab.anfrage_id = $1`,
|
||||
[anfrageId],
|
||||
@@ -315,8 +315,8 @@ async function getOverview() {
|
||||
`SELECT p.bezeichnung,
|
||||
SUM(p.menge)::int AS total_menge,
|
||||
COUNT(DISTINCT p.anfrage_id)::int AS anfrage_count
|
||||
FROM shop_anfrage_positionen p
|
||||
JOIN shop_anfragen a ON a.id = p.anfrage_id
|
||||
FROM ausruestung_anfrage_positionen p
|
||||
JOIN ausruestung_anfragen a ON a.id = p.anfrage_id
|
||||
WHERE a.status IN ('offen', 'genehmigt')
|
||||
GROUP BY p.bezeichnung
|
||||
ORDER BY total_menge DESC, p.bezeichnung`,
|
||||
@@ -327,10 +327,10 @@ async function getOverview() {
|
||||
COUNT(*) FILTER (WHERE status = 'offen')::int AS pending_count,
|
||||
COUNT(*) FILTER (WHERE status = 'genehmigt')::int AS approved_count,
|
||||
COALESCE(SUM(sub.total), 0)::int AS total_items
|
||||
FROM shop_anfragen a
|
||||
FROM ausruestung_anfragen a
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT SUM(p.menge) AS total
|
||||
FROM shop_anfrage_positionen p
|
||||
FROM ausruestung_anfrage_positionen p
|
||||
WHERE p.anfrage_id = a.id
|
||||
) sub ON true
|
||||
WHERE a.status IN ('offen', 'genehmigt')`,
|
||||
@@ -197,8 +197,8 @@ async function createOrder(data: { bezeichnung: string; lieferant_id?: number; b
|
||||
}
|
||||
}
|
||||
|
||||
await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId);
|
||||
await client.query('COMMIT');
|
||||
await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId);
|
||||
return order;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
|
||||
@@ -126,6 +126,45 @@ class CleanupService {
|
||||
logger.info(`Cleanup: deleted ${rowCount} equipment history entries older than ${olderThanDays} days`);
|
||||
return { count: rowCount ?? 0, deleted: true };
|
||||
}
|
||||
|
||||
async resetBestellungenSequence(confirm: boolean): Promise<CleanupResult> {
|
||||
if (!confirm) {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
|
||||
return { count: rows[0].count, deleted: false };
|
||||
}
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
|
||||
const count = rows[0].count;
|
||||
await pool.query('TRUNCATE bestellungen CASCADE');
|
||||
await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1');
|
||||
logger.info(`Cleanup: truncated bestellungen (${count} rows) and reset sequence`);
|
||||
return { count, deleted: true };
|
||||
}
|
||||
|
||||
async resetAusruestungAnfragenSequence(confirm: boolean): Promise<CleanupResult> {
|
||||
if (!confirm) {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
|
||||
return { count: rows[0].count, deleted: false };
|
||||
}
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
|
||||
const count = rows[0].count;
|
||||
await pool.query('TRUNCATE ausruestung_anfragen CASCADE');
|
||||
await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1');
|
||||
logger.info(`Cleanup: truncated ausruestung_anfragen (${count} rows) and reset sequence`);
|
||||
return { count, deleted: true };
|
||||
}
|
||||
|
||||
async resetIssuesSequence(confirm: boolean): Promise<CleanupResult> {
|
||||
if (!confirm) {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
|
||||
return { count: rows[0].count, deleted: false };
|
||||
}
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
|
||||
const count = rows[0].count;
|
||||
await pool.query('TRUNCATE issues CASCADE');
|
||||
await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1');
|
||||
logger.info(`Cleanup: truncated issues (${count} rows) and reset sequence`);
|
||||
return { count, deleted: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new CleanupService();
|
||||
|
||||
@@ -714,7 +714,8 @@ class EventsService {
|
||||
FROM users
|
||||
WHERE authentik_groups IS NOT NULL
|
||||
) g
|
||||
WHERE group_name != 'dashboard_admin'
|
||||
WHERE group_name LIKE 'dashboard_%'
|
||||
AND group_name != 'dashboard_admin'
|
||||
ORDER BY group_name`
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user