mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
Merge branch 'dev' of https://github.com/Memo-2023/manacore-monorepo into dev
This commit is contained in:
commit
241dc6173e
89 changed files with 5246 additions and 3492 deletions
68
apps/calendar/apps/backend/Dockerfile
Normal file
68
apps/calendar/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy calendar packages and backend
|
||||
COPY apps/calendar/packages ./apps/calendar/packages
|
||||
COPY apps/calendar/apps/backend ./apps/calendar/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/calendar/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/calendar ./apps/calendar
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/calendar/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/calendar/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3016
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3016/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/calendar/apps/backend/docker-entrypoint.sh
Normal file
23
apps/calendar/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Calendar Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/calendar/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/calendar/apps/web/Dockerfile
Normal file
84
apps/calendar/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://calendar-backend:3016
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by calendar web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy calendar packages and web
|
||||
COPY apps/calendar/packages ./apps/calendar/packages
|
||||
COPY apps/calendar/apps/web ./apps/calendar/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/calendar/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/calendar/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/calendar/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/calendar/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/calendar/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5186
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5186
|
||||
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:5186/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/calendar/apps/web/src/hooks.server.ts
Normal file
27
apps/calendar/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -2,14 +2,22 @@
|
|||
* Feedback Service Instance for Calendar Web App
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
|
||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const feedbackService = createFeedbackService({
|
||||
apiUrl: MANA_AUTH_URL,
|
||||
apiUrl: getAuthUrl(),
|
||||
appId: 'calendar',
|
||||
getAuthToken: async () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
import type { UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -17,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'calendar',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,52 +32,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Kalender"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<main
|
||||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://chat-backend:3002
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
|
|
@ -47,15 +55,19 @@ RUN pnpm build
|
|||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/chat/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/chat/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/chat/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/chat/apps/web/package.json ./
|
||||
|
||||
# Install only production dependencies for the built app
|
||||
RUN npm install --omit=dev 2>/dev/null || true
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
|
|
|
|||
68
apps/clock/apps/backend/Dockerfile
Normal file
68
apps/clock/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy clock packages and backend
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
COPY apps/clock/apps/backend ./apps/clock/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/clock/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/clock ./apps/clock
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/clock/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/clock/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3017
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3017/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/clock/apps/backend/docker-entrypoint.sh
Normal file
23
apps/clock/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Clock Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/clock/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/clock/apps/web/Dockerfile
Normal file
84
apps/clock/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://clock-backend:3017
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by clock web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy clock packages and web
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
COPY apps/clock/apps/web ./apps/clock/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/clock/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/clock/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/clock/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/clock/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/clock/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5187
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5187
|
||||
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:5187/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/clock/apps/web/src/hooks.server.ts
Normal file
27
apps/clock/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -6,8 +6,18 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -16,7 +26,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'clock',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { FeedbackPage } from '@manacore/shared-feedback-ui';
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
const feedbackService = createFeedbackService({
|
||||
appName: 'clock',
|
||||
apiUrl: 'http://localhost:3001', // Mana Core API
|
||||
apiUrl: getAuthUrl(),
|
||||
});
|
||||
|
||||
async function handleSubmit(data: { type: string; message: string; email?: string }) {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { ClockLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
// Get redirect URL from query params
|
||||
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/');
|
||||
|
||||
async function handleLogin(email: string, password: string) {
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
const result = await authStore.signIn(email, password);
|
||||
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
error = result.error || 'Login fehlgeschlagen';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
appName="Clock"
|
||||
appLogo=""
|
||||
{loading}
|
||||
{error}
|
||||
onSubmit={handleLogin}
|
||||
registerHref="/register"
|
||||
forgotPasswordHref="/forgot-password"
|
||||
logo={ClockLogo}
|
||||
primaryColor="#f59e0b"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#fef3c7"
|
||||
darkBackground="#1c1917"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export const BatchDocumentCreator: React.FC<BatchDocumentCreatorProps> = ({
|
|||
const [subjectList, setSubjectList] = useState<string[]>([]);
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
|
||||
const [documentFilter, setDocumentFilter] = useState<
|
||||
'all' | 'text' | 'context' | 'prompt'
|
||||
>('context');
|
||||
const [documentFilter, setDocumentFilter] = useState<'all' | 'text' | 'context' | 'prompt'>(
|
||||
'context'
|
||||
);
|
||||
const [promptDocuments, setPromptDocuments] = useState<Document[]>([]);
|
||||
const { mode, colors } = useTheme();
|
||||
const isDark = mode === 'dark';
|
||||
|
|
|
|||
|
|
@ -216,9 +216,7 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
|||
? 'font-medium text-gray-800 dark:text-gray-200'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
)}
|
||||
style={[
|
||||
pressed && !isLast && styles.textHovered,
|
||||
]}
|
||||
style={[pressed && !isLast && styles.textHovered]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const FilterPill: React.FC<FilterPillProps> = ({
|
|||
: isDark
|
||||
? '#1f2937'
|
||||
: '#d1d5db',
|
||||
opacity: disabled ? 0.6 : (pressed ? 0.8 : 1),
|
||||
opacity: disabled ? 0.6 : pressed ? 0.8 : 1,
|
||||
},
|
||||
]}
|
||||
onPress={disabled ? undefined : actionButton.onPress}
|
||||
|
|
|
|||
85
apps/manacore/apps/web/Dockerfile
Normal file
85
apps/manacore/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
ARG MIDDLEWARE_URL=http://mana-core-middleware:3000
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
ENV MIDDLEWARE_URL=$MIDDLEWARE_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by manacore web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-config ./packages/shared-config
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-types ./packages/shared-types
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy manacore web
|
||||
COPY apps/manacore/apps/web ./apps/manacore/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/manacore/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/manacore/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/manacore/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/manacore/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/manacore/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5173
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5173
|
||||
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:5173/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -16,8 +16,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-netlify": "^5.2.4",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.15.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
|
|
@ -50,15 +49,12 @@
|
|||
"@manacore/shared-profile-ui": "workspace:*",
|
||||
"@manacore/shared-subscription-types": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"svelte-dnd-action": "^0.9.68",
|
||||
"svelte-i18n": "^4.0.0"
|
||||
},
|
||||
|
|
|
|||
23
apps/manacore/apps/web/src/app.d.ts
vendored
23
apps/manacore/apps/web/src/app.d.ts
vendored
|
|
@ -1,18 +1,15 @@
|
|||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js';
|
||||
|
||||
/**
|
||||
* App type declarations for ManaCore web app
|
||||
*
|
||||
* Authentication is handled entirely by Mana Core Auth (@manacore/shared-auth).
|
||||
* No Supabase is needed - all data comes from mana-core-auth APIs.
|
||||
*/
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient;
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
}
|
||||
interface PageData {
|
||||
// Auth is handled by Mana Core Auth (@manacore/shared-auth), not Supabase
|
||||
// Supabase is used for database operations only
|
||||
supabase?: SupabaseClient;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface Locals {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,38 @@
|
|||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Server hooks for ManaCore web app
|
||||
*
|
||||
* Note: Authentication is handled client-side via Mana Core Auth.
|
||||
* Supabase is only used for database operations (not auth).
|
||||
* Injects runtime environment variables into the HTML for client-side access.
|
||||
* This is necessary because SvelteKit's $env/static/public bakes values at build time,
|
||||
* but Docker containers need runtime configuration.
|
||||
*/
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Create Supabase client for database operations only
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
getAll: () => event.cookies.getAll(),
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' });
|
||||
});
|
||||
},
|
||||
},
|
||||
}) as any;
|
||||
|
||||
// Auth URL
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
|
||||
// Backend URLs for dashboard widgets
|
||||
const PUBLIC_TODO_API_URL_CLIENT =
|
||||
process.env.PUBLIC_TODO_API_URL_CLIENT || process.env.PUBLIC_TODO_API_URL || '';
|
||||
const PUBLIC_CALENDAR_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CALENDAR_API_URL_CLIENT || process.env.PUBLIC_CALENDAR_API_URL || '';
|
||||
const PUBLIC_CLOCK_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CLOCK_API_URL_CLIENT || process.env.PUBLIC_CLOCK_API_URL || '';
|
||||
const PUBLIC_CONTACTS_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CONTACTS_API_URL_CLIENT || process.env.PUBLIC_CONTACTS_API_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range' || name === 'x-supabase-api-version';
|
||||
transformPageChunk: ({ html }) => {
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_TODO_API_URL__ = "${PUBLIC_TODO_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CALENDAR_API_URL__ = "${PUBLIC_CALENDAR_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CLOCK_API_URL__ = "${PUBLIC_CLOCK_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CONTACTS_API_URL__ = "${PUBLIC_CONTACTS_API_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches events from the Calendar backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const CALENDAR_API_URL = import.meta.env.PUBLIC_CALENDAR_API_URL || 'http://localhost:3014/api/v1';
|
||||
// Get Calendar API URL dynamically at runtime
|
||||
function getCalendarApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_CALENDAR_API_URL__?: string })
|
||||
.__PUBLIC_CALENDAR_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3016/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(CALENDAR_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getCalendarApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar entity from Calendar backend
|
||||
|
|
@ -59,7 +79,7 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
|
|
@ -75,7 +95,7 @@ export const calendarService = {
|
|||
*/
|
||||
async getTodayEvents(): Promise<ApiResult<CalendarEvent[]>> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${today}&endDate=${today}`
|
||||
);
|
||||
|
||||
|
|
@ -90,7 +110,7 @@ export const calendarService = {
|
|||
* Get all calendars
|
||||
*/
|
||||
async getCalendars(): Promise<ApiResult<Calendar[]>> {
|
||||
const result = await client.get<{ calendars: Calendar[] }>('/calendars');
|
||||
const result = await getClient().get<{ calendars: Calendar[] }>('/calendars');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -109,7 +129,7 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?calendarIds=${calendarId}&startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches contacts from the Contacts backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const CONTACTS_API_URL = import.meta.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015/api/v1';
|
||||
// Get Contacts API URL dynamically at runtime
|
||||
function getContactsApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_CONTACTS_API_URL__?: string })
|
||||
.__PUBLIC_CONTACTS_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3015/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(CONTACTS_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getContactsApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact entity from Contacts backend
|
||||
|
|
@ -55,7 +75,7 @@ export const contactsService = {
|
|||
* Get favorite contacts
|
||||
*/
|
||||
async getFavoriteContacts(limit: number = 5): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>(`/contacts?isFavorite=true&limit=${limit}`);
|
||||
const result = await getClient().get<Contact[]>(`/contacts?isFavorite=true&limit=${limit}`);
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
@ -63,7 +83,7 @@ export const contactsService = {
|
|||
* Get recent contacts (by updatedAt)
|
||||
*/
|
||||
async getRecentContacts(limit: number = 5): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>(`/contacts?limit=${limit}`);
|
||||
const result = await getClient().get<Contact[]>(`/contacts?limit=${limit}`);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result;
|
||||
|
|
@ -82,7 +102,7 @@ export const contactsService = {
|
|||
* Get contacts with upcoming birthdays
|
||||
*/
|
||||
async getUpcomingBirthdays(days: number = 30): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>('/contacts');
|
||||
const result = await getClient().get<Contact[]>('/contacts');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result;
|
||||
|
|
@ -113,7 +133,7 @@ export const contactsService = {
|
|||
* Get contact count
|
||||
*/
|
||||
async getContactCount(): Promise<ApiResult<{ total: number; favorites: number }>> {
|
||||
const result = await client.get<Contact[]>('/contacts');
|
||||
const result = await getClient().get<Contact[]>('/contacts');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches tasks from the Todo backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const TODO_API_URL = import.meta.env.PUBLIC_TODO_API_URL || 'http://localhost:3017/api/v1';
|
||||
// Get Todo API URL dynamically at runtime
|
||||
function getTodoApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_TODO_API_URL__?: string })
|
||||
.__PUBLIC_TODO_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3018/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(TODO_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getTodoApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task entity from Todo backend
|
||||
|
|
@ -49,7 +69,7 @@ export const todoService = {
|
|||
* Get today's tasks
|
||||
*/
|
||||
async getTodayTasks(): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/today');
|
||||
const result = await getClient().get<{ tasks: Task[] }>('/tasks/today');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -62,7 +82,7 @@ export const todoService = {
|
|||
* Get upcoming tasks for the next N days
|
||||
*/
|
||||
async getUpcomingTasks(days: number = 7): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>(`/tasks/upcoming?days=${days}`);
|
||||
const result = await getClient().get<{ tasks: Task[] }>(`/tasks/upcoming?days=${days}`);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -75,7 +95,7 @@ export const todoService = {
|
|||
* Get inbox tasks (unassigned to project)
|
||||
*/
|
||||
async getInboxTasks(): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/inbox');
|
||||
const result = await getClient().get<{ tasks: Task[] }>('/tasks/inbox');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -88,7 +108,7 @@ export const todoService = {
|
|||
* Get all projects
|
||||
*/
|
||||
async getProjects(): Promise<ApiResult<Project[]>> {
|
||||
const result = await client.get<{ projects: Project[] }>('/projects');
|
||||
const result = await getClient().get<{ projects: Project[] }>('/projects');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function getUser(event: RequestEvent) {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getSession(event: RequestEvent) {
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function requireAuth(event: RequestEvent) {
|
||||
const session = await getSession(event);
|
||||
|
||||
if (!session) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export function getSupabaseServerClient(event: RequestEvent) {
|
||||
return event.locals.supabase;
|
||||
}
|
||||
|
|
@ -7,9 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
import type { UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -18,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
@ -183,6 +192,29 @@ export const authStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*/
|
||||
async resetPassword(token: string, newPassword: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.resetPassword(token, newPassword);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'manacore',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Button, Input, Card, PageHeader } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { creditsService } from '$lib/api/credits';
|
||||
import type { CreditBalance } from '$lib/api/credits';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase }, url }) => {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email') as string;
|
||||
|
||||
if (!email) {
|
||||
return fail(400, {
|
||||
error: 'Email is required',
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the origin for the redirect URL
|
||||
const origin = url.origin;
|
||||
const redirectTo = `${origin}/auth/reset-password`;
|
||||
|
||||
// Send password reset email
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password reset error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
// Return success (we don't reveal if the email exists for security)
|
||||
return {
|
||||
success: true,
|
||||
email,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
// Validate inputs
|
||||
if (!password || !confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Both password fields are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return fail(400, {
|
||||
error: 'Password must be at least 6 characters long',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Passwords do not match',
|
||||
});
|
||||
}
|
||||
|
||||
// Update the user's password
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password update error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Success - redirect to dashboard
|
||||
throw redirect(303, '/dashboard');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,141 +1,103 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button, Input, Card } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let { form } = $props();
|
||||
let loading = $state(false);
|
||||
let hasToken = $state(false);
|
||||
let verifying = $state(true);
|
||||
let verificationError = $state<string | null>(null);
|
||||
let token = $state<string | null>(null);
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let success = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Check if we have tokens in the URL hash (from password recovery email)
|
||||
const hash = window.location.hash.substring(1); // Remove the '#'
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
const type = hashParams.get('type');
|
||||
|
||||
// Check if we have a token in the URL query params (from Supabase email link)
|
||||
const queryToken = $page.url.searchParams.get('token');
|
||||
const queryType = $page.url.searchParams.get('type');
|
||||
|
||||
if (accessToken && refreshToken && type === 'recovery') {
|
||||
// Have tokens in hash - need to establish session
|
||||
try {
|
||||
const response = await fetch('/api/auth/set-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing hash
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
} else {
|
||||
verificationError = result.error || 'Failed to establish session';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session establishment error:', error);
|
||||
verificationError = 'Failed to establish session';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else if (queryToken && queryType === 'recovery') {
|
||||
// Have token in query params - need to verify via OTP
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: queryToken, type: queryType }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing query params
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
} else {
|
||||
verificationError = result.error || 'Invalid or expired reset link';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
verificationError = 'Failed to verify reset link';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else {
|
||||
// No token found
|
||||
verifying = false;
|
||||
}
|
||||
onMount(() => {
|
||||
// Get token from URL query parameter
|
||||
token = $page.url.searchParams.get('token');
|
||||
hasToken = !!token;
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
|
||||
if (!token) {
|
||||
error = 'Reset token is missing';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
error = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 12) {
|
||||
error = 'Password must be at least 12 characters';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.resetPassword(token, password);
|
||||
|
||||
if (!result.success) {
|
||||
error = result.error || 'Failed to reset password';
|
||||
} else {
|
||||
success = true;
|
||||
// Redirect to login after 3 seconds
|
||||
setTimeout(() => {
|
||||
goto('/login');
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'An error occurred';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{#if verifying}
|
||||
Verifying your reset link...
|
||||
{#if success}
|
||||
Password reset successfully
|
||||
{:else if hasToken}
|
||||
Enter your new password
|
||||
{:else}
|
||||
Token missing or expired
|
||||
Invalid or missing reset token
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if verifying}
|
||||
{#if success}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Verifying your password reset link...</p>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if verificationError}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<div class="mb-4 text-6xl">✅</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{verificationError}
|
||||
Your password has been reset successfully. You will be redirected to the login page
|
||||
shortly.
|
||||
</p>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
href="/login"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Request a new reset link
|
||||
Go to login
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if hasToken}
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<form onsubmit={handleSubmit}>
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{form.error}
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -152,12 +114,13 @@
|
|||
name="password"
|
||||
id="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
placeholder="Enter new password"
|
||||
required
|
||||
minlength={6}
|
||||
minlength={12}
|
||||
bind:value={password}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
Must be at least 12 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -173,9 +136,10 @@
|
|||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
minlength={6}
|
||||
minlength={12}
|
||||
bind:value={confirmPassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -195,10 +159,10 @@
|
|||
This password reset link is invalid or has expired.
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
href="/forgot-password"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Back to login
|
||||
Request a new reset link
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
return {
|
||||
cookies: cookies.getAll(),
|
||||
};
|
||||
/**
|
||||
* Server layout load - minimal, auth handled by mana-core-auth client-side
|
||||
*/
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,35 +1,14 @@
|
|||
import { waitLocale } from '$lib/i18n';
|
||||
import '$lib/i18n'; // This triggers the init() call at module scope
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ data, depends }) => {
|
||||
/**
|
||||
* Layout load function
|
||||
*
|
||||
* Auth is handled entirely by Mana Core Auth (@manacore/shared-auth).
|
||||
* No Supabase is needed - all data comes from mana-core-auth APIs.
|
||||
*/
|
||||
export const load: LayoutLoad = async () => {
|
||||
await waitLocale();
|
||||
|
||||
/**
|
||||
* Declare a dependency so the layout will be invalidated when `invalidate('supabase:auth')` is called.
|
||||
*/
|
||||
depends('supabase:auth');
|
||||
|
||||
// Create Supabase client for database operations only
|
||||
// Auth is handled by Mana Core Auth (@manacore/shared-auth)
|
||||
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies;
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
// Browser client handles cookies automatically through the browser
|
||||
// This is a no-op as cookies are managed via document.cookie in the browser
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Note: Auth session is managed by Mana Core Auth via authStore,
|
||||
// not Supabase auth. Supabase is used for database operations only.
|
||||
return { supabase };
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals: { supabase } }) => {
|
||||
try {
|
||||
const { access_token, refresh_token } = await request.json();
|
||||
|
||||
if (!access_token || !refresh_token) {
|
||||
return json(
|
||||
{ success: false, error: 'Access token and refresh token are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set the session using the tokens from the URL hash
|
||||
const { data, error } = await supabase.auth.setSession({
|
||||
access_token,
|
||||
refresh_token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Set session error:', error);
|
||||
return json({ success: false, error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!data.session) {
|
||||
return json({ success: false, error: 'Failed to create session' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Session is now set via cookies by the Supabase client
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in set session:', error);
|
||||
return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals: { supabase } }) => {
|
||||
try {
|
||||
const { token, type } = await request.json();
|
||||
|
||||
if (!token || type !== 'recovery') {
|
||||
return json({ success: false, error: 'Invalid token or type' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify the OTP token and create a session
|
||||
const { data, error } = await supabase.auth.verifyOtp({
|
||||
token_hash: token,
|
||||
type: 'recovery',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return json({ success: false, error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!data.session) {
|
||||
return json({ success: false, error: 'Failed to create session' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Session is now set via cookies by the Supabase client
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in token verification:', error);
|
||||
return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
// Validate inputs
|
||||
if (!password || !confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Both password fields are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return fail(400, {
|
||||
error: 'Password must be at least 6 characters long',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Passwords do not match',
|
||||
});
|
||||
}
|
||||
|
||||
// Update the user's password
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password update error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Success - redirect to dashboard
|
||||
throw redirect(303, '/dashboard');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,214 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button, Input, Card } from '@manacore/shared-ui';
|
||||
|
||||
let { form, data } = $props();
|
||||
let loading = $state(false);
|
||||
let hasToken = $state(false);
|
||||
let verifying = $state(true);
|
||||
let verificationError = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
// Check if we have tokens in the URL hash (from password recovery email)
|
||||
const hash = window.location.hash.substring(1); // Remove the '#'
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
const type = hashParams.get('type');
|
||||
|
||||
// Check if we have a token in the URL query params (from Supabase email link)
|
||||
const queryToken = $page.url.searchParams.get('token');
|
||||
const queryType = $page.url.searchParams.get('type');
|
||||
|
||||
if (accessToken && refreshToken && type === 'recovery') {
|
||||
// Have tokens in hash - need to establish session
|
||||
try {
|
||||
const response = await fetch('/api/auth/set-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing hash
|
||||
window.history.replaceState({}, '', '/auth/reset-password');
|
||||
} else {
|
||||
verificationError = result.error || 'Failed to establish session';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session establishment error:', error);
|
||||
verificationError = 'Failed to establish session';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else if (queryToken && queryType === 'recovery') {
|
||||
// Have token in query params - need to verify via OTP
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: queryToken, type: queryType }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing query params
|
||||
window.history.replaceState({}, '', '/auth/reset-password');
|
||||
} else {
|
||||
verificationError = result.error || 'Invalid or expired reset link';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
verificationError = 'Failed to verify reset link';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else {
|
||||
// No token found
|
||||
verifying = false;
|
||||
}
|
||||
// Redirect to the main reset-password page, preserving the token query parameter
|
||||
onMount(() => {
|
||||
const token = $page.url.searchParams.get('token');
|
||||
const redirectUrl = token ? `/reset-password?token=${token}` : '/reset-password';
|
||||
goto(redirectUrl, { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reset Password - ManaCore</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-900 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{#if verifying}
|
||||
Verifying your reset link...
|
||||
{:else if hasToken}
|
||||
Enter your new password
|
||||
{:else}
|
||||
Token missing or expired
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if verifying}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Verifying your password reset link...</p>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if verificationError}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{verificationError}
|
||||
</p>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Request a new reset link
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if hasToken}
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength={6}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="confirmPassword"
|
||||
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" {loading} class="w-full">
|
||||
{loading ? 'Updating password...' : 'Update password'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
This password reset link is invalid or has expired.
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Back to login
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Redirecting...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import adapter from '@sveltejs/adapter-netlify';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
|
@ -6,7 +6,9 @@ const config = {
|
|||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
alias: {
|
||||
$lib: 'src/lib',
|
||||
$components: 'src/lib/components',
|
||||
|
|
|
|||
67
apps/todo/apps/backend/Dockerfile
Normal file
67
apps/todo/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy todo backend
|
||||
COPY apps/todo/apps/backend ./apps/todo/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/todo/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/todo/apps/backend ./apps/todo/apps/backend
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/todo/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/todo/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3018
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3018/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/todo/apps/backend/docker-entrypoint.sh
Normal file
23
apps/todo/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Todo Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/todo/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/todo/apps/web/Dockerfile
Normal file
84
apps/todo/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://todo-backend:3018
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by todo web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy todo packages and web
|
||||
COPY apps/todo/packages ./apps/todo/packages
|
||||
COPY apps/todo/apps/web ./apps/todo/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/todo/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/todo/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/todo/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/todo/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/todo/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5188
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5188
|
||||
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:5188/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/todo/apps/web/src/hooks.server.ts
Normal file
27
apps/todo/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -12,12 +12,28 @@ interface ApiError {
|
|||
statusCode: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backend URL, preferring runtime-injected value in browser
|
||||
* This allows Docker to inject PUBLIC_BACKEND_URL_CLIENT at runtime
|
||||
* instead of using the build-time PUBLIC_BACKEND_URL
|
||||
*/
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const runtimeUrl = (window as Window & { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
if (runtimeUrl) {
|
||||
return runtimeUrl;
|
||||
}
|
||||
}
|
||||
return PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
private accessToken: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
// Use getter to evaluate URL at request time (browser may hydrate after construction)
|
||||
private get baseUrl(): string {
|
||||
return getBackendUrl();
|
||||
}
|
||||
|
||||
setAccessToken(token: string | null) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -17,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'todo',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue