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

@@ -0,0 +1,120 @@
import {
describe,
it,
expect,
beforeEach,
vi
} from 'vitest';
import { WebSocket } from 'ws';
import { connectedUsers } from './state';
import { ConnectedUser } from './types';
import { handleWebSocketMessage } from './handler';
vi.mock('../services/server-access.service', () => ({
authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const })),
findServerMembership: vi.fn(async () => ({ id: 'membership-1' })),
usersShareServerMembership: vi.fn(async () => false)
}));
vi.mock('../services/session-auth.service', () => ({
consumeSessionToken: vi.fn(async (token: string) => {
if (token !== 'valid-token') {
return null;
}
return {
token,
user: {
id: 'user-1',
username: 'alice',
displayName: 'Alice',
passwordHash: 'hash',
createdAt: Date.now()
},
issuedAt: Date.now(),
expiresAt: Date.now() + 60_000
};
})
}));
function createMockWs(): WebSocket & { sentMessages: string[] } {
const sent: string[] = [];
const ws = {
readyState: WebSocket.OPEN,
send: (data: string) => { sent.push(data); },
close: () => {},
sentMessages: sent
} as unknown as WebSocket & { sentMessages: string[] };
return ws;
}
function createConnectedUser(connectionId: string): ConnectedUser {
const ws = createMockWs();
const user: ConnectedUser = {
oderId: connectionId,
ws,
authenticated: false,
serverIds: new Set(),
displayName: 'Test User',
lastPong: Date.now()
};
connectedUsers.set(connectionId, user);
return user;
}
describe('server websocket handler - authentication', () => {
beforeEach(() => {
connectedUsers.clear();
vi.clearAllMocks();
});
it('rejects non-identify messages until the connection is authenticated', async () => {
createConnectedUser('conn-1');
await handleWebSocketMessage('conn-1', { type: 'typing', serverId: 'server-1' });
const user = connectedUsers.get('conn-1');
const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages;
const response = JSON.parse(sentMessages[0]) as { type: string };
expect(response.type).toBe('auth_required');
expect(user?.authenticated).toBe(false);
});
it('rejects identify without a session token', async () => {
createConnectedUser('conn-1');
await handleWebSocketMessage('conn-1', {
type: 'identify',
oderId: 'user-1',
displayName: 'Alice'
});
const user = connectedUsers.get('conn-1');
const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages;
const response = JSON.parse(sentMessages[0]) as { type: string; code: string };
expect(response.type).toBe('auth_error');
expect(response.code).toBe('MISSING_TOKEN');
expect(user?.authenticated).toBe(false);
});
it('binds identify to the authenticated user id from the token', async () => {
createConnectedUser('conn-1');
await handleWebSocketMessage('conn-1', {
type: 'identify',
token: 'valid-token',
oderId: 'user-1',
displayName: 'Alice'
});
const user = connectedUsers.get('conn-1');
expect(user?.authenticated).toBe(true);
expect(user?.oderId).toBe('user-1');
});
});