Simple Managment webapp [LLM]

This commit is contained in:
2025-12-03 17:20:50 +01:00
parent b521f74951
commit e7f613b5dc
76 changed files with 20009 additions and 1 deletions

View File

@@ -0,0 +1,204 @@
<div class="page-content">
<div class="page-header">
<h2>Keys</h2>
<div class="header-actions">
<button nz-button (click)="loadKeys()">
<span nz-icon nzType="reload"></span>
Refresh
</button>
<button nz-button nzType="primary" (click)="openCreateModal()">
<span nz-icon nzType="plus"></span>
Create Key
</button>
</div>
</div>
<nz-card>
<nz-table
#keyTable
[nzData]="keys()"
[nzLoading]="loading()"
[nzShowPagination]="false"
nzSize="middle"
>
<thead>
<tr>
<th nzWidth="25%">Name</th>
<th nzWidth="25%">Permissions</th>
<th nzWidth="15%">Messages Sent</th>
<th nzWidth="20%">Last Used</th>
<th nzWidth="15%">Actions</th>
</tr>
</thead>
<tbody>
@for (key of keys(); track key.keytoken_id) {
<tr>
<td>
<div class="key-name">
{{ key.name }}
@if (isCurrentKey(key)) {
<nz-tag nzColor="cyan" class="current-tag">Current</nz-tag>
}
</div>
<div class="key-id mono">{{ key.keytoken_id }}</div>
</td>
<td>
<div class="permissions">
@for (perm of getPermissions(key); track perm) {
<nz-tag
[nzColor]="getPermissionColor(perm)"
nz-tooltip
[nzTooltipTitle]="getPermissionLabel(perm)"
>
{{ perm }}
</nz-tag>
}
@if (key.all_channels) {
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
All Channels
</nz-tag>
}
</div>
</td>
<td>{{ key.messages_sent }}</td>
<td>
@if (key.timestamp_lastused) {
<span nz-tooltip [nzTooltipTitle]="key.timestamp_lastused">
{{ key.timestamp_lastused | relativeTime }}
</span>
} @else {
<span class="text-muted">Never</span>
}
</td>
<td>
@if (!isCurrentKey(key)) {
<button
nz-button
nzSize="small"
nzDanger
nz-popconfirm
nzPopconfirmTitle="Are you sure you want to delete this key?"
(nzOnConfirm)="deleteKey(key)"
>
<span nz-icon nzType="delete"></span>
</button>
} @else {
<span class="text-muted" nz-tooltip nzTooltipTitle="Cannot delete the key you're currently using">
-
</span>
}
</td>
</tr>
} @empty {
<tr>
<td colspan="5">
<nz-empty nzNotFoundContent="No keys found"></nz-empty>
</td>
</tr>
}
</tbody>
</nz-table>
</nz-card>
</div>
<!-- Create Key Modal -->
<nz-modal
[(nzVisible)]="showCreateModal"
nzTitle="Create Key"
(nzOnCancel)="closeCreateModal()"
[nzFooter]="createModalFooter"
nzWidth="500px"
>
<ng-container *nzModalContent>
@if (createdKey()) {
<!-- Show created key -->
<nz-alert
nzType="success"
nzMessage="Key created successfully!"
nzDescription="Make sure to copy the token now. You won't be able to see it again."
nzShowIcon
class="mb-16"
></nz-alert>
<nz-form-item>
<nz-form-label>Key Token</nz-form-label>
<nz-form-control>
<nz-input-group [nzSuffix]="copyButton">
<input
type="text"
nz-input
[value]="createdKey()!.token"
readonly
class="mono"
/>
</nz-input-group>
<ng-template #copyButton>
<span
nz-icon
nzType="copy"
class="copy-icon"
nz-tooltip
nzTooltipTitle="Copy"
[appCopyToClipboard]="createdKey()!.token!"
></span>
</ng-template>
</nz-form-control>
</nz-form-item>
} @else {
<!-- Create form -->
<nz-form-item>
<nz-form-label>Name</nz-form-label>
<nz-form-control>
<input
type="text"
nz-input
placeholder="Enter a name for this key"
[(ngModel)]="newKeyName"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label>Permissions</nz-form-label>
<nz-form-control>
<div class="permission-checkboxes">
@for (opt of permissionOptions; track opt.value) {
<label
nz-checkbox
[nzChecked]="isPermissionChecked(opt.value)"
(nzCheckedChange)="onPermissionChange(opt.value, $event)"
>
<nz-tag [nzColor]="getPermissionColor(opt.value)">{{ opt.value }}</nz-tag>
{{ opt.label }}
<span class="perm-desc">- {{ opt.description }}</span>
</label>
}
</div>
</nz-form-control>
</nz-form-item>
<nz-form-item class="mb-0">
<label nz-checkbox [(ngModel)]="newKeyAllChannels">
Access to all channels
</label>
</nz-form-item>
}
</ng-container>
</nz-modal>
<ng-template #createModalFooter>
@if (createdKey()) {
<button nz-button nzType="primary" (click)="closeCreateModal()">Done</button>
} @else {
<button nz-button (click)="closeCreateModal()">Cancel</button>
<button
nz-button
nzType="primary"
[nzLoading]="creating()"
[disabled]="!newKeyName.trim() || newKeyPermissions.length === 0"
(click)="createKey()"
>
Create
</button>
}
</ng-template>

View File

@@ -0,0 +1,70 @@
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h2 {
margin: 0;
}
}
.header-actions {
display: flex;
gap: 8px;
}
.key-name {
font-weight: 500;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.key-id {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.current-tag {
font-size: 11px;
}
.permissions {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.text-muted {
color: #999;
}
.copy-icon {
cursor: pointer;
color: #999;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
.permission-checkboxes {
display: flex;
flex-direction: column;
gap: 8px;
label {
display: flex;
align-items: center;
gap: 4px;
}
.perm-desc {
color: #999;
font-size: 12px;
}
}

View File

@@ -0,0 +1,198 @@
import { Component, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service';
import { NotificationService } from '../../../core/services/notification.service';
import { KeyToken, parsePermissions, TokenPermission } from '../../../core/models';
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
interface PermissionOption {
value: TokenPermission;
label: string;
description: string;
}
@Component({
selector: 'app-key-list',
standalone: true,
imports: [
CommonModule,
FormsModule,
NzTableModule,
NzButtonModule,
NzIconModule,
NzTagModule,
NzEmptyModule,
NzCardModule,
NzPopconfirmModule,
NzModalModule,
NzFormModule,
NzInputModule,
NzCheckboxModule,
NzToolTipModule,
NzAlertModule,
RelativeTimePipe,
CopyToClipboardDirective,
],
templateUrl: './key-list.component.html',
styleUrl: './key-list.component.scss'
})
export class KeyListComponent implements OnInit {
private apiService = inject(ApiService);
private authService = inject(AuthService);
private notification = inject(NotificationService);
keys = signal<KeyToken[]>([]);
currentKeyId = signal<string | null>(null);
loading = signal(false);
// Create modal
showCreateModal = signal(false);
newKeyName = '';
newKeyPermissions: TokenPermission[] = ['CR'];
newKeyAllChannels = true;
creating = signal(false);
createdKey = signal<KeyToken | null>(null);
permissionOptions: PermissionOption[] = [
{ value: 'A', label: 'Admin', description: 'Full access to all operations' },
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
{ value: 'CS', label: 'Channel Send', description: 'Send messages to channels' },
{ value: 'UR', label: 'User Read', description: 'Read user information' },
];
ngOnInit(): void {
this.loadKeys();
this.loadCurrentKey();
}
loadKeys(): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.loading.set(true);
this.apiService.getKeys(userId).subscribe({
next: (response) => {
this.keys.set(response.keys);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
}
});
}
loadCurrentKey(): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.getCurrentKey(userId).subscribe({
next: (key) => {
this.currentKeyId.set(key.keytoken_id);
}
});
}
isCurrentKey(key: KeyToken): boolean {
return key.keytoken_id === this.currentKeyId();
}
deleteKey(key: KeyToken): void {
if (this.isCurrentKey(key)) {
this.notification.warning('Cannot delete the key you are currently using');
return;
}
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.deleteKey(userId, key.keytoken_id).subscribe({
next: () => {
this.notification.success('Key deleted');
this.loadKeys();
}
});
}
// Create key modal
openCreateModal(): void {
this.newKeyName = '';
this.newKeyPermissions = ['CR'];
this.newKeyAllChannels = true;
this.createdKey.set(null);
this.showCreateModal.set(true);
}
closeCreateModal(): void {
this.showCreateModal.set(false);
}
createKey(): void {
const userId = this.authService.getUserId();
if (!userId || !this.newKeyName.trim() || this.newKeyPermissions.length === 0) return;
this.creating.set(true);
this.apiService.createKey(userId, {
name: this.newKeyName.trim(),
permissions: this.newKeyPermissions.join(';'),
all_channels: this.newKeyAllChannels
}).subscribe({
next: (key) => {
this.createdKey.set(key);
this.creating.set(false);
this.loadKeys();
},
error: () => {
this.creating.set(false);
}
});
}
getPermissions(key: KeyToken): TokenPermission[] {
return parsePermissions(key.permissions);
}
getPermissionColor(perm: TokenPermission): string {
switch (perm) {
case 'A': return 'red';
case 'CR': return 'blue';
case 'CS': return 'green';
case 'UR': return 'purple';
default: return 'default';
}
}
getPermissionLabel(perm: TokenPermission): string {
const option = this.permissionOptions.find(o => o.value === perm);
return option?.label || perm;
}
onPermissionChange(perm: TokenPermission, checked: boolean): void {
if (checked) {
if (!this.newKeyPermissions.includes(perm)) {
this.newKeyPermissions = [...this.newKeyPermissions, perm];
}
} else {
this.newKeyPermissions = this.newKeyPermissions.filter(p => p !== perm);
}
}
isPermissionChecked(perm: TokenPermission): boolean {
return this.newKeyPermissions.includes(perm);
}
}