diff --git a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts index 170418e5a..bc566beb6 100644 --- a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts @@ -230,6 +230,31 @@ export const authStore = { } }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + // Pass the current app URL for post-verification redirect + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + 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 (raw token, no refresh) * @deprecated Use getValidToken() instead for automatic refresh diff --git a/apps/calendar/apps/web/src/routes/(auth)/login/+page.svelte b/apps/calendar/apps/web/src/routes/(auth)/login/+page.svelte index c0322d9fe..71cee1b52 100644 --- a/apps/calendar/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/calendar/apps/web/src/routes/(auth)/login/+page.svelte @@ -39,6 +39,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -50,6 +54,7 @@ logo={CalendarLogo} primaryColor="#0ea5e9" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts index 204acedbc..c1b3aa87c 100644 --- a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts @@ -205,6 +205,30 @@ export const authStore = { } }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, + /** * Get user credit balance */ diff --git a/apps/chat/apps/web/src/routes/(auth)/login/+page.svelte b/apps/chat/apps/web/src/routes/(auth)/login/+page.svelte index 86150c2cd..f8bc50b9a 100644 --- a/apps/chat/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/chat/apps/web/src/routes/(auth)/login/+page.svelte @@ -46,6 +46,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -57,6 +61,7 @@ logo={ChatLogo} primaryColor="#0ea5e9" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts index 2928ef6b2..b684571aa 100644 --- a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts @@ -204,6 +204,30 @@ export const authStore = { } }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + 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 (raw token, no refresh) * @deprecated Use getValidToken() instead for automatic refresh diff --git a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts index 63c3c1d3d..29f52d548 100644 --- a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts @@ -205,6 +205,30 @@ export const authStore = { } }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + 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 (raw token, no refresh) * @deprecated Use getValidToken() instead for automatic refresh diff --git a/apps/contacts/apps/web/src/routes/(auth)/login/+page.svelte b/apps/contacts/apps/web/src/routes/(auth)/login/+page.svelte index 6ec0d9078..aac3dcbcb 100644 --- a/apps/contacts/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(auth)/login/+page.svelte @@ -36,6 +36,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -47,6 +51,7 @@ logo={ContactsLogo} primaryColor="#3b82f6" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts index e0b1f0b49..705248c54 100644 --- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts @@ -248,6 +248,30 @@ export const authStore = { } }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + 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 (raw token, no refresh) * @deprecated Use getValidToken() instead for automatic refresh diff --git a/apps/manacore/apps/web/src/routes/(auth)/login/+page.svelte b/apps/manacore/apps/web/src/routes/(auth)/login/+page.svelte index bb1ad1753..ba9a08a30 100644 --- a/apps/manacore/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(auth)/login/+page.svelte @@ -19,6 +19,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -44,6 +48,7 @@ logo={NutriPhiLogo} primaryColor="#22C55E" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts index 4b74bcb68..4905f5842 100644 --- a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts @@ -199,6 +199,32 @@ export const authStore = { return await tokenManager.getValidToken(); }, + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = await getAuthService(); + if (!authService) { + return { success: false, error: 'Auth service not available' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to resend verification email', + }; + } + }, + // For compatibility with old code that reads user store directly setUser(userData: UserData | null) { user = userData; diff --git a/apps/picture/apps/web/src/routes/auth/login/+page.svelte b/apps/picture/apps/web/src/routes/auth/login/+page.svelte index 9a294d8a4..89a39cbdf 100644 --- a/apps/picture/apps/web/src/routes/auth/login/+page.svelte +++ b/apps/picture/apps/web/src/routes/auth/login/+page.svelte @@ -24,6 +24,10 @@ return authStore.signIn(email, password); } + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } + async function handleSignInWithGoogle() { // TODO: Implement OAuth with Mana Core Auth when ready return { success: false, error: 'Google Sign-In not yet implemented' }; @@ -48,6 +52,7 @@ logo={PictureLogo} primaryColor="#3b82f6" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} onSignInWithGoogle={PUBLIC_GOOGLE_CLIENT_ID ? handleSignInWithGoogle : undefined} onSignInWithApple={PUBLIC_APPLE_CLIENT_ID ? handleSignInWithApple : undefined} {goto} diff --git a/apps/planta/apps/web/src/lib/stores/auth.svelte.ts b/apps/planta/apps/web/src/lib/stores/auth.svelte.ts index 4870921fd..7774b83e8 100644 --- a/apps/planta/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/planta/apps/web/src/lib/stores/auth.svelte.ts @@ -168,4 +168,28 @@ export const authStore = { } return await tokenManager.getValidToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/planta/apps/web/src/routes/(auth)/+layout.svelte b/apps/planta/apps/web/src/routes/(auth)/+layout.svelte index 4e629c244..a54cfdcb7 100644 --- a/apps/planta/apps/web/src/routes/(auth)/+layout.svelte +++ b/apps/planta/apps/web/src/routes/(auth)/+layout.svelte @@ -2,12 +2,4 @@ let { children } = $props(); -
-
-
-

Planta

-

Deine Pflanzen dokumentieren

-
- {@render children()} -
-
+{@render children()} diff --git a/apps/planta/apps/web/src/routes/(auth)/login/+page.svelte b/apps/planta/apps/web/src/routes/(auth)/login/+page.svelte index 468afb672..d859eedc9 100644 --- a/apps/planta/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/planta/apps/web/src/routes/(auth)/login/+page.svelte @@ -1,107 +1,63 @@ -
- {#if showVerifiedBanner} -
- - E-Mail erfolgreich bestätigt! Du kannst dich jetzt anmelden. -
- {/if} + + {translations.title} | Planta + - {#if error} -
- {error} -
- {/if} - -
- - -
- -
- - -
- - - -

- Noch kein Konto? - Registrieren -

-
+ diff --git a/apps/presi/apps/web/src/lib/stores/auth.svelte.ts b/apps/presi/apps/web/src/lib/stores/auth.svelte.ts index 45f77eff5..ae60fbc2f 100644 --- a/apps/presi/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/presi/apps/web/src/lib/stores/auth.svelte.ts @@ -190,4 +190,28 @@ export const auth = { } return await authService.getAppToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/presi/apps/web/src/routes/(auth)/login/+page.svelte b/apps/presi/apps/web/src/routes/(auth)/login/+page.svelte index 7d6ee56af..5b7c13cdd 100644 --- a/apps/presi/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/presi/apps/web/src/routes/(auth)/login/+page.svelte @@ -23,6 +23,10 @@ async function handleSignIn(email: string, password: string) { return auth.login(email, password); } + + async function handleResendVerification(email: string) { + return auth.resendVerificationEmail(email); + } @@ -34,6 +38,7 @@ logo={PresiLogo} primaryColor="#f97316" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/questions/apps/web/src/lib/stores/auth.svelte.ts b/apps/questions/apps/web/src/lib/stores/auth.svelte.ts index 443ddd858..5074f5f24 100644 --- a/apps/questions/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/auth.svelte.ts @@ -183,4 +183,28 @@ export const authStore = { } return await tokenManager.getValidToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/questions/apps/web/src/routes/(auth)/login/+page.svelte b/apps/questions/apps/web/src/routes/(auth)/login/+page.svelte index 053068445..1bb3271bf 100644 --- a/apps/questions/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/questions/apps/web/src/routes/(auth)/login/+page.svelte @@ -39,6 +39,10 @@ } return result; } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -50,6 +54,7 @@ logo={QuestionsLogo} primaryColor="#8b5cf6" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts index c922cd75a..313eca06e 100644 --- a/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts @@ -209,4 +209,28 @@ export const authStore = { } return await tokenManager.getValidToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/skilltree/apps/web/src/routes/(auth)/login/+page.svelte b/apps/skilltree/apps/web/src/routes/(auth)/login/+page.svelte index b7bfce2c1..40cf255fa 100644 --- a/apps/skilltree/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/skilltree/apps/web/src/routes/(auth)/login/+page.svelte @@ -39,6 +39,10 @@ } return result; } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -50,6 +54,7 @@ logo={SkillTreeLogo} primaryColor="#10b981" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/apps/storage/apps/web/src/lib/stores/auth.svelte.ts b/apps/storage/apps/web/src/lib/stores/auth.svelte.ts index c44e8af58..a45cb3054 100644 --- a/apps/storage/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/storage/apps/web/src/lib/stores/auth.svelte.ts @@ -158,4 +158,28 @@ export const authStore = { } return await authService.getAppToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/storage/apps/web/src/routes/login/+page.svelte b/apps/storage/apps/web/src/routes/login/+page.svelte index 20754d544..fe3d63583 100644 --- a/apps/storage/apps/web/src/routes/login/+page.svelte +++ b/apps/storage/apps/web/src/routes/login/+page.svelte @@ -18,6 +18,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -29,6 +33,7 @@ logo={ManaIcon} primaryColor="#3b82f6" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} successRedirect="/files" registerPath="/register" diff --git a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts index 366aa6b28..797ff603b 100644 --- a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts @@ -250,4 +250,28 @@ export const authStore = { } return await tokenManager.getValidToken(); }, + + /** + * Resend verification email + */ + async resendVerificationEmail(email: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const sourceAppUrl = browser ? window.location.origin : undefined; + const result = await authService.resendVerificationEmail(email, sourceAppUrl); + + if (!result.success) { + return { success: false, error: result.error || 'Failed to resend verification email' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, }; diff --git a/apps/todo/apps/web/src/routes/(auth)/login/+page.svelte b/apps/todo/apps/web/src/routes/(auth)/login/+page.svelte index 9b33bf625..634c51e41 100644 --- a/apps/todo/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/todo/apps/web/src/routes/(auth)/login/+page.svelte @@ -38,6 +38,10 @@ async function handleSignIn(email: string, password: string) { return authStore.signIn(email, password); } + + async function handleResendVerification(email: string) { + return authStore.resendVerificationEmail(email); + } @@ -49,6 +53,7 @@ logo={TodoLogo} primaryColor="#8b5cf6" onSignIn={handleSignIn} + onResendVerification={handleResendVerification} {goto} enableGoogle={false} enableApple={false} diff --git a/packages/shared-auth-ui/src/pages/LoginPage.svelte b/packages/shared-auth-ui/src/pages/LoginPage.svelte index e46ab5d15..ef4bb1963 100644 --- a/packages/shared-auth-ui/src/pages/LoginPage.svelte +++ b/packages/shared-auth-ui/src/pages/LoginPage.svelte @@ -30,6 +30,10 @@ signInSuccess: string; googleSignInSuccess: string; emailVerified?: string; + emailNotVerified?: string; + resendVerification?: string; + resendingVerification?: string; + verificationEmailSent?: string; } const defaultTranslations: LoginTranslations = { @@ -56,6 +60,10 @@ signInSuccess: 'Successfully signed in. Redirecting...', googleSignInSuccess: 'Successfully signed in with Google. Redirecting...', emailVerified: 'Email successfully verified! Please sign in.', + emailNotVerified: 'Email not verified.', + resendVerification: 'Resend verification email', + resendingVerification: 'Sending...', + verificationEmailSent: 'Verification email sent! Please check your inbox.', }; interface Props { @@ -65,6 +73,7 @@ onSignIn: (email: string, password: string) => Promise; onSignInWithGoogle?: (idToken: string) => Promise; onSignInWithApple?: (identityToken: string) => Promise; + onResendVerification?: (email: string) => Promise; goto: (path: string) => void; enableGoogle?: boolean; enableApple?: boolean; @@ -91,6 +100,7 @@ onSignIn, onSignInWithGoogle, onSignInWithApple, + onResendVerification, goto, enableGoogle = false, enableApple = false, @@ -122,6 +132,9 @@ let passwordInput: HTMLInputElement; let successAnnouncement = $state(''); let showVerifiedBanner = $state(verified); + let showEmailNotVerified = $state(false); + let resendingVerification = $state(false); + let verificationEmailSent = $state(false); // Theme state - can be toggled manually, defaults to system preference let userThemePreference = $state<'light' | 'dark' | null>(null); @@ -192,6 +205,8 @@ async function handleLogin() { loading = true; clearError(); + showEmailNotVerified = false; + verificationEmailSent = false; if (!email) { setError(t.emailRequired, 'email'); @@ -216,6 +231,26 @@ showSuccess = true; successAnnouncement = t.signInSuccess; setTimeout(() => goto(successRedirect), 600); + } else if (result.error === 'EMAIL_NOT_VERIFIED') { + showEmailNotVerified = true; + setError(t.emailNotVerified || 'Email not verified.', 'general'); + } else { + setError(result.error || t.signInFailed, 'general'); + } + } + + async function handleResendVerification() { + if (!onResendVerification || !email || resendingVerification) return; + + resendingVerification = true; + clearError(); + + const result = await onResendVerification(email); + resendingVerification = false; + + if (result.success) { + verificationEmailSent = true; + showEmailNotVerified = false; } else { setError(result.error || t.signInFailed, 'general'); } @@ -333,10 +368,38 @@

{t.subtitle}

+ {#if verificationEmailSent} +
+ +

{t.verificationEmailSent}

+ +
+ {/if} + {#if error} {/if} @@ -679,7 +742,7 @@ .error-message { display: flex; - align-items: center; + align-items: flex-start; gap: 0.5rem; padding: 0.75rem; margin-bottom: 1rem; @@ -690,6 +753,32 @@ font-size: 0.875rem; } + .error-content { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .resend-link { + background: none; + border: none; + cursor: pointer; + font-weight: 500; + font-size: 0.875rem; + padding: 0; + text-align: left; + text-decoration: underline; + } + + .resend-link:hover { + opacity: 0.8; + } + + .resend-link:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .input-group { margin-bottom: 0.75rem; } diff --git a/packages/shared-auth/src/core/authService.ts b/packages/shared-auth/src/core/authService.ts index a65116dfb..2f29c137e 100644 --- a/packages/shared-auth/src/core/authService.ts +++ b/packages/shared-auth/src/core/authService.ts @@ -40,6 +40,7 @@ const DEFAULT_ENDPOINTS: AuthEndpoints = { validate: '/api/v1/auth/validate', forgotPassword: '/api/v1/auth/forgot-password', resetPassword: '/api/v1/auth/reset-password', + resendVerification: '/api/v1/auth/resend-verification', googleSignIn: '/api/v1/auth/google-signin', appleSignIn: '/api/v1/auth/apple-signin', credits: '/api/v1/credits/balance', @@ -247,6 +248,37 @@ export function createAuthService(config: AuthServiceConfig) { } }, + /** + * Resend verification email + * @param email - User's email address + * @param sourceAppUrl - Optional URL to redirect after verification (current app origin) + */ + async resendVerificationEmail(email: string, sourceAppUrl?: string): Promise { + try { + const response = await fetch(`${baseUrl}${endpoints.resendVerification}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, sourceAppUrl }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return { + success: false, + error: errorData.message || 'Failed to resend verification email', + }; + } + + return { success: true }; + } catch (error) { + console.error('Error resending verification email:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }, + /** * Refresh the authentication tokens */ diff --git a/packages/shared-auth/src/types/index.ts b/packages/shared-auth/src/types/index.ts index 32d233bb0..668a52d02 100644 --- a/packages/shared-auth/src/types/index.ts +++ b/packages/shared-auth/src/types/index.ts @@ -129,6 +129,7 @@ export interface AuthEndpoints { validate: string; forgotPassword: string; resetPassword: string; + resendVerification: string; googleSignIn: string; appleSignIn: string; credits: string; diff --git a/packages/shared-branding/src/config.ts b/packages/shared-branding/src/config.ts index 3a523acb4..d63da8518 100644 --- a/packages/shared-branding/src/config.ts +++ b/packages/shared-branding/src/config.ts @@ -246,6 +246,19 @@ export const APP_BRANDING: Record = { logoStroke: true, logoStrokeWidth: 1.5, }, + planta: { + id: 'planta', + name: 'Planta', + tagline: 'Plant Care Assistant', + primaryColor: '#22c55e', + secondaryColor: '#4ade80', + // Plant/leaf icon + logoPath: + 'M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418', + logoViewBox: '0 0 24 24', + logoStroke: true, + logoStrokeWidth: 1.5, + }, }; /** diff --git a/packages/shared-branding/src/index.ts b/packages/shared-branding/src/index.ts index d04cc36f4..d316eb959 100644 --- a/packages/shared-branding/src/index.ts +++ b/packages/shared-branding/src/index.ts @@ -33,6 +33,7 @@ export { ClockLogo, QuestionsLogo, SkillTreeLogo, + PlantaLogo, } from './logos'; // Configuration diff --git a/packages/shared-branding/src/logos/PlantaLogo.svelte b/packages/shared-branding/src/logos/PlantaLogo.svelte new file mode 100644 index 000000000..0ea5c1008 --- /dev/null +++ b/packages/shared-branding/src/logos/PlantaLogo.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/shared-branding/src/logos/index.ts b/packages/shared-branding/src/logos/index.ts index 94abe41b3..b39f38e61 100644 --- a/packages/shared-branding/src/logos/index.ts +++ b/packages/shared-branding/src/logos/index.ts @@ -20,3 +20,4 @@ export { default as InventoryLogo } from './InventoryLogo.svelte'; export { default as ClockLogo } from './ClockLogo.svelte'; export { default as QuestionsLogo } from './QuestionsLogo.svelte'; export { default as SkillTreeLogo } from './SkillTreeLogo.svelte'; +export { default as PlantaLogo } from './PlantaLogo.svelte'; diff --git a/packages/shared-branding/src/types.ts b/packages/shared-branding/src/types.ts index 4c53070f4..7e12a5b36 100644 --- a/packages/shared-branding/src/types.ts +++ b/packages/shared-branding/src/types.ts @@ -20,7 +20,8 @@ export type AppId = | 'moodlit' | 'inventory' | 'questions' - | 'skilltree'; + | 'skilltree' + | 'planta'; /** * App branding configuration diff --git a/packages/shared-i18n/src/translations/auth/de.json b/packages/shared-i18n/src/translations/auth/de.json index 9fa349bb9..1f5e0d9c6 100644 --- a/packages/shared-i18n/src/translations/auth/de.json +++ b/packages/shared-i18n/src/translations/auth/de.json @@ -21,7 +21,12 @@ "signInFailed": "Anmeldung fehlgeschlagen", "googleSignInFailed": "Google-Anmeldung fehlgeschlagen", "signInSuccess": "Erfolgreich angemeldet. Weiterleitung...", - "googleSignInSuccess": "Erfolgreich mit Google angemeldet. Weiterleitung..." + "googleSignInSuccess": "Erfolgreich mit Google angemeldet. Weiterleitung...", + "emailVerified": "E-Mail erfolgreich bestätigt! Bitte melde dich an.", + "emailNotVerified": "E-Mail nicht bestätigt.", + "resendVerification": "Bestätigungs-E-Mail erneut senden", + "resendingVerification": "Wird gesendet...", + "verificationEmailSent": "Bestätigungs-E-Mail wurde gesendet! Bitte überprüfe deinen Posteingang." }, "register": { "title": "Konto erstellen", diff --git a/packages/shared-i18n/src/translations/auth/en.json b/packages/shared-i18n/src/translations/auth/en.json index 2c5bf3536..27fd1ca12 100644 --- a/packages/shared-i18n/src/translations/auth/en.json +++ b/packages/shared-i18n/src/translations/auth/en.json @@ -21,7 +21,12 @@ "signInFailed": "Sign in failed", "googleSignInFailed": "Google sign in failed", "signInSuccess": "Successfully signed in. Redirecting...", - "googleSignInSuccess": "Successfully signed in with Google. Redirecting..." + "googleSignInSuccess": "Successfully signed in with Google. Redirecting...", + "emailVerified": "Email successfully verified! Please sign in.", + "emailNotVerified": "Email not verified.", + "resendVerification": "Resend verification email", + "resendingVerification": "Sending...", + "verificationEmailSent": "Verification email sent! Please check your inbox." }, "register": { "title": "Create Account", diff --git a/packages/shared-i18n/src/translations/auth/es.json b/packages/shared-i18n/src/translations/auth/es.json index 65b9f7851..a3973c9ce 100644 --- a/packages/shared-i18n/src/translations/auth/es.json +++ b/packages/shared-i18n/src/translations/auth/es.json @@ -21,7 +21,12 @@ "signInFailed": "Error al iniciar sesión", "googleSignInFailed": "Error al iniciar sesión con Google", "signInSuccess": "Sesión iniciada correctamente. Redirigiendo...", - "googleSignInSuccess": "Sesión iniciada con Google correctamente. Redirigiendo..." + "googleSignInSuccess": "Sesión iniciada con Google correctamente. Redirigiendo...", + "emailVerified": "¡Correo verificado exitosamente! Por favor inicia sesión.", + "emailNotVerified": "Correo no verificado.", + "resendVerification": "Reenviar correo de verificación", + "resendingVerification": "Enviando...", + "verificationEmailSent": "¡Correo de verificación enviado! Por favor revisa tu bandeja de entrada." }, "register": { "title": "Crear Cuenta", diff --git a/packages/shared-i18n/src/translations/auth/fr.json b/packages/shared-i18n/src/translations/auth/fr.json index 1721196a3..96e9cea02 100644 --- a/packages/shared-i18n/src/translations/auth/fr.json +++ b/packages/shared-i18n/src/translations/auth/fr.json @@ -21,7 +21,12 @@ "signInFailed": "Échec de la connexion", "googleSignInFailed": "Échec de la connexion Google", "signInSuccess": "Connexion réussie. Redirection...", - "googleSignInSuccess": "Connexion Google réussie. Redirection..." + "googleSignInSuccess": "Connexion Google réussie. Redirection...", + "emailVerified": "Email vérifié avec succès ! Veuillez vous connecter.", + "emailNotVerified": "Email non vérifié.", + "resendVerification": "Renvoyer l'email de vérification", + "resendingVerification": "Envoi en cours...", + "verificationEmailSent": "Email de vérification envoyé ! Veuillez vérifier votre boîte de réception." }, "register": { "title": "Créer un compte", diff --git a/packages/shared-i18n/src/translations/auth/index.ts b/packages/shared-i18n/src/translations/auth/index.ts index 1adada053..ca01017c3 100644 --- a/packages/shared-i18n/src/translations/auth/index.ts +++ b/packages/shared-i18n/src/translations/auth/index.ts @@ -37,6 +37,11 @@ export interface AuthTranslations { googleSignInFailed: string; signInSuccess: string; googleSignInSuccess: string; + emailVerified?: string; + emailNotVerified?: string; + resendVerification?: string; + resendingVerification?: string; + verificationEmailSent?: string; }; register: { title: string; diff --git a/packages/shared-i18n/src/translations/auth/it.json b/packages/shared-i18n/src/translations/auth/it.json index bbfcc11a1..737c1492d 100644 --- a/packages/shared-i18n/src/translations/auth/it.json +++ b/packages/shared-i18n/src/translations/auth/it.json @@ -21,7 +21,12 @@ "signInFailed": "Accesso fallito", "googleSignInFailed": "Accesso con Google fallito", "signInSuccess": "Accesso effettuato. Reindirizzamento...", - "googleSignInSuccess": "Accesso con Google effettuato. Reindirizzamento..." + "googleSignInSuccess": "Accesso con Google effettuato. Reindirizzamento...", + "emailVerified": "Email verificata con successo! Effettua l'accesso.", + "emailNotVerified": "Email non verificata.", + "resendVerification": "Invia di nuovo l'email di verifica", + "resendingVerification": "Invio in corso...", + "verificationEmailSent": "Email di verifica inviata! Controlla la tua casella di posta." }, "register": { "title": "Crea Account", diff --git a/services/mana-core-auth/src/auth/auth.controller.ts b/services/mana-core-auth/src/auth/auth.controller.ts index e5e1a2c1f..b07c0ca40 100644 --- a/services/mana-core-auth/src/auth/auth.controller.ts +++ b/services/mana-core-auth/src/auth/auth.controller.ts @@ -20,6 +20,7 @@ import { AcceptInvitationDto } from './dto/accept-invitation.dto'; import { SetActiveOrganizationDto } from './dto/set-active-organization.dto'; import { ForgotPasswordDto } from './dto/forgot-password.dto'; import { ResetPasswordDto } from './dto/reset-password.dto'; +import { ResendVerificationDto } from './dto/resend-verification.dto'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; /** @@ -173,6 +174,21 @@ export class AuthController { ); } + /** + * Resend verification email + * + * Sends a new verification email to the user. + * Always returns success to prevent email enumeration attacks. + */ + @Post('resend-verification') + @HttpCode(HttpStatus.OK) + async resendVerification(@Body() resendVerificationDto: ResendVerificationDto) { + return this.betterAuthService.resendVerificationEmail( + resendVerificationDto.email, + resendVerificationDto.sourceAppUrl + ); + } + // ========================================================================= // B2B Registration // ========================================================================= diff --git a/services/mana-core-auth/src/auth/dto/resend-verification.dto.ts b/services/mana-core-auth/src/auth/dto/resend-verification.dto.ts new file mode 100644 index 000000000..3c081dfa7 --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/resend-verification.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsOptional, IsString } from 'class-validator'; + +export class ResendVerificationDto { + @IsEmail() + email: string; + + @IsOptional() + @IsString() + sourceAppUrl?: string; +} diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index e071d8b19..87676e9db 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -996,6 +996,49 @@ export class BetterAuthService { } } + /** + * Resend verification email + * + * Sends a new verification email to the user. + * Uses Better Auth's sendVerificationEmail API. + * + * @param email - User's email address + * @param sourceAppUrl - Optional URL to redirect after verification + * @returns Success status (always returns success to prevent email enumeration) + */ + async resendVerificationEmail( + email: string, + sourceAppUrl?: string + ): Promise<{ success: boolean; message: string }> { + try { + // Store source app URL for email verification redirect + if (sourceAppUrl) { + sourceAppStore.set(email, sourceAppUrl); + } + + // Better Auth's sendVerificationEmail method + // See: https://www.better-auth.com/docs/authentication/email-verification + const api = this.auth.api as any; + + await api.sendVerificationEmail({ + body: { email }, + }); + + // Always return success to prevent email enumeration + return { + success: true, + message: 'If an account with that email exists, a verification email has been sent', + }; + } catch (error) { + console.error('[resendVerificationEmail] Error:', error); + // Always return success to prevent email enumeration attacks + return { + success: true, + message: 'If an account with that email exists, a verification email has been sent', + }; + } + } + /** * Get JWKS (JSON Web Key Set) *