mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
a4184f1bab
commit
9675520dbd
8 changed files with 506 additions and 157 deletions
|
|
@ -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:*",
|
||||
|
|
|
|||
5
apps/uload/apps/web/src/lib/stores/auth.svelte.ts
Normal file
5
apps/uload/apps/web/src/lib/stores/auth.svelte.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createManaAuthStore } from '@manacore/shared-auth-stores';
|
||||
|
||||
export const authStore = createManaAuthStore({
|
||||
devBackendPort: 3070,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue