detached 93e8e7a

fix(security): prevent sensitive user data exposure by enforcing SafeUser type throughout frontend

commit
93e8e7a9fe6d04110cb3b1b3bc5ec5f4b6efb348
author
Kit · · 2025-02-17 10:29 UTC
parent
17f3fb1
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
3 files · +59 -57
M src/lib/components/aim-login.svelte +14 -15
@@ -2,20 +2,19 @@
22 <script lang="ts">
33 import { draggable } from '$lib/actions/draggable';
44 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import type { User } from '$lib/types/chat';
5+ import type { User, SafeUser } from '$lib/types/chat';
66 import { invalidateAll } from '$app/navigation';
77 import { chatState } from '$lib/states/chat.svelte';
88 import Turnstile from './turnstile.svelte';
9+ import { createSafeUser } from '$lib/types/chat';
910
10- let {
11- onLoginSuccess,
12- showAuth = $bindable(),
13- activeTab: initialTab = 'signin'
14- }: {
15- onLoginSuccess?: (user: User) => void,
16- showAuth: Boolean,
17- activeTab?: 'signin' | 'signup'
18- } = $props();
11+ interface $$Props {
12+ showAuth: boolean;
13+ onLoginSuccess?: (user: SafeUser | null) => void;
14+ activeTab?: 'signin' | 'signup';
15+ }
16+
17+ let { showAuth, onLoginSuccess, activeTab: initialTab = 'signin' } = $props();
1918
2019 let activeTab = $state(initialTab);
2120
@@ -69,12 +68,12 @@
6968 return;
7069 }
7170
72- console.debug("Login succeeded:", data);
73- await chatState.setCurrentUser(data.user);
71+ console.debug("Login succeeded:", { ...data, user: data.user ? { ...data.user, password: '[REDACTED]' } : null });
72+ await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
7473 loginStatus = 'success';
7574
7675 // Call the callback prop if provided
77- onLoginSuccess?.(data.user);
76+ onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
7877 // Auto close login window after 3 seconds on successful login
7978 setTimeout(() => {
8079 console.debug("Auto closing AIM Login component after successful login");
@@ -152,8 +151,8 @@
152151 return;
153152 }
154153 console.debug("Auto login after registration succeeded:", data);
155- await chatState.setCurrentUser(data.user);
156- onLoginSuccess?.(data.user);
154+ await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
155+ onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
157156 setTimeout(() => {
158157 console.debug("Auto closing AIM Login component after auto login on registration");
159158 invalidateAll();
@@ -2,20 +2,19 @@
2 <script lang="ts">2 <script lang="ts">
3 import { draggable } from '$lib/actions/draggable';3 import { draggable } from '$lib/actions/draggable';
4 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';4 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import type { User } from '$lib/types/chat';5+ import type { User, SafeUser } from '$lib/types/chat';
6 import { invalidateAll } from '$app/navigation';6 import { invalidateAll } from '$app/navigation';
7 import { chatState } from '$lib/states/chat.svelte';7 import { chatState } from '$lib/states/chat.svelte';
8 import Turnstile from './turnstile.svelte';8 import Turnstile from './turnstile.svelte';
9+ import { createSafeUser } from '$lib/types/chat';
9 10
10- let { 11+ interface $$Props {
11- onLoginSuccess, 12+ showAuth: boolean;
12- showAuth = $bindable(),13+ onLoginSuccess?: (user: SafeUser | null) => void;
13- activeTab: initialTab = 'signin'14+ activeTab?: 'signin' | 'signup';
14- }: { 15+ }
15- onLoginSuccess?: (user: User) => void, 16+
16- showAuth: Boolean,17+ let { showAuth, onLoginSuccess, activeTab: initialTab = 'signin' } = $props();
17- activeTab?: 'signin' | 'signup'
18- } = $props();
19 18
20 let activeTab = $state(initialTab);19 let activeTab = $state(initialTab);
21 20
@@ -69,12 +68,12 @@
69 return;68 return;
70 }69 }
71 70
72- console.debug("Login succeeded:", data);71+ console.debug("Login succeeded:", { ...data, user: data.user ? { ...data.user, password: '[REDACTED]' } : null });
73- await chatState.setCurrentUser(data.user);72+ await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
74 loginStatus = 'success';73 loginStatus = 'success';
75 74
76 // Call the callback prop if provided75 // Call the callback prop if provided
77- onLoginSuccess?.(data.user);76+ onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
78 // Auto close login window after 3 seconds on successful login77 // Auto close login window after 3 seconds on successful login
79 setTimeout(() => {78 setTimeout(() => {
80 console.debug("Auto closing AIM Login component after successful login");79 console.debug("Auto closing AIM Login component after successful login");
@@ -152,8 +151,8 @@
152 return;151 return;
153 }152 }
154 console.debug("Auto login after registration succeeded:", data);153 console.debug("Auto login after registration succeeded:", data);
155- await chatState.setCurrentUser(data.user);154+ await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
156- onLoginSuccess?.(data.user);155+ onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
157 setTimeout(() => {156 setTimeout(() => {
158 console.debug("Auto closing AIM Login component after auto login on registration");157 console.debug("Auto closing AIM Login component after auto login on registration");
159 invalidateAll();158 invalidateAll();
M src/lib/components/chat-room.svelte +3 -3
@@ -1,6 +1,6 @@
11 <script lang="ts">
22 import { chatState } from '../states/chat.svelte';
3-import type { MessageType, User, ChatRoom, Message, EnrichedMessage } from '../types/chat';
3+import type { MessageType, User, ChatRoom, Message, EnrichedMessage, SafeUser } from '../types/chat';
44 import { onMount } from 'svelte';
55 import { browser } from '$app/environment';
66 import { draggable } from '$lib/actions/draggable';
@@ -63,8 +63,8 @@ function handleClose() {
6363
6464 // Reactive state using derived values
6565 let messages = $state<EnrichedMessage[]>([]);
66-let onlineUsers = $state<User[]>([]);
67-let currentUser = $state<User | null>(null);
66+let onlineUsers = $state<SafeUser[]>([]);
67+let currentUser = $state<SafeUser | null>(null);
6868 let isLoadingMore = $state(false);
6969 let hasMoreMessages = $state(true);
7070 let oldestMessageTimestamp = $state<number | null>(null);
@@ -1,6 +1,6 @@
1 <script lang="ts">1 <script lang="ts">
2 import { chatState } from '../states/chat.svelte';2 import { chatState } from '../states/chat.svelte';
3-import type { MessageType, User, ChatRoom, Message, EnrichedMessage } from '../types/chat';3+import type { MessageType, User, ChatRoom, Message, EnrichedMessage, SafeUser } from '../types/chat';
4 import { onMount } from 'svelte';4 import { onMount } from 'svelte';
5 import { browser } from '$app/environment';5 import { browser } from '$app/environment';
6 import { draggable } from '$lib/actions/draggable';6 import { draggable } from '$lib/actions/draggable';
@@ -63,8 +63,8 @@ function handleClose() {
63 63
64 // Reactive state using derived values64 // Reactive state using derived values
65 let messages = $state<EnrichedMessage[]>([]);65 let messages = $state<EnrichedMessage[]>([]);
66-let onlineUsers = $state<User[]>([]);66+let onlineUsers = $state<SafeUser[]>([]);
67-let currentUser = $state<User | null>(null);67+let currentUser = $state<SafeUser | null>(null);
68 let isLoadingMore = $state(false);68 let isLoadingMore = $state(false);
69 let hasMoreMessages = $state(true);69 let hasMoreMessages = $state(true);
70 let oldestMessageTimestamp = $state<number | null>(null);70 let oldestMessageTimestamp = $state<number | null>(null);
M src/lib/states/chat.svelte.ts +42 -39
@@ -1,4 +1,5 @@
1-import type { User, Message, EnrichedMessage } from '../types/chat';
1+import type { User, Message, EnrichedMessage, SafeUser } from '../types/chat';
2+import { createSafeUser } from '../types/chat';
23 import type {
34 SendMessageRequest,
45 SendMessageResponse,
@@ -11,16 +12,16 @@ import { env } from '$env/dynamic/public';
1112 const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001';
1213
1314 class ChatState {
14- private users = $state<User[]>([]);
15+ private users = $state<SafeUser[]>([]);
1516 private messages = $state<Message[]>([]);
16- private currentUser = $state<User | null>(null);
17+ private currentUser = $state<SafeUser | null>(null);
1718 private eventSource: EventSource | null = null;
1819 private reconnectAttempts = 0;
1920 private maxReconnectAttempts = 5;
2021 private reconnectDelay = 1000; // Start with 1 second
2122 private isInitializing = $state(false);
2223 private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID);
23- private userCache = $state<Record<string, User>>({});
24+ private userCache = $state<Record<string, SafeUser>>({});
2425 private sseError = $state<string | null>(null);
2526 private sseRetryAfter = $state<number | null>(null);
2627 private connectionTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -80,8 +81,8 @@ class ChatState {
8081 return this.currentUser;
8182 }
8283
83- async setCurrentUser(user: User | null) {
84- console.debug('setCurrentUser called with', user);
84+ async setCurrentUser(user: User | SafeUser | null) {
85+ console.debug('setCurrentUser called with', user ? { ...user, password: '[REDACTED]' } : null);
8586
8687 // Prevent concurrent setCurrentUser calls
8788 if (this.isSettingUser) {
@@ -97,9 +98,12 @@ class ChatState {
9798 console.debug('No user to set and no current user, skipping');
9899 return;
99100 }
101+
102+ // Convert to SafeUser if full User object is passed
103+ const safeUser = user ? ('password' in user ? createSafeUser(user) : user) : null;
100104
101105 // If same user is being set, check SSE connection
102- if (user?.id === this.currentUser?.id) {
106+ if (safeUser?.id === this.currentUser?.id) {
103107 if (!this.eventSource) {
104108 console.debug('Same user being set but no active SSE connection, reinitializing.');
105109 } else {
@@ -110,11 +114,11 @@ class ChatState {
110114
111115 // Clear existing messages before setting new user
112116 this.messages = [];
113- this.currentUser = user;
117+ this.currentUser = safeUser;
114118
115- if (user) {
119+ if (safeUser) {
116120 // Update user cache with current user
117- this.userCache[user.id] = user;
121+ this.userCache[safeUser.id] = safeUser;
118122
119123 // Only reinitialize if we have a valid user
120124 await this.reinitialize();
@@ -178,8 +182,8 @@ class ChatState {
178182
179183 getOnlineUsers() {
180184 // Sort users by status: online first, then away, then busy, then offline
181- return [...this.users].sort((a: User, b: User) => {
182- const statusOrder: Record<User['status'], number> = {
185+ return [...this.users].sort((a: SafeUser, b: SafeUser) => {
186+ const statusOrder: Record<SafeUser['status'], number> = {
183187 'online': 0,
184188 'away': 1,
185189 'busy': 2,
@@ -196,12 +200,10 @@ class ChatState {
196200 }
197201
198202 // Return a fallback object and cache it to avoid repeated fetches
199- const fallback = {
203+ const fallback: SafeUser = {
200204 id: userId,
201205 nickname: 'Unknown User',
202206 status: 'offline',
203- password: '', // system user, no password needed
204- createdAt: Date.now(),
205207 lastSeen: null,
206208 avatarUrl: null
207209 };
@@ -231,11 +233,11 @@ class ChatState {
231233 }
232234
233235 // Try to get the user from cache first
234- let user: User | undefined = this.userCache[userId];
236+ let user: SafeUser | undefined = this.userCache[userId];
235237
236238 // If not in cache, try to find in users array
237239 if (!user) {
238- const foundUser = this.users.find((u: User) => u.id === userId);
240+ const foundUser = this.users.find((u: SafeUser) => u.id === userId);
239241 if (foundUser) {
240242 user = foundUser;
241243 }
@@ -243,10 +245,9 @@ class ChatState {
243245
244246 const now = Date.now();
245247 if (user) {
246- const updatedUser: User = {
248+ const updatedUser: SafeUser = {
247249 ...user,
248250 status,
249- // Update lastSeen based on status
250251 lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)
251252 };
252253
@@ -254,7 +255,7 @@ class ChatState {
254255 this.userCache[userId] = updatedUser;
255256
256257 // Then update or add to users array
257- const index = this.users.findIndex((u: User) => u.id === userId);
258+ const index = this.users.findIndex((u: SafeUser) => u.id === userId);
258259 if (index !== -1) {
259260 // Create new array with updated user
260261 this.users = [
@@ -280,7 +281,7 @@ class ChatState {
280281 .then(response => response.json())
281282 .then(data => {
282283 if (data.success && data.user) {
283- const newUser: User = {
284+ const newUser: SafeUser = {
284285 ...data.user,
285286 status,
286287 lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen)
@@ -359,20 +360,21 @@ class ChatState {
359360 }
360361
361362 // Add method to update user cache for public access
362- updateUserCache(users: User[]) {
363- console.debug('Updating user cache with users:', users);
364- // Merge incoming users with the existing cache
363+ updateUserCache(users: (User | SafeUser)[]) {
364+ console.debug('Updating user cache with users:', users.map(u => ({ ...u, password: '[REDACTED]' })));
365+ // Merge incoming users with the existing cache, ensuring we only store SafeUser objects
365366 users.forEach(user => {
366- if (!this.userCache[user.id] || user.status !== this.userCache[user.id].status) {
367- this.userCache[user.id] = user;
367+ const safeUser = 'password' in user ? createSafeUser(user) : user;
368+ if (!this.userCache[user.id] || safeUser.status !== this.userCache[user.id].status) {
369+ this.userCache[user.id] = safeUser;
368370 // Update users array if not already present or if status changed
369371 const existingIndex = this.users.findIndex(u => u.id === user.id);
370372 if (existingIndex === -1) {
371- this.users = [...this.users, user];
372- } else if (user.status !== this.users[existingIndex].status) {
373+ this.users = [...this.users, safeUser];
374+ } else if (safeUser.status !== this.users[existingIndex].status) {
373375 this.users = [
374376 ...this.users.slice(0, existingIndex),
375- user,
377+ safeUser,
376378 ...this.users.slice(existingIndex + 1)
377379 ];
378380 }
@@ -381,27 +383,28 @@ class ChatState {
381383 }
382384
383385 // Add method to update online users for public access
384- updateOnlineUsers(users: User[]) {
386+ updateOnlineUsers(users: (User | SafeUser)[]) {
385387 const now = Date.now();
386- // Create a hash of the new users data
387- const newHash = JSON.stringify(users);
388+ // Convert all users to SafeUser and create a hash
389+ const safeUsers = users.map(user => 'password' in user ? createSafeUser(user) : user);
390+ const newHash = JSON.stringify(safeUsers);
388391
389392 // Only update if the data has changed
390393 if (newHash !== this.lastBuddyListHash) {
391- const statusCounts = users.reduce((acc, user) => {
394+ const statusCounts = safeUsers.reduce((acc, user) => {
392395 acc[user.status] = (acc[user.status] || 0) + 1;
393396 return acc;
394397 }, {} as Record<string, number>);
395398
396399 console.debug('Updating online users (data changed):', {
397- total: users.length,
400+ total: safeUsers.length,
398401 byStatus: statusCounts,
399402 timestamp: new Date(now).toISOString()
400403 });
401404
402- this.users = users;
405+ this.users = safeUsers;
403406 // Also update cache
404- users.forEach(user => {
407+ safeUsers.forEach(user => {
405408 this.userCache[user.id] = user;
406409 });
407410 this.lastBuddyListHash = newHash;
@@ -643,7 +646,7 @@ class ChatState {
643646 // Modify buddy list update handler
644647 this.eventSource.addEventListener('buddyListUpdate', async (event: MessageEvent) => {
645648 try {
646- const buddyList = JSON.parse(event.data) as User[];
649+ const buddyList = JSON.parse(event.data) as SafeUser[];
647650 this.updateOnlineUsers(buddyList);
648651 } catch (error) {
649652 console.debug('Error handling buddy list update via SSE:', error);
@@ -706,8 +709,8 @@ class ChatState {
706709 id: message.senderId,
707710 nickname: 'Unknown User',
708711 status: 'offline',
709- password: '',
710- createdAt: 0
712+ avatarUrl: null,
713+ lastSeen: null
711714 }
712715 }));
713716 }
@@ -1,4 +1,5 @@
1-import type { User, Message, EnrichedMessage } from '../types/chat';1+import type { User, Message, EnrichedMessage, SafeUser } from '../types/chat';
2+import { createSafeUser } from '../types/chat';
2 import type { 3 import type {
3 SendMessageRequest, 4 SendMessageRequest,
4 SendMessageResponse, 5 SendMessageResponse,
@@ -11,16 +12,16 @@ import { env } from '$env/dynamic/public';
11 const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001';12 const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001';
12 13
13 class ChatState {14 class ChatState {
14- private users = $state<User[]>([]);15+ private users = $state<SafeUser[]>([]);
15 private messages = $state<Message[]>([]);16 private messages = $state<Message[]>([]);
16- private currentUser = $state<User | null>(null);17+ private currentUser = $state<SafeUser | null>(null);
17 private eventSource: EventSource | null = null;18 private eventSource: EventSource | null = null;
18 private reconnectAttempts = 0;19 private reconnectAttempts = 0;
19 private maxReconnectAttempts = 5;20 private maxReconnectAttempts = 5;
20 private reconnectDelay = 1000; // Start with 1 second21 private reconnectDelay = 1000; // Start with 1 second
21 private isInitializing = $state(false);22 private isInitializing = $state(false);
22 private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID);23 private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID);
23- private userCache = $state<Record<string, User>>({});24+ private userCache = $state<Record<string, SafeUser>>({});
24 private sseError = $state<string | null>(null);25 private sseError = $state<string | null>(null);
25 private sseRetryAfter = $state<number | null>(null);26 private sseRetryAfter = $state<number | null>(null);
26 private connectionTimeout: ReturnType<typeof setTimeout> | null = null;27 private connectionTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -80,8 +81,8 @@ class ChatState {
80 return this.currentUser;81 return this.currentUser;
81 }82 }
82 83
83- async setCurrentUser(user: User | null) {84+ async setCurrentUser(user: User | SafeUser | null) {
84- console.debug('setCurrentUser called with', user);85+ console.debug('setCurrentUser called with', user ? { ...user, password: '[REDACTED]' } : null);
85 86
86 // Prevent concurrent setCurrentUser calls87 // Prevent concurrent setCurrentUser calls
87 if (this.isSettingUser) {88 if (this.isSettingUser) {
@@ -97,9 +98,12 @@ class ChatState {
97 console.debug('No user to set and no current user, skipping');98 console.debug('No user to set and no current user, skipping');
98 return;99 return;
99 }100 }
101+
102+ // Convert to SafeUser if full User object is passed
103+ const safeUser = user ? ('password' in user ? createSafeUser(user) : user) : null;
100 104
101 // If same user is being set, check SSE connection105 // If same user is being set, check SSE connection
102- if (user?.id === this.currentUser?.id) {106+ if (safeUser?.id === this.currentUser?.id) {
103 if (!this.eventSource) {107 if (!this.eventSource) {
104 console.debug('Same user being set but no active SSE connection, reinitializing.');108 console.debug('Same user being set but no active SSE connection, reinitializing.');
105 } else {109 } else {
@@ -110,11 +114,11 @@ class ChatState {
110 114
111 // Clear existing messages before setting new user115 // Clear existing messages before setting new user
112 this.messages = [];116 this.messages = [];
113- this.currentUser = user;117+ this.currentUser = safeUser;
114 118
115- if (user) {119+ if (safeUser) {
116 // Update user cache with current user120 // Update user cache with current user
117- this.userCache[user.id] = user;121+ this.userCache[safeUser.id] = safeUser;
118 122
119 // Only reinitialize if we have a valid user123 // Only reinitialize if we have a valid user
120 await this.reinitialize();124 await this.reinitialize();
@@ -178,8 +182,8 @@ class ChatState {
178 182
179 getOnlineUsers() {183 getOnlineUsers() {
180 // Sort users by status: online first, then away, then busy, then offline184 // Sort users by status: online first, then away, then busy, then offline
181- return [...this.users].sort((a: User, b: User) => {185+ return [...this.users].sort((a: SafeUser, b: SafeUser) => {
182- const statusOrder: Record<User['status'], number> = {186+ const statusOrder: Record<SafeUser['status'], number> = {
183 'online': 0,187 'online': 0,
184 'away': 1,188 'away': 1,
185 'busy': 2,189 'busy': 2,
@@ -196,12 +200,10 @@ class ChatState {
196 }200 }
197 201
198 // Return a fallback object and cache it to avoid repeated fetches202 // Return a fallback object and cache it to avoid repeated fetches
199- const fallback = {203+ const fallback: SafeUser = {
200 id: userId,204 id: userId,
201 nickname: 'Unknown User',205 nickname: 'Unknown User',
202 status: 'offline',206 status: 'offline',
203- password: '', // system user, no password needed
204- createdAt: Date.now(),
205 lastSeen: null,207 lastSeen: null,
206 avatarUrl: null208 avatarUrl: null
207 };209 };
@@ -231,11 +233,11 @@ class ChatState {
231 }233 }
232 234
233 // Try to get the user from cache first235 // Try to get the user from cache first
234- let user: User | undefined = this.userCache[userId];236+ let user: SafeUser | undefined = this.userCache[userId];
235 237
236 // If not in cache, try to find in users array238 // If not in cache, try to find in users array
237 if (!user) {239 if (!user) {
238- const foundUser = this.users.find((u: User) => u.id === userId);240+ const foundUser = this.users.find((u: SafeUser) => u.id === userId);
239 if (foundUser) {241 if (foundUser) {
240 user = foundUser;242 user = foundUser;
241 }243 }
@@ -243,10 +245,9 @@ class ChatState {
243 245
244 const now = Date.now();246 const now = Date.now();
245 if (user) {247 if (user) {
246- const updatedUser: User = {248+ const updatedUser: SafeUser = {
247 ...user,249 ...user,
248 status,250 status,
249- // Update lastSeen based on status
250 lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)251 lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)
251 };252 };
252 253
@@ -254,7 +255,7 @@ class ChatState {
254 this.userCache[userId] = updatedUser;255 this.userCache[userId] = updatedUser;
255 256
256 // Then update or add to users array257 // Then update or add to users array
257- const index = this.users.findIndex((u: User) => u.id === userId);258+ const index = this.users.findIndex((u: SafeUser) => u.id === userId);
258 if (index !== -1) {259 if (index !== -1) {
259 // Create new array with updated user260 // Create new array with updated user
260 this.users = [261 this.users = [
@@ -280,7 +281,7 @@ class ChatState {
280 .then(response => response.json())281 .then(response => response.json())
281 .then(data => {282 .then(data => {
282 if (data.success && data.user) {283 if (data.success && data.user) {
283- const newUser: User = {284+ const newUser: SafeUser = {
284 ...data.user,285 ...data.user,
285 status,286 status,
286 lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen)287 lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen)
@@ -359,20 +360,21 @@ class ChatState {
359 }360 }
360 361
361 // Add method to update user cache for public access362 // Add method to update user cache for public access
362- updateUserCache(users: User[]) {363+ updateUserCache(users: (User | SafeUser)[]) {
363- console.debug('Updating user cache with users:', users);364+ console.debug('Updating user cache with users:', users.map(u => ({ ...u, password: '[REDACTED]' })));
364- // Merge incoming users with the existing cache365+ // Merge incoming users with the existing cache, ensuring we only store SafeUser objects
365 users.forEach(user => {366 users.forEach(user => {
366- if (!this.userCache[user.id] || user.status !== this.userCache[user.id].status) {367+ const safeUser = 'password' in user ? createSafeUser(user) : user;
367- this.userCache[user.id] = user;368+ if (!this.userCache[user.id] || safeUser.status !== this.userCache[user.id].status) {
369+ this.userCache[user.id] = safeUser;
368 // Update users array if not already present or if status changed370 // Update users array if not already present or if status changed
369 const existingIndex = this.users.findIndex(u => u.id === user.id);371 const existingIndex = this.users.findIndex(u => u.id === user.id);
370 if (existingIndex === -1) {372 if (existingIndex === -1) {
371- this.users = [...this.users, user];373+ this.users = [...this.users, safeUser];
372- } else if (user.status !== this.users[existingIndex].status) {374+ } else if (safeUser.status !== this.users[existingIndex].status) {
373 this.users = [375 this.users = [
374 ...this.users.slice(0, existingIndex),376 ...this.users.slice(0, existingIndex),
375- user,377+ safeUser,
376 ...this.users.slice(existingIndex + 1)378 ...this.users.slice(existingIndex + 1)
377 ];379 ];
378 }380 }
@@ -381,27 +383,28 @@ class ChatState {
381 }383 }
382 384
383 // Add method to update online users for public access385 // Add method to update online users for public access
384- updateOnlineUsers(users: User[]) {386+ updateOnlineUsers(users: (User | SafeUser)[]) {
385 const now = Date.now();387 const now = Date.now();
386- // Create a hash of the new users data388+ // Convert all users to SafeUser and create a hash
387- const newHash = JSON.stringify(users);389+ const safeUsers = users.map(user => 'password' in user ? createSafeUser(user) : user);
390+ const newHash = JSON.stringify(safeUsers);
388 391
389 // Only update if the data has changed392 // Only update if the data has changed
390 if (newHash !== this.lastBuddyListHash) {393 if (newHash !== this.lastBuddyListHash) {
391- const statusCounts = users.reduce((acc, user) => {394+ const statusCounts = safeUsers.reduce((acc, user) => {
392 acc[user.status] = (acc[user.status] || 0) + 1;395 acc[user.status] = (acc[user.status] || 0) + 1;
393 return acc;396 return acc;
394 }, {} as Record<string, number>);397 }, {} as Record<string, number>);
395 398
396 console.debug('Updating online users (data changed):', {399 console.debug('Updating online users (data changed):', {
397- total: users.length,400+ total: safeUsers.length,
398 byStatus: statusCounts,401 byStatus: statusCounts,
399 timestamp: new Date(now).toISOString()402 timestamp: new Date(now).toISOString()
400 });403 });
401 404
402- this.users = users;405+ this.users = safeUsers;
403 // Also update cache406 // Also update cache
404- users.forEach(user => {407+ safeUsers.forEach(user => {
405 this.userCache[user.id] = user;408 this.userCache[user.id] = user;
406 });409 });
407 this.lastBuddyListHash = newHash;410 this.lastBuddyListHash = newHash;
@@ -643,7 +646,7 @@ class ChatState {
643 // Modify buddy list update handler646 // Modify buddy list update handler
644 this.eventSource.addEventListener('buddyListUpdate', async (event: MessageEvent) => {647 this.eventSource.addEventListener('buddyListUpdate', async (event: MessageEvent) => {
645 try {648 try {
646- const buddyList = JSON.parse(event.data) as User[];649+ const buddyList = JSON.parse(event.data) as SafeUser[];
647 this.updateOnlineUsers(buddyList);650 this.updateOnlineUsers(buddyList);
648 } catch (error) {651 } catch (error) {
649 console.debug('Error handling buddy list update via SSE:', error);652 console.debug('Error handling buddy list update via SSE:', error);
@@ -706,8 +709,8 @@ class ChatState {
706 id: message.senderId,709 id: message.senderId,
707 nickname: 'Unknown User',710 nickname: 'Unknown User',
708 status: 'offline',711 status: 'offline',
709- password: '',712+ avatarUrl: null,
710- createdAt: 0713+ lastSeen: null
711 }714 }
712 }));715 }));
713 }716 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close