feat(02-02): user CRUD API + admin page, tenant CRUD API + admin page

- Create UserController with GET/POST/PATCH/DELETE endpoints at /users
  - ADMIN sees own-tenant users only; SUPER_ADMIN sees all (T-02-10)
  - ADMIN cannot escalate to SUPER_ADMIN role (T-02-08)
  - ADMIN cannot delete self or cross-tenant users
- Create TenantController with GET/POST/PATCH/DELETE at /tenants
  - SUPER_ADMIN-only access (D-10)
  - Tenant deletion blocked if active users exist (T-02-09)
- Create CreateUserDto, UpdateUserDto, CreateTenantDto with class-validator
- Create admin/users page with user table, create/edit/delete modals
- Create admin/tenants page with tenant table, create/edit/deactivate (SUPER_ADMIN only)
- Add admin section to sidebar: Verwaltung > Benutzer + Mandanten
  - Verwaltung visible for ADMIN/SUPER_ADMIN; Tenants link SUPER_ADMIN only
- Install @nestjs/mapped-types for PartialType DTO pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 13:38:54 +02:00
parent e7b2a70fc9
commit bfb04eac66
12 changed files with 1240 additions and 0 deletions
@@ -3,6 +3,7 @@
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { useSidebarStore } from '@/lib/stores/sidebar-store';
import { useAuthStore } from '@/lib/stores/auth-store';
import { SidebarFooter } from '@/components/layout/sidebar-footer';
export function Sidebar() {
@@ -10,6 +11,11 @@ export function Sidebar() {
const tCommon = useTranslations('common');
const { isCollapsed, isMobileOpen, toggle, setMobileOpen } = useSidebarStore();
const [categoriesOpen, setCategoriesOpen] = useState(false);
const user = useAuthStore((s) => s.user);
// Admin section visibility based on role
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
const isSuperAdmin = user?.role === 'SUPER_ADMIN';
const sidebarContent = (
<div className="flex h-full flex-col bg-sidebar">
@@ -100,6 +106,74 @@ export function Sidebar() {
)}
</div>
)}
{/* Admin section -- Verwaltung (D-12) */}
{isAdmin && (
<div className="mt-6">
{!isCollapsed && (
<div className="px-2 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{t('admin')}
</div>
)}
<ul className="flex flex-col gap-1">
{/* Users link -- ADMIN and SUPER_ADMIN */}
<li>
<a
href="/admin/users"
className="flex items-center gap-3 rounded-md px-2 py-2 text-sm text-sidebar-foreground hover:bg-muted transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
{!isCollapsed && <span className="truncate">{t('users')}</span>}
</a>
</li>
{/* Tenants link -- SUPER_ADMIN only (D-10) */}
{isSuperAdmin && (
<li>
<a
href="/admin/tenants"
className="flex items-center gap-3 rounded-md px-2 py-2 text-sm text-sidebar-foreground hover:bg-muted transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0"
>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
{!isCollapsed && (
<span className="truncate">{t('tenants')}</span>
)}
</a>
</li>
)}
</ul>
</div>
)}
</nav>
{/* Collapse/Expand toggle */}