From c2ebc8daa0ebc0b4d156e3e22d687ed51b99d058 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 2 Oct 2026 13:31:08 +0200 Subject: [PATCH] feat(module-grants): Proxmox, Handelsware und DKV mit Freigabestufe Verwalten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Proxmox-Schreibwege und Handelsware-Einstellungen auf @ModuleManage umgestellt - DKV-Fleet: ganze Klasse Verwalten-Stufe, Benutzen allein bleibt ohne Zugriff - Metadaten-Test belegt umgestellte und bewusst Administratoren vorbehaltene Handler - Webseiten (Proxmox, Handelsware, Widget) folgen canManage, DKV-Zugriffsseite erklärt die Stufe Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/api/src/dkv/dkv.controller.ts | 29 +++-- .../handelsware-datev.controller.spec.ts | 13 ++- .../handelsware-datev.controller.ts | 9 +- .../module-manage-handlers.spec.ts | 100 ++++++++++++++++++ .../api/src/proxmox/proxmox-client.service.ts | 4 +- apps/api/src/proxmox/proxmox.controller.ts | 24 ++--- .../components/ImportTab.tsx | 8 +- .../handelsware-datev.test.tsx | 27 ++++- .../modules/handelsware-datev/page.tsx | 14 +-- .../kantine-datev/kantine-datev.test.tsx | 2 +- .../proxmox/components/ServerCard.test.tsx | 8 +- .../modules/proxmox/components/ServerCard.tsx | 14 +-- .../src/app/(portal)/modules/proxmox/page.tsx | 17 +-- .../proxmox/proxmox-page-roles.test.tsx | 48 ++++++++- .../settings/components/ServerForm.test.tsx | 20 ++-- .../settings/components/ServerForm.tsx | 28 ++--- .../modules/proxmox/settings/page.tsx | 27 ++--- .../dashboard/widgets/proxmox-widget.tsx | 8 +- .../modules/module-access-gate.test.tsx | 49 +++++++++ .../components/modules/module-access-gate.tsx | 30 +++++- apps/web/src/lib/module-access-actions.ts | 48 ++++++--- apps/web/src/messages/de.json | 7 +- apps/web/src/messages/en.json | 7 +- 23 files changed, 403 insertions(+), 138 deletions(-) create mode 100644 apps/api/src/module-registry/module-manage-handlers.spec.ts diff --git a/apps/api/src/dkv/dkv.controller.ts b/apps/api/src/dkv/dkv.controller.ts index cb81dd5..3c40eb5 100644 --- a/apps/api/src/dkv/dkv.controller.ts +++ b/apps/api/src/dkv/dkv.controller.ts @@ -15,8 +15,7 @@ import { UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { Role } from '@prisma/client'; -import { Roles } from '../auth/decorators/roles.decorator'; +import { ModuleManage } from '../module-registry/module.guard'; import type { AuthenticatedRequest, UploadedFileLike, @@ -29,11 +28,17 @@ import { DkvHistoryQueryDto } from './dto/dkv-history.dto'; import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto'; /** - * DkvController — all /dkv/* routes, ADMIN-only (V4). + * DkvController — all /dkv/* routes, manager level (V4, 261002-icv). * - * Every handler carries @Roles(Role.ADMIN, Role.SUPER_ADMIN). - * Global JwtAuthGuard enforces JWT authentication; RolesGuard enforces the - * @Roles decorator. No route is publicly accessible. + * The whole module is Verwalten-level: `@ModuleManage('dkv-fleet')` on the + * class replaces the former per-handler @Roles(ADMIN, SUPER_ADMIN). Access is + * therefore limited to administrators and to users with the grant level + * "Verwalten" (MANAGE) on the dkv-fleet module. Users with only "Benutzen" + * (USE) keep getting 403 exactly as before — nothing was widened. The class + * guard additionally requires the dkv-fleet module to be active for the + * tenant (the web page already required that). + * Global JwtAuthGuard enforces JWT authentication; ModuleGuard enforces the + * grant level. No route is publicly accessible. * * Tenant extraction: `req.tenantId` set by TenantGuard (runs after auth guards). * All operations are scoped to the authenticated tenant's data. @@ -52,6 +57,7 @@ import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto'; * POST /dkv/vehicles/import — bulk-import from CSV upload */ @Controller('dkv') +@ModuleManage('dkv-fleet') export class DkvController { constructor( private readonly dkvService: DkvService, @@ -62,7 +68,6 @@ export class DkvController { /** GET /dkv/config — returns module config with username + hasPassword. 404 when not yet configured. */ @Get('config') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async getConfig(@Req() req: AuthenticatedRequest) { const tenantId = this._requireTenant(req); const config = await this.dkvService.getConfigForApi(tenantId); @@ -79,7 +84,6 @@ export class DkvController { * or stops the cron job if isActive is false. */ @Put('config') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async saveConfig(@Req() req: AuthenticatedRequest, @Body() dto: DkvConfigDto) { const tenantId = this._requireTenant(req); const result = await this.dkvService.saveConfig(tenantId, dto); @@ -98,7 +102,6 @@ export class DkvController { /** POST /dkv/check-now — immediately run the inbox processing pipeline. */ @Post('check-now') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async checkNow(@Req() req: AuthenticatedRequest) { const tenantId = this._requireTenant(req); return this.dkvService.checkNow(tenantId); @@ -109,7 +112,6 @@ export class DkvController { * Used by the InboxConfigForm "Verbindung testen" button before saving. */ @Post('test-connection') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async testConnection(@Req() req: AuthenticatedRequest, @Body() dto: DkvConfigDto) { const tenantId = this._requireTenant(req); return this.dkvService.testConnection(tenantId, dto); @@ -122,7 +124,6 @@ export class DkvController { * T-07-06: pagination parameters validated by DkvHistoryQueryDto. */ @Get('history') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async getHistory(@Req() req: AuthenticatedRequest, @Query() query: DkvHistoryQueryDto) { const tenantId = this._requireTenant(req); const page = query.page ?? 1; @@ -140,7 +141,6 @@ export class DkvController { * containing path separators or non-whitelisted characters is rejected. */ @Get('exports/:filename') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async downloadExport( @Req() req: AuthenticatedRequest, @Param('filename') filename: string, @@ -168,7 +168,6 @@ export class DkvController { /** GET /dkv/vehicles — list all vehicle master records for this tenant. */ @Get('vehicles') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async listVehicles(@Req() req: AuthenticatedRequest) { const tenantId = this._requireTenant(req); return this.dkvService.listVehicles(tenantId); @@ -176,7 +175,6 @@ export class DkvController { /** POST /dkv/vehicles — create a new vehicle master record. */ @Post('vehicles') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async createVehicle(@Req() req: AuthenticatedRequest, @Body() dto: CreateVehicleDto) { const tenantId = this._requireTenant(req); return this.dkvService.createVehicle(tenantId, dto); @@ -184,7 +182,6 @@ export class DkvController { /** PUT /dkv/vehicles/:id — update an existing vehicle master record. */ @Put('vehicles/:id') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async updateVehicle( @Req() req: AuthenticatedRequest, @Param('id') id: string, @@ -196,7 +193,6 @@ export class DkvController { /** DELETE /dkv/vehicles/:id — delete a vehicle master record. */ @Delete('vehicles/:id') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) async deleteVehicle(@Req() req: AuthenticatedRequest, @Param('id') id: string) { const tenantId = this._requireTenant(req); return this.dkvService.deleteVehicle(tenantId, id); @@ -213,7 +209,6 @@ export class DkvController { * The controller reads `file.buffer.toString('utf-8')` and passes to DkvService. */ @Post('vehicles/import') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB — generous for any realistic vehicle list (WR-05) })) diff --git a/apps/api/src/handelsware-datev/handelsware-datev.controller.spec.ts b/apps/api/src/handelsware-datev/handelsware-datev.controller.spec.ts index 07c0dd1..b557c6c 100644 --- a/apps/api/src/handelsware-datev/handelsware-datev.controller.spec.ts +++ b/apps/api/src/handelsware-datev/handelsware-datev.controller.spec.ts @@ -1,9 +1,8 @@ import 'reflect-metadata'; import { BadRequestException, ForbiddenException, ValidationPipe } from '@nestjs/common'; -import { Role } from '@prisma/client'; import { describe, expect, it, vi } from 'vitest'; import { ROLES_KEY } from '../auth/decorators/roles.decorator'; -import { MODULE_SLUG_KEY } from '../module-registry/module.guard'; +import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY } from '../module-registry/module.guard'; import { HandelswareAccountDto } from './dto/handelsware-account.dto'; import { HandelswareSettingsDto } from './dto/handelsware-settings.dto'; import { HandelswareDatevController, parseNewAccountsField } from './handelsware-datev.controller'; @@ -36,11 +35,10 @@ describe('HandelswareDatevController — Metadaten', () => { ); }); - it('PUT settings verlangt ADMIN/SUPER_ADMIN, alles andere keine Routen-Rolle', () => { - expect(Reflect.getMetadata(ROLES_KEY, proto.saveSettings)).toEqual([ - Role.ADMIN, - Role.SUPER_ADMIN, - ]); + it('PUT settings verlangt die Freigabestufe Verwalten, alles andere keine Routen-Rolle und kein Verwalten (261002-icv)', () => { + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto.saveSettings)).toBe(true); + expect(Reflect.getMetadata(MODULE_SLUG_KEY, proto.saveSettings)).toBe('handelsware-datev'); + expect(Reflect.getMetadata(ROLES_KEY, proto.saveSettings)).toBeUndefined(); for (const name of [ 'getSettings', 'preview', @@ -53,6 +51,7 @@ describe('HandelswareDatevController — Metadaten', () => { 'deleteAccount', ]) { expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined(); + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBeUndefined(); } }); diff --git a/apps/api/src/handelsware-datev/handelsware-datev.controller.ts b/apps/api/src/handelsware-datev/handelsware-datev.controller.ts index c0d5d5b..d744ce1 100644 --- a/apps/api/src/handelsware-datev/handelsware-datev.controller.ts +++ b/apps/api/src/handelsware-datev/handelsware-datev.controller.ts @@ -13,11 +13,9 @@ import { UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { Role } from '@prisma/client'; import { decodeUploadFilename } from '../accounting/decode-upload-filename'; -import { Roles } from '../auth/decorators/roles.decorator'; import type { AuthenticatedRequest, UploadedFileLike } from '../auth/types/auth-user'; -import { UseModule } from '../module-registry/module.guard'; +import { ModuleManage, UseModule } from '../module-registry/module.guard'; import { HandelswareAccountDto } from './dto/handelsware-account.dto'; import { HandelswareSettingsDto } from './dto/handelsware-settings.dto'; import { HandelswareDatevService } from './handelsware-datev.service'; @@ -61,7 +59,8 @@ export function parseNewAccountsField(raw: unknown): { name: string; gegenkonto: /** * `@UseModule('handelsware-datev')` auf Klassenebene — Aktivierung UND Freigabe. * `tenantId` kommt ausschliesslich aus `req.tenantId`. Die Einstellungen aendern - * nur Administratoren (T-FM5-02); die Kontenliste pflegen alle Benutzer mit + * Administratoren und Benutzer mit der Freigabestufe Verwalten + * (`@ModuleManage`, 261002-icv; T-FM5-02); die Kontenliste pflegen alle Benutzer mit * Modulzugriff. * * REIHENFOLGE: alle statischen Routen (`accounts`, `accounts/export-csv`, @@ -88,7 +87,7 @@ export class HandelswareDatevController { } @Put('settings') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('handelsware-datev') async saveSettings(@Req() req: AuthenticatedRequest, @Body() dto: HandelswareSettingsDto) { return this.service.saveSettings(this.requireTenantId(req), dto); } diff --git a/apps/api/src/module-registry/module-manage-handlers.spec.ts b/apps/api/src/module-registry/module-manage-handlers.spec.ts new file mode 100644 index 0000000..45586a0 --- /dev/null +++ b/apps/api/src/module-registry/module-manage-handlers.spec.ts @@ -0,0 +1,100 @@ +import 'reflect-metadata'; +import { GUARDS_METADATA } from '@nestjs/common/constants'; +import { Role } from '@prisma/client'; +import { describe, expect, it } from 'vitest'; +import { ROLES_KEY } from '../auth/decorators/roles.decorator'; +import { DkvController } from '../dkv/dkv.controller'; +import { ModuleGrantsController } from '../groups/module-grants.controller'; +import { HandelswareDatevController } from '../handelsware-datev/handelsware-datev.controller'; +import { KantineDatevController } from '../kantine-datev/kantine-datev.controller'; +import { ProxmoxController } from '../proxmox/proxmox.controller'; +import { TendersController } from '../tenders/tenders.controller'; +import { ModuleRegistryController } from './module-registry.controller'; +import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY, ModuleGuard } from './module.guard'; + +/** + * Metadaten-Beweis für die Freigabestufe Verwalten (261002-icv, L-04/L-09): + * welche Handler auf `@ModuleManage` umgestellt wurden und welche bewusst + * Administratoren vorbehalten bleiben (T-icv-01/06/07/08). Reine Metadaten — + * kein Nest-Start, keine Datenbank. + */ + +const ADMIN_ONLY = [Role.ADMIN, Role.SUPER_ADMIN]; + +function methodsOf(controller: { prototype: object }): string[] { + return Object.getOwnPropertyNames(controller.prototype).filter( + (name) => name !== 'constructor' && typeof (controller.prototype as any)[name] === 'function', + ); +} + +function handler(controller: { prototype: object }, name: string) { + return (controller.prototype as any)[name]; +} + +function expectManage(controller: { prototype: object }, name: string, slug: string) { + const fn = handler(controller, name); + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn), `${name} MODULE_MANAGE_KEY`).toBe(true); + expect(Reflect.getMetadata(MODULE_SLUG_KEY, fn), `${name} MODULE_SLUG_KEY`).toBe(slug); + expect(Reflect.getMetadata(GUARDS_METADATA, fn), `${name} guards`).toContain(ModuleGuard); + expect(Reflect.getMetadata(ROLES_KEY, fn), `${name} ROLES_KEY`).toBeUndefined(); +} + +function expectAdminOnly(controller: { prototype: object }, name: string) { + const fn = handler(controller, name); + expect(Reflect.getMetadata(ROLES_KEY, fn), `${name} ROLES_KEY`).toEqual(ADMIN_ONLY); + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn), `${name} MODULE_MANAGE_KEY`).toBeUndefined(); +} + +describe('Umgestellte Handler (Verwalten)', () => { + it('DkvController: ganze Klasse Verwalten, kein Handler trägt @Roles', () => { + expect(Reflect.getMetadata(MODULE_SLUG_KEY, DkvController)).toBe('dkv-fleet'); + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, DkvController)).toBe(true); + expect(Reflect.getMetadata(GUARDS_METADATA, DkvController)).toContain(ModuleGuard); + const names = methodsOf(DkvController).filter((n) => Reflect.hasMetadata('path', handler(DkvController, n))); + expect(names.length).toBe(11); + for (const name of names) { + expect(Reflect.getMetadata(ROLES_KEY, handler(DkvController, name)), name).toBeUndefined(); + } + }); + + it.each(['create', 'update', 'remove', 'poll', 'test', 'testDraft'])( + 'ProxmoxController.%s verlangt Verwalten für proxmox', + (name) => { + expectManage(ProxmoxController, name, 'proxmox'); + }, + ); + + it('ProxmoxController.list bleibt auf Benutzen-Ebene', () => { + const fn = handler(ProxmoxController, 'list'); + expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn)).toBeUndefined(); + expect(Reflect.getMetadata(ROLES_KEY, fn)).toBeUndefined(); + }); + + it('KantineDatevController.saveSettings und HandelswareDatevController.saveSettings verlangen Verwalten', () => { + expectManage(KantineDatevController, 'saveSettings', 'kantine-datev'); + expectManage(HandelswareDatevController, 'saveSettings', 'handelsware-datev'); + }); +}); + +describe('Bewusst nur für Administratoren (T-icv-01, T-icv-07)', () => { + it.each(['getSourceConfig', 'saveSourceConfig', 'pollNow'])( + 'TendersController.%s bleibt @Roles(ADMIN, SUPER_ADMIN)', + (name) => { + expectAdminOnly(TendersController, name); + }, + ); + + it.each(['matrix', 'userAccess', 'create', 'remove'])( + 'ModuleGrantsController.%s bleibt @Roles(ADMIN, SUPER_ADMIN)', + (name) => { + expectAdminOnly(ModuleGrantsController, name); + }, + ); + + it.each(['activate', 'deactivate'])( + 'ModuleRegistryController.%s bleibt @Roles(ADMIN, SUPER_ADMIN)', + (name) => { + expectAdminOnly(ModuleRegistryController, name); + }, + ); +}); diff --git a/apps/api/src/proxmox/proxmox-client.service.ts b/apps/api/src/proxmox/proxmox-client.service.ts index 39fe02c..f469f9b 100644 --- a/apps/api/src/proxmox/proxmox-client.service.ts +++ b/apps/api/src/proxmox/proxmox-client.service.ts @@ -30,7 +30,9 @@ import type { ProxmoxErrorKind } from './proxmox.types'; * Keine SSRF-Adresspruefung wie `isPublicHttpUrl`: Proxmox-Server stehen * per Definition im privaten Netz, eine solche Pruefung wuerde jede reale * Adresse blockieren (T-DHH-02). Die Absicherung ist stattdessen, dass nur - * ein Administrator (`@Roles(ADMIN, SUPER_ADMIN)`) Adressen eintragen darf + * ein Administrator oder ein Benutzer, dem der Administrator ausdruecklich + * die Freigabestufe Verwalten fuer das Proxmox-Modul gegeben hat + * (`@ModuleManage('proxmox')`, 261002-icv), Adressen eintragen darf * — siehe Bedrohungsmodell T-DHH-02 im Plan. */ diff --git a/apps/api/src/proxmox/proxmox.controller.ts b/apps/api/src/proxmox/proxmox.controller.ts index dcd04b2..591b277 100644 --- a/apps/api/src/proxmox/proxmox.controller.ts +++ b/apps/api/src/proxmox/proxmox.controller.ts @@ -9,10 +9,8 @@ import { Put, Req, } from '@nestjs/common'; -import { Role } from '@prisma/client'; -import { Roles } from '../auth/decorators/roles.decorator'; import type { AuthenticatedRequest } from '../auth/types/auth-user'; -import { UseModule } from '../module-registry/module.guard'; +import { ModuleManage, UseModule } from '../module-registry/module.guard'; import { CreateProxmoxServerDto, TestProxmoxServerDto, @@ -26,9 +24,11 @@ import { ProxmoxService } from './proxmox.service'; * `domaincheck.controller.ts`) — Aktivierung UND Freigabe. `tenantId` kommt * ausschliesslich aus `req.tenantId` (gesetzt vom `TenantGuard`), nie aus * Body oder Query. Lesen (`GET servers`) steht jedem Benutzer mit - * Modulzugriff offen; Schreiben (`POST servers`, `POST servers/test`, - * `POST servers/:id/poll`, `POST servers/:id/test`) zusaetzlich - * `@Roles(ADMIN, SUPER_ADMIN)` (T-DHH-05). `servers/test` (statisch, zwei + * Modulzugriff offen; Schreiben (`POST servers`, `PUT`/`DELETE servers/:id`, + * `POST servers/test`, `POST servers/:id/poll`, `POST servers/:id/test`) + * zusaetzlich `@ModuleManage('proxmox')` — Administratoren und Benutzer mit + * der Freigabestufe Verwalten (261002-icv, vorher `@Roles(ADMIN, + * SUPER_ADMIN)`; T-DHH-05). `servers/test` (statisch, zwei * Segmente) und `servers/:id/test` (drei Segmente) ueberschneiden sich * nicht — beide POST, aber unterschiedliche Segmentzahl, deshalb keine * Reihenfolge-Abhaengigkeit (anders als `GET :id` vs. statische Routen). @@ -55,7 +55,7 @@ export class ProxmoxController { } @Post('servers') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateProxmoxServerDto) { const tenantId = this.requireTenantId(req); const created = await this.proxmoxService.createServer(tenantId, dto); @@ -65,7 +65,7 @@ export class ProxmoxController { } @Put('servers/:id') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async update( @Req() req: AuthenticatedRequest, @Param('id') id: string, @@ -78,7 +78,7 @@ export class ProxmoxController { } @Delete('servers/:id') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async remove(@Req() req: AuthenticatedRequest, @Param('id') id: string) { const tenantId = this.requireTenantId(req); const deleted = await this.proxmoxService.deleteServer(tenantId, id); @@ -87,7 +87,7 @@ export class ProxmoxController { } @Post('servers/:id/poll') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async poll(@Req() req: AuthenticatedRequest, @Param('id') id: string) { return this.proxmoxService.pollServer(this.requireTenantId(req), id); } @@ -101,7 +101,7 @@ export class ProxmoxController { * OHNE den Zwischenlagerstand zu ueberschreiben. */ @Post('servers/:id/test') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async test( @Req() req: AuthenticatedRequest, @Param('id') id: string, @@ -115,7 +115,7 @@ export class ProxmoxController { * noch keinen gespeicherten Server, `dto` ist deshalb die einzige Quelle. */ @Post('servers/test') - @Roles(Role.ADMIN, Role.SUPER_ADMIN) + @ModuleManage('proxmox') async testDraft(@Req() req: AuthenticatedRequest, @Body() dto: TestProxmoxServerDto) { this.requireTenantId(req); return this.proxmoxService.testDraftConnection(dto); diff --git a/apps/web/src/app/(portal)/modules/handelsware-datev/components/ImportTab.tsx b/apps/web/src/app/(portal)/modules/handelsware-datev/components/ImportTab.tsx index 5f44111..390988b 100644 --- a/apps/web/src/app/(portal)/modules/handelsware-datev/components/ImportTab.tsx +++ b/apps/web/src/app/(portal)/modules/handelsware-datev/components/ImportTab.tsx @@ -41,12 +41,12 @@ type Message = { kind: 'error' | 'ok'; text: string; code?: string | null }; */ export function ImportTab({ settings, - isAdmin, + canManage, onOpenSettings, onAccountsChanged, }: { settings: HandelswareSettings | null; - isAdmin: boolean; + canManage: boolean; onOpenSettings: () => void; onAccountsChanged: () => void; }) { @@ -154,8 +154,8 @@ export function ImportTab({
{blockedBySettings && (
-

{isAdmin ? t('notConfigured.admin') : t('notConfigured.user')}

- {isAdmin && ( +

{canManage ? t('notConfigured.admin') : t('notConfigured.user')}

+ {canManage && (
@@ -269,7 +269,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro placeholder={savedServer ? t('settings.secretUnchangedPlaceholder') : ''} className={inputCls} value={form.tokenSecret} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('tokenSecret', e.target.value)} />
@@ -286,7 +286,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro placeholder="admin@pam" className={inputCls} value={form.username} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('username', e.target.value)} /> @@ -300,7 +300,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro placeholder={savedServer ? t('settings.secretUnchangedPlaceholder') : ''} className={inputCls} value={form.password} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('password', e.target.value)} /> @@ -318,7 +318,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro max={1440} className={inputCls} value={form.pollIntervalMin} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('pollIntervalMin', e.target.value)} /> @@ -329,7 +329,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro id="proxmox-tls-reject" type="checkbox" checked={form.tlsRejectUnauthorized} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('tlsRejectUnauthorized', e.target.checked)} /> {t('settings.tlsRejectLabel')} @@ -343,7 +343,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro id="proxmox-active" type="checkbox" checked={form.isActive} - disabled={!isAdmin} + disabled={!canManage} onChange={(e) => update('isActive', e.target.checked)} /> {t('settings.activeLabel')} @@ -362,7 +362,7 @@ export function ServerForm({ server, isAdmin, onSaved, onCancel }: ServerFormPro

)} - {isAdmin && ( + {canManage && (