mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 19:49:40 +02:00
fix(manacore): auth flow and dashboard widget API fixes
Auth fixes: - Update fetchInterceptor skip patterns for ManaCore auth endpoints - Fix URL matching to compare full origins instead of partial matches - Update token manager state after successful login - Remove Supabase session dependency from layouts - Use authStore for auth state in route layouts Dashboard fixes: - Add network error detection in base-client to prevent infinite retries - Update all 9 dashboard widgets to not retry on service unavailable - Add /api/v1 prefix to all backend service URLs (chat, calendar, contacts, todo, zitare, picture, manadeck) Commands: - Add dev:manacore:backends to start all 9 dashboard backends - Add dev:manacore:full to start web + all backends together - Update COMMANDS.md with new commands and backend port table Auth service: - Fix TypeScript error: crossApp → cross_app in referrals schema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ee52f6c144
commit
a6cc0b83aa
33 changed files with 2634 additions and 68 deletions
3
apps/manacore/apps/web/src/app.d.ts
vendored
3
apps/manacore/apps/web/src/app.d.ts
vendored
|
|
@ -9,7 +9,8 @@ declare global {
|
|||
user: User | null;
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null;
|
||||
// Auth is handled by Mana Core Auth (@manacore/shared-auth), not Supabase
|
||||
// Supabase is used for database operations only
|
||||
supabase?: SupabaseClient;
|
||||
}
|
||||
// interface Error {}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,21 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a network/connection error that shouldn't be retried
|
||||
*/
|
||||
function isNetworkError(error: Error): boolean {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('failed to fetch') ||
|
||||
message.includes('network') ||
|
||||
message.includes('connection refused') ||
|
||||
message.includes('err_connection_refused') ||
|
||||
message.includes('econnrefused') ||
|
||||
message.includes('load failed')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with authentication and retry logic
|
||||
*
|
||||
|
|
@ -93,7 +108,16 @@ export async function fetchWithRetry<T>(
|
|||
const data = await response.json();
|
||||
return { data, error: null };
|
||||
} catch (e) {
|
||||
lastError = e instanceof Error ? e.message : 'Unknown error';
|
||||
const error = e instanceof Error ? e : new Error('Unknown error');
|
||||
lastError = error.message;
|
||||
|
||||
// Don't retry on network errors (service unavailable)
|
||||
if (isNetworkError(error)) {
|
||||
return {
|
||||
data: null,
|
||||
error: 'Service nicht erreichbar',
|
||||
};
|
||||
}
|
||||
|
||||
// Don't retry on last attempt
|
||||
if (attempt < config.maxRetries) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
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';
|
||||
const CALENDAR_API_URL = import.meta.env.PUBLIC_CALENDAR_API_URL || 'http://localhost:3014/api/v1';
|
||||
|
||||
const client = createApiClient(CALENDAR_API_URL);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const CHAT_API_URL = import.meta.env.PUBLIC_CHAT_API_URL || 'http://localhost:3002';
|
||||
const CHAT_API_URL = import.meta.env.PUBLIC_CHAT_API_URL || 'http://localhost:3002/api/v1';
|
||||
|
||||
const client = createApiClient(CHAT_API_URL);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
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';
|
||||
const CONTACTS_API_URL = import.meta.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015/api/v1';
|
||||
|
||||
const client = createApiClient(CONTACTS_API_URL);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const MANADECK_API_URL = import.meta.env.PUBLIC_MANADECK_API_URL || 'http://localhost:3009';
|
||||
const MANADECK_API_URL = import.meta.env.PUBLIC_MANADECK_API_URL || 'http://localhost:3009/api/v1';
|
||||
|
||||
const client = createApiClient(MANADECK_API_URL);
|
||||
|
||||
|
|
@ -64,21 +64,21 @@ export const manadeckService = {
|
|||
* Get user's decks
|
||||
*/
|
||||
async getDecks(): Promise<ApiResult<Deck[]>> {
|
||||
return client.get<Deck[]>('/api/decks');
|
||||
return client.get<Deck[]>('/decks');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get learning progress
|
||||
*/
|
||||
async getLearningProgress(): Promise<ApiResult<LearningProgress>> {
|
||||
return client.get<LearningProgress>('/api/progress');
|
||||
return client.get<LearningProgress>('/progress');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cards due for review today
|
||||
*/
|
||||
async getDueCards(limit = 10): Promise<ApiResult<Card[]>> {
|
||||
return client.get<Card[]>(`/api/cards/due?limit=${limit}`);
|
||||
return client.get<Card[]>(`/cards/due?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const PICTURE_API_URL = import.meta.env.PUBLIC_PICTURE_API_URL || 'http://localhost:3006';
|
||||
const PICTURE_API_URL = import.meta.env.PUBLIC_PICTURE_API_URL || 'http://localhost:3006/api/v1';
|
||||
|
||||
const client = createApiClient(PICTURE_API_URL);
|
||||
|
||||
|
|
@ -49,21 +49,21 @@ export const pictureService = {
|
|||
* Get user's recent generations
|
||||
*/
|
||||
async getRecentGenerations(limit = 6): Promise<ApiResult<GeneratedImage[]>> {
|
||||
return client.get<GeneratedImage[]>(`/api/generations?limit=${limit}&sort=createdAt:desc`);
|
||||
return client.get<GeneratedImage[]>(`/generations?limit=${limit}&sort=createdAt:desc`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user's favorite images
|
||||
*/
|
||||
async getFavorites(limit = 6): Promise<ApiResult<GeneratedImage[]>> {
|
||||
return client.get<GeneratedImage[]>(`/api/generations?favorite=true&limit=${limit}`);
|
||||
return client.get<GeneratedImage[]>(`/generations?favorite=true&limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get generation statistics
|
||||
*/
|
||||
async getStats(): Promise<ApiResult<GenerationStats>> {
|
||||
return client.get<GenerationStats>('/api/stats');
|
||||
return client.get<GenerationStats>('/stats');
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
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';
|
||||
const TODO_API_URL = import.meta.env.PUBLIC_TODO_API_URL || 'http://localhost:3017/api/v1';
|
||||
|
||||
const client = createApiClient(TODO_API_URL);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const ZITARE_API_URL = import.meta.env.PUBLIC_ZITARE_API_URL || 'http://localhost:3007';
|
||||
const ZITARE_API_URL = import.meta.env.PUBLIC_ZITARE_API_URL || 'http://localhost:3007/api/v1';
|
||||
|
||||
const client = createApiClient(ZITARE_API_URL);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@
|
|||
error = timersResult.error || alarmsResult.error || statsResult.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
@ -75,7 +77,7 @@
|
|||
<WidgetError {error} onRetry={load} {retrying} />
|
||||
{:else if data.length === 0}
|
||||
<div class="py-6 text-center">
|
||||
<div class="mb-2 text-3xl">=Ç</div>
|
||||
<div class="mb-2 text-3xl">=<EFBFBD></div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('dashboard.widgets.contacts.empty')}
|
||||
</p>
|
||||
|
|
@ -127,7 +129,7 @@
|
|||
rel="noopener"
|
||||
class="mt-3 block text-center text-sm text-primary hover:underline"
|
||||
>
|
||||
{$_('dashboard.widgets.contacts.view_all')} ’
|
||||
{$_('dashboard.widgets.contacts.view_all')} <EFBFBD>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@
|
|||
error = progressResult.error || decksResult.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
// Auto-retry up to 3 times
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@
|
|||
error = result.error;
|
||||
state = 'error';
|
||||
|
||||
if (retryCount < 3) {
|
||||
// Don't retry if service is unavailable (network error)
|
||||
const isServiceUnavailable = error?.includes('nicht erreichbar');
|
||||
if (!isServiceUnavailable && retryCount < 3) {
|
||||
retryCount++;
|
||||
setTimeout(load, 5000 * retryCount);
|
||||
}
|
||||
|
|
@ -53,7 +55,7 @@
|
|||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<span>=¡</span>
|
||||
<span>=<EFBFBD></span>
|
||||
{$_('dashboard.widgets.zitare.title')}
|
||||
</h3>
|
||||
{#if state === 'success' && data}
|
||||
|
|
@ -108,7 +110,7 @@
|
|||
rel="noopener"
|
||||
class="block text-center text-sm text-primary hover:underline"
|
||||
>
|
||||
{$_('dashboard.widgets.zitare.view_all')} ’
|
||||
{$_('dashboard.widgets.zitare.view_all')} <EFBFBD>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ export const authStore = {
|
|||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
|
||||
// Mark as initialized so initialize() doesn't override
|
||||
initialized = true;
|
||||
loading = false;
|
||||
|
||||
// Update token manager state to reflect valid token
|
||||
if (_tokenManager) {
|
||||
await _tokenManager.getValidToken();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
|
|
|||
|
|
@ -137,6 +137,9 @@
|
|||
});
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth store first
|
||||
await authStore.initialize();
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('manacore-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
|
|
@ -151,19 +154,19 @@
|
|||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
// Load user settings from server
|
||||
// Load user settings from server (don't await - let it load in background)
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
|
||||
// Redirect to start page if on /dashboard and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/dashboard' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/dashboard'
|
||||
) {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
userSettings.load().then(() => {
|
||||
// Redirect to start page if on /dashboard and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/dashboard' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/dashboard'
|
||||
) {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let { data, children }: { data: any; children: Snippet } = $props();
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
// Redirect authenticated users to dashboard
|
||||
// Auth state is managed by Mana Core Auth via authStore
|
||||
$effect(() => {
|
||||
if (data.session) {
|
||||
if (authStore.initialized && !authStore.loading && authStore.isAuthenticated) {
|
||||
goto('/dashboard');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export const load: LayoutLoad = async ({ data, depends }) => {
|
|||
*/
|
||||
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,
|
||||
|
|
@ -27,14 +29,7 @@ export const load: LayoutLoad = async ({ data, depends }) => {
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
return { session, supabase };
|
||||
// Note: Auth session is managed by Mana Core Auth via authStore,
|
||||
// not Supabase auth. Supabase is used for database operations only.
|
||||
return { supabase };
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue