Add role control
This commit is contained in:
+6
@@ -85,10 +85,16 @@
|
||||
<!-- Reimbursement mode: receipt file input -->
|
||||
<ng-container *ngIf="mode === 'reimbursement'">
|
||||
<label class="flex flex-col gap-1 md:col-span-2">Receipt (optional)
|
||||
<!--
|
||||
Stop the native 'cancel' DOM event (fired when the OS file picker is dismissed)
|
||||
from bubbling up to the host, where it would collide with this component's
|
||||
@Output() cancel and wrongly close the dialog. See Angular issues #50556 / #13997.
|
||||
-->
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,application/pdf"
|
||||
(change)="onFileSelected($event)"
|
||||
(cancel)="$event.stopPropagation()"
|
||||
class="block w-full text-sm text-gray-700 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:text-sm file:bg-gray-100 hover:file:bg-gray-200" />
|
||||
</label>
|
||||
</ng-container>
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<div class="k-p-4">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-mb-4">
|
||||
<h2 class="k-m-0">Role Permissions</h2>
|
||||
<button kendoButton themeColor="primary"
|
||||
[disabled]="!selectedRole || isSuperAdminSelected || isSaving"
|
||||
(click)="save()">
|
||||
{{ isSaving ? 'Saving...' : 'Save Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="k-mb-4" style="color:#666">
|
||||
Choose a role, then grant Read / Write / Delete / Approve per module. Changes apply
|
||||
immediately after saving — no re-login required. <strong>super_admin</strong> always has
|
||||
full access and cannot be edited.
|
||||
</p>
|
||||
|
||||
<div *ngIf="savedMessage" class="k-mb-3 k-p-2"
|
||||
style="background:#e8f5e9;border-radius:4px;color:#2e7d32">
|
||||
{{ savedMessage }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoading" class="k-mb-3">
|
||||
<kendo-loader type="infinite-spinner"></kendo-loader> Loading...
|
||||
</div>
|
||||
|
||||
<!-- Role selector -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-1 k-mb-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span style="font-weight:600">Role</span>
|
||||
<kendo-dropdownlist
|
||||
[data]="roleNames"
|
||||
[value]="selectedRole"
|
||||
(valueChange)="selectRole($event)"
|
||||
[valuePrimitive]="true">
|
||||
</kendo-dropdownlist>
|
||||
</label>
|
||||
<div class="flex flex-col gap-1" *ngIf="selectedDescription">
|
||||
<span style="font-weight:600">Description</span>
|
||||
<span style="padding-top:6px;color:#555">{{ selectedDescription }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isSuperAdminSelected" class="k-mb-3 k-p-2"
|
||||
style="background:#fff3e0;border-radius:4px;color:#e65100">
|
||||
super_admin bypasses all permission checks — every module is shown as fully granted and is read-only.
|
||||
</div>
|
||||
|
||||
<!-- Module × action matrix for the selected role -->
|
||||
<kendo-grid [data]="rows" [height]="520" *ngIf="selectedRole">
|
||||
<kendo-grid-column field="module" title="Module" [width]="220"></kendo-grid-column>
|
||||
|
||||
<kendo-grid-column title="Read" [width]="100">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<input type="checkbox" [(ngModel)]="dataItem.canRead" [disabled]="isSuperAdminSelected" />
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
|
||||
<kendo-grid-column title="Write" [width]="100">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<input type="checkbox" [(ngModel)]="dataItem.canWrite" [disabled]="isSuperAdminSelected" />
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
|
||||
<kendo-grid-column title="Delete" [width]="100">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<input type="checkbox" [(ngModel)]="dataItem.canDelete" [disabled]="isSuperAdminSelected" />
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
|
||||
<kendo-grid-column title="Approve" [width]="100">
|
||||
<ng-template kendoGridCellTemplate let-dataItem>
|
||||
<input type="checkbox" [(ngModel)]="dataItem.canApprove" [disabled]="isSuperAdminSelected" />
|
||||
</ng-template>
|
||||
</kendo-grid-column>
|
||||
</kendo-grid>
|
||||
</div>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='checkbox']:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { GridModule } from '@progress/kendo-angular-grid';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
|
||||
import { PermissionApiService } from '../../services/permission-api.service';
|
||||
import { PermissionService } from '../../../../core/services/permission.service';
|
||||
import {
|
||||
ModulePermissionDto,
|
||||
PermissionMatrixDto,
|
||||
RolePermissionRow,
|
||||
} from '../../../../core/models/permission.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-permissions-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule, FormsModule, GridModule, ButtonsModule, DropDownsModule, IndicatorsModule,
|
||||
],
|
||||
templateUrl: './permissions-page.component.html',
|
||||
styleUrls: ['./permissions-page.component.scss'],
|
||||
})
|
||||
export class PermissionsPageComponent implements OnInit {
|
||||
matrix: PermissionMatrixDto | null = null;
|
||||
roleNames: string[] = [];
|
||||
|
||||
selectedRole: string | null = null;
|
||||
selectedDescription: string | null = null;
|
||||
isSuperAdminSelected = false;
|
||||
|
||||
/** Editable copy of the selected role's per-module grants. */
|
||||
rows: ModulePermissionDto[] = [];
|
||||
|
||||
isLoading = false;
|
||||
isSaving = false;
|
||||
savedMessage: string | null = null;
|
||||
|
||||
constructor(
|
||||
private api: PermissionApiService,
|
||||
private permissions: PermissionService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadMatrix();
|
||||
}
|
||||
|
||||
loadMatrix(): void {
|
||||
this.isLoading = true;
|
||||
this.api.getMatrix().subscribe({
|
||||
next: matrix => {
|
||||
this.matrix = matrix;
|
||||
this.roleNames = matrix.roles.map(role => role.roleName);
|
||||
// Preserve the current selection across reloads, else pick the first editable role.
|
||||
const keep = this.selectedRole && this.roleNames.includes(this.selectedRole)
|
||||
? this.selectedRole
|
||||
: matrix.roles.find(role => !role.isSuperAdmin)?.roleName ?? this.roleNames[0] ?? null;
|
||||
this.selectRole(keep);
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => { this.isLoading = false; },
|
||||
});
|
||||
}
|
||||
|
||||
selectRole(roleName: string | null): void {
|
||||
this.selectedRole = roleName;
|
||||
this.savedMessage = null;
|
||||
const row = this.findRow(roleName);
|
||||
this.selectedDescription = row?.description ?? null;
|
||||
this.isSuperAdminSelected = row?.isSuperAdmin ?? false;
|
||||
// Clone so edits aren't committed until Save.
|
||||
this.rows = (row?.modules ?? []).map(module => ({ ...module }));
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.selectedRole || this.isSuperAdminSelected) {
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
this.api.updateRole(this.selectedRole, { modules: this.rows }).subscribe({
|
||||
next: () => {
|
||||
this.isSaving = false;
|
||||
this.savedMessage = `Saved permissions for "${this.selectedRole}".`;
|
||||
// Refresh the matrix and the current user's own permissions (in case they edited their effect).
|
||||
this.permissions.refresh().subscribe();
|
||||
this.loadMatrix();
|
||||
},
|
||||
error: () => { this.isSaving = false; },
|
||||
});
|
||||
}
|
||||
|
||||
private findRow(roleName: string | null): RolePermissionRow | undefined {
|
||||
if (!roleName || !this.matrix) {
|
||||
return undefined;
|
||||
}
|
||||
return this.matrix.roles.find(role => role.roleName === roleName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiConfigService } from '../../../core/services/api-config.service';
|
||||
import {
|
||||
PermissionMatrixDto,
|
||||
UpdateRolePermissionsRequest,
|
||||
} from '../../../core/models/permission.model';
|
||||
|
||||
/** Admin API for the configurable RBAC matrix (super_admin only on the server). */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PermissionApiService {
|
||||
private readonly endpoint: string;
|
||||
|
||||
constructor(private http: HttpClient, apiConfig: ApiConfigService) {
|
||||
this.endpoint = apiConfig.getApiUrl('permissions');
|
||||
}
|
||||
|
||||
/** GET /api/permissions — the full role × module matrix. */
|
||||
getMatrix(): Observable<PermissionMatrixDto> {
|
||||
return this.http.get<PermissionMatrixDto>(this.endpoint);
|
||||
}
|
||||
|
||||
/** PUT /api/permissions/{roleName} — replaces a role's grants. */
|
||||
updateRole(roleName: string, request: UpdateRolePermissionsRequest): Observable<void> {
|
||||
return this.http.put<void>(`${this.endpoint}/${encodeURIComponent(roleName)}`, request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user