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 && (