diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 338bf962..fb41f672 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -15,6 +15,7 @@ import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto import { AuthService } from '@gitroom/backend/services/auth/auth.service'; import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto'; import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto'; +import { ResendActivationDto } from '@gitroom/nestjs-libraries/dtos/auth/resend-activation.dto'; import { ApiTags } from '@nestjs/swagger'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; @@ -234,6 +235,21 @@ export class AuthController { return response.status(200).json({ can: true }); } + @Post('/resend-activation') + async resendActivation(@Body() body: ResendActivationDto) { + try { + await this._authService.resendActivationEmail(body.email); + return { + success: true, + }; + } catch (e: any) { + return { + success: false, + message: e.message, + }; + } + } + @Post('/oauth/:provider/exists') async oauthExists( @Body('code') code: string, diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index d2468d0d..c01c2ac5 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -222,6 +222,29 @@ export class AuthService { return false; } + async resendActivationEmail(email: string) { + const user = await this._userService.getUserByEmail(email); + + if (!user) { + throw new Error('User not found'); + } + + if (user.activated) { + throw new Error('Account is already activated'); + } + + const jwt = await this.jwt(user); + + await this._emailService.sendEmail( + user.email, + 'Activate your account', + `Click here to activate your account`, + 'top' + ); + + return true; + } + oauthLink(provider: string, query?: any) { const providerInstance = ProvidersFactory.loadProvider( provider as Provider diff --git a/apps/frontend/src/components/auth/activate.tsx b/apps/frontend/src/components/auth/activate.tsx index 3ffbd423..3f3f3cf1 100644 --- a/apps/frontend/src/components/auth/activate.tsx +++ b/apps/frontend/src/components/auth/activate.tsx @@ -1,11 +1,73 @@ 'use client'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Button } from '@gitroom/react/form/button'; +import { Input } from '@gitroom/react/form/input'; +import { useState, useEffect, useCallback } from 'react'; +import Link from 'next/link'; + +type ResendInputs = { + email: string; +}; + +type ResendStatus = 'idle' | 'sent' | 'already_activated'; + +const COOLDOWN_SECONDS = 60; export function Activate() { const t = useT(); + const fetch = useFetch(); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState('idle'); + const [cooldown, setCooldown] = useState(0); + const form = useForm(); + + useEffect(() => { + if (cooldown <= 0) return; + + const timer = setInterval(() => { + setCooldown((prev) => prev - 1); + }, 1000); + + return () => clearInterval(timer); + }, [cooldown]); + + const resetToForm = useCallback(() => { + setStatus('idle'); + setCooldown(COOLDOWN_SECONDS); + }, []); + + const onSubmit: SubmitHandler = async (data) => { + setLoading(true); + try { + const response = await fetch('/auth/resend-activation', { + method: 'POST', + body: JSON.stringify(data), + }); + const result = await response.json(); + if (result.success) { + setStatus('sent'); + setCooldown(COOLDOWN_SECONDS); + } else if (result.message === 'Account is already activated') { + setStatus('already_activated'); + } else { + form.setError('email', { + message: result.message || t('failed_to_resend', 'Failed to resend activation email'), + }); + } + } catch (e) { + form.setError('email', { + message: t('error_occurred', 'An error occurred. Please try again.'), + }); + } finally { + setLoading(false); + } + }; + return ( -
+

{t('activate_your_account', 'Activate your account')} @@ -19,6 +81,78 @@ export function Activate() { 'Please check your email to activate your account.' )}

+ +
+

+ {t('didnt_receive_email', "Didn't receive the email?")} +

+ {status === 'sent' ? ( +
+
+ {t( + 'activation_email_sent', + 'Activation email has been sent! Please check your inbox.' + )} +
+ {cooldown > 0 ? ( +

+ {t('resend_available_in', 'You can resend in')} {cooldown}s +

+ ) : ( + + )} +
+ ) : status === 'already_activated' ? ( +
+
+ {t( + 'account_already_activated', + 'Great news! Your account is already activated.' + )} +
+ + + +
+ ) : ( + +
+ + +
+
+ )} + {status !== 'already_activated' && ( +

+ {t('already_activated', 'Already activated?')}  + + {t('sign_in', 'Sign In')} + +

+ )} +
); } diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx index cb194be0..793fc92e 100644 --- a/apps/frontend/src/components/auth/login.tsx +++ b/apps/frontend/src/components/auth/login.tsx @@ -24,6 +24,7 @@ type Inputs = { export function Login() { const t = useT(); const [loading, setLoading] = useState(false); + const [notActivated, setNotActivated] = useState(false); const { isGeneral, neynarClientId, billingEnabled, genericOauth } = useVariables(); const resolver = useMemo(() => { @@ -39,6 +40,7 @@ export function Login() { const fetchData = useFetch(); const onSubmit: SubmitHandler = async (data) => { setLoading(true); + setNotActivated(false); const login = await fetchData('/auth/login', { method: 'POST', body: JSON.stringify({ @@ -47,9 +49,14 @@ export function Login() { }), }); if (login.status === 400) { - form.setError('email', { - message: await login.text(), - }); + const errorMessage = await login.text(); + if (errorMessage === 'User is not activated') { + setNotActivated(true); + } else { + form.setError('email', { + message: errorMessage, + }); + } setLoading(false); } }; @@ -103,6 +110,22 @@ export function Login() { placeholder={t('label_password', 'Password')} />
+ {notActivated && ( +
+

+ {t( + 'account_not_activated', + 'Your account is not activated yet. Please check your email for the activation link.' + )} +

+ + {t('resend_activation_email', 'Resend Activation Email')} + +
+ )}