Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.5.0",
"identifier": "com.kodski.eid-wallet",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,25 @@ export class AuthController {

this.eventEmitter.on(id, handler);

// Send heartbeat every 30 seconds to keep connection alive
const heartbeatInterval = setInterval(() => {
try {
res.write(`: heartbeat\n\n`);
} catch (error) {
clearInterval(heartbeatInterval);
}
}, 30000);

// Handle client disconnect
req.on("close", () => {
clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});

req.on("error", (error) => {
console.error("SSE Error:", error);
clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
Expand Down
51 changes: 40 additions & 11 deletions platforms/blabsy/src/components/login/login-main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import QRCode from 'react-qr-code';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef, useCallback } from 'react';
import { useAuth } from '@lib/context/auth-context';
import { NextImage } from '@components/ui/next-image';
import Image from 'next/image';
Expand All @@ -10,8 +10,10 @@ export function LoginMain(): JSX.Element {
const { signInWithCustomToken } = useAuth();
const [qr, setQr] = useState<string>();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);

function watchEventStream(id: string): void {
const watchEventStream = useCallback((id: string): EventSource => {
const sseUrl = new URL(
`/api/auth/sessions/${id}`,
process.env.NEXT_PUBLIC_BASE_URL
Expand Down Expand Up @@ -49,20 +51,39 @@ export function LoginMain(): JSX.Element {
console.error('SSE connection error');
eventSource.close();
};
}

const getOfferData = async (): Promise<void> => {
return eventSource;
}, [signInWithCustomToken]);

const getOfferData = useCallback(async (): Promise<void> => {
// Clean up existing SSE connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// Clean up existing refresh timer
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}

const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current = watchEventStream(
new URL(data.uri).searchParams.get('session') as string
);
Comment on lines 68 to 77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling and validate session parameter.

Two concerns consistent with the Svelte implementation:

  1. The axios.get call isn't wrapped in try/catch - if it fails, setQr won't be called and eventSourceRef.current remains with the old (closed) value.
  2. The as string assertion on line 76 is unsafe - searchParams.get('session') can return null.
🛠️ Suggested fix
 const getOfferData = useCallback(async (): Promise<void> => {
     // Clean up existing SSE connection
     if (eventSourceRef.current) {
         eventSourceRef.current.close();
     }
     // Clean up existing refresh timer
     if (refreshTimerRef.current) {
         clearTimeout(refreshTimerRef.current);
     }

-    const { data } = await axios.get<{ uri: string }>(
-        new URL(
-            '/api/auth/offer',
-            process.env.NEXT_PUBLIC_BASE_URL
-        ).toString()
-    );
-    setQr(data.uri);
-    eventSourceRef.current = watchEventStream(
-        new URL(data.uri).searchParams.get('session') as string
-    );
+    try {
+        const { data } = await axios.get<{ uri: string }>(
+            new URL(
+                '/api/auth/offer',
+                process.env.NEXT_PUBLIC_BASE_URL
+            ).toString()
+        );
+        setQr(data.uri);
+        
+        const session = new URL(data.uri).searchParams.get('session');
+        if (!session) {
+            console.error('Session parameter missing from offer URI');
+            return;
+        }
+        eventSourceRef.current = watchEventStream(session);
+    } catch (error) {
+        console.error('Failed to fetch offer:', error);
+        return;
+    }

     // Set up auto-refresh after 60 seconds
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current = watchEventStream(
new URL(data.uri).searchParams.get('session') as string
);
const getOfferData = useCallback(async (): Promise<void> => {
// Clean up existing SSE connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// Clean up existing refresh timer
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
try {
const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
const session = new URL(data.uri).searchParams.get('session');
if (!session) {
console.error('Session parameter missing from offer URI');
return;
}
eventSourceRef.current = watchEventStream(session);
} catch (error) {
console.error('Failed to fetch offer:', error);
return;
}
// Set up auto-refresh after 60 seconds
🤖 Prompt for AI Agents
In `@platforms/blabsy/src/components/login/login-main.tsx` around lines 68 - 77,
Wrap the axios.get call in a try/catch around the block that calls setQr and
assigns eventSourceRef.current so network errors don't leave the old/closed
EventSource in place; on error, log/report the error and ensure
eventSourceRef.current remains closed or is cleaned up. After receiving
data.uri, extract the session value from new
URL(data.uri).searchParams.get('session') and explicitly check for null/empty
before calling watchEventStream; only call watchEventStream(session) and assign
eventSourceRef.current when session is present, otherwise handle the missing
session (error log or user-visible fallback) instead of using the unsafe `as
string` assertion.

};

// Set up auto-refresh after 60 seconds
refreshTimerRef.current = setTimeout(() => {
console.log('Refreshing QR code after 60 seconds');
getOfferData().catch((error) =>
console.error('Error refreshing QR code:', error)
);
}, 60000);
}, [watchEventStream]);

const getAppStoreLink = () => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
Expand Down Expand Up @@ -106,7 +127,17 @@ export function LoginMain(): JSX.Element {
getOfferData().catch((error) =>
console.error('Error fetching QR code data:', error)
);
}, []);

// Cleanup on unmount
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
};
}, [getOfferData]);

const handleAutoLogin = async (
ename: string,
Expand Down Expand Up @@ -209,8 +240,7 @@ export function LoginMain(): JSX.Element {
The button is valid for 60 seconds
</span>
<span className='block font-light text-gray-600'>
Please refresh the page if it
expires
It will refresh automatically
</span>
</p>
</div>
Expand All @@ -237,8 +267,7 @@ export function LoginMain(): JSX.Element {
The code is valid for 60 seconds
</span>
<span className='block font-light text-gray-600'>
Please refresh the page if it
expires
It will refresh automatically
</span>
</p>
</div>
Expand Down
11 changes: 11 additions & 0 deletions platforms/pictique-api/src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,25 @@ export class AuthController {

this.eventEmitter.on(id, handler);

// Send heartbeat every 30 seconds to keep connection alive
const heartbeatInterval = setInterval(() => {
try {
res.write(`: heartbeat\n\n`);
} catch (error) {
clearInterval(heartbeatInterval);
}
}, 30000);

// Handle client disconnect
req.on("close", () => {
clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});

req.on("error", (error) => {
console.error("SSE Error:", error);
clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
Expand Down
122 changes: 77 additions & 45 deletions platforms/pictique/src/routes/(auth)/auth/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
let qrData = $state<string>('');
let isMobile = $state(false);
let errorMessage = $state<string | null>(null);
let eventSource: EventSource | null = null;
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
function checkMobile() {
isMobile = window.innerWidth <= 640; // Tailwind's `sm` breakpoint
Expand Down Expand Up @@ -69,6 +71,71 @@
}
}
function watchEventStream(id: string) {
const sseUrl = new URL(`/api/auth/sessions/${id}`, PUBLIC_PICTIQUE_BASE_URL).toString();
const newEventSource = new EventSource(sseUrl);
newEventSource.onopen = () => {
console.log('Successfully connected.');
errorMessage = null;
};
newEventSource.onmessage = (e) => {
const data = JSON.parse(e.data as string);
// Check for error messages (version mismatch)
if (data.error && data.type === 'version_mismatch') {
errorMessage =
data.message ||
'Your eID Wallet app version is outdated. Please update to continue.';
newEventSource.close();
return;
}
Comment on lines +83 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for JSON parsing.

JSON.parse(e.data) can throw if the server sends malformed data. Wrap in try/catch to prevent unhandled exceptions.

🛠️ Suggested fix
 newEventSource.onmessage = (e) => {
-    const data = JSON.parse(e.data as string);
+    let data;
+    try {
+        data = JSON.parse(e.data as string);
+    } catch (parseError) {
+        console.error('Failed to parse SSE message:', parseError);
+        return;
+    }

     // Check for error messages (version mismatch)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
newEventSource.onmessage = (e) => {
const data = JSON.parse(e.data as string);
// Check for error messages (version mismatch)
if (data.error && data.type === 'version_mismatch') {
errorMessage =
data.message ||
'Your eID Wallet app version is outdated. Please update to continue.';
newEventSource.close();
return;
}
newEventSource.onmessage = (e) => {
let data;
try {
data = JSON.parse(e.data as string);
} catch (parseError) {
console.error('Failed to parse SSE message:', parseError);
return;
}
// Check for error messages (version mismatch)
if (data.error && data.type === 'version_mismatch') {
errorMessage =
data.message ||
'Your eID Wallet app version is outdated. Please update to continue.';
newEventSource.close();
return;
}
🤖 Prompt for AI Agents
In `@platforms/pictique/src/routes/`(auth)/auth/+page.svelte around lines 83 - 93,
Wrap the JSON.parse call inside the newEventSource.onmessage handler in a
try/catch to guard against malformed server data: inside the onmessage callback
(newEventSource.onmessage) attempt to parse e.data with JSON.parse in a try
block, and in the catch set/append a user-facing errorMessage (or log the error)
and call newEventSource.close() to stop the stream and prevent an unhandled
exception; ensure subsequent usage of the parsed variable only occurs if parsing
succeeded so the existing version_mismatch check (data.error && data.type ===
'version_mismatch') remains valid.

// Handle successful authentication
if (data.user && data.token) {
const { user } = data;
setAuthId(user.id);
const { token } = data;
setAuthToken(token);
goto('/home');
}
};
newEventSource.onerror = () => {
console.error('SSE connection error');
newEventSource.close();
};
return newEventSource;
}
async function fetchOfferAndSetupSSE() {
// Clean up existing SSE connection
if (eventSource) {
eventSource.close();
eventSource = null;
}
// Clean up existing refresh timer
if (refreshTimer) {
clearTimeout(refreshTimer);
refreshTimer = null;
}
const { data } = await apiClient.get('/api/auth/offer');
qrData = data.uri;
eventSource = watchEventStream(new URL(qrData).searchParams.get('session') as string);
Comment on lines +125 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling and validate session parameter.

Two concerns:

  1. The apiClient.get call isn't wrapped in try/catch - if it fails, the component will be left in an inconsistent state with no QR code.
  2. The as string assertion on line 128 is unsafe - searchParams.get('session') can return null if the session parameter is missing from the URI.
🛠️ Suggested fix
 async function fetchOfferAndSetupSSE() {
     // Clean up existing SSE connection
     if (eventSource) {
         eventSource.close();
         eventSource = null;
     }
     // Clean up existing refresh timer
     if (refreshTimer) {
         clearTimeout(refreshTimer);
         refreshTimer = null;
     }

-    const { data } = await apiClient.get('/api/auth/offer');
-    qrData = data.uri;
-
-    eventSource = watchEventStream(new URL(qrData).searchParams.get('session') as string);
+    try {
+        const { data } = await apiClient.get('/api/auth/offer');
+        qrData = data.uri;
+
+        const session = new URL(qrData).searchParams.get('session');
+        if (!session) {
+            console.error('Session parameter missing from offer URI');
+            return;
+        }
+        eventSource = watchEventStream(session);
+    } catch (error) {
+        console.error('Failed to fetch offer:', error);
+        return;
+    }

     // Set up auto-refresh after 60 seconds
🤖 Prompt for AI Agents
In `@platforms/pictique/src/routes/`(auth)/auth/+page.svelte around lines 125 -
128, Wrap the apiClient.get('/api/auth/offer') call in a try/catch and only
assign qrData and call watchEventStream after a successful response; on error
set a local error state or fallback (so the component doesn't remain in an
inconsistent state). After setting qrData, parse the URL and validate that new
URL(qrData).searchParams.get('session') is non-null and non-empty before passing
it to watchEventStream (do not use an unconditional "as string" cast); if the
session param is missing, handle it by setting an error state or aborting the
eventSource creation instead of calling watchEventStream with a null value.
Ensure references to apiClient.get, qrData, and watchEventStream/eventSource are
updated accordingly.

// Set up auto-refresh after 60 seconds
refreshTimer = setTimeout(() => {
console.log('Refreshing QR code after 60 seconds');
fetchOfferAndSetupSSE().catch((error) =>
console.error('Error refreshing QR code:', error)
);
}, 60000);
}
onMount(async () => {
checkMobile();
window.addEventListener('resize', checkMobile);
Expand All @@ -90,51 +157,17 @@
}
// If no query params, proceed with normal flow
const { data } = await apiClient.get('/api/auth/offer');
qrData = data.uri;
function watchEventStream(id: string) {
const sseUrl = new URL(`/api/auth/sessions/${id}`, PUBLIC_PICTIQUE_BASE_URL).toString();
const eventSource = new EventSource(sseUrl);
eventSource.onopen = () => {
console.log('Successfully connected.');
errorMessage = null;
};
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data as string);
// Check for error messages (version mismatch)
if (data.error && data.type === 'version_mismatch') {
errorMessage =
data.message ||
'Your eID Wallet app version is outdated. Please update to continue.';
eventSource.close();
return;
}
// Handle successful authentication
if (data.user && data.token) {
const { user } = data;
setAuthId(user.id);
const { token } = data;
setAuthToken(token);
goto('/home');
}
};
await fetchOfferAndSetupSSE();
});
eventSource.onerror = () => {
console.error('SSE connection error');
eventSource.close();
};
onDestroy(() => {
window.removeEventListener('resize', checkMobile);
if (eventSource) {
eventSource.close();
}
if (refreshTimer) {
clearTimeout(refreshTimer);
}
watchEventStream(new URL(qrData).searchParams.get('session') as string);
onDestroy(() => {
window.removeEventListener('resize', checkMobile);
});
});
</script>

Expand Down Expand Up @@ -211,8 +244,7 @@
<span class="mb-1 block font-bold text-gray-600"
>The {isMobileDevice() ? 'button' : 'code'} is valid for 60 seconds</span
>
<span class="block font-light text-gray-600">Please refresh the page if it expires</span
>
<span class="block font-light text-gray-600">It will refresh automatically</span>
</p>

<p class="w-full rounded-md bg-white/60 p-4 text-center text-xs leading-4 text-black/40">
Expand Down