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
|
||||
|
||||
**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`.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
.key-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user