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 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 11:42:41 +02:00
parent 57db32f1b0
commit 9d77f12c1e
6 changed files with 108 additions and 828 deletions

View file

@ -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"]

View file

@ -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;

View file

@ -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',

View file

@ -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<AuthResult> {
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<AuthResult> {
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<AuthResult> {
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<AuthResult> {
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<boolean> {
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<void> {
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);
},
};

View file

@ -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<AuthState>({
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<void> {
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);

View file

@ -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