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

@@ -1,4 +1,8 @@
import { expect, type Page } from '@playwright/test';
import {
expect,
type APIRequestContext,
type Page
} from '@playwright/test';
import { test, type Client } from '../../fixtures/multi-client';
import { installTestServerEndpoints, type SeededEndpointInput } from '../../helpers/seed-test-endpoint';
import { startTestServer } from '../../helpers/test-server';
@@ -11,6 +15,11 @@ import {
waitForConnectedPeerCount,
waitForPeerConnected
} from '../../helpers/webrtc-helpers';
import {
authHeaders,
readAuthTokenFromPage,
registerTestUser
} from '../../helpers/auth-api';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatRoomPage } from '../../pages/chat-room.page';
@@ -104,6 +113,7 @@ function endpointsForGroup(
test.describe('Mixed signal-config voice', () => {
test('8 users with different signal configs can voice, mute, deafen, and chat concurrently', async ({
createClient,
request,
testServer
}) => {
test.setTimeout(720_000);
@@ -144,6 +154,26 @@ test.describe('Mixed signal-config voice', () => {
await test.step('Create voice room on primary and chat room on secondary', async () => {
// Use a "both" user (client 0) to create both rooms
const searchPage = new ServerSearchPage(clients[0].page);
const secondarySession = await registerTestUser(
request,
secondaryServer.url,
clients[0].user.username,
clients[0].user.password,
clients[0].user.displayName
);
await clients[0].page.evaluate(({ serverUrl, token, expiresAt }) => {
const storageKey = 'metoyou.authTokens';
const store = JSON.parse(localStorage.getItem(storageKey) || '{}') as Record<string, { token: string; expiresAt: number }>;
const normalizedUrl = serverUrl.trim().replace(/\/+$/, '');
store[normalizedUrl] = { token, expiresAt };
localStorage.setItem(storageKey, JSON.stringify(store));
}, {
serverUrl: secondaryServer.url,
token: secondarySession.token,
expiresAt: secondarySession.expiresAt
});
await searchPage.createServer(VOICE_ROOM_NAME, {
description: 'Voice room on primary signal',
@@ -152,12 +182,14 @@ test.describe('Mixed signal-config voice', () => {
await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
await searchPage.createServer(SECONDARY_ROOM_NAME, {
description: 'Chat room on secondary signal',
sourceId: SECONDARY_SIGNAL_ID
});
const secondaryRoom = await createServerViaApi(
request,
secondaryServer.url,
secondarySession,
SECONDARY_ROOM_NAME
);
await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
secondaryRoomId = secondaryRoom.id;
});
// ── Create invite links ─────────────────────────────────────
@@ -166,31 +198,39 @@ test.describe('Mixed signal-config voice', () => {
// Group D (secondary-only) needs invite to primary room.
let primaryRoomInviteUrl: string;
let secondaryRoomInviteUrl: string;
let secondaryRoomId = '';
await test.step('Create invite links for cross-signal rooms', async () => {
// Navigate to voice room to get its ID
await openSavedRoomByName(clients[0].page, VOICE_ROOM_NAME);
const primaryRoomId = await getCurrentRoomId(clients[0].page);
const userId = await getCurrentUserId(clients[0].page);
// Navigate to secondary room to get its ID
await openSavedRoomByName(clients[0].page, SECONDARY_ROOM_NAME);
const secondaryRoomId = await getCurrentRoomId(clients[0].page);
// Create invite for primary room (voice) via API
const primaryToken = await readAuthTokenFromPage(clients[0].page, testServer.url);
if (!primaryToken) {
throw new Error('Missing session token for primary signal invite creation');
}
const primaryInvite = await createInviteViaApi(
testServer.url,
primaryRoomId,
userId,
primaryToken,
clients[0].user.displayName
);
primaryRoomInviteUrl = `/invite/${primaryInvite.id}?server=${encodeURIComponent(testServer.url)}`;
// Create invite for secondary room (chat) via API
const secondaryToken = await readAuthTokenFromPage(clients[0].page, secondaryServer.url);
if (!secondaryToken) {
throw new Error('Missing session token for secondary signal invite creation');
}
const secondaryInvite = await createInviteViaApi(
secondaryServer.url,
secondaryRoomId,
userId,
secondaryToken,
clients[0].user.displayName
);
@@ -463,17 +503,55 @@ function buildUsers(): TestUser[] {
// ── API helpers ──────────────────────────────────────────────────────
async function createServerViaApi(
request: APIRequestContext,
serverBaseUrl: string,
owner: { id: string; token: string },
serverName: string
): Promise<{ id: string }> {
const response = await request.post(`${serverBaseUrl}/api/servers`, {
headers: authHeaders(owner.token),
data: {
channels: [
{
id: 'general-text',
name: 'general',
position: 0,
type: 'text'
}
],
description: `E2E room on ${serverBaseUrl}`,
id: `mixed-signal-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`,
isPrivate: false,
name: serverName,
ownerId: owner.id,
ownerPublicKey: 'mixed-signal-owner-public-key',
tags: ['e2e']
}
});
if (!response.ok()) {
throw new Error(`Failed to create server via API: ${response.status()} ${await response.text()}`);
}
return await response.json() as { id: string };
}
async function createInviteViaApi(
serverBaseUrl: string,
roomId: string,
userId: string,
authToken: string,
displayName: string
): Promise<{ id: string }> {
const response = await fetch(`${serverBaseUrl}/api/servers/${roomId}/invites`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`
},
body: JSON.stringify({
requesterUserId: userId,
requesterDisplayName: displayName
})
});
@@ -510,34 +588,6 @@ async function getCurrentRoomId(page: Page): Promise<string> {
});
}
async function getCurrentUserId(page: Page): Promise<string> {
return await page.evaluate(() => {
interface AngularDebugApi {
getComponent: (element: Element) => Record<string, unknown>;
}
interface UserShape {
id: string;
}
const host = document.querySelector('app-rooms-side-panel');
const debugApi = (window as { ng?: AngularDebugApi }).ng;
if (!host || !debugApi?.getComponent) {
throw new Error('Angular debug API unavailable');
}
const component = debugApi.getComponent(host);
const user = (component['currentUser'] as (() => UserShape | null) | undefined)?.();
if (!user?.id) {
throw new Error('Current user not found');
}
return user.id;
});
}
// ── Navigation helpers ───────────────────────────────────────────────
async function installDeterministicVoiceSettings(page: Page): Promise<void> {