feat(picture): migrate to mana-core auth and update AI models

Web app:
- Replace Supabase auth with @manacore/shared-auth
- Add Svelte 5 runes auth store (auth.svelte.ts)
- Remove deprecated auth files (LoginForm, SignupForm, authService, supabase.ts)
- Update all components to use authStore

Backend:
- Update AI models: flux-schnell, seedream-3, nano-banana
- Remove old models: sdxl, flux-pro
- Make models endpoint public (no auth required)
- Add unique constraint to model name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-28 20:27:39 +01:00
parent 468b7b785c
commit 5e15f57816
25 changed files with 177 additions and 617 deletions

View file

@ -2,7 +2,7 @@ import { pgTable, uuid, text, timestamp, boolean, integer, real } from 'drizzle-
export const models = pgTable('models', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
name: text('name').notNull().unique(),
displayName: text('display_name').notNull(),
description: text('description'),
replicateId: text('replicate_id').notNull(),

View file

@ -1,63 +1,85 @@
import * as dotenv from 'dotenv';
import { eq } from 'drizzle-orm';
import { getDb, closeConnection } from './connection';
import { models } from './schema';
dotenv.config();
const defaultModels = [
{
name: 'sdxl',
displayName: 'Stable Diffusion XL',
description: 'High-quality image generation with excellent prompt adherence',
replicateId: 'stability-ai/sdxl',
version: '39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b',
defaultWidth: 1024,
defaultHeight: 1024,
defaultSteps: 25,
defaultGuidanceScale: 7.5,
supportsNegativePrompt: true,
supportsImg2Img: true,
supportsSeed: true,
isActive: true,
isDefault: true,
sortOrder: 0,
estimatedTimeSeconds: 15,
},
{
name: 'flux-schnell',
displayName: 'FLUX Schnell',
description: 'Fast image generation with good quality',
description:
'Schnellstes Modell von Black Forest Labs - generiert hochwertige Bilder in nur 1-4 Schritten. Ideal für schnelle Iterationen.',
replicateId: 'black-forest-labs/flux-schnell',
version: 'f2ab8a5bfe79f02f0789a146cf5e73d2a4ff2684a98c2b303d1e1ff3814271db',
version: 'c846a69991daf4c0e5d016514849d14ee5b2e6846ce6b9d6f21369e564cfe51e',
defaultWidth: 1024,
defaultHeight: 1024,
defaultSteps: 4,
defaultGuidanceScale: 0,
minWidth: 256,
minHeight: 256,
maxWidth: 1440,
maxHeight: 1440,
maxSteps: 4,
supportsNegativePrompt: false,
supportsImg2Img: false,
supportsSeed: true,
isActive: true,
isDefault: true,
sortOrder: 0,
costPerGeneration: 0.003,
estimatedTimeSeconds: 3,
},
{
name: 'seedream-3',
displayName: 'Seedream 3',
description:
'ByteDances hochauflösendes Modell mit nativer 2K-Ausgabe. Exzellente Text-Darstellung und fotorealistische Portraits.',
replicateId: 'bytedance/seedream-3',
version: 'ed344813bc9f4996be6de4febd8b9c14c7849ad7b21ab047572e3620ee374ee7',
defaultWidth: 2048,
defaultHeight: 2048,
defaultSteps: 1, // Model handles steps internally
defaultGuidanceScale: 2.5,
minWidth: 512,
minHeight: 512,
maxWidth: 2048,
maxHeight: 2048,
maxSteps: 1,
supportsNegativePrompt: false,
supportsImg2Img: false,
supportsSeed: true,
isActive: true,
isDefault: false,
sortOrder: 1,
costPerGeneration: 0.03,
estimatedTimeSeconds: 5,
},
{
name: 'flux-pro',
displayName: 'FLUX Pro',
description: 'Professional quality image generation',
replicateId: 'black-forest-labs/flux-pro',
version: '7d6fbcd3da3f4e1c1c08d8ab0e7a4c2e0e5e3c9e8f8e8e8e8e8e8e8e8e8e8e8e',
name: 'nano-banana',
displayName: 'Nano Banana',
description:
'Googles multimodales Gemini-Modell für Bildgenerierung und -bearbeitung. Beste Textdarstellung in Bildern und Charakter-Konsistenz.',
replicateId: 'google/nano-banana',
version: 'd05a591283da31be3eea28d5634ef9e26989b351718b6489bd308426ebd0a3e8',
defaultWidth: 1024,
defaultHeight: 1024,
defaultSteps: 25,
defaultGuidanceScale: 3.5,
defaultSteps: 1, // Model handles steps internally
defaultGuidanceScale: 0,
minWidth: 512,
minHeight: 512,
maxWidth: 2048,
maxHeight: 2048,
maxSteps: 1,
supportsNegativePrompt: false,
supportsImg2Img: false,
supportsSeed: true,
supportsImg2Img: true, // Supports image editing
supportsSeed: false, // No seed parameter exposed
isActive: true,
isDefault: false,
sortOrder: 2,
estimatedTimeSeconds: 20,
costPerGeneration: 0.039,
estimatedTimeSeconds: 3,
},
];
@ -69,12 +91,58 @@ async function seed() {
const db = getDb(databaseUrl);
console.log('Clearing old models...');
// Remove old models that are not in the new list
const oldModelNames = ['sdxl', 'flux-pro'];
for (const name of oldModelNames) {
try {
await db.delete(models).where(eq(models.name, name));
console.log(` - Removed: ${name}`);
} catch (error) {
console.error(` - Error removing ${name}:`, error);
}
}
console.log('Seeding models...');
for (const model of defaultModels) {
try {
await db.insert(models).values(model).onConflictDoNothing();
console.log(` - ${model.displayName}`);
// Check if model exists
const existing = await db.select().from(models).where(eq(models.name, model.name));
if (existing.length > 0) {
// Update existing
await db
.update(models)
.set({
displayName: model.displayName,
description: model.description,
replicateId: model.replicateId,
version: model.version,
defaultWidth: model.defaultWidth,
defaultHeight: model.defaultHeight,
defaultSteps: model.defaultSteps,
defaultGuidanceScale: model.defaultGuidanceScale,
minWidth: model.minWidth,
minHeight: model.minHeight,
maxWidth: model.maxWidth,
maxHeight: model.maxHeight,
maxSteps: model.maxSteps,
supportsNegativePrompt: model.supportsNegativePrompt,
supportsImg2Img: model.supportsImg2Img,
supportsSeed: model.supportsSeed,
isActive: model.isActive,
isDefault: model.isDefault,
sortOrder: model.sortOrder,
costPerGeneration: model.costPerGeneration,
estimatedTimeSeconds: model.estimatedTimeSeconds,
})
.where(eq(models.name, model.name));
console.log(` - Updated: ${model.displayName}`);
} else {
// Insert new
await db.insert(models).values(model);
console.log(` - Added: ${model.displayName}`);
}
} catch (error) {
console.error(` - Error seeding ${model.displayName}:`, error);
}

View file

@ -1,12 +1,11 @@
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { Controller, Get, Param } from '@nestjs/common';
import { ModelService } from './model.service';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
@Controller('models')
@UseGuards(JwtAuthGuard)
export class ModelController {
constructor(private readonly modelService: ModelService) {}
// Models are public - no auth required
@Get()
async getActiveModels() {
return this.modelService.getActiveModels();

View file

@ -16,6 +16,7 @@
"clean": "rm -rf .svelte-kit build node_modules"
},
"dependencies": {
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
@ -26,7 +27,6 @@
"@manacore/shared-ui": "workspace:*",
"@picture/design-tokens": "workspace:*",
"@picture/shared": "workspace:*",
"@supabase/supabase-js": "^2.38.4",
"konva": "^10.0.2",
"posthog-js": "^1.273.1",
"svelte-i18n": "^4.0.1"

View file

@ -1,140 +0,0 @@
<script lang="ts">
import Button from '../ui/Button.svelte';
import Input from '../ui/Input.svelte';
import Card from '../ui/Card.svelte';
import { supabase } from '$lib/supabase';
import { goto } from '$app/navigation';
let email = $state('');
let password = $state('');
let loading = $state(false);
let error = $state('');
async function handleLogin() {
if (!email || !password) {
error = 'Please fill in all fields';
return;
}
loading = true;
error = '';
const { data, error: authError } = await supabase.auth.signInWithPassword({
email,
password,
});
loading = false;
if (authError) {
error = authError.message;
return;
}
if (data.session) {
goto('/app/gallery');
}
}
async function handleGoogleLogin() {
loading = true;
const { error: authError } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/app/gallery`,
},
});
if (authError) {
error = authError.message;
loading = false;
}
}
</script>
<Card class="w-full max-w-md">
<div class="mb-6 text-center">
<h2 class="text-2xl font-bold text-gray-900">Welcome back</h2>
<p class="mt-2 text-sm text-gray-600">Sign in to your account</p>
</div>
{#if error}
<div class="mb-4 rounded-md bg-red-50 p-3">
<p class="text-sm text-red-800">{error}</p>
</div>
{/if}
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
class="space-y-4"
>
<div>
<Input
type="email"
label="Email"
placeholder="you@example.com"
bind:value={email}
required
autocomplete="email"
/>
</div>
<div>
<Input
type="password"
label="Password"
placeholder="••••••••"
bind:value={password}
required
autocomplete="current-password"
/>
</div>
<div class="flex items-center justify-between">
<a href="/auth/forgot-password" class="text-sm text-blue-600 hover:text-blue-500">
Forgot password?
</a>
</div>
<Button type="submit" variant="primary" class="w-full" {loading}>Sign in</Button>
</form>
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white px-2 text-gray-500">Or continue with</span>
</div>
</div>
<Button variant="outline" class="w-full" onclick={handleGoogleLogin} {loading}>
<svg class="mr-2 h-5 w-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Google
</Button>
<p class="mt-6 text-center text-sm text-gray-600">
Don't have an account?
<a href="/auth/signup" class="font-medium text-blue-600 hover:text-blue-500"> Sign up </a>
</p>
</Card>

View file

@ -1,126 +0,0 @@
<script lang="ts">
import Button from '../ui/Button.svelte';
import Input from '../ui/Input.svelte';
import Card from '../ui/Card.svelte';
import { supabase } from '$lib/supabase';
import { goto } from '$app/navigation';
let email = $state('');
let password = $state('');
let confirmPassword = $state('');
let loading = $state(false);
let error = $state('');
let success = $state(false);
async function handleSignup() {
if (!email || !password || !confirmPassword) {
error = 'Please fill in all fields';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 6) {
error = 'Password must be at least 6 characters';
return;
}
loading = true;
error = '';
const { data, error: authError } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/app/gallery`,
},
});
loading = false;
if (authError) {
error = authError.message;
return;
}
// Check if email confirmation is required
if (data.user && !data.session) {
success = true;
} else if (data.session) {
goto('/app/gallery');
}
}
</script>
<Card class="w-full max-w-md">
<div class="mb-6 text-center">
<h2 class="text-2xl font-bold text-gray-900">Create account</h2>
<p class="mt-2 text-sm text-gray-600">Start generating AI images today</p>
</div>
{#if success}
<div class="mb-4 rounded-md bg-green-50 p-4">
<h3 class="text-sm font-medium text-green-800">Check your email</h3>
<p class="mt-2 text-sm text-green-700">
We've sent you a confirmation link. Please check your email to verify your account.
</p>
</div>
{:else}
{#if error}
<div class="mb-4 rounded-md bg-red-50 p-3">
<p class="text-sm text-red-800">{error}</p>
</div>
{/if}
<form
onsubmit={(e) => {
e.preventDefault();
handleSignup();
}}
class="space-y-4"
>
<div>
<Input
type="email"
label="Email"
placeholder="you@example.com"
bind:value={email}
required
autocomplete="email"
/>
</div>
<div>
<Input
type="password"
label="Password"
placeholder="••••••••"
bind:value={password}
required
autocomplete="new-password"
/>
</div>
<div>
<Input
type="password"
label="Confirm Password"
placeholder="••••••••"
bind:value={confirmPassword}
required
autocomplete="new-password"
/>
</div>
<Button type="submit" variant="primary" class="w-full" {loading}>Create account</Button>
</form>
<p class="mt-6 text-center text-sm text-gray-600">
Already have an account?
<a href="/auth/login" class="font-medium text-blue-600 hover:text-blue-500"> Sign in </a>
</p>
{/if}
</Card>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { images, isLoading as isLoadingImages } from '$lib/stores/images';
import { canvasItems, addCanvasItem } from '$lib/stores/canvas';
import { getImages } from '$lib/api/images';
@ -30,18 +30,18 @@
// Load images when modal opens
$effect(() => {
if (open && $user) {
if (open && authStore.user) {
loadImages();
}
});
async function loadImages() {
if (!$user) return;
if (!authStore.user) return;
isLoadingImages.set(true);
try {
const data = await getImages({
userId: $user.id,
userId: authStore.user.id,
page: 1,
limit: 50,
archived: false,

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { models, selectedModel, isLoadingModels } from '$lib/stores/models';
import { isGenerating, generationProgress, generationError } from '$lib/stores/generate';
import { isSidebarCollapsed } from '$lib/stores/sidebar';
@ -70,7 +70,7 @@
}
async function handleQuickGenerate() {
if (!$user || !selectedModelId || !prompt.trim()) return;
if (!authStore.user || !selectedModelId || !prompt.trim()) return;
isGenerating.set(true);
generationError.set('');

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { models, selectedModel, isLoadingModels } from '$lib/stores/models';
import { isGenerating, generationProgress, generationError } from '$lib/stores/generate';
import { getActiveModels } from '$lib/api/models';
@ -45,7 +45,7 @@
}
async function handleGenerate() {
if (!$user || !selectedModelId || !prompt.trim()) return;
if (!authStore.user || !selectedModelId || !prompt.trim()) return;
isGenerating.set(true);
generationError.set('');

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { supabase } from '$lib/supabase';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
@ -8,7 +7,7 @@
let showMobileMenu = $state(false);
async function handleLogout() {
await supabase.auth.signOut();
await authStore.signOut();
goto('/auth/login');
}
@ -86,7 +85,7 @@
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-blue-600 text-white dark:bg-blue-500"
>
{$user?.email?.charAt(0).toUpperCase()}
{authStore.user?.email?.charAt(0).toUpperCase()}
</div>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
@ -103,7 +102,7 @@
class="absolute right-0 mt-2 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<div class="border-b border-gray-100 px-4 py-2 dark:border-gray-700">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{$user?.email}</p>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{authStore.user?.email}</p>
</div>
<a
href="/app/profile"

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { supabase } from '$lib/supabase';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { currentTheme } from '$lib/stores/theme';
@ -26,7 +25,7 @@
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
async function handleLogout() {
await supabase.auth.signOut();
await authStore.signOut();
goto('/auth/login');
}
@ -469,11 +468,11 @@
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
style="background-color: {$currentTheme.primary.default};"
>
{$user?.email?.charAt(0).toUpperCase()}
{authStore.user?.email?.charAt(0).toUpperCase()}
</div>
<div class="flex-1 overflow-hidden text-left">
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
{$user?.email?.split('@')[0]}
{authStore.user?.email?.split('@')[0]}
</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">Account</p>
</div>
@ -545,7 +544,7 @@
class="flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold text-white"
style="background-color: {$currentTheme.primary.default};"
>
{$user?.email?.charAt(0).toUpperCase()}
{authStore.user?.email?.charAt(0).toUpperCase()}
</button>
</div>

View file

@ -7,7 +7,7 @@
hideTagSubmenu,
} from '$lib/stores/contextMenu';
import { tags } from '$lib/stores/tags';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { addTagToImage, removeTagFromImage, getImageTags } from '$lib/api/tags';
import { archiveImage, unarchiveImage, deleteImage, toggleFavorite } from '$lib/api/images';
import { images } from '$lib/stores/images';
@ -29,7 +29,7 @@
const isFavorite = $derived($contextMenu.image?.is_favorite === true);
// Check if current image belongs to current user
const isOwnImage = $derived($contextMenu.image?.user_id === $user?.id);
const isOwnImage = $derived($contextMenu.image?.user_id === authStore.user?.id);
interface MenuItem {
label: string;

View file

@ -1,221 +0,0 @@
/**
* Authentication service for Picture Web
* Uses Supabase auth with compatible interface for shared-auth-ui
*/
import { supabase } from '$lib/supabase';
export interface AuthResult {
success: boolean;
error?: string;
needsVerification?: boolean;
}
export interface UserData {
id: string;
email: string;
role: string;
}
/**
* Authentication service compatible with @manacore/shared-auth-ui
*/
export const authService = {
/**
* Sign in with email and password
*/
async signIn(email: string, password: string): Promise<AuthResult> {
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
// Handle specific error cases
if (error.message?.includes('Invalid login credentials')) {
return {
success: false,
error: 'INVALID_CREDENTIALS',
};
}
if (error.message?.includes('Email not confirmed')) {
return {
success: false,
error: 'EMAIL_NOT_VERIFIED',
};
}
return {
success: false,
error: error.message || 'Sign in failed',
};
}
if (data.session) {
return { success: true };
}
return {
success: false,
error: 'No session returned',
};
} catch (error) {
console.error('Error signing in:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during sign in',
};
}
},
/**
* Sign up with email and password
*/
async signUp(email: string, password: string): Promise<AuthResult> {
try {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) {
if (error.message?.includes('already registered')) {
return {
success: false,
error: 'This email is already in use',
};
}
return {
success: false,
error: error.message || 'Registration failed',
};
}
// Check if email confirmation is required
if (data.user && !data.session) {
return {
success: true,
needsVerification: true,
};
}
return { success: true };
} catch (error) {
console.error('Error signing up:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during registration',
};
}
},
/**
* Sign in with Google (OAuth)
*/
async signInWithGoogle(): Promise<AuthResult> {
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/app/gallery`,
},
});
if (error) {
return {
success: false,
error: error.message || 'Google Sign-In failed',
};
}
// OAuth redirects, so if we get here, it's working
return { success: true };
} catch (error) {
console.error('Error signing in with Google:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during Google Sign-In',
};
}
},
/**
* Sign in with Apple (OAuth)
*/
async signInWithApple(): Promise<AuthResult> {
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo: `${window.location.origin}/app/gallery`,
},
});
if (error) {
return {
success: false,
error: error.message || 'Apple Sign-In failed',
};
}
return { success: true };
} catch (error) {
console.error('Error signing in with Apple:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during Apple Sign-In',
};
}
},
/**
* Sign out
*/
async signOut(): Promise<void> {
try {
await supabase.auth.signOut();
} catch (error) {
console.error('Error signing out:', error);
}
},
/**
* Forgot password
*/
async forgotPassword(email: string): Promise<AuthResult> {
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`,
});
if (error) {
if (error.message?.includes('rate limit')) {
return {
success: false,
error:
'Too many password reset attempts. Please wait a few minutes before trying again.',
};
}
return {
success: false,
error: error.message || 'Password reset failed',
};
}
return { success: true };
} catch (error) {
console.error('Error sending password reset email:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during password reset',
};
}
},
};

View file

@ -1,6 +0,0 @@
import { writable } from 'svelte/store';
import type { User, Session } from '@supabase/supabase-js';
export const user = writable<User | null>(null);
export const session = writable<Session | null>(null);
export const loading = writable(true);

View file

@ -1,11 +0,0 @@
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@picture/shared/types';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
});

View file

@ -1,18 +1,15 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
const unsubscribe = user.subscribe((currentUser) => {
if (currentUser) {
goto('/app/gallery');
} else {
goto('/auth/login');
}
});
return unsubscribe;
onMount(async () => {
await authStore.initialize();
if (authStore.isAuthenticated) {
goto('/app/gallery');
} else {
goto('/auth/login');
}
});
</script>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import {
archivedImages,
isLoadingArchive,
@ -47,11 +47,11 @@
});
async function loadInitialImages() {
if (!$user) return;
if (!authStore.user) return;
isLoadingArchive.set(true);
try {
const data = await getImages({ userId: $user.id, page: 1, archived: true });
const data = await getImages({ userId: authStore.user.id, page: 1, archived: true });
archivedImages.set(data);
currentArchivePage.set(1);
hasMoreArchive.set(data.length === 20);
@ -63,13 +63,13 @@
}
async function loadMoreImages() {
if (!$user || !$hasMoreArchive || $isLoadingArchive || loadingMore) return;
if (!authStore.user || !$hasMoreArchive || $isLoadingArchive || loadingMore) return;
loadingMore = true;
const nextPage = $currentArchivePage + 1;
try {
const newImages = await getImages({ userId: $user.id, page: nextPage, archived: true });
const newImages = await getImages({ userId: authStore.user.id, page: nextPage, archived: true });
if (newImages.length > 0) {
archivedImages.update((current) => [...current, ...newImages]);
currentArchivePage.set(nextPage);

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import {
boards,
isLoadingBoards,
@ -58,11 +58,11 @@
});
async function loadInitialBoards() {
if (!$user) return;
if (!authStore.user) return;
isLoadingBoards.set(true);
try {
const data = await getBoards({ userId: $user.id, page: 1 });
const data = await getBoards({ userId: authStore.user.id, page: 1 });
boards.set(data);
currentBoardsPage.set(1);
hasBoardsMore.set(data.length === 20);
@ -75,13 +75,13 @@
}
async function loadMoreBoards() {
if (!$user || !$hasBoardsMore || $isLoadingBoards || loadingMore) return;
if (!authStore.user || !$hasBoardsMore || $isLoadingBoards || loadingMore) return;
loadingMore = true;
const nextPage = $currentBoardsPage + 1;
try {
const newBoards = await getBoards({ userId: $user.id, page: nextPage });
const newBoards = await getBoards({ userId: authStore.user.id, page: nextPage });
if (newBoards.length > 0) {
boards.update((current) => [...current, ...newBoards]);
currentBoardsPage.set(nextPage);
@ -97,13 +97,13 @@
}
async function handleCreateBoard() {
if (!$user || !boardName.trim()) return;
if (!authStore.user || !boardName.trim()) return;
isCreating = true;
try {
const { createBoard } = await import('$lib/api/boards');
const newBoard = await createBoard({
user_id: $user.id,
user_id: authStore.user.id,
name: boardName,
description: boardDescription || null,
});
@ -136,10 +136,10 @@
}
async function handleDuplicateBoard(boardId: string) {
if (!$user) return;
if (!authStore.user) return;
try {
const newBoard = await duplicateBoard(boardId, $user.id);
const newBoard = await duplicateBoard(boardId, authStore.user.id);
addBoard({ ...newBoard, item_count: 0 });
showToast('Board dupliziert', 'success');
} catch (error) {

View file

@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { currentBoard } from '$lib/stores/boards';
import {
canvasItems,
@ -40,7 +40,7 @@
});
async function loadBoard() {
if (!$user) return;
if (!authStore.user) return;
isLoading = true;
try {
@ -48,7 +48,7 @@
const board = await getBoardById(boardId);
// Check if user has access
if (board.user_id !== $user.id && !board.is_public) {
if (board.user_id !== authStore.user.id && !board.is_public) {
showToast('Zugriff verweigert', 'error');
goto('/app/board');
return;
@ -75,7 +75,7 @@
}
async function handleAddText() {
if (!$user || !boardId) return;
if (!authStore.user || !boardId) return;
try {
// Add text to center of visible area

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import {
images,
isLoading,
@ -67,12 +67,12 @@
});
async function loadInitialImages() {
if (!$user) return;
if (!authStore.user) return;
isLoading.set(true);
try {
const data = await getImages({
userId: $user.id,
userId: authStore.user.id,
page: 1,
tagIds: $selectedTags.length > 0 ? $selectedTags : undefined,
favoritesOnly: $showFavoritesOnly,
@ -88,14 +88,14 @@
}
async function loadMoreImages() {
if (!$user || !$hasMore || $isLoading || loadingMore) return;
if (!authStore.user || !$hasMore || $isLoading || loadingMore) return;
loadingMore = true;
const nextPage = $currentPage + 1;
try {
const newImages = await getImages({
userId: $user.id,
userId: authStore.user.id,
page: nextPage,
tagIds: $selectedTags.length > 0 ? $selectedTags : undefined,
favoritesOnly: $showFavoritesOnly,

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { supabase } from '$lib/supabase';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import Card from '$lib/components/ui/Card.svelte';
import Button from '$lib/components/ui/Button.svelte';
@ -11,8 +10,7 @@
async function handleLogout() {
isLoggingOut = true;
try {
const { error } = await supabase.auth.signOut();
if (error) throw error;
await authStore.signOut();
goto('/');
} catch (error) {
console.error('Error logging out:', error);
@ -58,9 +56,9 @@
>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</h3>
<p class="mt-1 text-gray-900 dark:text-gray-100">{$user?.email || 'Not available'}</p>
<p class="mt-1 text-gray-900 dark:text-gray-100">{authStore.user?.email || 'Not available'}</p>
</div>
{#if $user?.email_confirmed_at}
{#if authStore.user?.email}
<span class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800">
Verified
</span>
@ -77,7 +75,7 @@
<div class="flex items-center justify-between border-b border-gray-200 pb-4">
<div>
<h3 class="text-sm font-medium text-gray-500">User ID</h3>
<p class="mt-1 font-mono text-sm text-gray-900">{$user?.id || 'Not available'}</p>
<p class="mt-1 font-mono text-sm text-gray-900">{authStore.user?.id || 'Not available'}</p>
</div>
</div>
@ -85,7 +83,7 @@
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-500">Member Since</h3>
<p class="mt-1 text-gray-900">{formatDate($user?.created_at)}</p>
<p class="mt-1 text-gray-900">-</p>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { user } from '$lib/stores/auth';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { uploadMultipleImages, type UploadProgress } from '$lib/api/upload';
import { showToast } from '$lib/stores/toast';
@ -11,7 +11,7 @@
let successCount = $state(0);
async function handleFilesSelected(files: File[]) {
if (!$user) {
if (!authStore.user) {
showToast('Bitte melde dich an', 'error');
return;
}
@ -20,7 +20,7 @@
successCount = 0;
try {
const uploadedImages = await uploadMultipleImages(files, $user.id, (progress) => {
const uploadedImages = await uploadMultipleImages(files, authStore.user.id, (progress) => {
uploadProgress = progress;
});

View file

@ -3,13 +3,13 @@
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
import { authService } from '$lib/services/authService';
import { authStore } from '$lib/stores/auth.svelte';
// Default to German
const translations = getForgotPasswordTranslations('de');
async function handleForgotPassword(email: string) {
return authService.forgotPassword(email);
return authStore.resetPassword(email);
}
</script>

View file

@ -6,7 +6,7 @@
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import { authService } from '$lib/services/authService';
import { authStore } from '$lib/stores/auth.svelte';
import { onMount } from 'svelte';
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
@ -20,15 +20,17 @@
});
async function handleSignIn(email: string, password: string) {
return authService.signIn(email, password);
return authStore.signIn(email, password);
}
async function handleSignInWithGoogle() {
return authService.signInWithGoogle();
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Google Sign-In not yet implemented' };
}
async function handleSignInWithApple() {
return authService.signInWithApple();
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Apple Sign-In not yet implemented' };
}
</script>

View file

@ -3,7 +3,7 @@
import { RegisterPage, setGoogleClientId } from '@manacore/shared-auth-ui';
import { getRegisterTranslations } from '@manacore/shared-i18n';
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
import { authService } from '$lib/services/authService';
import { authStore } from '$lib/stores/auth.svelte';
import { onMount } from 'svelte';
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
@ -17,15 +17,17 @@
});
async function handleSignUp(email: string, password: string) {
return authService.signUp(email, password);
return authStore.signUp(email, password);
}
async function handleSignUpWithGoogle() {
return authService.signInWithGoogle();
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Google Sign-Up not yet implemented' };
}
async function handleSignUpWithApple() {
return authService.signInWithApple();
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Apple Sign-Up not yet implemented' };
}
</script>