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
**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`.

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
.login-card {
width: 100%;
max-width: 400px;
max-width: 650px;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
@@ -38,13 +38,15 @@
}
}
.key-toggle {
cursor: pointer;
color: #999;
transition: color 0.3s;
.login-form {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px 16px;
align-items: center;
margin-bottom: 16px;
&:hover {
color: #1890ff;
label {
font-weight: 500;
}
}
@@ -58,7 +60,3 @@
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 { Router, ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card';
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 { ApiService } from '../../../core/services/api.service';
import { isAdminKey } from '../../../core/models';
@@ -19,13 +16,10 @@ import { isAdminKey } from '../../../core/models';
imports: [
CommonModule,
FormsModule,
NzFormModule,
NzInputModule,
NzButtonModule,
NzCardModule,
NzAlertModule,
NzIconModule,
NzSpinModule,
],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
@@ -40,7 +34,6 @@ export class LoginComponent {
adminKey = '';
loading = signal(false);
error = signal<string | null>(null);
showKey = signal(false);
async login(): Promise<void> {
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!"
></span>
</ng-template>
<div class="key-actions">
@if (expertMode()) {
<button
nz-button
nzSize="small"
nz-popconfirm
nzPopconfirmTitle="Regenerate subscribe key? The existing key will no longer be valid."
(nzOnConfirm)="regenerateSubscribeKey()"
>
Invalidate & Regenerate
</button>
</div>
}
</div>
</scn-metadata-value>
<scn-metadata-value label="Subscribe QR">
@@ -110,42 +109,6 @@
</div>
</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>
</nz-card>
@@ -238,18 +201,20 @@
<span nz-icon nzType="close"></span>
</button>
} @else {
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Revoke"
nz-popconfirm
nzPopconfirmTitle="Revoke this subscription?"
(nzOnConfirm)="revokeSubscription(sub)"
>
<span nz-icon nzType="delete"></span>
</button>
@if (expertMode()) {
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Revoke"
nz-popconfirm
nzPopconfirmTitle="Revoke this subscription?"
(nzOnConfirm)="revokeSubscription(sub)"
>
<span nz-icon nzType="delete"></span>
</button>
}
}
</div>
</td>

View File

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

View File

@@ -280,7 +280,7 @@ export class ChannelDetailComponent implements OnInit {
if (!channel || !userId) return;
this.apiService.updateChannel(userId, channel.channel_id, {
subscribe_key: 'true'
subscribe_key: true
}).subscribe({
next: (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 } {
const channel = this.channel();
if (!channel) return { label: 'Unknown', color: 'default' };

View File

@@ -161,18 +161,20 @@
}
}
<!-- Confirmed or outgoing: can revoke -->
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Revoke"
nz-popconfirm
nzPopconfirmTitle="Revoke this subscription?"
(nzOnConfirm)="revokeSubscription(sub)"
>
<span nz-icon nzType="delete"></span>
</button>
@if (expertMode()) {
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Revoke"
nz-popconfirm
nzPopconfirmTitle="Revoke this subscription?"
(nzOnConfirm)="revokeSubscription(sub)"
>
<span nz-icon nzType="delete"></span>
</button>
}
}
</div>
</td>

View File

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