AI Implementation feature(913): Admin Area Players Management 1.00 (#57)

This commit was merged in pull request #57.
This commit is contained in:
2026-07-23 08:43:46 +00:00
parent e4ccf0fe13
commit d468cca766
11 changed files with 674 additions and 53 deletions
+100 -2
View File
@@ -10,10 +10,16 @@ import {
import {
CROSS_TAB_CHANNEL_NAME,
CROSS_TAB_STORAGE_KEY,
CROSS_TAB_ROLE_CHANNEL_NAME,
CROSS_TAB_ROLE_STORAGE_KEY,
CrossTabInvalidationMessage,
CrossTabRoleChangeMessage,
encodeInvalidationMessage,
encodeRoleChangeMessage,
isCrossTabInvalidationMessage,
isCrossTabRoleChangeMessage,
isStorageInvalidationEvent,
isStorageRoleChangeEvent,
} from './auth-session-events.pure';
export interface CurrentUser {
@@ -93,10 +99,12 @@ export class AuthService {
? crypto.randomUUID()
: `tab-${Math.random().toString(36).slice(2)}-${Date.now()}`;
private channel: BroadcastChannel | null = null;
private roleChannel: BroadcastChannel | null = null;
private storageListener: ((ev: StorageEvent) => void) | null = null;
private suppressBroadcast = false;
private peerInvalidationListeners = new Set<(reason: 'peer-logout' | 'sse-unauthorized') => void>();
private peerRoleChangeListeners = new Set<(msg: CrossTabRoleChangeMessage) => void>();
constructor(http?: HttpClient) {
this.http = http ?? inject(HttpClient);
@@ -242,6 +250,44 @@ export class AuthService {
return () => this.peerInvalidationListeners.delete(cb);
}
onPeerRoleChange(cb: (msg: CrossTabRoleChangeMessage) => void): () => void {
this.peerRoleChangeListeners.add(cb);
return () => this.peerRoleChangeListeners.delete(cb);
}
applyPeerRoleChange(newRole: 'admin' | 'player'): void {
const current = this.user();
if (!current) return;
if (current.role === newRole) return;
const updated = { ...current, role: newRole };
this.user.set(updated);
if (typeof window !== 'undefined') {
try {
writeStoredSession(this.sessionStorage(), { token: this.accessToken() ?? '', user: updated });
} catch {
// ignore storage failures
}
}
}
publishRoleChange(userId: string, newRole: 'admin' | 'player'): void {
const message = encodeRoleChangeMessage(this.crossTabOrigin, userId, newRole);
if (this.roleChannel) {
try {
this.roleChannel.postMessage(message);
} catch {
// ignore serialization failures; storage fallback still fires
}
}
if (typeof window !== 'undefined' && window.localStorage) {
try {
window.localStorage.setItem(CROSS_TAB_ROLE_STORAGE_KEY, JSON.stringify(message));
} catch {
// ignore quota / serialization errors
}
}
}
getAccessToken(): string | null {
return this.accessToken();
}
@@ -272,10 +318,21 @@ export class AuthService {
} catch {
this.channel = null;
}
try {
this.roleChannel = new BroadcastChannel(CROSS_TAB_ROLE_CHANNEL_NAME);
this.roleChannel.addEventListener('message', this.onRoleBroadcastMessage);
} catch {
this.roleChannel = null;
}
}
this.storageListener = (ev: StorageEvent) => {
if (!isStorageInvalidationEvent(ev)) return;
this.handlePeerInvalidation(ev.newValue, 'peer-logout');
if (isStorageInvalidationEvent(ev)) {
this.handlePeerInvalidation(ev.newValue, 'peer-logout');
return;
}
if (isStorageRoleChangeEvent(ev)) {
this.handlePeerRoleChange(ev.newValue);
}
};
window.addEventListener('storage', this.storageListener);
}
@@ -290,6 +347,15 @@ export class AuthService {
}
this.channel = null;
}
if (this.roleChannel) {
try {
this.roleChannel.removeEventListener('message', this.onRoleBroadcastMessage);
this.roleChannel.close();
} catch {
// ignore
}
this.roleChannel = null;
}
if (this.storageListener && typeof window !== 'undefined') {
try {
window.removeEventListener('storage', this.storageListener);
@@ -299,6 +365,7 @@ export class AuthService {
this.storageListener = null;
}
this.peerInvalidationListeners.clear();
this.peerRoleChangeListeners.clear();
}
private readonly onBroadcastMessage = (ev: MessageEvent<unknown>) => {
@@ -307,6 +374,12 @@ export class AuthService {
this.handlePeerInvalidation(JSON.stringify(ev.data), 'peer-logout');
};
private readonly onRoleBroadcastMessage = (ev: MessageEvent<unknown>) => {
if (!isCrossTabRoleChangeMessage(ev.data)) return;
if (ev.data.origin === this.crossTabOrigin) return;
this.handlePeerRoleChange(JSON.stringify(ev.data));
};
private handlePeerInvalidation(
raw: string | null,
reason: 'peer-logout' | 'sse-unauthorized',
@@ -337,6 +410,31 @@ export class AuthService {
}
}
private handlePeerRoleChange(raw: string | null): void {
if (!raw) return;
let parsed: CrossTabRoleChangeMessage | null = null;
try {
const candidate = JSON.parse(raw) as unknown;
if (!isCrossTabRoleChangeMessage(candidate)) return;
if (candidate.origin === this.crossTabOrigin) return;
parsed = candidate;
} catch {
return;
}
if (!parsed) return;
const me = this.user();
if (!me) return;
if (me.id !== parsed.userId) return;
this.applyPeerRoleChange(parsed.newRole);
for (const cb of this.peerRoleChangeListeners) {
try {
cb(parsed);
} catch {
// ignore listener errors
}
}
}
private sessionStorage(): Storage {
return typeof window !== 'undefined' ? window.sessionStorage : (undefined as unknown as Storage);
}