From 9d77f12c1efd8c1a7baf4fc4c570c57f285649fe Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 1 Apr 2026 11:42:41 +0200 Subject: [PATCH] feat(memoro/web): add Dockerfile + docker-compose for production deployment - Dockerfile using sveltekit-base:local pattern (port 5038) - docker-compose.macmini.yml entry with Traefik labels for memoro.mana.how - Delete legacy authService.ts and auth.ts (app uses shared-auth-stores) - Remove middleware env vars from env.ts and app.d.ts (dead code) Co-Authored-By: Claude Sonnet 4.6 --- apps/memoro/apps/web/Dockerfile | 62 +++ apps/memoro/apps/web/src/app.d.ts | 3 - apps/memoro/apps/web/src/lib/config/env.ts | 12 +- .../apps/web/src/lib/services/authService.ts | 491 ------------------ apps/memoro/apps/web/src/lib/stores/auth.ts | 323 ------------ docker-compose.macmini.yml | 45 ++ 6 files changed, 108 insertions(+), 828 deletions(-) create mode 100644 apps/memoro/apps/web/Dockerfile delete mode 100644 apps/memoro/apps/web/src/lib/services/authService.ts delete mode 100644 apps/memoro/apps/web/src/lib/stores/auth.ts diff --git a/apps/memoro/apps/web/Dockerfile b/apps/memoro/apps/web/Dockerfile new file mode 100644 index 000000000..0a8713c84 --- /dev/null +++ b/apps/memoro/apps/web/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1 +# Build stage - inherits pre-built shared packages from sveltekit-base +FROM sveltekit-base:local AS builder + +# Build arguments for SvelteKit static env vars (baked into the build) +ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 +ARG PUBLIC_MANA_CORE_AUTH_URL_CLIENT=https://auth.mana.how +ARG PUBLIC_MEMORO_SERVER_URL=http://memoro-server:3015 +ARG PUBLIC_SUPABASE_URL +ARG PUBLIC_SUPABASE_ANON_KEY +ARG PUBLIC_MANA_SYNC_URL=ws://mana-sync:3050 + +# Set as environment variables for build +ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL +ENV PUBLIC_MANA_CORE_AUTH_URL_CLIENT=$PUBLIC_MANA_CORE_AUTH_URL_CLIENT +ENV PUBLIC_MEMORO_SERVER_URL=$PUBLIC_MEMORO_SERVER_URL +ENV PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL +ENV PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY +ENV PUBLIC_MANA_SYNC_URL=$PUBLIC_MANA_SYNC_URL + +# Copy web app (memoro has no local packages/) +COPY apps/memoro/apps/web ./apps/memoro/apps/web + +# Install app-specific dependencies +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --no-frozen-lockfile --ignore-scripts + +# Build the web app +WORKDIR /app/apps/memoro/apps/web +RUN pnpm exec svelte-kit sync +RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build + +# Production stage +FROM node:20-alpine AS production + +# Keep same directory structure as builder so pnpm symlinks resolve correctly +WORKDIR /app/apps/memoro/apps/web + +# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm) +COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm + +# Copy the app's node_modules (contains symlinks to the pnpm store) +COPY --from=builder /app/apps/memoro/apps/web/node_modules ./node_modules + +# Copy built application +COPY --from=builder /app/apps/memoro/apps/web/build ./build +COPY --from=builder /app/apps/memoro/apps/web/package.json ./ + +# Expose port +EXPOSE 5038 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=5038 +ENV HOST=0.0.0.0 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:5038/health || exit 1 + +# Run the app +CMD ["node", "build"] diff --git a/apps/memoro/apps/web/src/app.d.ts b/apps/memoro/apps/web/src/app.d.ts index ff0daba9a..3460dd08a 100644 --- a/apps/memoro/apps/web/src/app.d.ts +++ b/apps/memoro/apps/web/src/app.d.ts @@ -21,9 +21,6 @@ declare module '$env/static/public' { export const PUBLIC_MEMORO_SERVER_URL: string; export const PUBLIC_MANA_CORE_AUTH_URL: string; export const PUBLIC_MANA_CORE_AUTH_URL_CLIENT: string; - export const PUBLIC_MEMORO_MIDDLEWARE_URL: string; - export const PUBLIC_MANA_MIDDLEWARE_URL: string; - export const PUBLIC_MIDDLEWARE_APP_ID: string; export const PUBLIC_STORAGE_BUCKET: string; export const PUBLIC_GOOGLE_CLIENT_ID: string; export const PUBLIC_APPLE_CLIENT_ID: string; diff --git a/apps/memoro/apps/web/src/lib/config/env.ts b/apps/memoro/apps/web/src/lib/config/env.ts index 9532fd3f0..1d375eac3 100644 --- a/apps/memoro/apps/web/src/lib/config/env.ts +++ b/apps/memoro/apps/web/src/lib/config/env.ts @@ -6,10 +6,7 @@ import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, - PUBLIC_MEMORO_MIDDLEWARE_URL, PUBLIC_MEMORO_SERVER_URL, - PUBLIC_MANA_MIDDLEWARE_URL, - PUBLIC_MIDDLEWARE_APP_ID, PUBLIC_STORAGE_BUCKET, PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID, @@ -31,13 +28,6 @@ export const env = { memoroUrl: PUBLIC_MEMORO_SERVER_URL, }, - // Middleware APIs (legacy — kept for authService compatibility during migration) - middleware: { - memoroUrl: PUBLIC_MEMORO_MIDDLEWARE_URL, - manaUrl: PUBLIC_MANA_MIDDLEWARE_URL, - appId: PUBLIC_MIDDLEWARE_APP_ID, - }, - // Storage storage: { bucket: PUBLIC_STORAGE_BUCKET, @@ -74,7 +64,7 @@ export const features = { if (typeof window !== 'undefined') { console.log('🔧 Memoro Environment Configuration:', { supabase: !!env.supabase.url ? '✅ Configured' : '❌ Missing', - middleware: !!env.middleware.memoroUrl ? '✅ Configured' : '❌ Missing', + server: !!env.server.memoroUrl ? '✅ Configured' : '❌ Missing', appleClientId: env.oauth.appleClientId || '❌ NOT SET', appleRedirectUri: env.oauth.appleRedirectUri || '❌ NOT SET', googleOAuth: !!env.oauth.googleClientId ? '✅ Configured' : '❌ Missing', diff --git a/apps/memoro/apps/web/src/lib/services/authService.ts b/apps/memoro/apps/web/src/lib/services/authService.ts deleted file mode 100644 index b96b1f512..000000000 --- a/apps/memoro/apps/web/src/lib/services/authService.ts +++ /dev/null @@ -1,491 +0,0 @@ -/** - * Authentication service for Memoro Web - * Uses Mana middleware for authentication instead of direct Supabase auth - */ - -import { env } from '$lib/config/env'; - -const MIDDLEWARE_URL = env.middleware.memoroUrl; -const APP_ID = env.middleware.appId; - -// Storage keys for tokens -const STORAGE_KEYS = { - APP_TOKEN: 'memoro_app_token', - REFRESH_TOKEN: 'memoro_refresh_token', - USER_EMAIL: 'memoro_user_email', -}; - -/** - * Get device information for authentication - */ -function getDeviceInfo() { - return { - deviceId: getBrowserFingerprint(), - deviceName: getBrowserName(), - deviceType: 'web', - platform: 'web', - }; -} - -/** - * Generate a browser fingerprint for device identification - */ -function getBrowserFingerprint(): string { - // Simple browser fingerprint based on available info - const ua = navigator.userAgent; - const screen = `${window.screen.width}x${window.screen.height}`; - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const lang = navigator.language; - - // Create a consistent hash - const data = `${ua}|${screen}|${timezone}|${lang}`; - return btoa(data).slice(0, 32); -} - -/** - * Get browser name - */ -function getBrowserName(): string { - const ua = navigator.userAgent; - if (ua.includes('Chrome')) return 'Chrome'; - if (ua.includes('Firefox')) return 'Firefox'; - if (ua.includes('Safari')) return 'Safari'; - if (ua.includes('Edge')) return 'Edge'; - return 'Unknown Browser'; -} - -/** - * Decode JWT token - */ -function decodeToken(token: string) { - try { - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const payload = JSON.parse(window.atob(base64)); - return payload; - } catch (error) { - console.error('Error decoding token:', error); - return null; - } -} - -/** - * Check if token is expired - */ -function isTokenExpired(token: string): boolean { - try { - const payload = decodeToken(token); - if (!payload || !payload.exp) return true; - - // Add 10 second buffer - const bufferTime = 10 * 1000; - return Date.now() >= payload.exp * 1000 - bufferTime; - } catch (error) { - return true; - } -} - -export interface AuthResult { - success: boolean; - error?: string; - needsVerification?: boolean; - appToken?: string; - refreshToken?: string; - email?: string; -} - -export interface UserData { - id: string; - email: string; - role: string; -} - -/** - * Authentication service - */ -export const authService = { - /** - * Sign in with email and password - */ - async signIn(email: string, password: string): Promise { - try { - const deviceInfo = getDeviceInfo(); - - const response = await fetch(`${MIDDLEWARE_URL}/auth/signin?appId=${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, password, deviceInfo }), - }); - - if (!response.ok) { - const errorData = await response.json(); - - // Handle specific error cases - if (response.status === 401) { - if ( - errorData.message?.includes('Firebase user detected') || - errorData.message?.includes('password reset required') - ) { - return { - success: false, - error: 'FIREBASE_USER_PASSWORD_RESET_REQUIRED', - }; - } - - if ( - errorData.message?.includes('Email not confirmed') || - errorData.message?.includes('Email not verified') - ) { - return { - success: false, - error: 'EMAIL_NOT_VERIFIED', - }; - } - - return { - success: false, - error: 'INVALID_CREDENTIALS', - }; - } - - return { - success: false, - error: errorData.message || 'Sign in failed', - }; - } - - const { appToken, refreshToken } = await response.json(); - - return { - success: true, - appToken, - refreshToken, - email, - }; - } catch (error) { - console.error('Error signing in:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error during sign in', - }; - } - }, - - /** - * Sign up with email and password - */ - async signUp(email: string, password: string): Promise { - try { - const deviceInfo = getDeviceInfo(); - - const response = await fetch(`${MIDDLEWARE_URL}/auth/signup?appId=${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, password, deviceInfo }), - }); - - if (!response.ok) { - const errorData = await response.json(); - - if (response.status === 409) { - return { - success: false, - error: 'This email is already in use', - }; - } - - return { - success: false, - error: errorData.message || 'Registration failed', - }; - } - - const responseData = await response.json(); - - // Check if email verification is required - if (responseData.confirmationRequired) { - return { - success: true, - needsVerification: true, - }; - } - - const { appToken, refreshToken } = responseData; - - return { - success: true, - appToken, - refreshToken, - email, - }; - } catch (error) { - console.error('Error signing up:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error during registration', - }; - } - }, - - /** - * Sign in with Google ID token - */ - async signInWithGoogle(idToken: string): Promise { - try { - const deviceInfo = getDeviceInfo(); - - const response = await fetch(`${MIDDLEWARE_URL}/auth/google-signin?appId=${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ token: idToken, deviceInfo }), - }); - - if (!response.ok) { - const errorData = await response.json(); - return { - success: false, - error: errorData.message || 'Google Sign-In failed', - }; - } - - const responseData = await response.json(); - const { appToken, refreshToken } = responseData; - - // Try to extract email from token - let email = responseData.email; - if (!email && appToken) { - const payload = decodeToken(appToken); - email = payload?.email || payload?.user_metadata?.email || ''; - } - - return { - success: true, - appToken, - refreshToken, - email, - }; - } catch (error) { - console.error('Error signing in with Google:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error during Google Sign-In', - }; - } - }, - - /** - * Sign in with Apple identity token - */ - async signInWithApple(identityToken: string): Promise { - try { - const deviceInfo = getDeviceInfo(); - - const response = await fetch(`${MIDDLEWARE_URL}/auth/apple-signin?appId=${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ token: identityToken, deviceInfo }), - }); - - if (!response.ok) { - const errorData = await response.json(); - return { - success: false, - error: errorData.message || 'Apple Sign-In failed', - }; - } - - const responseData = await response.json(); - const { appToken, refreshToken } = responseData; - - // Try to extract email from token - let email = responseData.email; - if (!email && appToken) { - const payload = decodeToken(appToken); - email = payload?.email || payload?.user_metadata?.email || ''; - } - - return { - success: true, - appToken, - refreshToken, - email, - }; - } catch (error) { - console.error('Error signing in with Apple:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error during Apple Sign-In', - }; - } - }, - - /** - * Refresh authentication tokens - */ - async refreshTokens(currentRefreshToken: string): Promise<{ - appToken: string; - refreshToken: string; - userData?: UserData | null; - }> { - try { - const deviceInfo = getDeviceInfo(); - - const response = await fetch(`${MIDDLEWARE_URL}/auth/refresh?appId=${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ refreshToken: currentRefreshToken, deviceInfo }), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || 'Failed to refresh tokens'); - } - - const responseData = await response.json(); - const { appToken, refreshToken } = responseData; - - if (!appToken || !refreshToken) { - throw new Error('Invalid response from token refresh'); - } - - // Extract user data from token - let userData: UserData | null = null; - try { - const payload = decodeToken(appToken); - if (payload) { - userData = { - id: payload.sub, - email: payload.email || '', - role: payload.role || 'user', - }; - } - } catch (error) { - console.error('Error decoding refreshed token:', error); - } - - return { appToken, refreshToken, userData }; - } catch (error) { - console.error('Error refreshing tokens:', error); - throw error; - } - }, - - /** - * Validate token - */ - async validateToken(appToken: string): Promise { - try { - // First check if token is expired locally - if (isTokenExpired(appToken)) { - return false; - } - - // Validate with server - const response = await fetch(`${MIDDLEWARE_URL}/auth/validate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ appToken, appId: APP_ID }), - }); - - return response.ok; - } catch (error) { - console.error('Error validating token:', error); - return false; - } - }, - - /** - * Sign out - */ - async signOut(refreshToken: string): Promise { - try { - await fetch(`${MIDDLEWARE_URL}/auth/logout`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ refreshToken }), - }).catch((err) => console.error('Error logging out on server:', err)); - } catch (error) { - console.error('Error signing out:', error); - } - }, - - /** - * Forgot password - */ - async forgotPassword(email: string): Promise<{ success: boolean; error?: string }> { - try { - const response = await fetch(`${MIDDLEWARE_URL}/auth/forgot-password`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email }), - }); - - if (!response.ok) { - const errorData = await response.json(); - - if (errorData.message?.includes('rate limit')) { - return { - success: false, - error: - 'Too many password reset attempts. Please wait a few minutes before trying again.', - }; - } - - return { - success: false, - error: errorData.message || 'Password reset failed', - }; - } - - return { success: true }; - } catch (error) { - console.error('Error sending password reset email:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error during password reset', - }; - } - }, - - /** - * Get user data from token - */ - getUserFromToken(appToken: string): UserData | null { - try { - const payload = decodeToken(appToken); - if (!payload) return null; - - return { - id: payload.sub, - email: payload.email || '', - role: payload.role || 'user', - }; - } catch (error) { - console.error('Error getting user from token:', error); - return null; - } - }, - - /** - * Check if token is valid locally (without network call) - */ - isTokenValidLocally(token: string): boolean { - return !isTokenExpired(token); - }, -}; diff --git a/apps/memoro/apps/web/src/lib/stores/auth.ts b/apps/memoro/apps/web/src/lib/stores/auth.ts deleted file mode 100644 index 5dc870d81..000000000 --- a/apps/memoro/apps/web/src/lib/stores/auth.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Auth Store for memoro-web - * Manages authentication state using Mana middleware pattern from memoro_app - */ - -import { writable, derived } from 'svelte/store'; -import { browser } from '$app/environment'; -import { authService, type UserData } from '$lib/services/authService'; -import { tokenManager, TokenState } from '$lib/services/tokenManager'; -import { clearAuthClient } from '$lib/supabaseClient'; - -// Storage keys -const STORAGE_KEYS = { - APP_TOKEN: 'memoro_app_token', - REFRESH_TOKEN: 'memoro_refresh_token', - USER_EMAIL: 'memoro_user_email', -}; - -// Auth state -interface AuthState { - user: UserData | null; - isAuthenticated: boolean; - isLoading: boolean; - error: string | null; -} - -// Create writable store -function createAuthStore() { - const { subscribe, set, update } = writable({ - user: null, - isAuthenticated: false, - isLoading: true, - error: null, - }); - - // Initialize auth state from localStorage - async function initialize() { - if (!browser) return; - - try { - const token = localStorage.getItem(STORAGE_KEYS.APP_TOKEN); - - if (!token) { - set({ user: null, isAuthenticated: false, isLoading: false, error: null }); - return; - } - - // Check if token is valid locally - if (authService.isTokenValidLocally(token)) { - const userData = authService.getUserFromToken(token); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return; - } - } - - // Token expired, try to refresh - const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); - if (refreshToken) { - try { - const result = await authService.refreshTokens(refreshToken); - if (result.appToken && result.refreshToken) { - // Store new tokens - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, result.appToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken); - - const userData = authService.getUserFromToken(result.appToken); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return; - } - } - } catch (error) { - console.error('Failed to refresh token on init:', error); - } - } - - // Could not refresh, clear state - set({ user: null, isAuthenticated: false, isLoading: false, error: null }); - } catch (error) { - console.error('Error initializing auth:', error); - set({ - user: null, - isAuthenticated: false, - isLoading: false, - error: 'Failed to initialize authentication', - }); - } - } - - // Sign in with email and password - async function signIn( - email: string, - password: string - ): Promise<{ success: boolean; error?: string }> { - update((state) => ({ ...state, isLoading: true, error: null })); - - try { - const result = await authService.signIn(email, password); - - if (!result.success) { - update((state) => ({ - ...state, - isLoading: false, - error: result.error || 'Sign in failed', - })); - return { success: false, error: result.error }; - } - - // Store tokens - if (result.appToken && result.refreshToken) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, result.appToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken); - if (result.email) { - localStorage.setItem(STORAGE_KEYS.USER_EMAIL, result.email); - } - - const userData = authService.getUserFromToken(result.appToken); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return { success: true }; - } - } - - throw new Error('Invalid auth response'); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error during sign in'; - update((state) => ({ ...state, isLoading: false, error: errorMessage })); - return { success: false, error: errorMessage }; - } - } - - // Sign up with email and password - async function signUp( - email: string, - password: string - ): Promise<{ success: boolean; error?: string; needsVerification?: boolean }> { - update((state) => ({ ...state, isLoading: true, error: null })); - - try { - const result = await authService.signUp(email, password); - - if (!result.success) { - update((state) => ({ - ...state, - isLoading: false, - error: result.error || 'Sign up failed', - })); - return { success: false, error: result.error }; - } - - // Check if email verification is required - if (result.needsVerification) { - update((state) => ({ ...state, isLoading: false, error: null })); - return { success: true, needsVerification: true }; - } - - // Store tokens - if (result.appToken && result.refreshToken) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, result.appToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken); - if (result.email) { - localStorage.setItem(STORAGE_KEYS.USER_EMAIL, result.email); - } - - const userData = authService.getUserFromToken(result.appToken); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return { success: true }; - } - } - - throw new Error('Invalid auth response'); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error during sign up'; - update((state) => ({ ...state, isLoading: false, error: errorMessage })); - return { success: false, error: errorMessage }; - } - } - - // Sign in with Google - async function signInWithGoogle(idToken: string): Promise<{ success: boolean; error?: string }> { - update((state) => ({ ...state, isLoading: true, error: null })); - - try { - const result = await authService.signInWithGoogle(idToken); - - if (!result.success) { - update((state) => ({ - ...state, - isLoading: false, - error: result.error || 'Google Sign-In failed', - })); - return { success: false, error: result.error }; - } - - // Store tokens - if (result.appToken && result.refreshToken) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, result.appToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken); - if (result.email) { - localStorage.setItem(STORAGE_KEYS.USER_EMAIL, result.email); - } - - const userData = authService.getUserFromToken(result.appToken); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return { success: true }; - } - } - - throw new Error('Invalid auth response'); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error during Google Sign-In'; - update((state) => ({ ...state, isLoading: false, error: errorMessage })); - return { success: false, error: errorMessage }; - } - } - - // Sign in with Apple - // Note: On web, Apple returns an authorization code (not identity token like mobile) - // The middleware needs to exchange the code for an identity token - async function signInWithApple( - authorizationCode: string - ): Promise<{ success: boolean; error?: string }> { - update((state) => ({ ...state, isLoading: true, error: null })); - - try { - // For web, we send the authorization code - // Middleware will need to exchange it for an identity token - // TODO: Confirm middleware supports authorization code exchange for Apple - const result = await authService.signInWithApple(authorizationCode); - - if (!result.success) { - update((state) => ({ - ...state, - isLoading: false, - error: result.error || 'Apple Sign-In failed', - })); - return { success: false, error: result.error }; - } - - // Store tokens - if (result.appToken && result.refreshToken) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, result.appToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken); - if (result.email) { - localStorage.setItem(STORAGE_KEYS.USER_EMAIL, result.email); - } - - const userData = authService.getUserFromToken(result.appToken); - if (userData) { - set({ user: userData, isAuthenticated: true, isLoading: false, error: null }); - return { success: true }; - } - } - - throw new Error('Invalid auth response'); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error during Apple Sign-In'; - update((state) => ({ ...state, isLoading: false, error: errorMessage })); - return { success: false, error: errorMessage }; - } - } - - // Sign out - async function signOut(): Promise { - update((state) => ({ ...state, isLoading: true })); - - try { - const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); - if (refreshToken) { - await authService.signOut(refreshToken); - } - } catch (error) { - console.error('Error during sign out:', error); - } finally { - // Clear local storage - localStorage.removeItem(STORAGE_KEYS.APP_TOKEN); - localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); - localStorage.removeItem(STORAGE_KEYS.USER_EMAIL); - - // Clear token manager - await tokenManager.clearTokens(); - - // Clear Supabase client - clearAuthClient(); - - // Reset state - set({ user: null, isAuthenticated: false, isLoading: false, error: null }); - } - } - - // Forgot password - async function forgotPassword(email: string): Promise<{ success: boolean; error?: string }> { - return authService.forgotPassword(email); - } - - // Initialize on load - if (browser) { - initialize(); - } - - return { - subscribe, - signIn, - signUp, - signInWithGoogle, - signInWithApple, - signOut, - forgotPassword, - initialize, - }; -} - -export const auth = createAuthStore(); - -// Derived stores for convenience -export const user = derived(auth, ($auth) => $auth.user); -export const isAuthenticated = derived(auth, ($auth) => $auth.isAuthenticated); -export const isLoading = derived(auth, ($auth) => $auth.isLoading); diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index a199d845d..ba5a4dcf0 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1527,6 +1527,51 @@ services: retries: 3 start_period: 20s + memoro-web: + build: + context: . + dockerfile: apps/memoro/apps/web/Dockerfile + args: + PUBLIC_MANA_CORE_AUTH_URL: http://mana-auth:3001 + PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.mana.how + PUBLIC_MEMORO_SERVER_URL: http://memoro-server:3015 + PUBLIC_SUPABASE_URL: ${MEMORO_SUPABASE_URL} + PUBLIC_SUPABASE_ANON_KEY: ${MEMORO_SUPABASE_ANON_KEY} + PUBLIC_MANA_SYNC_URL: ws://mana-sync:3050 + image: memoro-web:local + container_name: mana-app-memoro-web + restart: always + mem_limit: 128m + depends_on: + memoro-server: + condition: service_healthy + mana-auth: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 5038 + HOST: 0.0.0.0 + PUBLIC_MANA_CORE_AUTH_URL: http://mana-auth:3001 + PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.mana.how + PUBLIC_MEMORO_SERVER_URL: http://memoro-server:3015 + PUBLIC_SUPABASE_URL: ${MEMORO_SUPABASE_URL} + PUBLIC_SUPABASE_ANON_KEY: ${MEMORO_SUPABASE_ANON_KEY} + PUBLIC_MANA_SYNC_URL: ws://mana-sync:3050 + ports: + - "5038:5038" + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5038/health"] + interval: 180s + timeout: 10s + retries: 3 + start_period: 20s + labels: + - "traefik.enable=true" + - "traefik.http.routers.memoro-web.rule=Host(`memoro.mana.how`)" + - "traefik.http.routers.memoro-web.entrypoints=websecure" + - "traefik.http.routers.memoro-web.tls.certresolver=letsencrypt" + - "traefik.http.services.memoro-web.loadbalancer.server.port=5038" + mana-llm: build: context: ./services/mana-llm