More webapp changes+fixes
This commit is contained in:
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
<input
|
||||||
<nz-form-control [nzSpan]="17">
|
id="userId"
|
||||||
<nz-input-group nzPrefixIcon="user">
|
type="text"
|
||||||
<input
|
nz-input
|
||||||
type="text"
|
placeholder="Enter your User ID"
|
||||||
nz-input
|
[(ngModel)]="userId"
|
||||||
placeholder="Enter your User ID"
|
[disabled]="loading()"
|
||||||
[(ngModel)]="userId"
|
/>
|
||||||
name="userId"
|
|
||||||
[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>
|
<input
|
||||||
<nz-form-control [nzSpan]="17">
|
id="adminKey"
|
||||||
<nz-input-group nzPrefixIcon="key" [nzSuffix]="keySuffix">
|
type="text"
|
||||||
<input
|
nz-input
|
||||||
[type]="showKey() ? 'text' : 'password'"
|
placeholder="Enter your Admin Key"
|
||||||
nz-input
|
[(ngModel)]="adminKey"
|
||||||
placeholder="Enter your Admin Key"
|
[disabled]="loading()"
|
||||||
[(ngModel)]="adminKey"
|
/>
|
||||||
name="adminKey"
|
</div>
|
||||||
[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>
|
|
||||||
|
|
||||||
<nz-form-item class="mb-0">
|
<button
|
||||||
<button
|
nz-button
|
||||||
nz-button
|
nzType="primary"
|
||||||
nzType="primary"
|
nzBlock
|
||||||
nzBlock
|
[nzLoading]="loading()"
|
||||||
type="submit"
|
(click)="login()"
|
||||||
[nzLoading]="loading()"
|
>
|
||||||
>
|
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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,18 +201,20 @@
|
|||||||
<span nz-icon nzType="close"></span>
|
<span nz-icon nzType="close"></span>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
@if (expertMode()) {
|
||||||
nz-button
|
<button
|
||||||
nzSize="small"
|
nz-button
|
||||||
nzDanger
|
nzSize="small"
|
||||||
nz-tooltip
|
nzDanger
|
||||||
nzTooltipTitle="Revoke"
|
nz-tooltip
|
||||||
nz-popconfirm
|
nzTooltipTitle="Revoke"
|
||||||
nzPopconfirmTitle="Revoke this subscription?"
|
nz-popconfirm
|
||||||
(nzOnConfirm)="revokeSubscription(sub)"
|
nzPopconfirmTitle="Revoke this subscription?"
|
||||||
>
|
(nzOnConfirm)="revokeSubscription(sub)"
|
||||||
<span nz-icon nzType="delete"></span>
|
>
|
||||||
</button>
|
<span nz-icon nzType="delete"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
.key-field {
|
.key-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' };
|
||||||
|
|||||||
@@ -161,18 +161,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
<!-- Confirmed or outgoing: can revoke -->
|
<!-- Confirmed or outgoing: can revoke -->
|
||||||
<button
|
@if (expertMode()) {
|
||||||
nz-button
|
<button
|
||||||
nzSize="small"
|
nz-button
|
||||||
nzDanger
|
nzSize="small"
|
||||||
nz-tooltip
|
nzDanger
|
||||||
nzTooltipTitle="Revoke"
|
nz-tooltip
|
||||||
nz-popconfirm
|
nzTooltipTitle="Revoke"
|
||||||
nzPopconfirmTitle="Revoke this subscription?"
|
nz-popconfirm
|
||||||
(nzOnConfirm)="revokeSubscription(sub)"
|
nzPopconfirmTitle="Revoke this subscription?"
|
||||||
>
|
(nzOnConfirm)="revokeSubscription(sub)"
|
||||||
<span nz-icon nzType="delete"></span>
|
>
|
||||||
</button>
|
<span nz-icon nzType="delete"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user