resend activation email
This commit is contained in:
parent
6859f0d049
commit
5665774260
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 <a href="${process.env.FRONTEND_URL}/auth/activate/${jwt}">here</a> to activate your account`,
|
||||
'top'
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
oauthLink(provider: string, query?: any) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
|
|
|
|||
|
|
@ -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<ResendStatus>('idle');
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const form = useForm<ResendInputs>();
|
||||
|
||||
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<ResendInputs> = 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 (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col flex-1">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-start mb-4 cursor-pointer">
|
||||
{t('activate_your_account', 'Activate your account')}
|
||||
|
|
@ -19,6 +81,78 @@ export function Activate() {
|
|||
'Please check your email to activate your account.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 border-t border-fifth pt-6">
|
||||
<h2 className="text-lg font-semibold mb-4">
|
||||
{t('didnt_receive_email', "Didn't receive the email?")}
|
||||
</h2>
|
||||
{status === 'sent' ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-green-400">
|
||||
{t(
|
||||
'activation_email_sent',
|
||||
'Activation email has been sent! Please check your inbox.'
|
||||
)}
|
||||
</div>
|
||||
{cooldown > 0 ? (
|
||||
<p className="text-sm text-textColor">
|
||||
{t('resend_available_in', 'You can resend in')} {cooldown}s
|
||||
</p>
|
||||
) : (
|
||||
<Button
|
||||
onClick={resetToForm}
|
||||
className="rounded-[10px] !h-[52px]"
|
||||
>
|
||||
{t('send_again', 'Send Again')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : status === 'already_activated' ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-green-400">
|
||||
{t(
|
||||
'account_already_activated',
|
||||
'Great news! Your account is already activated.'
|
||||
)}
|
||||
</div>
|
||||
<Link href="/auth/login">
|
||||
<Button className="rounded-[10px] !h-[52px] w-full">
|
||||
{t('go_to_login', 'Go to Login')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<Input
|
||||
label={t('label_email', 'Email')}
|
||||
translationKey="label_email"
|
||||
{...form.register('email', { required: true })}
|
||||
type="email"
|
||||
placeholder={t('email_address', 'Email Address')}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="rounded-[10px] !h-[52px]"
|
||||
loading={loading}
|
||||
disabled={cooldown > 0}
|
||||
>
|
||||
{cooldown > 0
|
||||
? `${t('resend_available_in', 'You can resend in')} ${cooldown}s`
|
||||
: t('resend_activation_email', 'Resend Activation Email')}
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)}
|
||||
{status !== 'already_activated' && (
|
||||
<p className="mt-4 text-sm text-textColor">
|
||||
{t('already_activated', 'Already activated?')}
|
||||
<Link href="/auth/login" className="underline cursor-pointer">
|
||||
{t('sign_in', 'Sign In')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Inputs> = 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')}
|
||||
/>
|
||||
</div>
|
||||
{notActivated && (
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-[10px] p-4 mb-4">
|
||||
<p className="text-amber-400 text-sm mb-2">
|
||||
{t(
|
||||
'account_not_activated',
|
||||
'Your account is not activated yet. Please check your email for the activation link.'
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
href="/auth/activate"
|
||||
className="text-amber-400 underline hover:font-bold text-sm"
|
||||
>
|
||||
{t('resend_activation_email', 'Resend Activation Email')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center mt-6">
|
||||
<div className="w-full flex">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { IsDefined, IsEmail, IsString } from 'class-validator';
|
||||
|
||||
export class ResendActivationDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue