From f9f54b7e07445120dd3463c3ae9d47752f3ce472 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 24 Mar 2026 17:10:01 +0100 Subject: [PATCH] fix permissions --- backend/src/controllers/issue.controller.ts | 6 +- .../056_issues_widget_permission.sql | 20 +++ .../dashboard/IssueQuickAddWidget.tsx | 148 ++++++++++++++++++ frontend/src/components/dashboard/index.ts | 1 + frontend/src/constants/widgets.ts | 1 + frontend/src/pages/Dashboard.tsx | 9 ++ frontend/src/pages/Issues.tsx | 13 +- 7 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 backend/src/database/migrations/056_issues_widget_permission.sql create mode 100644 frontend/src/components/dashboard/IssueQuickAddWidget.tsx diff --git a/backend/src/controllers/issue.controller.ts b/backend/src/controllers/issue.controller.ts index 2f53e67..ab0622a 100644 --- a/backend/src/controllers/issue.controller.ts +++ b/backend/src/controllers/issue.controller.ts @@ -10,7 +10,7 @@ class IssueController { try { const userId = req.user!.id; const groups: string[] = (req.user as any).groups || []; - const canViewAll = permissionService.hasPermission(groups, 'issues:view_all'); + const canViewAll = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'issues:view_all'); // Parse filter query params const filters: { @@ -60,7 +60,7 @@ class IssueController { } const userId = req.user!.id; const groups: string[] = (req.user as any).groups || []; - const canViewAll = permissionService.hasPermission(groups, 'issues:view_all'); + const canViewAll = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'issues:view_all'); if (!canViewAll && issue.erstellt_von !== userId && issue.zugewiesen_an !== userId) { res.status(403).json({ success: false, message: 'Kein Zugriff' }); return; @@ -227,7 +227,7 @@ class IssueController { } const userId = req.user!.id; const groups: string[] = (req.user as any).groups || []; - const canViewAll = permissionService.hasPermission(groups, 'issues:view_all'); + const canViewAll = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'issues:view_all'); if (!canViewAll && issue.erstellt_von !== userId && issue.zugewiesen_an !== userId) { res.status(403).json({ success: false, message: 'Kein Zugriff' }); return; diff --git a/backend/src/database/migrations/056_issues_widget_permission.sql b/backend/src/database/migrations/056_issues_widget_permission.sql new file mode 100644 index 0000000..3b7538c --- /dev/null +++ b/backend/src/database/migrations/056_issues_widget_permission.sql @@ -0,0 +1,20 @@ +-- Migration 056: Add issues:widget permission +-- Gated permission for the Issue Quick Add dashboard widget. +-- Granted to all groups that currently have issues:create. + +-- 1. Insert the new permission +INSERT INTO permissions (id, feature_group_id, label, description, sort_order) +VALUES ('issues:widget', 'issues', 'Widget', 'Issue-Schnelleingabe auf dem Dashboard', 8) +ON CONFLICT (id) DO NOTHING; + +-- 2. Grant to every group that already has issues:create +INSERT INTO group_permissions (authentik_group, permission_id) + SELECT authentik_group, 'issues:widget' + FROM group_permissions + WHERE permission_id = 'issues:create' +ON CONFLICT DO NOTHING; + +-- 3. Add dependency: issues:widget requires issues:create +UPDATE app_settings +SET value = value || '{"issues:widget": ["issues:create"]}'::jsonb +WHERE key = 'permission_deps'; diff --git a/frontend/src/components/dashboard/IssueQuickAddWidget.tsx b/frontend/src/components/dashboard/IssueQuickAddWidget.tsx new file mode 100644 index 0000000..c629a9c --- /dev/null +++ b/frontend/src/components/dashboard/IssueQuickAddWidget.tsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { + Card, + CardContent, + Typography, + Box, + TextField, + Button, + MenuItem, + Select, + FormControl, + InputLabel, + Skeleton, + SelectChangeEvent, +} from '@mui/material'; +import { BugReport } from '@mui/icons-material'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { issuesApi } from '../../services/issues'; +import { useNotification } from '../../contexts/NotificationContext'; + +const PRIO_OPTIONS = [ + { value: 'niedrig', label: 'Niedrig' }, + { value: 'mittel', label: 'Mittel' }, + { value: 'hoch', label: 'Hoch' }, +]; + +const IssueQuickAddWidget: React.FC = () => { + const [titel, setTitel] = useState(''); + const [typId, setTypId] = useState(''); + const [prioritaet, setPrioritaet] = useState('mittel'); + const { showSuccess, showError } = useNotification(); + const queryClient = useQueryClient(); + + const { data: types = [], isLoading: typesLoading } = useQuery({ + queryKey: ['issue-types'], + queryFn: issuesApi.getTypes, + staleTime: 10 * 60 * 1000, + }); + + const activeTypes = types.filter((t) => t.aktiv); + const defaultTypId = activeTypes[0]?.id; + + const mutation = useMutation({ + mutationFn: () => + issuesApi.createIssue({ + titel: titel.trim(), + typ_id: typId !== '' ? (typId as number) : defaultTypId, + prioritaet, + }), + onSuccess: () => { + showSuccess('Issue erstellt'); + setTitel(''); + setTypId(''); + setPrioritaet('mittel'); + queryClient.invalidateQueries({ queryKey: ['issues'] }); + }, + onError: () => { + showError('Issue konnte nicht erstellt werden'); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!titel.trim()) return; + mutation.mutate(); + }; + + return ( + + + + + Issue melden + + + {typesLoading ? ( + + + + + + ) : ( + + setTitel(e.target.value)} + inputProps={{ maxLength: 255 }} + autoComplete="off" + /> + + + Typ + + + + + Priorität + + + + + + )} + + + ); +}; + +export default IssueQuickAddWidget; diff --git a/frontend/src/components/dashboard/index.ts b/frontend/src/components/dashboard/index.ts index 513c5ad..3ca16c4 100644 --- a/frontend/src/components/dashboard/index.ts +++ b/frontend/src/components/dashboard/index.ts @@ -20,3 +20,4 @@ export { default as LinksWidget } from './LinksWidget'; export { default as WidgetGroup } from './WidgetGroup'; export { default as BestellungenWidget } from './BestellungenWidget'; export { default as AusruestungsanfrageWidget } from './AusruestungsanfrageWidget'; +export { default as IssueQuickAddWidget } from './IssueQuickAddWidget'; diff --git a/frontend/src/constants/widgets.ts b/frontend/src/constants/widgets.ts index a2d178f..5dc0e2b 100644 --- a/frontend/src/constants/widgets.ts +++ b/frontend/src/constants/widgets.ts @@ -15,6 +15,7 @@ export const WIDGETS = [ { key: 'links', label: 'Links', defaultVisible: true }, { key: 'bestellungen', label: 'Bestellungen', defaultVisible: true }, { key: 'ausruestungsanfragen', label: 'Interne Bestellungen', defaultVisible: true }, + { key: 'issueQuickAdd', label: 'Issue melden', defaultVisible: true }, ] as const; export type WidgetKey = typeof WIDGETS[number]['key']; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 49f106a..8497c5e 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -30,6 +30,7 @@ import BannerWidget from '../components/dashboard/BannerWidget'; import WidgetGroup from '../components/dashboard/WidgetGroup'; import BestellungenWidget from '../components/dashboard/BestellungenWidget'; import AusruestungsanfrageWidget from '../components/dashboard/AusruestungsanfrageWidget'; +import IssueQuickAddWidget from '../components/dashboard/IssueQuickAddWidget'; import { preferencesApi } from '../services/settings'; import { configApi } from '../services/config'; import { WidgetKey } from '../constants/widgets'; @@ -218,6 +219,14 @@ function Dashboard() { )} + + {hasPermission('issues:widget') && widgetVisible('issueQuickAdd') && ( + + + + + + )} {/* Information Group */} diff --git a/frontend/src/pages/Issues.tsx b/frontend/src/pages/Issues.tsx index bba18b8..87970ab 100644 --- a/frontend/src/pages/Issues.tsx +++ b/frontend/src/pages/Issues.tsx @@ -27,6 +27,9 @@ import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayl const formatDate = (iso?: string) => iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'; +const formatIssueId = (issue: Issue) => + `${new Date(issue.created_at).getFullYear()}/${issue.id}`; + const STATUS_COLORS: Record = { offen: 'info', in_bearbeitung: 'warning', @@ -217,7 +220,7 @@ function IssueRow({ sx={{ cursor: 'pointer', '& > *': { borderBottom: expanded ? 'unset' : undefined } }} onClick={() => setExpanded(!expanded)} > - #{issue.id} + {formatIssueId(issue)} {getTypIcon(issue.typ_icon, issue.typ_farbe)} @@ -341,7 +344,7 @@ function IssueRow({ {/* Reopen Dialog */} setReopenOpen(false)} maxWidth="sm" fullWidth> Issue wiedereröffnen - + setCreateOpen(false)} maxWidth="sm" fullWidth> Neue Kategorie erstellen - + setCreateData({ ...createData, name: e.target.value })} autoFocus /> Übergeordnete Kategorie @@ -876,7 +879,7 @@ export default function Issues() { {/* Create Issue Dialog */} setCreateOpen(false)} maxWidth="sm" fullWidth> Neues Issue erstellen - + {/* FAB */} - {canCreate && ( + {canCreate && activeTab === 'mine' && (