feat: Security

This commit is contained in:
2026-06-05 18:34:01 +02:00
parent ee293d7daf
commit 45675192a5
134 changed files with 4128 additions and 446 deletions

View File

@@ -3,6 +3,7 @@
*/
/* eslint-disable @typescript-eslint/member-ordering */
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import {
Actions,
createEffect,
@@ -20,7 +21,8 @@ import {
catchError,
withLatestFrom,
tap,
switchMap
switchMap,
filter
} from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { MessagesActions } from '../messages/messages.actions';
@@ -47,9 +49,11 @@ import {
Room,
User
} from '../../shared-kernel';
import { setStoredCurrentUserId } from '../../core/storage/current-user-storage';
import { clearStoredCurrentUserId, setStoredCurrentUserId } from '../../core/storage/current-user-storage';
import { findRoomMember, removeRoomMember } from '../rooms/room-members.helpers';
import { AppI18nService } from '../../core/i18n';
import { AuthTokenStoreService } from '../../domains/authentication/application/services/auth-token-store.service';
import { hasValidPersistedSession, SESSION_EXPIRED_ERROR_CODE } from '../../domains/authentication/domain/logic/auth-session.rules';
type IncomingModerationExtraAction =
| ReturnType<typeof RoomsActions.forgetRoom>
@@ -68,6 +72,8 @@ export class UsersEffects {
private serverDirectory = inject(ServerDirectoryFacade);
private webrtc = inject(RealtimeSessionFacade);
private readonly i18n = inject(AppI18nService);
private readonly authTokenStore = inject(AuthTokenStoreService);
private readonly router = inject(Router);
/** Prepares persisted state for a successful login before exposing the user in-memory. */
authenticateUser$ = createEffect(() =>
@@ -106,6 +112,14 @@ export class UsersEffects {
const sanitizedUser = this.clearStartupVoiceConnection(user);
if (!this.hasPersistedSessionToken(sanitizedUser)) {
clearStoredCurrentUserId();
return of(UsersActions.loadCurrentUserFailure({
error: SESSION_EXPIRED_ERROR_CODE
}));
}
if (sanitizedUser === user) {
return of(UsersActions.loadCurrentUserSuccess({ user }));
}
@@ -205,8 +219,6 @@ export class UsersEffects {
return this.serverDirectory.kickServerMember(
room.id,
{
actorUserId: currentUser.id,
actorRole: currentUser.role,
targetUserId: userId
},
this.toSourceSelector(room)
@@ -287,8 +299,6 @@ export class UsersEffects {
return this.serverDirectory.banServerMember(
room.id,
{
actorUserId: currentUser.id,
actorRole: currentUser.role,
targetUserId: userId,
banId: ban.oderId,
displayName: ban.displayName,
@@ -358,8 +368,6 @@ export class UsersEffects {
return this.serverDirectory.unbanServerMember(
room.id,
{
actorUserId: currentUser.id,
actorRole: currentUser.role,
banId: oderId
},
this.toSourceSelector(room)
@@ -477,6 +485,24 @@ export class UsersEffects {
{ dispatch: false }
);
/** Send users back to login when their persisted session token is missing or rejected. */
redirectOnSessionExpired$ = createEffect(
() =>
this.actions$.pipe(
ofType(UsersActions.loadCurrentUserFailure),
filter(({ error }) => error === SESSION_EXPIRED_ERROR_CODE),
tap(() => {
clearStoredCurrentUserId();
void this.router.navigate(['/login'], {
queryParams: {
returnUrl: this.router.url
}
});
})
),
{ dispatch: false }
);
/** Keep signaling identity aligned with the current profile to avoid stale fallback names. */
syncSignalingIdentity$ = createEffect(
() =>
@@ -511,6 +537,15 @@ export class UsersEffects {
return savedRooms.find((room) => room.id === roomId) ?? null;
}
private hasPersistedSessionToken(user: User): boolean {
return hasValidPersistedSession(
user,
this.serverDirectory.activeServer()?.url,
(serverUrl) => this.authTokenStore.getToken(serverUrl),
() => this.authTokenStore.hasAnyValidToken()
);
}
private resolveDisplayName(user: Pick<User, 'displayName' | 'username'>): string {
const displayName = user.displayName?.trim();