More webapp changes+fixes
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m45s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m31s
Build Docker and Deploy / Deploy to Server (push) Successful in 22s

This commit is contained in:
2025-12-09 16:34:50 +01:00
parent c81143ecdc
commit 202603d16c
12 changed files with 83 additions and 166 deletions

View File

@@ -26,7 +26,7 @@ The app follows a feature-based module organization with standalone components:
### Key Patterns ### Key Patterns
**Authentication**: Uses a custom `SCN` token scheme. Credentials (user_id and admin_key) are stored in sessionStorage and attached via `authInterceptor`. The `authGuard` protects all routes except `/login`. **Authentication**: Uses a custom `SCN` token scheme. Credentials (user_id and admin_key) are stored in localStorage and attached via `authInterceptor`. The `authGuard` protects all routes except `/login`.
**API Communication**: All API calls go through `ApiService` (`src/app/core/services/api.service.ts`). The base URL is configured in `src/environments/environment.ts`. **API Communication**: All API calls go through `ApiService` (`src/app/core/services/api.service.ts`). The base URL is configured in `src/environments/environment.ts`.

View File

@@ -7,7 +7,6 @@ export interface Channel {
display_name: string; display_name: string;
description_name: string | null; description_name: string | null;
subscribe_key?: string; subscribe_key?: string;
send_key?: string;
timestamp_created: string; timestamp_created: string;
timestamp_lastsent: string | null; timestamp_lastsent: string | null;
messages_sent: number; messages_sent: number;
@@ -34,8 +33,7 @@ export interface CreateChannelRequest {
export interface UpdateChannelRequest { export interface UpdateChannelRequest {
display_name?: string; display_name?: string;
description_name?: string; description_name?: string;
subscribe_key?: string; subscribe_key?: boolean; // RefreshSubscribeKey
send_key?: string;
} }
export interface ChannelListResponse { export interface ChannelListResponse {

View File

@@ -17,8 +17,8 @@ export class AuthService {
} }
private loadFromStorage(): void { private loadFromStorage(): void {
const userId = sessionStorage.getItem(USER_ID_KEY); const userId = localStorage.getItem(USER_ID_KEY);
const adminKey = sessionStorage.getItem(ADMIN_KEY_KEY); const adminKey = localStorage.getItem(ADMIN_KEY_KEY);
if (userId && adminKey) { if (userId && adminKey) {
this.userId.set(userId); this.userId.set(userId);
this.adminKey.set(adminKey); this.adminKey.set(adminKey);
@@ -26,15 +26,15 @@ export class AuthService {
} }
login(userId: string, adminKey: string): void { login(userId: string, adminKey: string): void {
sessionStorage.setItem(USER_ID_KEY, userId); localStorage.setItem(USER_ID_KEY, userId);
sessionStorage.setItem(ADMIN_KEY_KEY, adminKey); localStorage.setItem(ADMIN_KEY_KEY, adminKey);
this.userId.set(userId); this.userId.set(userId);
this.adminKey.set(adminKey); this.adminKey.set(adminKey);
} }
logout(): void { logout(): void {
sessionStorage.removeItem(USER_ID_KEY); localStorage.removeItem(USER_ID_KEY);
sessionStorage.removeItem(ADMIN_KEY_KEY); localStorage.removeItem(ADMIN_KEY_KEY);
this.userId.set(null); this.userId.set(null);
this.adminKey.set(null); this.adminKey.set(null);
} }

View File

@@ -16,9 +16,7 @@ export class SettingsService {
private loadFromStorage(): void { private loadFromStorage(): void {
const stored = localStorage.getItem(EXPERT_MODE_KEY); const stored = localStorage.getItem(EXPERT_MODE_KEY);
if (stored === 'true') { this._expertMode.set(stored === 'true');
this._expertMode.set(true);
}
} }
setExpertMode(enabled: boolean): void { setExpertMode(enabled: boolean): void {

View File

@@ -14,59 +14,37 @@
></nz-alert> ></nz-alert>
} }
<form nz-form nzLayout="horizontal" (ngSubmit)="login()"> <div class="login-form">
<nz-form-item> <label for="userId">User ID</label>
<nz-form-label [nzSpan]="7">User ID</nz-form-label>
<nz-form-control [nzSpan]="17">
<nz-input-group nzPrefixIcon="user">
<input <input
id="userId"
type="text" type="text"
nz-input nz-input
placeholder="Enter your User ID" placeholder="Enter your User ID"
[(ngModel)]="userId" [(ngModel)]="userId"
name="userId"
[disabled]="loading()" [disabled]="loading()"
/> />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item> <label for="adminKey">Admin Key</label>
<nz-form-label [nzSpan]="7">Admin Key</nz-form-label>
<nz-form-control [nzSpan]="17">
<nz-input-group nzPrefixIcon="key" [nzSuffix]="keySuffix">
<input <input
[type]="showKey() ? 'text' : 'password'" id="adminKey"
type="text"
nz-input nz-input
placeholder="Enter your Admin Key" placeholder="Enter your Admin Key"
[(ngModel)]="adminKey" [(ngModel)]="adminKey"
name="adminKey"
[disabled]="loading()" [disabled]="loading()"
/> />
</nz-input-group> </div>
<ng-template #keySuffix>
<span
nz-icon
[nzType]="showKey() ? 'eye' : 'eye-invisible'"
class="key-toggle"
(click)="toggleShowKey()"
></span>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item class="mb-0">
<button <button
nz-button nz-button
nzType="primary" nzType="primary"
nzBlock nzBlock
type="submit"
[nzLoading]="loading()" [nzLoading]="loading()"
(click)="login()"
> >
Sign In Sign In
</button> </button>
</nz-form-item>
</form>
<div class="login-footer"> <div class="login-footer">
<p>You need an admin key to access.</p> <p>You need an admin key to access.</p>

View File

@@ -9,7 +9,7 @@
.login-card { .login-card {
width: 100%; width: 100%;
max-width: 400px; max-width: 650px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
} }
@@ -38,13 +38,15 @@
} }
} }
.key-toggle { .login-form {
cursor: pointer; display: grid;
color: #999; grid-template-columns: auto 1fr;
transition: color 0.3s; gap: 12px 16px;
align-items: center;
margin-bottom: 16px;
&:hover { label {
color: #1890ff; font-weight: 500;
} }
} }
@@ -58,7 +60,3 @@
color: #999; color: #999;
} }
} }
nz-form-label {
font-weight: 500;
}

View File

@@ -2,13 +2,10 @@ import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input'; import { NzInputModule } from 'ng-zorro-antd/input';
import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card'; import { NzCardModule } from 'ng-zorro-antd/card';
import { NzAlertModule } from 'ng-zorro-antd/alert'; import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { AuthService } from '../../../core/services/auth.service'; import { AuthService } from '../../../core/services/auth.service';
import { ApiService } from '../../../core/services/api.service'; import { ApiService } from '../../../core/services/api.service';
import { isAdminKey } from '../../../core/models'; import { isAdminKey } from '../../../core/models';
@@ -19,13 +16,10 @@ import { isAdminKey } from '../../../core/models';
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
NzFormModule,
NzInputModule, NzInputModule,
NzButtonModule, NzButtonModule,
NzCardModule, NzCardModule,
NzAlertModule, NzAlertModule,
NzIconModule,
NzSpinModule,
], ],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.scss' styleUrl: './login.component.scss'
@@ -40,7 +34,6 @@ export class LoginComponent {
adminKey = ''; adminKey = '';
loading = signal(false); loading = signal(false);
error = signal<string | null>(null); error = signal<string | null>(null);
showKey = signal(false);
async login(): Promise<void> { async login(): Promise<void> {
if (!this.userId.trim() || !this.adminKey.trim()) { if (!this.userId.trim() || !this.adminKey.trim()) {
@@ -80,8 +73,4 @@ export class LoginComponent {
} }
}); });
} }
toggleShowKey(): void {
this.showKey.update(v => !v);
}
} }

View File

@@ -90,17 +90,16 @@
[appCopyToClipboard]="channel()!.subscribe_key!" [appCopyToClipboard]="channel()!.subscribe_key!"
></span> ></span>
</ng-template> </ng-template>
<div class="key-actions"> @if (expertMode()) {
<button <button
nz-button nz-button
nzSize="small"
nz-popconfirm nz-popconfirm
nzPopconfirmTitle="Regenerate subscribe key? The existing key will no longer be valid." nzPopconfirmTitle="Regenerate subscribe key? The existing key will no longer be valid."
(nzOnConfirm)="regenerateSubscribeKey()" (nzOnConfirm)="regenerateSubscribeKey()"
> >
Invalidate & Regenerate Invalidate & Regenerate
</button> </button>
</div> }
</div> </div>
</scn-metadata-value> </scn-metadata-value>
<scn-metadata-value label="Subscribe QR"> <scn-metadata-value label="Subscribe QR">
@@ -110,42 +109,6 @@
</div> </div>
</scn-metadata-value> </scn-metadata-value>
} }
@if (isOwner() && channel()!.send_key) {
<scn-metadata-value label="Send Key">
<div class="key-field">
<nz-input-group [nzSuffix]="sendKeySuffix">
<input
type="text"
nz-input
[value]="channel()!.send_key"
readonly
class="mono"
/>
</nz-input-group>
<ng-template #sendKeySuffix>
<span
nz-icon
nzType="copy"
class="action-icon"
nz-tooltip
nzTooltipTitle="Copy"
[appCopyToClipboard]="channel()!.send_key!"
></span>
</ng-template>
<div class="key-actions">
<button
nz-button
nzSize="small"
nz-popconfirm
nzPopconfirmTitle="Regenerate send key?"
(nzOnConfirm)="regenerateSendKey()"
>
Regenerate
</button>
</div>
</div>
</scn-metadata-value>
}
</scn-metadata-grid> </scn-metadata-grid>
</nz-card> </nz-card>
@@ -238,6 +201,7 @@
<span nz-icon nzType="close"></span> <span nz-icon nzType="close"></span>
</button> </button>
} @else { } @else {
@if (expertMode()) {
<button <button
nz-button nz-button
nzSize="small" nzSize="small"
@@ -251,6 +215,7 @@
<span nz-icon nzType="delete"></span> <span nz-icon nzType="delete"></span>
</button> </button>
} }
}
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -12,7 +12,7 @@
.key-field { .key-field {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 8px; gap: 8px;
} }

View File

@@ -280,7 +280,7 @@ export class ChannelDetailComponent implements OnInit {
if (!channel || !userId) return; if (!channel || !userId) return;
this.apiService.updateChannel(userId, channel.channel_id, { this.apiService.updateChannel(userId, channel.channel_id, {
subscribe_key: 'true' subscribe_key: true
}).subscribe({ }).subscribe({
next: (updated) => { next: (updated) => {
this.channel.set(updated); this.channel.set(updated);
@@ -289,21 +289,6 @@ export class ChannelDetailComponent implements OnInit {
}); });
} }
regenerateSendKey(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.apiService.updateChannel(userId, channel.channel_id, {
send_key: 'true'
}).subscribe({
next: (updated) => {
this.channel.set(updated);
this.notification.success('Send key regenerated');
}
});
}
getSubscriptionStatus(): { label: string; color: string } { getSubscriptionStatus(): { label: string; color: string } {
const channel = this.channel(); const channel = this.channel();
if (!channel) return { label: 'Unknown', color: 'default' }; if (!channel) return { label: 'Unknown', color: 'default' };

View File

@@ -161,6 +161,7 @@
} }
} }
<!-- Confirmed or outgoing: can revoke --> <!-- Confirmed or outgoing: can revoke -->
@if (expertMode()) {
<button <button
nz-button nz-button
nzSize="small" nzSize="small"
@@ -174,6 +175,7 @@
<span nz-icon nzType="delete"></span> <span nz-icon nzType="delete"></span>
</button> </button>
} }
}
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -18,6 +18,7 @@ import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { ApiService } from '../../../core/services/api.service'; import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service'; import { AuthService } from '../../../core/services/auth.service';
import { NotificationService } from '../../../core/services/notification.service'; import { NotificationService } from '../../../core/services/notification.service';
import { SettingsService } from '../../../core/services/settings.service';
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service'; import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
import { Subscription, SubscriptionFilter } from '../../../core/models'; import { Subscription, SubscriptionFilter } from '../../../core/models';
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
@@ -67,8 +68,11 @@ export class SubscriptionListComponent implements OnInit {
private apiService = inject(ApiService); private apiService = inject(ApiService);
private authService = inject(AuthService); private authService = inject(AuthService);
private notification = inject(NotificationService); private notification = inject(NotificationService);
private settingsService = inject(SettingsService);
private userCacheService = inject(UserCacheService); private userCacheService = inject(UserCacheService);
expertMode = this.settingsService.expertMode;
subscriptions = signal<Subscription[]>([]); subscriptions = signal<Subscription[]>([]);
userNames = signal<Map<string, ResolvedUser>>(new Map()); userNames = signal<Map<string, ResolvedUser>>(new Map());
loading = signal(false); loading = signal(false);