import { Component, Inject, ChangeDetectorRef } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipsModule, MatChipInputEvent } from '@angular/material/chips'; import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { BreakpointObserver } from '@angular/cdk/layout'; import { I18nService } from '../services/i18n.service'; import { DialogCancelButtonComponent } from '../components/dialog-cancel-button.component'; export interface AclUserDialogData { username: string; rules: string; isNew: boolean; } export interface AclUserDialogResult { username: string; rules: string[]; } @Component({ selector: 'p3xr-acl-user-dialog', standalone: true, imports: [ FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, MatToolbarModule, MatTooltipModule, MatSlideToggleModule, MatCheckboxModule, MatChipsModule, DialogCancelButtonComponent, ], template: ` {{ data.isNew ? (strings().page?.acl?.createUser || 'Create User') : (strings().page?.acl?.editUser || 'Edit User') }} {{ strings().page?.acl?.username || 'Username' }}
{{ strings().page?.acl?.enabled || 'Enabled' }}
{{ strings().page?.acl?.noPassword || 'No password (nopass)' }}
@if (!nopass) { {{ strings().page?.acl?.password || 'Password' }} @if (!data.isNew) { {{ strings().page?.acl?.passwordHint || 'Leave empty to keep current password' }} } } {{ strings().page?.acl?.commands || 'Commands' }} @for (rule of commandsList; track rule) { {{ rule }} } {{ strings().page?.acl?.commandsHint || 'e.g., +@all or +@read -@dangerous' }} {{ strings().page?.acl?.keys || 'Key Patterns' }} @for (pattern of keysList; track pattern) { {{ pattern }} } {{ strings().page?.acl?.keysHint || 'e.g., ~* or ~user:*' }} {{ strings().page?.acl?.channels || 'Pub/Sub Channels' }} @for (pattern of channelsList; track pattern) { {{ pattern }} } {{ strings().page?.acl?.channelsHint || 'e.g., &* or ¬ifications:*' }}
`, styles: [` .md-block { width: 100%; } mat-chip-row { font-size: 13px; } .p3xr-chip-deny { --mdc-chip-elevated-container-color: var(--p3xr-btn-warn-bg, #f44336); --mdc-chip-label-text-color: #fff; } `], }) export class AclUserDialogComponent { strings; username: string; enabled = true; nopass = false; password = ''; commandsList: string[] = []; keysList: string[] = []; channelsList: string[] = []; isWide = true; readonly separatorKeys = [ENTER, COMMA, SPACE]; constructor( @Inject(MAT_DIALOG_DATA) public data: AclUserDialogData, @Inject(MatDialogRef) private dialogRef: MatDialogRef, @Inject(BreakpointObserver) private breakpointObserver: BreakpointObserver, @Inject(I18nService) private i18n: I18nService, @Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef, ) { this.strings = this.i18n.strings; this.username = data.username; this.parseRules(data.rules); this.breakpointObserver.observe('(min-width: 600px)').subscribe(r => { this.isWide = r.matches; this.cdr.markForCheck(); }); } addChip(list: string[], event: MatChipInputEvent): void { const value = (event.value || '').trim(); if (value && !list.includes(value)) list.push(value); event.chipInput!.clear(); } removeChip(list: string[], value: string): void { const idx = list.indexOf(value); if (idx >= 0) list.splice(idx, 1); } private parseRules(rules: string): void { const tokens = rules.trim().split(/\s+/).filter(Boolean); for (const t of tokens) { if (t === 'on') this.enabled = true; else if (t === 'off') this.enabled = false; else if (t === 'nopass') this.nopass = true; else if (t.startsWith('>') || t.startsWith('<') || t.startsWith('#') || t === 'resetpass' || t === 'sanitize-payload' || t === 'skip-sanitize-payload') continue; else if (t.startsWith('+') || t.startsWith('-') || t === 'allcommands' || t === 'nocommands') this.commandsList.push(t); else if (t.startsWith('~') || t.startsWith('%') || t === 'allkeys' || t === 'resetkeys') this.keysList.push(t); else if (t.startsWith('&') || t === 'allchannels' || t === 'resetchannels') this.channelsList.push(t); } } onSave(): void { const u = this.username?.trim(); if (!u) return; const rules: string[] = [this.enabled ? 'on' : 'off']; // When editing, reset permissions first so removals take effect if (!this.data.isNew) { rules.push('nocommands', 'resetkeys', 'resetchannels'); if (this.nopass) rules.push('resetpass', 'nopass'); else if (this.password.trim()) rules.push('resetpass', '>' + this.password.trim()); // No password change → existing passwords preserved (no resetpass sent) } else { if (this.nopass) rules.push('nopass'); else if (this.password.trim()) rules.push('>' + this.password.trim()); } rules.push(...this.commandsList, ...this.keysList, ...this.channelsList); this.dialogRef.close({ username: u, rules } as AclUserDialogResult); } onCancel(): void { this.dialogRef.close(undefined); } }