From 6b8ab585d9ef5e7a63044b3d19213251440d1899 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:03:29 +0100 Subject: [PATCH] feat(wisekeep): add auth routes and protected layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add auth store using shared-auth - Add (auth) route group with login, register, forgot-password pages - Move main pages to (protected) route group - Add AppSlider and Header components - Update layout for authenticated routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../web/src/lib/components/AppSlider.svelte | 32 +++ .../apps/web/src/lib/stores/auth.svelte.ts | 204 ++++++++++++++++++ .../apps/web/src/routes/(auth)/+layout.svelte | 5 + .../(auth)/forgot-password/+page.svelte | 46 ++++ .../web/src/routes/(auth)/login/+page.svelte | 65 ++++++ .../src/routes/(auth)/register/+page.svelte | 60 ++++++ .../src/routes/(protected)/+layout.server.ts | 13 ++ .../web/src/routes/(protected)/+layout.svelte | 114 ++++++++++ .../src/routes/{ => (protected)}/+page.svelte | 8 +- .../{ => (protected)}/playlists/+page.svelte | 2 +- .../{ => (protected)}/transcribe/+page.svelte | 10 +- .../transcripts/+page.svelte | 6 +- .../apps/web/src/routes/+layout.svelte | 40 +--- 13 files changed, 554 insertions(+), 51 deletions(-) create mode 100644 apps/wisekeep/apps/web/src/lib/components/AppSlider.svelte create mode 100644 apps/wisekeep/apps/web/src/lib/stores/auth.svelte.ts create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/+layout.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/forgot-password/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/+layout.server.ts create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/+layout.svelte rename apps/wisekeep/apps/web/src/routes/{ => (protected)}/+page.svelte (91%) rename apps/wisekeep/apps/web/src/routes/{ => (protected)}/playlists/+page.svelte (97%) rename apps/wisekeep/apps/web/src/routes/{ => (protected)}/transcribe/+page.svelte (91%) rename apps/wisekeep/apps/web/src/routes/{ => (protected)}/transcripts/+page.svelte (88%) diff --git a/apps/wisekeep/apps/web/src/lib/components/AppSlider.svelte b/apps/wisekeep/apps/web/src/lib/components/AppSlider.svelte new file mode 100644 index 000000000..ac8bb846d --- /dev/null +++ b/apps/wisekeep/apps/web/src/lib/components/AppSlider.svelte @@ -0,0 +1,32 @@ + + + diff --git a/apps/wisekeep/apps/web/src/lib/stores/auth.svelte.ts b/apps/wisekeep/apps/web/src/lib/stores/auth.svelte.ts new file mode 100644 index 000000000..39d73edb0 --- /dev/null +++ b/apps/wisekeep/apps/web/src/lib/stores/auth.svelte.ts @@ -0,0 +1,204 @@ +/** + * Auth Store - Manages authentication state using Svelte 5 runes + * Using Mana Core Auth + */ + +import { browser } from '$app/environment'; +import { initializeWebAuth, type UserData } from '@manacore/shared-auth'; +import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; + +// Initialize Mana Core Auth only on the client side +const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; + +// Lazy initialization to avoid SSR issues with localStorage +let _authService: ReturnType['authService'] | null = null; +let _tokenManager: ReturnType['tokenManager'] | null = null; + +function getAuthService() { + if (!browser) return null; + if (!_authService) { + const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL }); + _authService = auth.authService; + _tokenManager = auth.tokenManager; + } + return _authService; +} + +// State +let user = $state(null); +let loading = $state(true); +let initialized = $state(false); + +export const authStore = { + // Getters + get user() { + return user; + }, + get loading() { + return loading; + }, + get isAuthenticated() { + return !!user; + }, + get initialized() { + return initialized; + }, + + /** + * Initialize auth state from stored tokens + */ + async initialize() { + if (initialized) return; + + const authService = getAuthService(); + if (!authService) { + initialized = true; + loading = false; + return; + } + + loading = true; + try { + const authenticated = await authService.isAuthenticated(); + if (authenticated) { + const userData = await authService.getUserFromToken(); + user = userData; + } + initialized = true; + } catch (error) { + console.error('Failed to initialize auth:', error); + user = null; + } finally { + loading = false; + } + }, + + /** + * Sign in with email and password + */ + async signIn(email: string, password: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const result = await authService.signIn(email, password); + + if (!result.success) { + return { success: false, error: result.error || 'Login failed' }; + } + + // Get user data from token + const userData = await authService.getUserFromToken(); + user = userData; + + return { success: true, error: null }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, + + /** + * Sign up with email and password + */ + async signUp(email: string, password: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server', needsVerification: false }; + } + + try { + const result = await authService.signUp(email, password); + + if (!result.success) { + return { success: false, error: result.error || 'Signup failed', needsVerification: false }; + } + + // Mana Core Auth requires separate login after signup + if (result.needsVerification) { + return { success: true, error: null, needsVerification: true }; + } + + // Auto sign in after successful signup + const signInResult = await this.signIn(email, password); + return { ...signInResult, needsVerification: false }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage, needsVerification: false }; + } + }, + + /** + * Sign out + */ + async signOut() { + const authService = getAuthService(); + if (!authService) { + user = null; + return; + } + + try { + await authService.signOut(); + user = null; + } catch (error) { + console.error('Sign out error:', error); + // Clear user even if sign out fails + user = null; + } + }, + + /** + * Send password reset email + */ + async resetPassword(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const result = await authService.forgotPassword(email); + + if (!result.success) { + return { success: false, error: result.error || 'Password reset failed' }; + } + + return { success: true, error: null }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, + + /** + * Get user credit balance + */ + async getCredits() { + const authService = getAuthService(); + if (!authService) { + return null; + } + + try { + const credits = await authService.getUserCredits(); + return credits; + } catch (error) { + console.error('Failed to get credits:', error); + return null; + } + }, + + /** + * Get access token for API calls + */ + async getAccessToken() { + const authService = getAuthService(); + if (!authService) { + return null; + } + return await authService.getAppToken(); + }, +}; diff --git a/apps/wisekeep/apps/web/src/routes/(auth)/+layout.svelte b/apps/wisekeep/apps/web/src/routes/(auth)/+layout.svelte new file mode 100644 index 000000000..a54cfdcb7 --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(auth)/+layout.svelte @@ -0,0 +1,5 @@ + + +{@render children()} diff --git a/apps/wisekeep/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/wisekeep/apps/web/src/routes/(auth)/forgot-password/+page.svelte new file mode 100644 index 000000000..fe6cae31a --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(auth)/forgot-password/+page.svelte @@ -0,0 +1,46 @@ + + + + Passwort vergessen | Wisekeep + + + + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte b/apps/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte new file mode 100644 index 000000000..ce1106f31 --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,65 @@ + + + + Anmelden | Wisekeep + + + + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte b/apps/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte new file mode 100644 index 000000000..6b69815f0 --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte @@ -0,0 +1,60 @@ + + + + Registrieren | Wisekeep + + + + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/wisekeep/apps/web/src/routes/(protected)/+layout.server.ts b/apps/wisekeep/apps/web/src/routes/(protected)/+layout.server.ts new file mode 100644 index 000000000..168a82c04 --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(protected)/+layout.server.ts @@ -0,0 +1,13 @@ +/** + * Protected routes layout server + * Auth checking is done client-side via Mana Core Auth + */ + +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ url }) => { + // Return the current path for client-side redirect logic + return { + pathname: url.pathname, + }; +}; diff --git a/apps/wisekeep/apps/web/src/routes/(protected)/+layout.svelte b/apps/wisekeep/apps/web/src/routes/(protected)/+layout.svelte new file mode 100644 index 000000000..d30ed0e71 --- /dev/null +++ b/apps/wisekeep/apps/web/src/routes/(protected)/+layout.svelte @@ -0,0 +1,114 @@ + + +{#if isChecking} + +
+
+
+{:else} +
+
+
+ Wisekeep + +
+
+ + + {$isConnected ? 'Connected' : 'Disconnected'} + +
+ {#if authStore.user} + + {/if} + +
+
+
+ +
+ {@render children()} +
+ +
+
+ Wisekeep - AI-powered wisdom extraction from video +
+
+
+{/if} diff --git a/apps/wisekeep/apps/web/src/routes/+page.svelte b/apps/wisekeep/apps/web/src/routes/(protected)/+page.svelte similarity index 91% rename from apps/wisekeep/apps/web/src/routes/+page.svelte rename to apps/wisekeep/apps/web/src/routes/(protected)/+page.svelte index 1be08ef35..a880d2b52 100644 --- a/apps/wisekeep/apps/web/src/routes/+page.svelte +++ b/apps/wisekeep/apps/web/src/routes/(protected)/+page.svelte @@ -24,7 +24,7 @@ - Transcriber - Dashboard + Dashboard | Wisekeep
@@ -38,7 +38,7 @@
Total Transcripts
-
{stats.totalTranscripts}
+
{stats.totalTranscripts}
Storage Used
@@ -59,7 +59,7 @@

Quick Start

Start New Transcription @@ -87,7 +87,7 @@
diff --git a/apps/wisekeep/apps/web/src/routes/playlists/+page.svelte b/apps/wisekeep/apps/web/src/routes/(protected)/playlists/+page.svelte similarity index 97% rename from apps/wisekeep/apps/web/src/routes/playlists/+page.svelte rename to apps/wisekeep/apps/web/src/routes/(protected)/playlists/+page.svelte index 2b0202b90..cad866fd7 100644 --- a/apps/wisekeep/apps/web/src/routes/playlists/+page.svelte +++ b/apps/wisekeep/apps/web/src/routes/(protected)/playlists/+page.svelte @@ -29,7 +29,7 @@ - Playlists - Transcriber + Playlists | Wisekeep
diff --git a/apps/wisekeep/apps/web/src/routes/transcribe/+page.svelte b/apps/wisekeep/apps/web/src/routes/(protected)/transcribe/+page.svelte similarity index 91% rename from apps/wisekeep/apps/web/src/routes/transcribe/+page.svelte rename to apps/wisekeep/apps/web/src/routes/(protected)/transcribe/+page.svelte index bc096d9ac..b9aadcd95 100644 --- a/apps/wisekeep/apps/web/src/routes/transcribe/+page.svelte +++ b/apps/wisekeep/apps/web/src/routes/(protected)/transcribe/+page.svelte @@ -53,7 +53,7 @@ - New Transcription - Transcriber + New Transcription | Wisekeep
@@ -72,7 +72,7 @@ bind:value={url} placeholder="https://www.youtube.com/watch?v=..." required - class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" + class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
@@ -81,7 +81,7 @@ {#each models as m} @@ -128,7 +128,7 @@ diff --git a/apps/wisekeep/apps/web/src/routes/transcripts/+page.svelte b/apps/wisekeep/apps/web/src/routes/(protected)/transcripts/+page.svelte similarity index 88% rename from apps/wisekeep/apps/web/src/routes/transcripts/+page.svelte rename to apps/wisekeep/apps/web/src/routes/(protected)/transcripts/+page.svelte index d241dead7..f3ff56e47 100644 --- a/apps/wisekeep/apps/web/src/routes/transcripts/+page.svelte +++ b/apps/wisekeep/apps/web/src/routes/(protected)/transcripts/+page.svelte @@ -18,7 +18,7 @@ - Transcripts - Transcriber + Transcripts | Wisekeep
@@ -31,7 +31,7 @@

No transcripts yet

Create your first transcript @@ -54,7 +54,7 @@
{#if job.transcriptText}
- + View transcript
 	import '../app.css';
-	import { onMount, onDestroy } from 'svelte';
-	import { initWebSocket, cleanup, isConnected } from '$lib/stores/jobs';
 
-	onMount(() => {
-		initWebSocket();
-	});
-
-	onDestroy(() => {
-		cleanup();
-	});
+	let { children } = $props();
 
 
-
-
-
- Transcriber - -
- - - {$isConnected ? 'Connected' : 'Disconnected'} - -
-
-
- -
- -
- - -
+{@render children()}