diff --git a/infrastructure/control-panel/src/lib/services/evaultService.ts b/infrastructure/control-panel/src/lib/services/evaultService.ts index 216b0f481..4c1f00118 100644 --- a/infrastructure/control-panel/src/lib/services/evaultService.ts +++ b/infrastructure/control-panel/src/lib/services/evaultService.ts @@ -79,7 +79,10 @@ export class EVaultService { */ static async getEVaultLogs(evaultId: string, tail?: number): Promise { try { - const url = new URL(`/api/evaults/${encodeURIComponent(evaultId)}/logs`, window.location.origin); + const url = new URL( + `/api/evaults/${encodeURIComponent(evaultId)}/logs`, + window.location.origin + ); if (tail) { url.searchParams.set('tail', tail.toString()); } @@ -100,9 +103,7 @@ export class EVaultService { */ static async getEVaultDetails(evaultId: string): Promise { try { - const response = await fetch( - `/api/evaults/${encodeURIComponent(evaultId)}/details` - ); + const response = await fetch(`/api/evaults/${encodeURIComponent(evaultId)}/details`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -113,4 +114,42 @@ export class EVaultService { throw error; } } + + /** + * Get logs for a specific eVault by namespace and podName + */ + static async getEVaultLogsByPod(namespace: string, podName: string): Promise { + try { + const response = await fetch( + `/api/evaults/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/logs` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + return data.logs || []; + } catch (error) { + console.error('Failed to fetch eVault logs by pod:', error); + throw error; + } + } + + /** + * Get metrics for a specific eVault by namespace and podName + */ + static async getEVaultMetrics(namespace: string, podName: string): Promise { + try { + const response = await fetch( + `/api/evaults/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/metrics` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + return data.metrics || {}; + } catch (error) { + console.error('Failed to fetch eVault metrics:', error); + throw error; + } + } } diff --git a/infrastructure/control-panel/src/lib/services/loki.ts b/infrastructure/control-panel/src/lib/services/loki.ts index 361249dc2..78144489a 100644 --- a/infrastructure/control-panel/src/lib/services/loki.ts +++ b/infrastructure/control-panel/src/lib/services/loki.ts @@ -134,9 +134,7 @@ export class LokiService { ).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); // Extract log lines and limit to requested number - const logLines = uniqueLogs - .map((log) => log.line) - .slice(-limit); // Get last N lines + const logLines = uniqueLogs.map((log) => log.line).slice(-limit); // Get last N lines return logLines; } diff --git a/infrastructure/control-panel/src/lib/services/registry.ts b/infrastructure/control-panel/src/lib/services/registry.ts index 28696b759..cbd4172b9 100644 --- a/infrastructure/control-panel/src/lib/services/registry.ts +++ b/infrastructure/control-panel/src/lib/services/registry.ts @@ -1,160 +1,140 @@ -import { env } from "$env/dynamic/public"; +import { env } from '$env/dynamic/public'; export interface Platform { - name: string; - url: string; - status: "Active" | "Inactive"; - uptime: string; + name: string; + url: string; + status: 'Active' | 'Inactive'; + uptime: string; } export interface RegistryVault { - ename: string; - uri: string; - evault: string; - originalUri?: string; - resolved?: boolean; + ename: string; + uri: string; + evault: string; + originalUri?: string; + resolved?: boolean; } export class RegistryService { - private baseUrl: string; - - constructor() { - this.baseUrl = - env.PUBLIC_REGISTRY_URL || - "https://registry.staging.metastate.foundation"; - } - - async getEVaults(): Promise { - try { - const response = await fetch(`${this.baseUrl}/list`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const vaults: RegistryVault[] = await response.json(); - return vaults; - } catch (error) { - console.error("Error fetching evaults from registry:", error); - return []; - } - } - - async getPlatforms(): Promise { - try { - const response = await fetch(`${this.baseUrl}/platforms`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const platformUrls: (string | null | undefined)[] = - await response.json(); - - // Filter out null/undefined values and convert URLs to platform objects - const platforms = platformUrls - .filter( - (url): url is string => url != null && url.trim() !== "", - ) - .map((url) => { - // Use the original URL from the registry (it already has the correct format) - let displayUrl = url.trim(); - - // Ensure URL has protocol if it doesn't - if ( - !displayUrl.startsWith("http://") && - !displayUrl.startsWith("https://") - ) { - displayUrl = `http://${displayUrl}`; - } - - // Parse URL to extract platform name - let name = "Unknown"; - try { - const urlObj = new URL(displayUrl); - const hostname = urlObj.hostname; - // Extract platform name from hostname (remove port if present) - const hostnameWithoutPort = hostname.split(":")[0]; - const namePart = hostnameWithoutPort.split(".")[0]; - - // Capitalize and format the name - if (namePart === "pictique") name = "Pictique"; - else if (namePart === "blabsy") name = "Blabsy"; - else if (namePart === "charter") name = "Group Charter"; - else if (namePart === "cerberus") name = "Cerberus"; - else if (namePart === "evoting") name = "eVoting"; - else if (namePart === "dreamsync") name = "DreamSync"; - else if (namePart === "ereputation") - name = "eReputation"; - else - name = - namePart.charAt(0).toUpperCase() + - namePart.slice(1); - } catch { - // If URL parsing fails, try to extract name from the URL string - const match = displayUrl.match( - /(?:https?:\/\/)?([^:./]+)/, - ); - if (match) { - const namePart = match[1].toLowerCase(); - if (namePart === "pictique") name = "Pictique"; - else if (namePart === "blabsy") name = "Blabsy"; - else if (namePart === "charter") - name = "Group Charter"; - else if (namePart === "cerberus") name = "Cerberus"; - else if (namePart === "evoting") name = "eVoting"; - else if (namePart === "dreamsync") - name = "DreamSync"; - else if (namePart === "ereputation") - name = "eReputation"; - else - name = - namePart.charAt(0).toUpperCase() + - namePart.slice(1); - } - } - - return { - name, - url: displayUrl, - status: "Active" as const, - uptime: "24h", - }; - }); - - return platforms; - } catch (error) { - console.error("Error fetching platforms from registry:", error); - - // Return fallback platforms if registry is unavailable - return [ - { - name: "Blabsy", - url: "http://192.168.0.235:4444", - status: "Active", - uptime: "24h", - }, - { - name: "Pictique", - url: "http://192.168.0.235:1111", - status: "Active", - uptime: "24h", - }, - { - name: "Group Charter", - url: "http://192.168.0.235:5555", - status: "Active", - uptime: "24h", - }, - { - name: "Cerberus", - url: "http://192.168.0.235:6666", - status: "Active", - uptime: "24h", - }, - ]; - } - } + private baseUrl: string; + + constructor() { + this.baseUrl = env.PUBLIC_REGISTRY_URL || 'https://registry.staging.metastate.foundation'; + } + + async getEVaults(): Promise { + try { + const response = await fetch(`${this.baseUrl}/list`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const vaults: RegistryVault[] = await response.json(); + return vaults; + } catch (error) { + console.error('Error fetching evaults from registry:', error); + return []; + } + } + + async getPlatforms(): Promise { + try { + const response = await fetch(`${this.baseUrl}/platforms`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const platformUrls: (string | null | undefined)[] = await response.json(); + + // Filter out null/undefined values and convert URLs to platform objects + const platforms = platformUrls + .filter((url): url is string => url != null && url.trim() !== '') + .map((url) => { + // Use the original URL from the registry (it already has the correct format) + let displayUrl = url.trim(); + + // Ensure URL has protocol if it doesn't + if (!displayUrl.startsWith('http://') && !displayUrl.startsWith('https://')) { + displayUrl = `http://${displayUrl}`; + } + + // Parse URL to extract platform name + let name = 'Unknown'; + try { + const urlObj = new URL(displayUrl); + const hostname = urlObj.hostname; + // Extract platform name from hostname (remove port if present) + const hostnameWithoutPort = hostname.split(':')[0]; + const namePart = hostnameWithoutPort.split('.')[0]; + + // Capitalize and format the name + if (namePart === 'pictique') name = 'Pictique'; + else if (namePart === 'blabsy') name = 'Blabsy'; + else if (namePart === 'charter') name = 'Group Charter'; + else if (namePart === 'cerberus') name = 'Cerberus'; + else if (namePart === 'evoting') name = 'eVoting'; + else if (namePart === 'dreamsync') name = 'DreamSync'; + else if (namePart === 'ereputation') name = 'eReputation'; + else name = namePart.charAt(0).toUpperCase() + namePart.slice(1); + } catch { + // If URL parsing fails, try to extract name from the URL string + const match = displayUrl.match(/(?:https?:\/\/)?([^:./]+)/); + if (match) { + const namePart = match[1].toLowerCase(); + if (namePart === 'pictique') name = 'Pictique'; + else if (namePart === 'blabsy') name = 'Blabsy'; + else if (namePart === 'charter') name = 'Group Charter'; + else if (namePart === 'cerberus') name = 'Cerberus'; + else if (namePart === 'evoting') name = 'eVoting'; + else if (namePart === 'dreamsync') name = 'DreamSync'; + else if (namePart === 'ereputation') name = 'eReputation'; + else name = namePart.charAt(0).toUpperCase() + namePart.slice(1); + } + } + + return { + name, + url: displayUrl, + status: 'Active' as const, + uptime: '24h' + }; + }); + + return platforms; + } catch (error) { + console.error('Error fetching platforms from registry:', error); + + // Return fallback platforms if registry is unavailable + return [ + { + name: 'Blabsy', + url: 'http://192.168.0.235:4444', + status: 'Active', + uptime: '24h' + }, + { + name: 'Pictique', + url: 'http://192.168.0.235:1111', + status: 'Active', + uptime: '24h' + }, + { + name: 'Group Charter', + url: 'http://192.168.0.235:5555', + status: 'Active', + uptime: '24h' + }, + { + name: 'Cerberus', + url: 'http://192.168.0.235:6666', + status: 'Active', + uptime: '24h' + } + ]; + } + } } export const registryService = new RegistryService(); diff --git a/infrastructure/control-panel/src/routes/+layout.svelte b/infrastructure/control-panel/src/routes/+layout.svelte index b2226b9a1..6094f2863 100644 --- a/infrastructure/control-panel/src/routes/+layout.svelte +++ b/infrastructure/control-panel/src/routes/+layout.svelte @@ -80,7 +80,8 @@ // Navigate to monitoring goto('/monitoring'); - }}>Start MonitoringStart Monitoring {:else} diff --git a/infrastructure/control-panel/src/routes/+page.svelte b/infrastructure/control-panel/src/routes/+page.svelte index fc311dc1c..fc6af1356 100644 --- a/infrastructure/control-panel/src/routes/+page.svelte +++ b/infrastructure/control-panel/src/routes/+page.svelte @@ -306,11 +306,11 @@ }, Uptime: { type: 'text', - value: evault.age + value: 'N/A' }, IP: { type: 'text', - value: evault.ip + value: 'N/A' }, URI: { type: 'link', diff --git a/infrastructure/control-panel/src/routes/api/evaults/+server.ts b/infrastructure/control-panel/src/routes/api/evaults/+server.ts index 3e9fbaae3..3c9e78241 100644 --- a/infrastructure/control-panel/src/routes/api/evaults/+server.ts +++ b/infrastructure/control-panel/src/routes/api/evaults/+server.ts @@ -22,7 +22,7 @@ export const GET: RequestHandler = async () => { registryVaults.map(async (vault) => { // Use evault identifier as the primary ID, fallback to ename const evaultId = vault.evault || vault.ename; - + // Determine display name (prefer ename, fallback to evault) const displayName = vault.ename || vault.evault || 'Unknown'; diff --git a/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/details/+server.ts b/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/details/+server.ts index 13469106f..e22159307 100644 --- a/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/details/+server.ts +++ b/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/details/+server.ts @@ -8,9 +8,7 @@ export const GET: RequestHandler = async ({ params }) => { try { // Get evault information from registry const evaults = await registryService.getEVaults(); - const evault = evaults.find( - (v) => v.evault === evaultId || v.ename === evaultId - ); + const evault = evaults.find((v) => v.evault === evaultId || v.ename === evaultId); if (!evault) { return json({ error: `eVault '${evaultId}' not found in registry.` }, { status: 404 }); @@ -42,4 +40,3 @@ export const GET: RequestHandler = async ({ params }) => { return json({ error: 'Failed to fetch evault details' }, { status: 500 }); } }; - diff --git a/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/logs/+server.ts b/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/logs/+server.ts index e4d4a49ce..0b7178356 100644 --- a/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/logs/+server.ts +++ b/infrastructure/control-panel/src/routes/api/evaults/[evaultId]/logs/+server.ts @@ -10,9 +10,7 @@ export const GET: RequestHandler = async ({ params, url }) => { try { // Get evault information from registry to find ename const evaults = await registryService.getEVaults(); - const evault = evaults.find( - (v) => v.evault === evaultId || v.ename === evaultId - ); + const evault = evaults.find((v) => v.evault === evaultId || v.ename === evaultId); if (!evault) { return json( @@ -25,11 +23,7 @@ export const GET: RequestHandler = async ({ params, url }) => { } // Query Loki for logs using evault identifier - const logs = await lokiService.getEVaultLogs( - evault.evault || evaultId, - evault.ename, - tail - ); + const logs = await lokiService.getEVaultLogs(evault.evault || evaultId, evault.ename, tail); return json({ logs }); } catch (error: any) { @@ -44,4 +38,3 @@ export const GET: RequestHandler = async ({ params, url }) => { ); } }; - diff --git a/infrastructure/control-panel/src/routes/evaults/[evaultId]/+page.svelte b/infrastructure/control-panel/src/routes/evaults/[evaultId]/+page.svelte index 09cbbf8ee..372b688d7 100644 --- a/infrastructure/control-panel/src/routes/evaults/[evaultId]/+page.svelte +++ b/infrastructure/control-panel/src/routes/evaults/[evaultId]/+page.svelte @@ -4,9 +4,9 @@ import { onMount } from 'svelte'; import type { EVault } from '../../api/evaults/+server'; - let evault: EVault | null = null; - let logs: string[] = []; - let details: any = null; + let evault = $state(null); + let logs = $state([]); + let details = $state(null); let isLoading = $state(true); let error = $state(null); let selectedTab = $state('logs'); @@ -186,4 +186,3 @@ {/if} {/if} - diff --git a/infrastructure/control-panel/src/routes/monitoring/+page.svelte b/infrastructure/control-panel/src/routes/monitoring/+page.svelte index 3ad8e02c9..5858ff301 100644 --- a/infrastructure/control-panel/src/routes/monitoring/+page.svelte +++ b/infrastructure/control-panel/src/routes/monitoring/+page.svelte @@ -36,8 +36,12 @@ async function convertIDsToObjects() { // Load selected items from sessionStorage - const evaultsData = sessionStorage.getItem('selectedEVaultsData') || sessionStorage.getItem('selectedEVaults'); - const platformsData = sessionStorage.getItem('selectedPlatformsData') || sessionStorage.getItem('selectedPlatforms'); + const evaultsData = + sessionStorage.getItem('selectedEVaultsData') || + sessionStorage.getItem('selectedEVaults'); + const platformsData = + sessionStorage.getItem('selectedPlatformsData') || + sessionStorage.getItem('selectedPlatforms'); // Process eVaults if (evaultsData) { @@ -49,7 +53,9 @@ try { const allEVaults = await EVaultService.getEVaults(); selectedEVaults = evaultIds - .map((id) => allEVaults.find((e) => (e.evault || e.ename || e.id) === id)) + .map((id) => + allEVaults.find((e) => (e.evault || e.ename || e.id) === id) + ) .filter((e): e is EVault => e !== undefined); console.log('Converted eVault IDs to objects:', selectedEVaults); } catch (error) { diff --git a/infrastructure/control-panel/src/routes/monitoring/[namespace]/[service]/+page.svelte b/infrastructure/control-panel/src/routes/monitoring/[namespace]/[service]/+page.svelte index 1d758384c..c09fa977e 100644 --- a/infrastructure/control-panel/src/routes/monitoring/[namespace]/[service]/+page.svelte +++ b/infrastructure/control-panel/src/routes/monitoring/[namespace]/[service]/+page.svelte @@ -2,53 +2,6 @@ import { onMount, onDestroy } from 'svelte'; import { EVaultService } from '$lib/services/evaultService'; - // Debug: Check if methods exist - console.log('🔍 EVaultService imported:', EVaultService); - console.log('🔍 getEVaultLogs exists:', typeof EVaultService.getEVaultLogs); - console.log('🔍 getEVaultMetrics exists:', typeof EVaultService.getEVaultMetrics); - - // Temporary workaround: Add methods directly if they don't exist - if (!EVaultService.getEVaultLogs) { - console.log('⚠️ Adding getEVaultLogs method directly'); - EVaultService.getEVaultLogs = async (namespace: string, podName: string) => { - console.log('🔍 Direct getEVaultLogs called with:', { namespace, podName }); - try { - const response = await fetch( - `/api/evaults/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/logs` - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const responseData = await response.json(); - console.log('✅ Direct logs fetched successfully:', responseData); - return responseData.logs || []; - } catch (error) { - console.error('❌ Direct logs fetch failed:', error); - throw error; - } - }; - } - - if (!EVaultService.getEVaultMetrics) { - console.log('⚠️ Adding getEVaultMetrics method directly'); - EVaultService.getEVaultMetrics = async (namespace: string, podName: string) => { - console.log('🔍 Direct getEVaultMetrics called with:', { namespace, podName }); - try { - const response = await fetch( - `/api/evaults/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/metrics` - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const responseData = await response.json(); - console.log('✅ Direct metrics fetched successfully:', responseData); - return responseData.metrics || {}; - } catch (error) { - console.error('❌ Direct metrics fetch failed:', error); - throw error; - } - }; - } import { ButtonAction } from '$lib/ui'; import { RefreshCw, Clock, Activity, Server, Globe, ArrowLeft } from 'lucide-svelte'; import { goto } from '$app/navigation'; @@ -113,12 +66,12 @@ } console.log( - 'Calling EVaultService.getEVaultLogs with namespace:', + 'Calling EVaultService.getEVaultLogsByPod with namespace:', namespace, 'pod:', evaultData.podName ); - const logsArray = await EVaultService.getEVaultLogs(namespace, evaultData.podName); + const logsArray = await EVaultService.getEVaultLogsByPod(namespace, evaultData.podName); console.log('Got logs array:', logsArray); if (logsArray && logsArray.length > 0) { diff --git a/infrastructure/control-panel/svelte.config.js b/infrastructure/control-panel/svelte.config.js index 9b5edcd7c..8490c05a4 100644 --- a/infrastructure/control-panel/svelte.config.js +++ b/infrastructure/control-panel/svelte.config.js @@ -1,18 +1,18 @@ -import adapter from "@sveltejs/adapter-node"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - // Load .env from the root of the monorepo (parent directory) - env: { - dir: "../../", - }, - }, + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + // Load .env from the root of the monorepo (parent directory) + env: { + dir: '../../' + } + } }; export default config; diff --git a/infrastructure/control-panel/vite.config.ts b/infrastructure/control-panel/vite.config.ts index da537e98e..47bd3a6f2 100644 --- a/infrastructure/control-panel/vite.config.ts +++ b/infrastructure/control-panel/vite.config.ts @@ -1,24 +1,24 @@ -import { dirname, resolve } from "path"; -import { fileURLToPath } from "url"; -import { sveltekit } from "@sveltejs/kit/vite"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], - // Load .env from the root of the monorepo (parent directory) - envDir: resolve(__dirname, "../.."), - // PUBLIC_ prefix is for client-side env vars, server-side vars (like LOKI_*) are loaded automatically - envPrefix: "PUBLIC_", - optimizeDeps: { - exclude: ["lowdb", "steno"], - }, - build: { - rollupOptions: { - external: ["lowdb", "lowdb/node", "steno"], - }, - }, + plugins: [tailwindcss(), sveltekit()], + // Load .env from the root of the monorepo (parent directory) + envDir: resolve(__dirname, '../..'), + // PUBLIC_ prefix is for client-side env vars, server-side vars (like LOKI_*) are loaded automatically + envPrefix: 'PUBLIC_', + optimizeDeps: { + exclude: ['lowdb', 'steno'] + }, + build: { + rollupOptions: { + external: ['lowdb', 'lowdb/node', 'steno'] + } + } }); diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json index f1cd48af1..6e6c8369b 100644 --- a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json +++ b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json @@ -14,7 +14,8 @@ "deep-link:default", "crypto-hw:default", "notification:default", - "process:default" + "process:default", + "opener:allow-default-urls" ], "platforms": [ "iOS", diff --git a/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts b/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts index 98f9d9b03..9d4027251 100644 --- a/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts +++ b/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts @@ -64,11 +64,42 @@ export class HardwareKeyManager implements KeyManager { async signPayload(keyId: string, payload: string): Promise { try { + console.log("=".repeat(70)); + console.log("🔐 [HardwareKeyManager] signPayload called"); + console.log("=".repeat(70)); + console.log(`Key ID: ${keyId}`); + console.log(`Payload: "${payload}"`); + console.log(`Payload length: ${payload.length} bytes`); + const payloadHex = Array.from(new TextEncoder().encode(payload)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + console.log(`Payload (hex): ${payloadHex}`); + + // Get and log the public key + try { + const publicKey = await this.getPublicKey(keyId); + if (publicKey) { + console.log(`Public key: ${publicKey.substring(0, 60)}...`); + console.log(`Public key (full): ${publicKey}`); + } else { + console.log("⚠️ Public key not available"); + } + } catch (error) { + console.log( + `⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + console.log("Signing with hardware key..."); const signature = await hwSignPayload(keyId, payload); - console.log(`Hardware signature created for ${keyId}`); + console.log(`✅ Hardware signature created for ${keyId}`); + console.log(`Signature: ${signature.substring(0, 50)}...`); + console.log(`Signature (full): ${signature}`); + console.log(`Signature length: ${signature.length} chars`); + console.log("=".repeat(70)); return signature; } catch (error) { - console.error("Hardware signing failed:", error); + console.error("❌ Hardware signing failed:", error); throw new KeyManagerError( "Failed to sign payload with hardware key", KeyManagerErrorCodes.SIGNING_FAILED, diff --git a/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts b/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts index b7e703449..99045bf3d 100644 --- a/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts +++ b/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts @@ -115,6 +115,17 @@ export class SoftwareKeyManager implements KeyManager { async signPayload(keyId: string, payload: string): Promise { try { + console.log("=".repeat(70)); + console.log("🔐 [SoftwareKeyManager] signPayload called"); + console.log("=".repeat(70)); + console.log(`Key ID: ${keyId}`); + console.log(`Payload: "${payload}"`); + console.log(`Payload length: ${payload.length} bytes`); + const payloadHex = Array.from(new TextEncoder().encode(payload)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + console.log(`Payload (hex): ${payloadHex}`); + const keyPair = await this.getKeyPair(keyId); if (!keyPair) { throw new KeyManagerError( @@ -124,10 +135,20 @@ export class SoftwareKeyManager implements KeyManager { ); } + // Log the public key that corresponds to this private key + console.log( + `Public key (base64): ${keyPair.publicKey.substring(0, 60)}...`, + ); + console.log(`Public key (full): ${keyPair.publicKey}`); + // Import the private key const privateKeyBuffer = this.base64ToArrayBuffer( keyPair.privateKey, ); + console.log( + `Private key buffer length: ${privateKeyBuffer.byteLength} bytes`, + ); + const privateKey = await crypto.subtle.importKey( "pkcs8", privateKeyBuffer, @@ -138,11 +159,19 @@ export class SoftwareKeyManager implements KeyManager { false, ["sign"], ); + console.log("✅ Private key imported successfully"); // Convert payload to ArrayBuffer const payloadBuffer = new TextEncoder().encode(payload); + console.log( + `Payload buffer length: ${payloadBuffer.byteLength} bytes`, + ); + console.log( + `Payload buffer (hex): ${this.arrayBufferToHex(payloadBuffer)}`, + ); // Sign the payload + console.log("Signing with ECDSA P-256, SHA-256..."); const signature = await crypto.subtle.sign( { name: "ECDSA", @@ -152,12 +181,25 @@ export class SoftwareKeyManager implements KeyManager { payloadBuffer, ); + console.log( + `Signature buffer length: ${signature.byteLength} bytes`, + ); + console.log( + `Signature buffer (hex): ${this.arrayBufferToHex(signature)}`, + ); + // Convert signature to base64 string const signatureString = this.arrayBufferToBase64(signature); - console.log(`Software signature created for ${keyId}`); + console.log(`✅ Software signature created for ${keyId}`); + console.log( + `Signature (base64): ${signatureString.substring(0, 50)}...`, + ); + console.log(`Signature (full): ${signatureString}`); + console.log(`Signature length: ${signatureString.length} chars`); + console.log("=".repeat(70)); return signatureString; } catch (error) { - console.error("Software signing failed:", error); + console.error("❌ Software signing failed:", error); if (error instanceof KeyManagerError) { throw error; } diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 349f96186..a506e3b0b 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -138,6 +138,14 @@ export class VaultController { const isFake = await this.#userController.isFake; const context = isFake ? "pre-verification" : "onboarding"; + console.log("=".repeat(70)); + console.log("🔄 [VaultController] syncPublicKey called"); + console.log("=".repeat(70)); + console.log(`eName: ${eName}`); + console.log(`Key ID for sync: ${KEY_ID}`); + console.log(`Context for sync: ${context}`); + console.log(`Is fake/pre-verification: ${isFake}`); + // Get public key using the same method as getApplicationPublicKey() in onboarding/verify let publicKey: string | undefined; try { @@ -145,6 +153,10 @@ export class VaultController { KEY_ID, context, ); + console.log( + `Public key retrieved: ${publicKey?.substring(0, 60)}...`, + ); + console.log(`Public key (full): ${publicKey}`); } catch (error) { console.error( `Failed to get public key for ${KEY_ID} with context ${context}:`, @@ -159,6 +171,7 @@ export class VaultController { ); return; } + console.log("=".repeat(70)); // Get authentication token from environment variable const authToken = PUBLIC_EID_WALLET_TOKEN || null; diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/key.ts b/infrastructure/eid-wallet/src/lib/global/controllers/key.ts index a2d69aa82..8b60d7df3 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/key.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/key.ts @@ -145,8 +145,45 @@ export class KeyService { context: KeyServiceContext, payload: string, ): Promise { + console.log("=".repeat(70)); + console.log("🔐 [KeyService] signPayload called"); + console.log("=".repeat(70)); + console.log(`Key ID: ${keyId}`); + console.log(`Context: ${context}`); + console.log(`Payload: "${payload}"`); + console.log(`Payload length: ${payload.length} bytes`); + const payloadHex = Array.from(new TextEncoder().encode(payload)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + console.log(`Payload (hex): ${payloadHex}`); + const manager = await this.getManager(keyId, context); + const managerType = manager.getType(); + console.log(`Manager type: ${managerType}`); + + // Get and log the public key that will be used for signing + try { + const publicKey = await manager.getPublicKey(keyId); + if (publicKey) { + console.log(`Public key: ${publicKey.substring(0, 60)}...`); + console.log(`Public key (full): ${publicKey}`); + } else { + console.log("⚠️ Public key not available"); + } + } catch (error) { + console.log( + `⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + console.log("=".repeat(70)); const signature = await manager.signPayload(keyId, payload); + console.log( + `✅ [KeyService] Signature created: ${signature.substring(0, 50)}...`, + ); + console.log(`Signature length: ${signature.length} chars`); + console.log("=".repeat(70)); + await this.#touchContext(this.#getCacheKey(keyId, context), manager); return signature; } diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts index 8f0917bbe..9c6d036cb 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts @@ -7,6 +7,7 @@ import { requestPermissions, scan, } from "@tauri-apps/plugin-barcode-scanner"; +import { openUrl } from "@tauri-apps/plugin-opener"; import axios from "axios"; import { type Writable, get, writable } from "svelte/store"; @@ -238,9 +239,25 @@ export function createScanLogic({ authLoading.set(true); try { + const KEY_ID = "default"; + const isFake = await globalState.userController.isFake; + const signingContext = isFake ? "pre-verification" : "onboarding"; + + console.log("=".repeat(70)); + console.log( + "🔐 [scanLogic] handleAuth - Preparing to sign payload", + ); + console.log("=".repeat(70)); + console.log(`⚠️ Using keyId: ${KEY_ID} (NOT ${vault.ename})`); + console.log(`⚠️ Using context: ${signingContext} (NOT "signing")`); + console.log( + "⚠️ This ensures we use the SAME key that was synced to eVault", + ); + console.log("=".repeat(70)); + const { created } = await globalState.keyService.ensureKey( - vault.ename, - "signing", + KEY_ID, + signingContext, ); console.log( "Key generation result for signing:", @@ -252,25 +269,14 @@ export function createScanLogic({ throw new Error("Failed to get W3ID"); } - const sessionPayload = JSON.stringify({ - session: get(session), - ename: vault.ename, - timestamp: Date.now(), - }); + const sessionPayload = get(session) as string; const signature = await globalState.keyService.signPayload( - vault.ename, - "signing", + KEY_ID, + signingContext, sessionPayload, ); - const authPayload = { - ename: vault.ename, - session: get(session), - signature: signature, - appVersion: "0.4.0", - }; - const redirectUrl = get(redirect); if (!redirectUrl) { throw new Error( @@ -278,7 +284,17 @@ export function createScanLogic({ ); } - await axios.post(redirectUrl, authPayload); + // Strip path from redirectUri and append /deeplink-login + const loginUrl = new URL("/deeplink-login", redirectUrl); + loginUrl.searchParams.set("ename", vault.ename); + loginUrl.searchParams.set("session", get(session) as string); + loginUrl.searchParams.set("signature", signature); + loginUrl.searchParams.set("appVersion", "0.4.0"); + + console.log(`🔗 Opening login URL: ${loginUrl.toString()}`); + + // Open URL in browser using tauri opener + await openUrl(loginUrl.toString()); // Close the auth drawer first codeScannedDrawerOpen.set(false); @@ -458,15 +474,6 @@ export function createScanLogic({ } signingData.set(decodedData); - console.log("🔍 DEBUG: Decoded signing data:", decodedData); - console.log( - "🔍 DEBUG: Data keys:", - Object.keys(decodedData || {}), - ); - console.log( - "🔍 DEBUG: Is poll request?", - !!(decodedData?.pollId && decodedData?.voteData), - ); isSigningRequest.set(true); signingDrawerOpen.set(true); @@ -569,9 +576,26 @@ export function createScanLogic({ throw new Error("No vault available for signing"); } + // ⚠️ CRITICAL: Use the SAME keyId and context that was synced to eVault! + // The key synced to eVault uses keyId="default" with context="onboarding" or "pre-verification" + // NOT vault.ename with context="signing"! + const KEY_ID = "default"; + const isFake = await globalState.userController.isFake; + const signingContext = isFake ? "pre-verification" : "onboarding"; + + console.log("=".repeat(70)); + console.log("🔐 [scanLogic] Preparing to sign payload"); + console.log("=".repeat(70)); + console.log(`⚠️ Using keyId: ${KEY_ID} (NOT ${vault.ename})`); + console.log(`⚠️ Using context: ${signingContext} (NOT "signing")`); + console.log( + "⚠️ This ensures we use the SAME key that was synced to eVault", + ); + console.log("=".repeat(70)); + const { created } = await globalState.keyService.ensureKey( - vault.ename, - "signing", + KEY_ID, + signingContext, ); console.log( "Key generation result for signing:", @@ -583,33 +607,16 @@ export function createScanLogic({ throw new Error("Failed to get W3ID"); } - let messageToSign: string; - - if (currentSigningData.pollId && currentSigningData.voteData) { - messageToSign = JSON.stringify({ - pollId: currentSigningData.pollId, - voteData: currentSigningData.voteData, - userId: currentSigningData.userId, - sessionId: currentSigningSessionId, - timestamp: Date.now(), - }); - } else { - messageToSign = JSON.stringify({ - message: currentSigningData.message, - sessionId: currentSigningData.sessionId, - timestamp: Date.now(), - }); - } + const messageToSign = currentSigningSessionId; console.log( "🔐 Starting cryptographic signing process with KeyManager...", ); - console.log("✍️ Signing message:", messageToSign); const signature = await globalState.keyService.signPayload( - vault.ename, - "signing", - messageToSign, + KEY_ID, + signingContext, + currentSigningSessionId, ); console.log("✅ Message signed successfully"); diff --git a/infrastructure/signature-validator/package.json b/infrastructure/signature-validator/package.json index e9e0ca638..620fb2c60 100644 --- a/infrastructure/signature-validator/package.json +++ b/infrastructure/signature-validator/package.json @@ -6,8 +6,11 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", + "postinstall": "npm run build", "test": "vitest", - "dev": "tsc --watch" + "dev": "tsc --watch", + "test:signature": "tsx test-example.ts", + "test:signature:custom": "tsx test-signature.ts" }, "dependencies": { "axios": "^1.6.7", @@ -16,6 +19,7 @@ "devDependencies": { "@types/node": "^20.11.24", "typescript": "^5.3.3", + "tsx": "^4.7.0", "vitest": "^1.6.1" } } \ No newline at end of file diff --git a/infrastructure/signature-validator/src/index.ts b/infrastructure/signature-validator/src/index.ts index 5690b0eb8..952d7482c 100644 --- a/infrastructure/signature-validator/src/index.ts +++ b/infrastructure/signature-validator/src/index.ts @@ -1,5 +1,14 @@ import axios from "axios"; -import { base58btc } from "multiformats/bases/base58"; + +// Lazy initialization for base58btc to handle ESM module resolution +let base58btcModule: { base58btc: { decode: (input: string) => Uint8Array } } | null = null; + +async function getBase58btc() { + if (!base58btcModule) { + base58btcModule = await import("multiformats/bases/base58"); + } + return base58btcModule.base58btc; +} /** * Options for signature verification @@ -32,7 +41,7 @@ export interface VerifySignatureResult { * Supports 'z' prefix for base58btc or hex encoding * Based on the format used in SoftwareKeyManager: 'z' + hex */ -function decodeMultibasePublicKey(multibaseKey: string): Uint8Array { +async function decodeMultibasePublicKey(multibaseKey: string): Promise { if (!multibaseKey.startsWith("z")) { throw new Error("Public key must start with 'z' multibase prefix"); } @@ -58,6 +67,7 @@ function decodeMultibasePublicKey(multibaseKey: string): Uint8Array { // Try base58btc (standard multibase 'z' prefix) try { + const base58btc = await getBase58btc(); return base58btc.decode(encoded); } catch (error) { throw new Error( @@ -67,20 +77,23 @@ function decodeMultibasePublicKey(multibaseKey: string): Uint8Array { } /** - * Decodes a multibase-encoded signature - * Supports base58btc or base64 + * Decodes a signature + * Supports: + * - Multibase base58btc (starts with 'z') + * - Base64 (default for software keys) */ -function decodeSignature(signature: string): Uint8Array { +async function decodeSignature(signature: string): Promise { // If it starts with 'z', it's multibase base58btc if (signature.startsWith("z")) { try { + const base58btc = await getBase58btc(); return base58btc.decode(signature.slice(1)); } catch (error) { throw new Error(`Failed to decode multibase signature: ${error instanceof Error ? error.message : String(error)}`); } } - // Otherwise, try base64 (software keys return base64) + // Default: decode as base64 (software keys return base64-encoded signatures) try { const binaryString = atob(signature); const bytes = new Uint8Array(binaryString.length); @@ -99,7 +112,7 @@ function decodeSignature(signature: string): Uint8Array { * @param registryBaseUrl - Base URL of the registry service * @returns The public key in multibase format */ -async function getPublicKey(eName: string, registryBaseUrl: string): Promise { +async function getPublicKey(eName: string, registryBaseUrl: string): Promise { // Step 1: Resolve eVault URL from registry const resolveUrl = new URL(`/resolve?w3id=${encodeURIComponent(eName)}`, registryBaseUrl).toString(); const resolveResponse = await axios.get(resolveUrl, { @@ -123,7 +136,7 @@ async function getPublicKey(eName: string, registryBaseUrl: string): Promise { try { - const { ename, session, appVersion } = req.body; + const { ename, session, appVersion, signature } = req.body; console.log(req.body) @@ -67,6 +68,10 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + // Check app version - missing version is treated as old version if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { const errorMessage = { @@ -81,11 +86,35 @@ export class AuthController { }); } + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + const token = await auth().createCustomToken(ename); console.log(token); - this.eventEmitter.emit(session, { token }); - res.status(200).send(); + const data = { token }; + // Emit via SSE for backward compatibility + this.eventEmitter.emit(session, data); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/blabsy/src/components/login/login-main.tsx b/platforms/blabsy/src/components/login/login-main.tsx index 76b759f30..69b95e832 100644 --- a/platforms/blabsy/src/components/login/login-main.tsx +++ b/platforms/blabsy/src/components/login/login-main.tsx @@ -83,12 +83,70 @@ export function LoginMain(): JSX.Element { return 'https://play.google.com/store/apps/details?id=foundation.metastate.eid_wallet'; }; + // Check for query parameters and auto-login useEffect(() => { + if (typeof window === 'undefined') return; + + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow getOfferData().catch((error) => console.error('Error fetching QR code data:', error) ); }, []); + const handleAutoLogin = async ( + ename: string, + session: string, + signature: string, + appVersion: string + ) => { + try { + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; + if (!baseUrl) { + console.error('NEXT_PUBLIC_BASE_URL not configured'); + return; + } + + const response = await fetch(`${baseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token) { + await signInWithCustomToken(data.token); + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + setErrorMessage( + errorData.message || + 'Your eID Wallet app version is outdated. Please update to continue.' + ); + } + } + } catch (error) { + console.error('Login request failed:', error); + } + }; + return (
{/* Left side image */} diff --git a/platforms/blabsy/src/pages/deeplink-login.tsx b/platforms/blabsy/src/pages/deeplink-login.tsx new file mode 100644 index 000000000..f78f4cf6b --- /dev/null +++ b/platforms/blabsy/src/pages/deeplink-login.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useAuth } from '@lib/context/auth-context'; + +export default function DeeplinkLogin(): JSX.Element | null { + const { signInWithCustomToken } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError('Missing required authentication parameters'); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; + if (!baseUrl) { + setError('Server configuration error'); + setIsLoading(false); + return; + } + + const loginUrl = `${baseUrl}/api/auth`; + const requestBody = { + ename, + session, + signature, + appVersion: appVersion || '0.4.0' + }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token) { + await signInWithCustomToken(data.token); + } else { + setError('Invalid response from server'); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { + error: `Server error: ${response.status}` + }; + } + setError(errorData.error || 'Authentication failed'); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError('Failed to connect to server'); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, [signInWithCustomToken]); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/dreamSync/client/src/App.tsx b/platforms/dreamSync/client/src/App.tsx index a2117dc63..60a0033a5 100644 --- a/platforms/dreamSync/client/src/App.tsx +++ b/platforms/dreamSync/client/src/App.tsx @@ -9,6 +9,7 @@ import { Heart } from "lucide-react"; import NotFound from "@/pages/not-found"; import Login from "@/pages/login"; import WishlistEditor from "@/pages/wishlist-editor"; +import DeeplinkLogin from "@/pages/deeplink-login"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -28,6 +29,7 @@ function Router() { return ( + {!isAuthenticated ? ( ) : ( diff --git a/platforms/dreamSync/client/src/components/auth/login-screen.tsx b/platforms/dreamSync/client/src/components/auth/login-screen.tsx index 760387234..dfc1b0cb1 100644 --- a/platforms/dreamSync/client/src/components/auth/login-screen.tsx +++ b/platforms/dreamSync/client/src/components/auth/login-screen.tsx @@ -13,7 +13,24 @@ export function LoginScreen() { const [isLoading, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(null); + // Check for query parameters and auto-login useEffect(() => { + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow const getAuthOffer = async () => { try { console.log("🔍 Getting auth offer from:", apiClient.defaults.baseURL); @@ -36,6 +53,39 @@ export function LoginScreen() { getAuthOffer(); }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsConnecting(true); + try { + const apiBaseUrl = import.meta.env.VITE_DREAMSYNC_BASE_URL || "http://localhost:8888"; + const response = await fetch(`${apiBaseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("dreamsync_token", data.token); + localStorage.setItem("dreamsync_user_id", data.user.id); + window.location.href = "/"; + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + setErrorMessage(errorData.message || 'Your eID Wallet app version is outdated. Please update to continue.'); + } + setIsConnecting(false); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsConnecting(false); + setIsLoading(false); + } + }; + useEffect(() => { if (!sessionId) return; diff --git a/platforms/dreamSync/client/src/pages/deeplink-login.tsx b/platforms/dreamSync/client/src/pages/deeplink-login.tsx new file mode 100644 index 000000000..c19bdf2dd --- /dev/null +++ b/platforms/dreamSync/client/src/pages/deeplink-login.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from "react"; +import { useAuth } from "@/hooks/useAuth"; + +export default function DeeplinkLogin() { + const { login } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError("Missing required authentication parameters"); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const apiBaseUrl = import.meta.env.VITE_DREAMSYNC_BASE_URL; + const loginUrl = `${apiBaseUrl}/api/auth`; + const requestBody = { ename, session, signature, appVersion: appVersion || '0.4.0' }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("dreamsync_token", data.token); + localStorage.setItem("dreamsync_user_id", data.user.id); + window.location.href = "/"; + } else { + setError("Invalid response from server"); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: `Server error: ${response.status}` }; + } + setError(errorData.error || "Authentication failed"); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError("Failed to connect to server"); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, []); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/dreamsync-api/package.json b/platforms/dreamsync-api/package.json index 43e9f56b2..23656bb42 100644 --- a/platforms/dreamsync-api/package.json +++ b/platforms/dreamsync-api/package.json @@ -28,6 +28,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "link:../../infrastructure/web3-adapter" }, "devDependencies": { diff --git a/platforms/dreamsync-api/src/controllers/AuthController.ts b/platforms/dreamsync-api/src/controllers/AuthController.ts index b068d7a9f..0a44068c8 100644 --- a/platforms/dreamsync-api/src/controllers/AuthController.ts +++ b/platforms/dreamsync-api/src/controllers/AuthController.ts @@ -4,6 +4,7 @@ import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { signToken } from "../utils/jwt"; import { isVersionValid } from "../utils/version"; +import { verifySignature } from "signature-validator"; const MIN_REQUIRED_VERSION = "0.4.0"; @@ -66,6 +67,10 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + // Check app version - missing version is treated as old version if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { const errorMessage = { @@ -80,6 +85,27 @@ export class AuthController { }); } + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + // Find user by ename (handles @ symbol variations) const user = await this.userService.findByEname(ename); @@ -111,8 +137,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/eCurrency-api/package.json b/platforms/eCurrency-api/package.json index 8c63a7ee1..7a34f384e 100644 --- a/platforms/eCurrency-api/package.json +++ b/platforms/eCurrency-api/package.json @@ -22,6 +22,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "link:../../infrastructure/web3-adapter" }, "devDependencies": { diff --git a/platforms/eCurrency-api/src/controllers/AuthController.ts b/platforms/eCurrency-api/src/controllers/AuthController.ts index 545d13388..3a88587a5 100644 --- a/platforms/eCurrency-api/src/controllers/AuthController.ts +++ b/platforms/eCurrency-api/src/controllers/AuthController.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { signToken } from "../utils/jwt"; +import { verifySignature } from "signature-validator"; export class AuthController { private userService: UserService; @@ -56,6 +57,8 @@ export class AuthController { try { const { ename, session, w3id, signature } = req.body; + console.log(signature, ename, session) + if (!ename) { return res.status(400).json({ error: "ename is required" }); } @@ -64,6 +67,31 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + // Only find existing users - don't create new ones during auth const user = await this.userService.findUser(ename); @@ -95,8 +123,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/eCurrency/client/src/App.tsx b/platforms/eCurrency/client/src/App.tsx index 15b1bd6fb..dfced0058 100644 --- a/platforms/eCurrency/client/src/App.tsx +++ b/platforms/eCurrency/client/src/App.tsx @@ -1,4 +1,4 @@ -import { Switch, Route } from "wouter"; +import { Switch, Route, useLocation } from "wouter"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AuthProvider } from "./lib/auth-context"; import { useAuth } from "./hooks/useAuth"; @@ -6,11 +6,22 @@ import AuthPage from "./pages/auth-page"; import Dashboard from "./pages/dashboard"; import Currencies from "./pages/currencies"; import CurrencyDetail from "./pages/currency-detail"; +import DeeplinkLogin from "./pages/deeplink-login"; const queryClient = new QueryClient(); function Router() { const { isAuthenticated, isLoading } = useAuth(); + const [location] = useLocation(); + + // Allow deeplink-login to be accessible even when not authenticated + if (location === "/deeplink-login") { + return ( + + + + ); + } // Show auth page if loading or not authenticated if (isLoading || !isAuthenticated) { @@ -19,6 +30,7 @@ function Router() { return ( + diff --git a/platforms/eCurrency/client/src/components/auth/login-screen.tsx b/platforms/eCurrency/client/src/components/auth/login-screen.tsx index be9f3a0c7..35379174d 100644 --- a/platforms/eCurrency/client/src/components/auth/login-screen.tsx +++ b/platforms/eCurrency/client/src/components/auth/login-screen.tsx @@ -11,29 +11,91 @@ export function LoginScreen() { const [sessionId, setSessionId] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + // Check for query parameters and auto-login useEffect(() => { - const getAuthOffer = async () => { - try { - console.log("🔍 Getting auth offer from:", apiClient.defaults.baseURL); - const response = await apiClient.get("/api/auth/offer"); - console.log("✅ Auth offer response:", response.data); + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow + getAuthOffer(); + }, []); + + const getAuthOffer = async () => { + setIsLoading(true); + setError(null); + try { + console.log("🔍 Getting auth offer from:", apiClient.defaults.baseURL); + const response = await apiClient.get("/api/auth/offer"); + console.log("✅ Auth offer response:", response.data); + if (response.data.offer && response.data.sessionId) { setQrCode(response.data.offer); setSessionId(response.data.sessionId); - setIsLoading(false); - } catch (error: unknown) { - console.error("❌ Failed to get auth offer:", error); - if (error && typeof error === 'object' && 'response' in error) { - const axiosError = error as { response?: { data?: unknown; status?: number } }; - console.error("❌ Error details:", axiosError.response?.data); - console.error("❌ Error status:", axiosError.response?.status); + setError(null); + } else { + throw new Error("Invalid response from server"); + } + } catch (error: unknown) { + console.error("❌ Failed to get auth offer:", error); + let errorMessage = "Failed to load login. Please try again."; + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { response?: { data?: unknown; status?: number } }; + console.error("❌ Error details:", axiosError.response?.data); + console.error("❌ Error status:", axiosError.response?.status); + if (axiosError.response?.status === 0 || !axiosError.response) { + errorMessage = "Cannot connect to server. Please check your connection."; } - setIsLoading(false); } - }; + setError(errorMessage); + setQrCode(""); + setSessionId(""); + } finally { + setIsLoading(false); + } + }; - getAuthOffer(); - }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsConnecting(true); + try { + const apiBaseUrl = import.meta.env.VITE_ECURRENCY_API_URL || "http://localhost:8989"; + const response = await fetch(`${apiBaseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("ecurrency_token", data.token); + localStorage.setItem("ecurrency_user", JSON.stringify(data.user)); + window.location.href = "/dashboard"; + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + setIsConnecting(false); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsConnecting(false); + setIsLoading(false); + } + }; useEffect(() => { if (!sessionId) return; @@ -105,36 +167,82 @@ export function LoginScreen() {

- Scan the QR code using your eID App to login + {isMobileDevice() + ? "Login using your eID Wallet app" + : "Scan the QR code using your eID App to login" + }

- {qrCode && ( -
- {isMobileDevice() ? ( -
- - Login with eID Wallet - -
- Click the button to open your eID wallet app +
+ {isMobileDevice() ? ( +
+ {error ? ( +
+
{error}
+
-
- ) : ( -
- -
- )} -
- )} + ) : qrCode ? ( + <> + + Login with eID Wallet + +
+ Click the button to open your eID wallet app +
+ + ) : ( + <> + +
+ {isLoading ? "Connecting to server..." : "Preparing login..."} +
+ + )} +
+ ) : ( + <> + {error ? ( +
+
{error}
+ +
+ ) : qrCode ? ( +
+ +
+ ) : ( +
+
+

Loading QR code...

+
+ )} + + )} +

diff --git a/platforms/eCurrency/client/src/lib/apiClient.ts b/platforms/eCurrency/client/src/lib/apiClient.ts index 9925b61ef..8a8c1327c 100644 --- a/platforms/eCurrency/client/src/lib/apiClient.ts +++ b/platforms/eCurrency/client/src/lib/apiClient.ts @@ -1,12 +1,6 @@ import axios, { AxiosResponse, AxiosError, InternalAxiosRequestConfig } from "axios"; -const baseURL = import.meta.env.VITE_ECURRENCY_API_URL || "http://localhost:8989"; - -console.log("🔍 Environment variables:", { - VITE_ECURRENCY_API_URL: import.meta.env.VITE_ECURRENCY_API_URL, - baseURL: baseURL, - allEnv: import.meta.env -}); +const baseURL = import.meta.env.VITE_ECURRENCY_BASE_URL; export const apiClient = axios.create({ baseURL, diff --git a/platforms/eCurrency/client/src/pages/deeplink-login.tsx b/platforms/eCurrency/client/src/pages/deeplink-login.tsx new file mode 100644 index 000000000..88c7ba4b9 --- /dev/null +++ b/platforms/eCurrency/client/src/pages/deeplink-login.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from "react"; +import { useAuth } from "@/hooks/useAuth"; + +export default function DeeplinkLogin() { + const { login } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError("Missing required authentication parameters"); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const apiBaseUrl = import.meta.env.VITE_ECURRENCY_BASE_URL; + const loginUrl = `${apiBaseUrl}/api/auth`; + const requestBody = { ename, session, signature, appVersion: appVersion || '0.4.0' }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("ecurrency_token", data.token); + localStorage.setItem("ecurrency_user", JSON.stringify(data.user)); + window.location.href = "/dashboard"; + } else { + setError("Invalid response from server"); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: `Server error: ${response.status}` }; + } + setError(errorData.error || "Authentication failed"); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError("Failed to connect to server"); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, []); + + if (isLoading) { + return ( +

+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/eReputation-api/package.json b/platforms/eReputation-api/package.json index e249de606..eafc379ef 100644 --- a/platforms/eReputation-api/package.json +++ b/platforms/eReputation-api/package.json @@ -23,6 +23,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "link:../../infrastructure/web3-adapter" }, "devDependencies": { diff --git a/platforms/eReputation-api/src/controllers/AuthController.ts b/platforms/eReputation-api/src/controllers/AuthController.ts index 0f97e8456..7814defeb 100644 --- a/platforms/eReputation-api/src/controllers/AuthController.ts +++ b/platforms/eReputation-api/src/controllers/AuthController.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { signToken } from "../utils/jwt"; +import { verifySignature } from "signature-validator"; export class AuthController { private userService: UserService; @@ -64,6 +65,31 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + // Only find existing users - don't create new ones during auth const user = await this.userService.findUser(ename); @@ -95,8 +121,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/eReputation/client/src/App.tsx b/platforms/eReputation/client/src/App.tsx index ae3ddbd45..7af80b7e7 100644 --- a/platforms/eReputation/client/src/App.tsx +++ b/platforms/eReputation/client/src/App.tsx @@ -1,4 +1,4 @@ -import { Switch, Route } from "wouter"; +import { Switch, Route, useLocation } from "wouter"; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; @@ -8,9 +8,20 @@ import { useAuth } from "@/hooks/useAuth"; import AuthPage from "@/pages/auth-page"; import Dashboard from "@/pages/dashboard"; import NotFound from "@/pages/not-found"; +import DeeplinkLogin from "@/pages/deeplink-login"; function Router() { const { isAuthenticated, isLoading } = useAuth(); + const [location] = useLocation(); + + // Allow deeplink-login to be accessible even when not authenticated + if (location === "/deeplink-login") { + return ( + + + + ); + } // Show auth page if loading or not authenticated if (isLoading || !isAuthenticated) { @@ -19,6 +30,7 @@ function Router() { return ( + diff --git a/platforms/eReputation/client/src/components/auth/login-screen.tsx b/platforms/eReputation/client/src/components/auth/login-screen.tsx index 183c015ec..34e7a2746 100644 --- a/platforms/eReputation/client/src/components/auth/login-screen.tsx +++ b/platforms/eReputation/client/src/components/auth/login-screen.tsx @@ -12,7 +12,24 @@ export function LoginScreen() { const [isConnecting, setIsConnecting] = useState(false); const [isLoading, setIsLoading] = useState(true); + // Check for query parameters and auto-login useEffect(() => { + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow const getAuthOffer = async () => { try { console.log("🔍 Getting auth offer from:", apiClient.defaults.baseURL); @@ -35,6 +52,36 @@ export function LoginScreen() { getAuthOffer(); }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsConnecting(true); + try { + const apiBaseUrl = import.meta.env.VITE_EREPUTATION_BASE_URL || "http://localhost:8765"; + const response = await fetch(`${apiBaseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("ereputation_token", data.token); + localStorage.setItem("ereputation_user_id", data.user.id); + window.location.href = "/"; + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + setIsConnecting(false); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsConnecting(false); + setIsLoading(false); + } + }; + useEffect(() => { if (!sessionId) return; diff --git a/platforms/eReputation/client/src/pages/deeplink-login.tsx b/platforms/eReputation/client/src/pages/deeplink-login.tsx new file mode 100644 index 000000000..1eb8ee349 --- /dev/null +++ b/platforms/eReputation/client/src/pages/deeplink-login.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from "react"; +import { useAuth } from "@/hooks/useAuth"; + +export default function DeeplinkLogin() { + const { login } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError("Missing required authentication parameters"); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const apiBaseUrl = import.meta.env.VITE_EREPUTATION_BASE_URL; + const loginUrl = `${apiBaseUrl}/api/auth`; + const requestBody = { ename, session, signature, appVersion: appVersion || '0.4.0' }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("ereputation_token", data.token); + localStorage.setItem("ereputation_user_id", data.user.id); + window.location.href = "/"; + } else { + setError("Invalid response from server"); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: `Server error: ${response.status}` }; + } + setError(errorData.error || "Authentication failed"); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError("Failed to connect to server"); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, []); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/eVoting/src/app/(auth)/login/page.tsx b/platforms/eVoting/src/app/(auth)/login/page.tsx index 60e029e94..2402579c8 100644 --- a/platforms/eVoting/src/app/(auth)/login/page.tsx +++ b/platforms/eVoting/src/app/(auth)/login/page.tsx @@ -19,7 +19,26 @@ export default function LoginPage() { setIsMobile(isMobileDevice()); }, []); + // Check for query parameters and auto-login useEffect(() => { + if (typeof window === 'undefined') return; + + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow const fetchQRCode = async () => { try { const response = await fetch( @@ -44,6 +63,43 @@ export default function LoginPage() { fetchQRCode(); }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsLoading(true); + try { + const baseUrl = process.env.NEXT_PUBLIC_EVOTING_BASE_URL; + if (!baseUrl) { + console.error('NEXT_PUBLIC_EVOTING_BASE_URL not configured'); + setIsLoading(false); + return; + } + + const response = await fetch(`${baseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + setAuthToken(data.token); + setAuthId(data.user.id); + window.location.href = "/"; + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + setErrorMessage(errorData.message || 'Your eID Wallet app version is outdated. Please update to continue.'); + } + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsLoading(false); + } + }; + useEffect(() => { if (!sessionId) return; diff --git a/platforms/eVoting/src/app/deeplink-login/page.tsx b/platforms/eVoting/src/app/deeplink-login/page.tsx new file mode 100644 index 000000000..6c947c0ec --- /dev/null +++ b/platforms/eVoting/src/app/deeplink-login/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { setAuthToken, setAuthId } from "@/lib/authUtils"; + +export default function DeeplinkLogin() { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError("Missing required authentication parameters"); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const baseUrl = process.env.NEXT_PUBLIC_EVOTING_BASE_URL; + if (!baseUrl) { + setError("Server configuration error"); + setIsLoading(false); + return; + } + + const loginUrl = `${baseUrl}/api/auth`; + const requestBody = { ename, session, signature, appVersion: appVersion || '0.4.0' }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + setAuthToken(data.token); + setAuthId(data.user.id); + window.location.href = "/"; + } else { + setError("Invalid response from server"); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: `Server error: ${response.status}` }; + } + setError(errorData.error || "Authentication failed"); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError("Failed to connect to server"); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, []); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/eVoting/src/components/auth/login-screen.tsx b/platforms/eVoting/src/components/auth/login-screen.tsx index 6c27558c4..88f18018d 100644 --- a/platforms/eVoting/src/components/auth/login-screen.tsx +++ b/platforms/eVoting/src/components/auth/login-screen.tsx @@ -12,7 +12,26 @@ export function LoginScreen() { const [isConnecting, setIsConnecting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + // Check for query parameters and auto-login useEffect(() => { + if (typeof window === 'undefined') return; + + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow const getAuthOffer = async () => { try { const response = await apiClient.get("/api/auth/offer"); @@ -26,6 +45,37 @@ export function LoginScreen() { getAuthOffer(); }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsConnecting(true); + try { + const baseUrl = process.env.NEXT_PUBLIC_EVOTING_BASE_URL || "http://localhost:4000"; + const response = await fetch(`${baseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + localStorage.setItem("evoting_token", data.token); + localStorage.setItem("evoting_user_id", data.user.id); + window.location.href = "/"; + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + setErrorMessage(errorData.message || 'Your eID Wallet app version is outdated. Please update to continue.'); + } + setIsConnecting(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsConnecting(false); + } + }; + useEffect(() => { if (!sessionId) return; diff --git a/platforms/evoting-api/package.json b/platforms/evoting-api/package.json index 23faba7c4..edafb445e 100644 --- a/platforms/evoting-api/package.json +++ b/platforms/evoting-api/package.json @@ -27,6 +27,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "workspace:*" }, "devDependencies": { diff --git a/platforms/evoting-api/src/controllers/AuthController.ts b/platforms/evoting-api/src/controllers/AuthController.ts index 7ab9543d4..f52b826f6 100644 --- a/platforms/evoting-api/src/controllers/AuthController.ts +++ b/platforms/evoting-api/src/controllers/AuthController.ts @@ -4,6 +4,7 @@ import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { signToken } from "../utils/jwt"; import { isVersionValid } from "../utils/version"; +import { verifySignature } from "signature-validator"; const MIN_REQUIRED_VERSION = "0.4.0"; @@ -56,7 +57,7 @@ export class AuthController { login = async (req: Request, res: Response) => { try { - const { ename, session, appVersion } = req.body; + const { ename, session, appVersion, signature } = req.body; if (!ename) { return res.status(400).json({ error: "ename is required" }); @@ -66,6 +67,10 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + // Check app version - missing version is treated as old version if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { const errorMessage = { @@ -80,6 +85,27 @@ export class AuthController { }); } + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + // Find user by ename (handles @ symbol variations) const user = await this.userService.findByEname(ename); @@ -102,8 +128,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/group-charter-manager-api/package.json b/platforms/group-charter-manager-api/package.json index f381e1e66..5da80f182 100644 --- a/platforms/group-charter-manager-api/package.json +++ b/platforms/group-charter-manager-api/package.json @@ -24,6 +24,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "workspace:*" }, "devDependencies": { diff --git a/platforms/group-charter-manager-api/src/controllers/AuthController.ts b/platforms/group-charter-manager-api/src/controllers/AuthController.ts index d0b2221a1..55d5ec326 100644 --- a/platforms/group-charter-manager-api/src/controllers/AuthController.ts +++ b/platforms/group-charter-manager-api/src/controllers/AuthController.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { isVersionValid } from "../utils/version"; +import { verifySignature } from "signature-validator"; const MIN_REQUIRED_VERSION = "0.4.0"; @@ -57,7 +58,7 @@ export class AuthController { login = async (req: Request, res: Response) => { try { - const { ename, session, appVersion } = req.body; + const { ename, session, appVersion, signature } = req.body; if (!ename) { return res.status(400).json({ error: "ename is required" }); @@ -67,6 +68,10 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + // Check app version - missing version is treated as old version if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { const errorMessage = { @@ -81,6 +86,27 @@ export class AuthController { }); } + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + const { user, token } = await this.userService.findUserByEname(ename); @@ -93,8 +119,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); diff --git a/platforms/group-charter-manager/src/app/deeplink-login/page.tsx b/platforms/group-charter-manager/src/app/deeplink-login/page.tsx new file mode 100644 index 000000000..a45679534 --- /dev/null +++ b/platforms/group-charter-manager/src/app/deeplink-login/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { setAuthToken, setAuthId } from "@/lib/authUtils"; + +export default function DeeplinkLogin() { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleDeeplinkLogin = async () => { + try { + // Try parsing from search string first + let params: URLSearchParams; + let searchString = window.location.search; + + // If search is empty, try parsing from hash or full URL + if (!searchString || searchString === '') { + const hash = window.location.hash; + if (hash && hash.includes('?')) { + searchString = hash.substring(hash.indexOf('?')); + } else { + try { + const fullUrl = new URL(window.location.href); + searchString = fullUrl.search; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Remove leading ? if present + if (searchString.startsWith('?')) { + searchString = searchString.substring(1); + } + + // Parse the search string + params = new URLSearchParams(searchString); + + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (!ename || !session || !signature) { + setError("Missing required authentication parameters"); + setIsLoading(false); + return; + } + + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Make POST request to login endpoint + const baseUrl = process.env.NEXT_PUBLIC_GROUP_CHARTER_BASE_URL; + const loginUrl = `${baseUrl}/api/auth`; + const requestBody = { ename, session, signature, appVersion: appVersion || '0.4.0' }; + + const response = await fetch(loginUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + setAuthId(data.user.id); + setAuthToken(data.token); + window.location.reload(); + } else { + setError("Invalid response from server"); + setIsLoading(false); + } + } else { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: `Server error: ${response.status}` }; + } + setError(errorData.error || "Authentication failed"); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setError("Failed to connect to server"); + setIsLoading(false); + } + }; + + handleDeeplinkLogin(); + }, []); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+ +
+
+ ); + } + + return null; +} diff --git a/platforms/group-charter-manager/src/components/auth/login-screen.tsx b/platforms/group-charter-manager/src/components/auth/login-screen.tsx index c91d299b8..a76d4183c 100644 --- a/platforms/group-charter-manager/src/components/auth/login-screen.tsx +++ b/platforms/group-charter-manager/src/components/auth/login-screen.tsx @@ -15,10 +15,62 @@ export default function LoginScreen() { const [errorMessage, setErrorMessage] = useState(null); const router = useRouter(); + // Check for query parameters and auto-login useEffect(() => { + if (typeof window === 'undefined') return; + + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow initializeAuth(); }, []); + const handleAutoLogin = async (ename: string, session: string, signature: string, appVersion: string) => { + setIsAuthenticating(true); + try { + const baseUrl = process.env.NEXT_PUBLIC_GROUP_CHARTER_BASE_URL || 'http://localhost:3001'; + const response = await fetch(`${baseUrl}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + setAuthId(data.user.id); + setAuthToken(data.token); + window.location.reload(); + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + setErrorMessage(errorData.message || 'Your eID Wallet app version is outdated. Please update to continue.'); + } + setIsAuthenticating(false); + setIsLoading(false); + } + } catch (error) { + console.error('Login request failed:', error); + setIsAuthenticating(false); + setIsLoading(false); + } + }; + const initializeAuth = async () => { try { const { data } = await apiClient.get('/api/auth/offer'); diff --git a/platforms/pictique-api/package.json b/platforms/pictique-api/package.json index 1667d3f94..593dec804 100644 --- a/platforms/pictique-api/package.json +++ b/platforms/pictique-api/package.json @@ -24,6 +24,7 @@ "reflect-metadata": "^0.2.1", "typeorm": "^0.3.24", "uuid": "^9.0.1", + "signature-validator": "workspace:*", "web3-adapter": "workspace:*" }, "devDependencies": { diff --git a/platforms/pictique-api/src/controllers/AuthController.ts b/platforms/pictique-api/src/controllers/AuthController.ts index 8a0bafad1..5677d5895 100644 --- a/platforms/pictique-api/src/controllers/AuthController.ts +++ b/platforms/pictique-api/src/controllers/AuthController.ts @@ -4,6 +4,7 @@ import { UserService } from "../services/UserService"; import { EventEmitter } from "events"; import { signToken } from "../utils/jwt"; import { isVersionValid } from "../utils/version"; +import { verifySignature } from "signature-validator"; const MIN_REQUIRED_VERSION = "0.4.0"; @@ -68,7 +69,7 @@ export class AuthController { login = async (req: Request, res: Response) => { try { - const { ename, session, appVersion } = req.body; + const { ename, session, appVersion, signature } = req.body; if (!ename) { return res.status(400).json({ error: "ename is required" }); @@ -78,6 +79,10 @@ export class AuthController { return res.status(400).json({ error: "session is required" }); } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + // Check app version - missing version is treated as old version if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { const errorMessage = { @@ -92,6 +97,27 @@ export class AuthController { }); } + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.error("PUBLIC_REGISTRY_URL not configured"); + return res.status(500).json({ error: "Server configuration error" }); + } + + const verificationResult = await verifySignature({ + eName: ename, + signature: signature, + payload: session, + registryBaseUrl: registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.error("Signature validation failed:", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error + }); + } + // Find user by ename (handles @ symbol variations) let user = await this.userService.findByEname(ename); @@ -111,8 +137,10 @@ export class AuthController { }, token, }; + // Emit via SSE for backward compatibility this.eventEmitter.emit(session, data); - res.status(200).send(); + // Return JSON response for direct POST requests + res.status(200).json(data); } catch (error) { console.error("Error during login:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/platforms/pictique/src/routes/(auth)/auth/+page.svelte b/platforms/pictique/src/routes/(auth)/auth/+page.svelte index 6a10a4307..20544245c 100644 --- a/platforms/pictique/src/routes/(auth)/auth/+page.svelte +++ b/platforms/pictique/src/routes/(auth)/auth/+page.svelte @@ -35,10 +35,61 @@ return 'https://play.google.com/store/apps/details?id=foundation.metastate.eid_wallet'; } + async function handleAutoLogin( + ename: string, + session: string, + signature: string, + appVersion: string + ) { + try { + const response = await fetch(`${PUBLIC_PICTIQUE_BASE_URL}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ename, session, signature, appVersion }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.token && data.user) { + setAuthId(data.user.id); + setAuthToken(data.token); + goto('/home'); + } + } else { + const errorData = await response.json(); + console.error('Login failed:', errorData); + if (errorData.error && errorData.type === 'version_mismatch') { + errorMessage = + errorData.message || + 'Your eID Wallet app version is outdated. Please update to continue.'; + } + } + } catch (error) { + console.error('Login request failed:', error); + } + } + onMount(async () => { checkMobile(); window.addEventListener('resize', checkMobile); + // Check for query parameters and auto-login + const params = new URLSearchParams(window.location.search); + const ename = params.get('ename'); + const session = params.get('session'); + const signature = params.get('signature'); + const appVersion = params.get('appVersion'); + + if (ename && session && signature) { + // Clean up URL + window.history.replaceState({}, '', window.location.pathname); + + // Auto-submit login + await handleAutoLogin(ename, session, signature, appVersion || '0.4.0'); + return; + } + + // If no query params, proceed with normal flow const { data } = await apiClient.get('/api/auth/offer'); qrData = data.uri; diff --git a/platforms/pictique/src/routes/(auth)/deeplink-login/+page.svelte b/platforms/pictique/src/routes/(auth)/deeplink-login/+page.svelte new file mode 100644 index 000000000..9730a91a2 --- /dev/null +++ b/platforms/pictique/src/routes/(auth)/deeplink-login/+page.svelte @@ -0,0 +1,116 @@ + + +{#if isLoading} +
+
+
+

Authenticating...

+
+
+{:else if error} +
+
+
{error}
+ +
+
+{/if} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53fc32f8d..571568445 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -487,6 +487,9 @@ importers: '@types/node': specifier: ^20.11.24 version: 20.16.11 + tsx: + specifier: ^4.7.0 + version: 4.20.6 typescript: specifier: ^5.3.3 version: 5.8.2 @@ -786,6 +789,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.20 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -1237,6 +1243,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -1410,6 +1419,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -1694,6 +1706,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -1954,6 +1969,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -2202,6 +2220,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2)) @@ -2622,6 +2643,9 @@ importers: reflect-metadata: specifier: ^0.2.1 version: 0.2.2 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator typeorm: specifier: ^0.3.24 version: 0.3.27(babel-plugin-macros@3.1.0)(ioredis@5.8.2)(pg@8.16.3)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.8.2))