[WebApp] AccountSwitcher
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
|||||||
PlayCircleOutline,
|
PlayCircleOutline,
|
||||||
StopOutline,
|
StopOutline,
|
||||||
ArrowLeftOutline,
|
ArrowLeftOutline,
|
||||||
|
DownOutline,
|
||||||
} from '@ant-design/icons-angular/icons';
|
} from '@ant-design/icons-angular/icons';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
@@ -91,6 +92,7 @@ const icons: IconDefinition[] = [
|
|||||||
PlayCircleOutline,
|
PlayCircleOutline,
|
||||||
StopOutline,
|
StopOutline,
|
||||||
ArrowLeftOutline,
|
ArrowLeftOutline,
|
||||||
|
DownOutline,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
|
|||||||
@@ -1,54 +1,150 @@
|
|||||||
import { Injectable, signal, computed } from '@angular/core';
|
import { Injectable, signal, computed } from '@angular/core';
|
||||||
|
|
||||||
const USER_ID_KEY = 'scn_user_id';
|
export interface Account {
|
||||||
const ADMIN_KEY_KEY = 'scn_admin_key';
|
userId: string;
|
||||||
|
adminKey: string;
|
||||||
|
username: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCOUNTS_KEY = 'scn_accounts';
|
||||||
|
const ACTIVE_KEY = 'scn_active_user_id';
|
||||||
|
|
||||||
|
// Legacy single-account storage keys (migrated on first load).
|
||||||
|
const LEGACY_USER_ID_KEY = 'scn_user_id';
|
||||||
|
const LEGACY_ADMIN_KEY_KEY = 'scn_admin_key';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private userId = signal<string | null>(null);
|
private _accounts = signal<Account[]>([]);
|
||||||
private adminKey = signal<string | null>(null);
|
private _activeUserId = signal<string | null>(null);
|
||||||
|
|
||||||
isAuthenticated = computed(() => !!this.userId() && !!this.adminKey());
|
/** All logged-in accounts. */
|
||||||
|
accounts = this._accounts.asReadonly();
|
||||||
|
activeUserId = this._activeUserId.asReadonly();
|
||||||
|
|
||||||
|
activeAccount = computed(() => {
|
||||||
|
const id = this._activeUserId();
|
||||||
|
return this._accounts().find(a => a.userId === id) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
isAuthenticated = computed(() => !!this.activeAccount());
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loadFromStorage();
|
this.loadFromStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadFromStorage(): void {
|
private loadFromStorage(): void {
|
||||||
const userId = localStorage.getItem(USER_ID_KEY);
|
const raw = localStorage.getItem(ACCOUNTS_KEY);
|
||||||
const adminKey = localStorage.getItem(ADMIN_KEY_KEY);
|
if (raw) {
|
||||||
if (userId && adminKey) {
|
try {
|
||||||
this.userId.set(userId);
|
const parsed = JSON.parse(raw);
|
||||||
this.adminKey.set(adminKey);
|
if (Array.isArray(parsed)) {
|
||||||
|
this._accounts.set(
|
||||||
|
parsed
|
||||||
|
.filter(a => a && a.userId && a.adminKey)
|
||||||
|
.map(a => ({ userId: a.userId, adminKey: a.adminKey, username: a.username ?? null }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed storage; treat as logged out.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = localStorage.getItem(ACTIVE_KEY);
|
||||||
|
|
||||||
|
// Migrate legacy single-account storage into the multi-account format.
|
||||||
|
if (this._accounts().length === 0) {
|
||||||
|
const legacyId = localStorage.getItem(LEGACY_USER_ID_KEY);
|
||||||
|
const legacyKey = localStorage.getItem(LEGACY_ADMIN_KEY_KEY);
|
||||||
|
if (legacyId && legacyKey) {
|
||||||
|
this._accounts.set([{ userId: legacyId, adminKey: legacyKey, username: null }]);
|
||||||
|
active = legacyId;
|
||||||
|
localStorage.removeItem(LEGACY_USER_ID_KEY);
|
||||||
|
localStorage.removeItem(LEGACY_ADMIN_KEY_KEY);
|
||||||
|
this.persist(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the active pointer references an existing account.
|
||||||
|
if (!active || !this._accounts().some(a => a.userId === active)) {
|
||||||
|
active = this._accounts()[0]?.userId ?? null;
|
||||||
|
}
|
||||||
|
this._activeUserId.set(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private persist(activeOverride?: string | null): void {
|
||||||
|
localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(this._accounts()));
|
||||||
|
const active = activeOverride !== undefined ? activeOverride : this._activeUserId();
|
||||||
|
if (active) {
|
||||||
|
localStorage.setItem(ACTIVE_KEY, active);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(ACTIVE_KEY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Add (or update) an account and make it the active one. */
|
||||||
login(userId: string, adminKey: string): void {
|
login(userId: string, adminKey: string): void {
|
||||||
localStorage.setItem(USER_ID_KEY, userId);
|
if (this._accounts().some(a => a.userId === userId)) {
|
||||||
localStorage.setItem(ADMIN_KEY_KEY, adminKey);
|
this._accounts.update(list =>
|
||||||
this.userId.set(userId);
|
list.map(a => (a.userId === userId ? { ...a, adminKey } : a))
|
||||||
this.adminKey.set(adminKey);
|
);
|
||||||
|
} else {
|
||||||
|
this._accounts.update(list => [...list, { userId, adminKey, username: null }]);
|
||||||
|
}
|
||||||
|
this._activeUserId.set(userId);
|
||||||
|
this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Log out the currently active account. */
|
||||||
logout(): void {
|
logout(): void {
|
||||||
localStorage.removeItem(USER_ID_KEY);
|
const active = this._activeUserId();
|
||||||
localStorage.removeItem(ADMIN_KEY_KEY);
|
if (active) {
|
||||||
this.userId.set(null);
|
this.removeAccount(active);
|
||||||
this.adminKey.set(null);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Log out a specific account (active or background). */
|
||||||
|
logoutAccount(userId: string): void {
|
||||||
|
this.removeAccount(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeAccount(userId: string): void {
|
||||||
|
this._accounts.update(list => list.filter(a => a.userId !== userId));
|
||||||
|
if (this._activeUserId() === userId) {
|
||||||
|
this._activeUserId.set(this._accounts()[0]?.userId ?? null);
|
||||||
|
}
|
||||||
|
this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Switch which logged-in account is active. */
|
||||||
|
switchAccount(userId: string): void {
|
||||||
|
if (!this._accounts().some(a => a.userId === userId)) return;
|
||||||
|
this._activeUserId.set(userId);
|
||||||
|
this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache the resolved username on the active account. */
|
||||||
|
setActiveUsername(username: string | null): void {
|
||||||
|
const active = this._activeUserId();
|
||||||
|
if (!active) return;
|
||||||
|
this._accounts.update(list =>
|
||||||
|
list.map(a => (a.userId === active ? { ...a, username } : a))
|
||||||
|
);
|
||||||
|
this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserId(): string | null {
|
getUserId(): string | null {
|
||||||
return this.userId();
|
return this._activeUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAdminKey(): string | null {
|
getAdminKey(): string | null {
|
||||||
return this.adminKey();
|
return this.activeAccount()?.adminKey ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthHeader(): string | null {
|
getAuthHeader(): string | null {
|
||||||
const key = this.adminKey();
|
const key = this.getAdminKey();
|
||||||
return key ? `SCN ${key}` : null;
|
return key ? `SCN ${key}` : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,12 @@ export class AccountInfoComponent implements OnInit {
|
|||||||
next: () => {
|
next: () => {
|
||||||
this.notification.success('Account deleted');
|
this.notification.success('Account deleted');
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
this.router.navigate(['/login']);
|
if (this.authService.isAuthenticated()) {
|
||||||
|
// Another account is still logged in — reload into it cleanly.
|
||||||
|
window.location.assign('/');
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.deleting.set(false);
|
this.deleting.set(false);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, inject, signal } from '@angular/core';
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
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';
|
||||||
@@ -27,7 +27,6 @@ import { isAdminKey } from '../../../core/models';
|
|||||||
export class LoginComponent {
|
export class LoginComponent {
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
private router = inject(Router);
|
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
|
||||||
userId = '';
|
userId = '';
|
||||||
@@ -56,9 +55,11 @@ export class LoginComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login successful
|
// Login successful. Use a full navigation so all per-account state
|
||||||
|
// (singleton caches, component signals) is initialised for the new
|
||||||
|
// active account — important when adding a second account.
|
||||||
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/messages';
|
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/messages';
|
||||||
this.router.navigateByUrl(returnUrl);
|
window.location.assign(returnUrl);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
|
|||||||
@@ -56,27 +56,25 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="expert-mode-toggle">
|
<div
|
||||||
<nz-switch
|
class="account-trigger"
|
||||||
[ngModel]="expertMode()"
|
nz-dropdown
|
||||||
(ngModelChange)="settingsService.setExpertMode($event)"
|
nzTrigger="click"
|
||||||
nzSize="small"
|
[nzClickHide]="false"
|
||||||
></nz-switch>
|
nzPlacement="bottomRight"
|
||||||
<span class="expert-mode-label">Expert</span>
|
[nzDropdownMenu]="accountMenu"
|
||||||
|
>
|
||||||
|
<div class="user-info">
|
||||||
|
@if (username()) {
|
||||||
|
<span class="username">{{ username() }}</span>
|
||||||
|
}
|
||||||
|
<span class="user-id mono">{{ userId }}</span>
|
||||||
|
@if (currentKey()) {
|
||||||
|
<span class="key-id mono">{{ currentKey()!.keytoken_id }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span nz-icon nzType="down" class="account-caret"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info">
|
|
||||||
@if (username()) {
|
|
||||||
<span class="username">{{ username() }}</span>
|
|
||||||
}
|
|
||||||
<span class="user-id mono">{{ userId }}</span>
|
|
||||||
@if (currentKey()) {
|
|
||||||
<span class="key-id mono">{{ currentKey()!.keytoken_id }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<button nz-button nzType="text" nzDanger (click)="logout()">
|
|
||||||
<span nz-icon nzType="logout"></span>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nz-header>
|
</nz-header>
|
||||||
<nz-content class="content-area">
|
<nz-content class="content-area">
|
||||||
@@ -84,3 +82,65 @@
|
|||||||
</nz-content>
|
</nz-content>
|
||||||
</nz-layout>
|
</nz-layout>
|
||||||
</nz-layout>
|
</nz-layout>
|
||||||
|
|
||||||
|
<nz-dropdown-menu #accountMenu="nzDropdownMenu">
|
||||||
|
<div class="account-dropdown">
|
||||||
|
<!-- Settings -->
|
||||||
|
<div class="dropdown-section">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">
|
||||||
|
<span nz-icon nzType="setting"></span>
|
||||||
|
Expert mode
|
||||||
|
</span>
|
||||||
|
<nz-switch
|
||||||
|
[ngModel]="expertMode()"
|
||||||
|
(ngModelChange)="settingsService.setExpertMode($event)"
|
||||||
|
nzSize="small"
|
||||||
|
></nz-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
|
<!-- Accounts -->
|
||||||
|
<div class="dropdown-section accounts-section">
|
||||||
|
@for (acc of accounts(); track acc.userId) {
|
||||||
|
<div
|
||||||
|
class="account-item"
|
||||||
|
[class.active]="acc.userId === userId"
|
||||||
|
(click)="switchAccount(acc.userId)"
|
||||||
|
>
|
||||||
|
<span nz-icon nzType="user" class="account-avatar"></span>
|
||||||
|
<div class="account-meta">
|
||||||
|
<span class="account-name">{{ acc.username || acc.userId }}</span>
|
||||||
|
<span class="account-sub mono">{{ acc.userId }}</span>
|
||||||
|
</div>
|
||||||
|
@if (acc.userId === userId) {
|
||||||
|
<span nz-icon nzType="check" class="account-check"></span>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
nz-button
|
||||||
|
nzType="text"
|
||||||
|
nzSize="small"
|
||||||
|
nzDanger
|
||||||
|
class="account-logout"
|
||||||
|
title="Logout"
|
||||||
|
(click)="logoutAccount(acc.userId, $event)"
|
||||||
|
>
|
||||||
|
<span nz-icon nzType="logout"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
|
<!-- Add account -->
|
||||||
|
<div class="dropdown-section">
|
||||||
|
<button nz-button nzType="text" class="add-account-btn" (click)="addAccount()">
|
||||||
|
<span nz-icon nzType="user-add"></span>
|
||||||
|
Login with another account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nz-dropdown-menu>
|
||||||
|
|||||||
@@ -51,17 +51,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expert-mode-toggle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.expert-mode-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-trigger {
|
.header-trigger {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -79,11 +68,43 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Account switcher trigger (in header) ---
|
||||||
|
.account-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 260px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-caret {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.username,
|
||||||
|
.user-id,
|
||||||
|
.key-id {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
color: #333;
|
color: #333;
|
||||||
@@ -102,6 +123,120 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Account switcher dropdown panel ---
|
||||||
|
.account-dropdown {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
margin: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accounts-section {
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #e6f4ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d6ecff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-avatar {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #888;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.3;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.account-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-check {
|
||||||
|
color: #1890ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-logout {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-account-btn {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.content-area {
|
.content-area {
|
||||||
margin-left: 240px;
|
margin-left: 240px;
|
||||||
transition: margin-left 0.2s;
|
transition: margin-left 0.2s;
|
||||||
@@ -122,3 +257,25 @@ nz-layout:has(.ant-layout-sider-collapsed) .content-area {
|
|||||||
margin-left: 80px;
|
margin-left: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mobile ---
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.app-header {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-trigger {
|
||||||
|
max-width: 62vw;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
// Keep the trigger to one identifying line on small screens.
|
||||||
|
.user-info .key-id {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export class MainLayoutComponent implements OnInit {
|
|||||||
|
|
||||||
isCollapsed = signal(false);
|
isCollapsed = signal(false);
|
||||||
userId = this.authService.getUserId();
|
userId = this.authService.getUserId();
|
||||||
|
accounts = this.authService.accounts;
|
||||||
currentKey = signal<KeyToken | null>(null);
|
currentKey = signal<KeyToken | null>(null);
|
||||||
username = signal<string | null>(null);
|
username = signal<string | null>(null);
|
||||||
expertMode = this.settingsService.expertMode;
|
expertMode = this.settingsService.expertMode;
|
||||||
@@ -62,7 +63,11 @@ export class MainLayoutComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.apiService.getUser(userId).subscribe({
|
this.apiService.getUser(userId).subscribe({
|
||||||
next: (user) => this.username.set(user.username)
|
next: (user) => {
|
||||||
|
this.username.set(user.username);
|
||||||
|
// Cache the resolved name so the account switcher can label it.
|
||||||
|
this.authService.setActiveUsername(user.username);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +75,27 @@ export class MainLayoutComponent implements OnInit {
|
|||||||
this.isCollapsed.update(v => !v);
|
this.isCollapsed.update(v => !v);
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void {
|
switchAccount(userId: string): void {
|
||||||
this.authService.logout();
|
if (userId === this.userId) return;
|
||||||
|
this.authService.switchAccount(userId);
|
||||||
|
// Full reload so all per-account state (caches, component signals) resets cleanly.
|
||||||
|
window.location.assign('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutAccount(userId: string, event: Event): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
const wasActive = userId === this.authService.getUserId();
|
||||||
|
this.authService.logoutAccount(userId);
|
||||||
|
|
||||||
|
if (!this.authService.isAuthenticated()) {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
} else if (wasActive) {
|
||||||
|
window.location.assign('/');
|
||||||
|
}
|
||||||
|
// Otherwise a background account was removed; the accounts() signal updates the list.
|
||||||
|
}
|
||||||
|
|
||||||
|
addAccount(): void {
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user