feat(uload): integrate mana-core-auth with guest mode

- Add auth store using createManaAuthStore
- Wrap app layout with AuthGate (allowGuest=true)
- Add GuestWelcomeModal and SessionExpiredBanner
- Start sync on login, stop on logout
- Rewrite login/register/forgot-password to use shared auth UI
- Remove all PocketBase auth references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 09:23:39 +02:00
parent a4184f1bab
commit 9675520dbd
8 changed files with 506 additions and 157 deletions

View file

@ -59,6 +59,8 @@
},
"dependencies": {
"@manacore/local-store": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-ui": "workspace:*",

View file

@ -0,0 +1,5 @@
import { createManaAuthStore } from '@manacore/shared-auth-stores';
export const authStore = createManaAuthStore({
devBackendPort: 3070,
});

View file

@ -5,13 +5,16 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
import { AuthGate, GuestWelcomeModal, SessionExpiredBanner } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { uloadStore } from '$lib/data/local-store';
let { children } = $props();
const appItems = getPillAppItems('uload');
// TODO: integrate mana-core-auth, set userEmail from authStore
let userEmail = $state('');
let userEmail = $derived(authStore.isAuthenticated ? (authStore.user?.email ?? '') : '');
const navItems: PillNavItem[] = [
{ href: '/my/links', label: 'Links', icon: 'link' },
@ -20,10 +23,10 @@
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
let loading = $state(true);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
let showGuestWelcome = $state(false);
const navRoutes = ['/my/links', '/my/tags', '/my/analytics', '/settings'];
@ -59,11 +62,23 @@
}
async function handleLogout() {
localStorage?.removeItem('auth_token');
uloadStore.stopSync();
await authStore.signOut();
goto('/login');
}
onMount(() => {
function handleAuthReady() {
// Start sync if authenticated
if (authStore.isAuthenticated) {
uloadStore.startSync(() => authStore.getValidToken());
}
// Show guest welcome for first-time guests
if (!authStore.isAuthenticated && shouldShowGuestWelcome('uload')) {
showGuestWelcome = true;
}
// Restore nav preferences
const savedSidebar = localStorage?.getItem('uload-nav-sidebar');
if (savedSidebar === 'true') isSidebarMode = true;
const savedCollapsed = localStorage?.getItem('uload-nav-collapsed');
@ -73,19 +88,12 @@
isDark = true;
document.documentElement.classList.add('dark');
}
loading = false;
});
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if loading}
<div class="flex min-h-screen items-center justify-center">
<div
class="inline-block h-10 w-10 animate-spin rounded-full border-4 border-solid border-indigo-500 border-r-transparent"
></div>
</div>
{:else}
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
@ -124,4 +132,17 @@
</div>
</main>
</div>
{/if}
<GuestWelcomeModal
appId="uload"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale="de"
/>
{#if authStore.isAuthenticated}
<SessionExpiredBanner locale="de" loginHref="/login" />
{/if}
</AuthGate>

View file

@ -2,42 +2,25 @@
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { UloadLogo } from '@manacore/shared-branding';
import { pb } from '$lib/pocketbase';
import { authStore } from '$lib/stores/auth.svelte';
async function handleForgotPassword(email: string) {
try {
await pb.collection('users').requestPasswordReset(email);
return { success: true };
} catch (err: any) {
// PocketBase doesn't reveal if email exists for security
// So we always show success message
return { success: true };
}
async function handleResetPassword(email: string) {
return authStore.resetPassword(email);
}
</script>
<ForgotPasswordPage
appName="uLoad"
logo={UloadLogo}
primaryColor="#3b82f6"
onForgotPassword={handleForgotPassword}
primaryColor="#6366f1"
onResetPassword={handleResetPassword}
{goto}
loginPath="/login"
lightBackground="#f8fafc"
darkBackground="#0f172a"
translations={{
titleForm: 'Passwort zurücksetzen',
titleSuccess: 'E-Mail gesendet',
description:
'Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen deines Passworts.',
title: 'Passwort zurücksetzen',
subtitle: 'Gib deine E-Mail-Adresse ein',
emailPlaceholder: 'E-Mail',
sendResetLinkButton: 'Link senden',
sending: 'Wird gesendet...',
resetButton: 'Link senden',
backToLogin: 'Zurück zum Login',
resendEmail: 'E-Mail erneut senden',
successMessage:
'Wir haben einen Link zum Zurücksetzen deines Passworts an {email} gesendet. Bitte überprüfe deinen Posteingang.',
emailRequired: 'E-Mail ist erforderlich',
sendFailed: 'Senden der E-Mail fehlgeschlagen',
}}
/>

View file

@ -1,40 +1,33 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { goto } from '$app/navigation';
import { LoginPage } from '@manacore/shared-auth-ui';
import { UloadLogo } from '@manacore/shared-branding';
import { pb } from '$lib/pocketbase';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignIn(email: string, password: string) {
try {
await pb.collection('users').authWithPassword(email, password);
// Invalidate all data to refresh server-side auth state
await invalidateAll();
return { success: true };
} catch (err: any) {
return {
success: false,
error: err?.message || 'Ungültige E-Mail oder Passwort',
};
}
return authStore.signIn(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<LoginPage
appName="uLoad"
logo={UloadLogo}
primaryColor="#3b82f6"
primaryColor="#6366f1"
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
passkeyAvailable={authStore.isPasskeyAvailable()}
onSignInWithPasskey={() => authStore.signInWithPasskey()}
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect="/my"
successRedirect="/my/links"
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#f8fafc"
darkBackground="#0f172a"
translations={{
title: 'Anmelden',
subtitle: 'Melde dich mit deinem uLoad Account an',
@ -55,8 +48,6 @@
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
passwordRequired: 'Passwort ist erforderlich',
signInFailed: 'Anmeldung fehlgeschlagen',
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...',
}}
/>

View file

@ -1,89 +1,31 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { UloadLogo } from '@manacore/shared-branding';
import { pb } from '$lib/pocketbase';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignUp(email: string, password: string) {
try {
// Create user
await pb.collection('users').create({
email: email.toLowerCase().trim(),
password,
passwordConfirm: password,
emailVisibility: true,
});
// Request verification email
try {
await pb.collection('users').requestVerification(email);
} catch (emailErr) {
console.error('Failed to send verification email:', emailErr);
}
return {
success: true,
needsVerification: true,
};
} catch (err: any) {
const errorData = err?.response?.data || err?.data || {};
if (errorData.email?.message?.includes('unique')) {
return {
success: false,
error: 'Diese E-Mail ist bereits registriert. Bitte melde dich an.',
};
}
if (errorData.email?.message) {
return { success: false, error: errorData.email.message };
}
if (errorData.password?.message) {
return { success: false, error: errorData.password.message };
}
return {
success: false,
error: err?.message || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.',
};
}
return authStore.signUp(email, password);
}
</script>
<RegisterPage
appName="uLoad"
logo={UloadLogo}
primaryColor="#3b82f6"
primaryColor="#6366f1"
onSignUp={handleSignUp}
{goto}
successRedirect="/login?registered=true"
successRedirect="/my/links"
loginPath="/login"
lightBackground="#f8fafc"
darkBackground="#0f172a"
translations={{
title: 'Account erstellen',
title: 'Registrieren',
subtitle: 'Erstelle deinen uLoad Account',
emailPlaceholder: 'E-Mail',
passwordPlaceholder: 'Passwort',
confirmPasswordPlaceholder: 'Passwort bestätigen',
passwordRequirements:
'Passwort muss mindestens 8 Zeichen mit Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten.',
createAccountButton: 'Account erstellen',
creatingAccount: 'Wird erstellt...',
backToLogin: 'Zurück zum Login',
showPassword: 'Passwort anzeigen',
hidePassword: 'Passwort verbergen',
emailRequired: 'E-Mail ist erforderlich',
passwordRequired: 'Passwort ist erforderlich',
confirmPasswordRequired: 'Bitte bestätige dein Passwort',
passwordsDoNotMatch: 'Passwörter stimmen nicht überein',
passwordTooShort: 'Passwort muss mindestens 8 Zeichen haben',
passwordStrengthError:
'Passwort muss Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten',
registrationFailed: 'Registrierung fehlgeschlagen',
accountCreated: 'Account erstellt! Bitte überprüfe deine E-Mail zur Verifizierung.',
signUpButton: 'Registrieren',
signingUp: 'Wird erstellt...',
alreadyHaveAccount: 'Bereits registriert?',
signIn: 'Anmelden',
}}
/>

View file

@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { Toaster } from 'svelte-sonner';
import { uloadStore } from '$lib/data/local-store';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
@ -13,6 +14,7 @@
onMount(async () => {
initLocale();
await authStore.initialize();
await uloadStore.initialize();
loading = false;
});