17 changed files with 1600 additions and 37 deletions
-
8api/.env.example
-
10api/nest-cli.json
-
4api/package.json
-
1175api/pnpm-lock.yaml
-
21api/src/auth/auth.controller.ts
-
16api/src/auth/auth.dto.ts
-
10api/src/auth/auth.module.ts
-
68api/src/auth/auth.service.ts
-
21api/src/auth/schemas/password-reset.schema.ts
-
9api/src/mail/mail.config.ts
-
26api/src/mail/mail.module.ts
-
23api/src/mail/mail.service.ts
-
14api/src/mail/templates/password-reset-request.hbs
-
8api/src/mail/templates/password-reset-success.hbs
-
13web/pages/login.tsx
-
195web/pages/reset-password.tsx
-
16web/services/authService.ts
@ -1,4 +1,10 @@ |
|||
{ |
|||
"collection": "@nestjs/schematics", |
|||
"sourceRoot": "src" |
|||
} |
|||
"sourceRoot": "src", |
|||
"compilerOptions": { |
|||
"assets": [ |
|||
"mail/templates/**/*" |
|||
], |
|||
"watchAssets": true |
|||
} |
|||
} |
|||
1175
api/pnpm-lock.yaml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,21 @@ |
|||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' |
|||
import { Document, Types } from 'mongoose' |
|||
import { User } from '../../users/schemas/user.schema' |
|||
|
|||
export type PasswordResetDocument = PasswordReset & Document |
|||
|
|||
@Schema({ timestamps: true }) |
|||
export class PasswordReset { |
|||
_id?: Types.ObjectId |
|||
|
|||
@Prop({ type: Types.ObjectId, ref: User.name }) |
|||
user: User |
|||
|
|||
@Prop({ type: String }) |
|||
otp: string |
|||
|
|||
@Prop({ type: Date }) |
|||
expiresAt: Date |
|||
} |
|||
|
|||
export const PasswordResetSchema = SchemaFactory.createForClass(PasswordReset) |
|||
@ -0,0 +1,9 @@ |
|||
export const mailTransportConfig = { |
|||
host: process.env.MAIL_HOST, |
|||
port: process.env.MAIL_PORT ? parseInt(process.env.MAIL_PORT, 10) : 465, |
|||
secure: false, |
|||
auth: { |
|||
user: process.env.MAIL_USER, |
|||
pass: process.env.MAIL_PASS, |
|||
}, |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
import { HandlebarsAdapter, MailerModule } from '@nest-modules/mailer' |
|||
import { Module } from '@nestjs/common' |
|||
import { join } from 'path' |
|||
import { mailTransportConfig } from './mail.config' |
|||
import { MailService } from './mail.service' |
|||
|
|||
@Module({ |
|||
imports: [ |
|||
MailerModule.forRoot({ |
|||
transport: mailTransportConfig, |
|||
defaults: { |
|||
from: `No Reply ${process.env.MAIL_FROM}`, |
|||
}, |
|||
template: { |
|||
dir: join(__dirname, 'templates'), |
|||
adapter: new HandlebarsAdapter(), |
|||
options: { |
|||
strict: true, |
|||
}, |
|||
}, |
|||
}), |
|||
], |
|||
providers: [MailService], |
|||
exports: [MailService], |
|||
}) |
|||
export class MailModule {} |
|||
@ -0,0 +1,23 @@ |
|||
import { MailerService } from '@nest-modules/mailer' |
|||
import { Injectable } from '@nestjs/common' |
|||
|
|||
@Injectable() |
|||
export class MailService { |
|||
constructor(private readonly mailerService: MailerService) {} |
|||
|
|||
async sendEmail({ to, subject, html }) { |
|||
try { |
|||
await this.mailerService.sendMail({ to, subject, html }) |
|||
} catch (e) { |
|||
console.log(e) |
|||
} |
|||
} |
|||
|
|||
async sendEmailFromTemplate({ to, subject, template, context }) { |
|||
try { |
|||
await this.mailerService.sendMail({ to, subject, template, context }) |
|||
} catch (e) { |
|||
console.log(e) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<h1>Hello {{name}},</h1> |
|||
<p> |
|||
We have received a request to reset your password. If you did not make this |
|||
request, please ignore this email. Otherwise, you can reset your password |
|||
using the OTP below. |
|||
</p> |
|||
<hr/> |
|||
<strong> {{otp}} </strong> |
|||
<hr /> |
|||
<div> |
|||
Thank you, |
|||
<br /> |
|||
<i>TextBee.dev</i> |
|||
</div> |
|||
@ -0,0 +1,8 @@ |
|||
<h1>Hello {{name}}</h1> |
|||
<p> |
|||
Your password has been successfully reset. You can now login with your new password. |
|||
</p> |
|||
<div> |
|||
Thank you, |
|||
<br /> |
|||
<i>TextBee.dev</i> |
|||
@ -0,0 +1,195 @@ |
|||
import { |
|||
Flex, |
|||
Box, |
|||
FormControl, |
|||
FormLabel, |
|||
Input, |
|||
InputGroup, |
|||
InputRightElement, |
|||
Stack, |
|||
Button, |
|||
Heading, |
|||
Text, |
|||
useColorModeValue, |
|||
useToast, |
|||
} from '@chakra-ui/react' |
|||
|
|||
import Link from 'next/link' |
|||
import { useState } from 'react' |
|||
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' |
|||
import { authService } from '../services/authService' |
|||
|
|||
export default function LoginPage() { |
|||
const [showPassword, setShowPassword] = useState<boolean>(false) |
|||
|
|||
const [loading, setLoading] = useState<boolean>(false) |
|||
const [otpSent, setOtpSent] = useState<boolean>(false) |
|||
const [resetSuccess, setResetSuccess] = useState<boolean>(false) |
|||
|
|||
const [formData, setFormData] = useState({ |
|||
email: '', |
|||
otp: '', |
|||
newPassword: '', |
|||
}) |
|||
|
|||
const toast = useToast() |
|||
|
|||
const handleRequestResetPassword = async (e) => { |
|||
setLoading(true) |
|||
|
|||
authService |
|||
.requestPasswordReset(formData) |
|||
.then((res) => { |
|||
setOtpSent(true) |
|||
toast({ |
|||
title: 'OTP sent successfully', |
|||
status: 'success', |
|||
duration: 5000, |
|||
isClosable: true, |
|||
}) |
|||
}) |
|||
.catch((err) => { |
|||
toast({ |
|||
title: 'Error', |
|||
description: err.response.data.message || 'Something went wrong', |
|||
status: 'error', |
|||
duration: 5000, |
|||
isClosable: true, |
|||
}) |
|||
}) |
|||
.finally(() => { |
|||
setLoading(false) |
|||
}) |
|||
} |
|||
|
|||
const handleResetPassword = async (e) => { |
|||
setLoading(true) |
|||
|
|||
authService |
|||
.resetPassword(formData) |
|||
.then((res) => { |
|||
toast({ |
|||
title: 'Password reset successfully', |
|||
status: 'success', |
|||
}) |
|||
setResetSuccess(true) |
|||
}) |
|||
.catch((err) => { |
|||
toast({ |
|||
title: 'Error', |
|||
description: err.response?.data?.message || 'Something went wrong', |
|||
status: 'error', |
|||
}) |
|||
}) |
|||
.finally(() => { |
|||
setLoading(false) |
|||
}) |
|||
} |
|||
const onChange = (e) => { |
|||
setFormData({ |
|||
...formData, |
|||
[e.target.name]: e.target.value, |
|||
}) |
|||
} |
|||
|
|||
if (resetSuccess) { |
|||
return ( |
|||
<> |
|||
<Flex |
|||
minH={'90vh'} |
|||
align={'center'} |
|||
justify={'center'} |
|||
bg={useColorModeValue('gray.50', 'gray.800')} |
|||
> |
|||
<Stack pt={6}> |
|||
<Text align={'center'}>Password reset successfully</Text> |
|||
<Link href='/login'> |
|||
<Button variant={'ghost'}>Go back to login page</Button> |
|||
</Link> |
|||
</Stack> |
|||
</Flex> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<Flex |
|||
minH={'90vh'} |
|||
align={'center'} |
|||
justify={'center'} |
|||
bg={useColorModeValue('gray.50', 'gray.800')} |
|||
> |
|||
<Stack spacing={8} mx={'auto'} maxW={'lg'} py={12} px={6}> |
|||
<Stack align={'center'}> |
|||
<Heading fontSize={'2xl'} textAlign={'center'}> |
|||
Reset Password |
|||
</Heading> |
|||
</Stack> |
|||
<Box |
|||
rounded={'lg'} |
|||
bg={useColorModeValue('white', 'gray.700')} |
|||
boxShadow={'lg'} |
|||
p={8} |
|||
> |
|||
<Stack spacing={4}> |
|||
<FormControl id='email' isRequired> |
|||
<FormLabel>Email address</FormLabel> |
|||
<Input type='email' name='email' onChange={onChange} /> |
|||
</FormControl> |
|||
{otpSent && ( |
|||
<> |
|||
<FormControl id='otp' isRequired> |
|||
<FormLabel>OTP</FormLabel> |
|||
<Input type='number' name='otp' onChange={onChange} /> |
|||
</FormControl> |
|||
<FormControl id='newPassword' isRequired> |
|||
<FormLabel>New Password</FormLabel> |
|||
<InputGroup> |
|||
<Input |
|||
type={showPassword ? 'text' : 'password'} |
|||
name='newPassword' |
|||
onChange={onChange} |
|||
/> |
|||
<InputRightElement h={'full'}> |
|||
<Button |
|||
variant={'ghost'} |
|||
onClick={() => |
|||
setShowPassword((showPassword) => !showPassword) |
|||
} |
|||
> |
|||
{showPassword ? <ViewIcon /> : <ViewOffIcon />} |
|||
</Button> |
|||
</InputRightElement> |
|||
</InputGroup> |
|||
</FormControl> |
|||
</> |
|||
)} |
|||
<Stack spacing={10} pt={2}> |
|||
<Button |
|||
loadingText='Submitting' |
|||
size='lg' |
|||
bg={'blue.400'} |
|||
color={'white'} |
|||
_hover={{ |
|||
bg: 'blue.500', |
|||
}} |
|||
onClick={ |
|||
otpSent ? handleResetPassword : handleRequestResetPassword |
|||
} |
|||
disabled={loading} |
|||
> |
|||
{loading ? 'Please Wait...' : 'Continue'} |
|||
</Button> |
|||
</Stack> |
|||
|
|||
<Stack pt={6}> |
|||
<Text align={'center'}> |
|||
<Link href='/login'>Go back to login</Link> |
|||
</Text> |
|||
</Stack> |
|||
</Stack> |
|||
</Box> |
|||
</Stack> |
|||
</Flex> |
|||
) |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue