mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:41:08 +02:00
✨ feat(auth): add resend verification email to all login pages
Add ability to resend verification email when login fails with "Email not verified" error. Implemented across all 14 apps using Mana Core Auth. Changes: - Add POST /api/v1/auth/resend-verification endpoint to mana-core-auth - Add resendVerificationEmail method to shared-auth client - Update LoginPage component with resend UI and translations - Add resendVerificationEmail to all app auth stores - Add translations for de, en, fr, es, it - Add PlantaLogo to shared-branding - Migrate planta login to shared LoginPage component
This commit is contained in:
parent
f911243bf0
commit
0c150df0f1
45 changed files with 691 additions and 110 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -50,6 +54,7 @@
|
|||
logo={CalendarLogo}
|
||||
primaryColor="#0ea5e9"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -57,6 +61,7 @@
|
|||
logo={ChatLogo}
|
||||
primaryColor="#0ea5e9"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -47,6 +51,7 @@
|
|||
logo={ContactsLogo}
|
||||
primaryColor="#3b82f6"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
|
|
@ -26,6 +30,7 @@
|
|||
logo={ManaCoreLogo}
|
||||
primaryColor="#6366f1"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ export const authStore = {
|
|||
return authService.forgotPassword(email);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resend verification email
|
||||
*/
|
||||
async resendVerificationEmail(email: string) {
|
||||
const sourceAppUrl = browser ? window.location.origin : undefined;
|
||||
return authService.resendVerificationEmail(email, sourceAppUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls (raw token, no refresh)
|
||||
* @deprecated Use getValidToken() instead for automatic refresh
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
|
|
@ -26,6 +30,7 @@
|
|||
logo={ManaDeckLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@
|
|||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
|
||||
async function handleResendVerification(email: string) {
|
||||
return authStore.resendVerificationEmail(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -44,6 +48,7 @@
|
|||
logo={NutriPhiLogo}
|
||||
primaryColor="#22C55E"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,4 @@
|
|||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen flex-col items-center justify-center px-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-3xl font-bold text-foreground">Planta</h1>
|
||||
<p class="mt-2 text-muted-foreground">Deine Pflanzen dokumentieren</p>
|
||||
</div>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -1,107 +1,63 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { PlantaLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
const redirectTo = $derived.by(() => {
|
||||
const queryRedirect = $page.url.searchParams.get('redirectTo');
|
||||
if (queryRedirect) return queryRedirect;
|
||||
|
||||
if (browser) {
|
||||
const sessionRedirect = sessionStorage.getItem('auth-return-url');
|
||||
if (sessionRedirect) {
|
||||
sessionStorage.removeItem('auth-return-url');
|
||||
return sessionRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
return '/dashboard';
|
||||
});
|
||||
|
||||
// German translations (Planta is German-focused)
|
||||
const translations = $derived(getLoginTranslations('de'));
|
||||
|
||||
// Read verification status from query params (set after email verification)
|
||||
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
||||
const initialEmailFromUrl = $derived($page.url.searchParams.get('email') || '');
|
||||
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
let showVerifiedBanner = $state(false);
|
||||
|
||||
// Initialize email from URL if provided
|
||||
$effect(() => {
|
||||
if (initialEmailFromUrl && !email) {
|
||||
email = initialEmailFromUrl;
|
||||
}
|
||||
if (verified) {
|
||||
showVerifiedBanner = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
loading = true;
|
||||
|
||||
const result = await authStore.signIn(email, password);
|
||||
|
||||
if (result.success) {
|
||||
goto('/dashboard');
|
||||
} else {
|
||||
error = result.error || 'Login fehlgeschlagen';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
|
||||
function dismissBanner() {
|
||||
showVerifiedBanner = false;
|
||||
async function handleResendVerification(email: string) {
|
||||
return authStore.resendVerificationEmail(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
{#if showVerifiedBanner}
|
||||
<div
|
||||
class="relative rounded-md bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-200"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={dismissBanner}
|
||||
class="absolute right-2 top-2 text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
E-Mail erfolgreich bestätigt! Du kannst dich jetzt anmelden.
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:head>
|
||||
<title>{translations.title} | Planta</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-foreground">E-Mail</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="input mt-1 w-full"
|
||||
placeholder="deine@email.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-foreground">Passwort</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
class="input mt-1 w-full"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
||||
{#if loading}
|
||||
<span
|
||||
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-r-transparent"
|
||||
></span>
|
||||
{:else}
|
||||
Anmelden
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
Noch kein Konto?
|
||||
<a href="/register" class="text-primary hover:underline">Registrieren</a>
|
||||
</p>
|
||||
</form>
|
||||
<LoginPage
|
||||
appName="Planta"
|
||||
logo={PlantaLogo}
|
||||
primaryColor="#22c55e"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#dcfce7"
|
||||
darkBackground="#052e16"
|
||||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -34,6 +38,7 @@
|
|||
logo={PresiLogo}
|
||||
primaryColor="#f97316"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function handleResendVerification(email: string) {
|
||||
return authStore.resendVerificationEmail(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -50,6 +54,7 @@
|
|||
logo={QuestionsLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function handleResendVerification(email: string) {
|
||||
return authStore.resendVerificationEmail(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -50,6 +54,7 @@
|
|||
logo={SkillTreeLogo}
|
||||
primaryColor="#10b981"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -29,6 +33,7 @@
|
|||
logo={ManaIcon}
|
||||
primaryColor="#3b82f6"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
successRedirect="/files"
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -49,6 +53,7 @@
|
|||
logo={TodoLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
|
|
|
|||
|
|
@ -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<AuthResult>;
|
||||
onSignInWithGoogle?: (idToken: string) => Promise<AuthResult>;
|
||||
onSignInWithApple?: (identityToken: string) => Promise<AuthResult>;
|
||||
onResendVerification?: (email: string) => Promise<AuthResult>;
|
||||
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 @@
|
|||
<p class="form-subtitle">{t.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{#if verificationEmailSent}
|
||||
<div class="verified-banner" role="status" aria-live="polite">
|
||||
<Check size={18} class="text-green-500 shrink-0" />
|
||||
<p>{t.verificationEmailSent}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="verified-banner-close"
|
||||
onclick={() => (verificationEmailSent = false)}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="error-message" id="form-error" role="alert" aria-live="assertive">
|
||||
<Warning size={18} class="text-red-500 shrink-0" />
|
||||
<p>{error}</p>
|
||||
<div class="error-content">
|
||||
<p>{error}</p>
|
||||
{#if showEmailNotVerified && onResendVerification}
|
||||
<button
|
||||
type="button"
|
||||
class="resend-link"
|
||||
onclick={handleResendVerification}
|
||||
disabled={resendingVerification}
|
||||
style:color={primaryColor}
|
||||
>
|
||||
{resendingVerification ? t.resendingVerification : t.resendVerification}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthResult> {
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export interface AuthEndpoints {
|
|||
validate: string;
|
||||
forgotPassword: string;
|
||||
resetPassword: string;
|
||||
resendVerification: string;
|
||||
googleSignIn: string;
|
||||
appleSignIn: string;
|
||||
credits: string;
|
||||
|
|
|
|||
|
|
@ -246,6 +246,19 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
|
|||
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,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export {
|
|||
ClockLogo,
|
||||
QuestionsLogo,
|
||||
SkillTreeLogo,
|
||||
PlantaLogo,
|
||||
} from './logos';
|
||||
|
||||
// Configuration
|
||||
|
|
|
|||
13
packages/shared-branding/src/logos/PlantaLogo.svelte
Normal file
13
packages/shared-branding/src/logos/PlantaLogo.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import AppLogo from '../AppLogo.svelte';
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { size = 55, color, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AppLogo app="planta" {size} {color} class={className} />
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ export type AppId =
|
|||
| 'moodlit'
|
||||
| 'inventory'
|
||||
| 'questions'
|
||||
| 'skilltree';
|
||||
| 'skilltree'
|
||||
| 'planta';
|
||||
|
||||
/**
|
||||
* App branding configuration
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { IsEmail, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class ResendVerificationDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sourceAppUrl?: string;
|
||||
}
|
||||
|
|
@ -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)
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue