fix: Fix multiple bugs with new authentication flow

This commit is contained in:
2026-06-07 15:04:21 +02:00
parent 9fc26b1ccf
commit 83456c018c
137 changed files with 4710 additions and 281 deletions
@@ -164,7 +164,11 @@ The browser also sends a lightweight `keepalive` message on the signaling socket
### Server-side connection hygiene
Browsers do not reliably fire WebSocket close events during page refresh or navigation (especially Chromium). The server's `handleIdentify` now closes any existing connection that shares the same `oderId` but a different `connectionId`. This guarantees `findUserByOderId` always routes offers and presence events to the freshest socket, eliminating a class of bugs where signaling messages landed on a dead tab's socket and were silently lost.
Browsers do not reliably fire WebSocket close events during page refresh or navigation (especially Chromium). On `identify`, the server evicts stale sockets that share the same `(oderId, connectionScope, clientInstanceId)` tuple so a refreshed tab does not leave a zombie connection behind.
Multi-device sessions keep **multiple** open connections for the same `oderId` (different `clientInstanceId` values). Server broadcasts exclude only the sending **connection id**, not the whole identity, so chat/typing/voice-state updates reach every logged-in device. Presence `user_joined` / `user_left` broadcasts still exclude the whole identity so other users never see duplicate join/leave events.
RTC offers/answers/ICE are routed to the connection marked `voiceActive` for the target user (fallback: any open connection). Voice ownership is tracked per connection from `voice_state` payloads that include `clientInstanceId`.
Join and leave broadcasts are also identity-aware: `handleJoinServer` only broadcasts `user_joined` when the identity is genuinely new to that server (not just a second WebSocket connection for the same user), and `handleLeaveServer` / dead-connection cleanup only broadcast `user_left` when no other open connection for that identity remains in the server. The `user_left` payload includes `serverIds` listing the rooms the identity still belongs to, so the client can subtract correctly without over-removing.
@@ -41,6 +41,7 @@ import { SignalingManager } from './signaling/signaling.manager';
import { SignalingTransportHandler } from './signaling/signaling-transport-handler';
import { WebRtcStateController } from './state/webrtc-state-controller';
import { AuthTokenStoreService } from '../../domains/authentication';
import { ClientInstanceService } from '../../core/platform/client-instance.service';
@Injectable({
providedIn: 'root'
@@ -51,6 +52,7 @@ export class WebRTCService implements OnDestroy {
private readonly screenShareSourcePicker = inject(ScreenShareSourcePickerService);
private readonly iceServerSettings = inject(IceServerSettingsService);
private readonly authTokenStore = inject(AuthTokenStoreService);
private readonly clientInstance = inject(ClientInstanceService);
private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
private readonly state = new WebRtcStateController();
@@ -161,7 +163,8 @@ export class WebRTCService implements OnDestroy {
}
return null;
}
},
getClientInstanceId: () => this.clientInstance.getClientInstanceId()
});
// Now wire up cross-references (all managers are instantiated)
@@ -691,11 +694,17 @@ export class WebRTCService implements OnDestroy {
}
private relayBroadcastEvent(event: ChatEvent): void {
const clientInstanceId = this.clientInstance.getClientInstanceId();
if (event.type === 'chat-message' && event.message?.roomId) {
this.signalingTransportHandler.sendRawMessage({
type: 'chat_message',
serverId: event.message.roomId,
message: event.message
message: {
...event.message,
clientInstanceId
},
clientInstanceId
});
return;
@@ -705,11 +714,27 @@ export class WebRTCService implements OnDestroy {
this.signalingTransportHandler.sendRawMessage({
...event,
type: 'voice_state',
serverId: event.voiceState.serverId
serverId: event.voiceState.serverId,
voiceState: {
...event.voiceState,
clientInstanceId
},
clientInstanceId
});
}
}
requestVoiceClientTakeover(): void {
this.signalingTransportHandler.sendRawMessage({
type: 'voice_client_takeover',
clientInstanceId: this.clientInstance.getClientInstanceId()
});
}
getClientInstanceId(): string {
return this.clientInstance.getClientInstanceId();
}
/** Disconnect from the signaling server and clean up all state. */
disconnect(): void {
this.leaveRoom();
@@ -44,6 +44,8 @@ export interface IdentifyCredentials {
profileUpdatedAt?: number;
/** Public signal-server URL where this user registered. */
homeSignalServerUrl?: string;
/** Stable per-install client id used for multi-device session routing. */
clientInstanceId?: string;
}
/** Last-joined server info, used for reconnection. */
@@ -76,4 +78,6 @@ export interface VoiceStateSnapshot {
roomId?: string;
/** The voice channel server ID, if applicable. */
serverId?: string;
/** Install-scoped client id that owns active voice for this snapshot. */
clientInstanceId?: string;
}
@@ -14,6 +14,7 @@ interface SignalingTransportHandlerDependencies<TMessage> {
logger: WebRTCLogger;
getLocalPeerId(): string;
resolveSessionToken(signalUrl?: string): string | null;
getClientInstanceId(): string;
}
export class SignalingTransportHandler<TMessage> {
@@ -201,13 +202,16 @@ export class SignalingTransportHandler<TMessage> {
return;
}
const clientInstanceId = this.dependencies.getClientInstanceId();
this.lastIdentifyCredentials = {
oderId,
token,
displayName: normalizedDisplayName,
description: normalizedDescription,
profileUpdatedAt: normalizedProfileUpdatedAt,
homeSignalServerUrl: normalizedHomeSignalServerUrl
homeSignalServerUrl: normalizedHomeSignalServerUrl,
clientInstanceId
};
if (signalUrl) {
@@ -219,7 +223,8 @@ export class SignalingTransportHandler<TMessage> {
description: normalizedDescription,
profileUpdatedAt: normalizedProfileUpdatedAt,
homeSignalServerUrl: normalizedHomeSignalServerUrl,
connectionScope: signalUrl
connectionScope: signalUrl,
clientInstanceId
});
return;
@@ -240,7 +245,8 @@ export class SignalingTransportHandler<TMessage> {
description: normalizedDescription,
profileUpdatedAt: normalizedProfileUpdatedAt,
homeSignalServerUrl: normalizedHomeSignalServerUrl,
connectionScope: managerSignalUrl
connectionScope: managerSignalUrl,
clientInstanceId
});
}
}
@@ -379,7 +379,8 @@ export class SignalingManager {
description: credentials.description,
profileUpdatedAt: credentials.profileUpdatedAt,
homeSignalServerUrl: credentials.homeSignalServerUrl,
connectionScope: this.lastSignalingUrl ?? undefined
connectionScope: this.lastSignalingUrl ?? undefined,
clientInstanceId: credentials.clientInstanceId
});
}