mirror of
https://github.com/vernu/textbee.git
synced 2026-05-24 16:28:53 -04:00
feat: reset password
This commit is contained in:
@@ -7,4 +7,10 @@ FIREBASE_PRIVATE_KEY_ID=
|
||||
FIREBASE_PRIVATE_KEY=
|
||||
FIREBASE_CLIENT_EMAIL=
|
||||
FIREBASE_CLIENT_ID=
|
||||
FIREBASE_CLIENT_C509_CERT_URL=
|
||||
FIREBASE_CLIENT_C509_CERT_URL=
|
||||
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USER=
|
||||
MAIL_PASS=
|
||||
MAIL_FROM=
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"assets": [
|
||||
"mail/templates/**/*"
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nest-modules/mailer": "^1.3.22",
|
||||
"@nestjs/common": "^8.4.7",
|
||||
"@nestjs/core": "^8.4.7",
|
||||
"@nestjs/jwt": "^8.0.1",
|
||||
@@ -31,7 +32,9 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"firebase-admin": "^10.3.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"mongoose": "^6.11.1",
|
||||
"nodemailer": "^6.9.13",
|
||||
"passport": "^0.5.3",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@@ -47,6 +50,7 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/node": "^16.18.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
|
||||
1167
api/pnpm-lock.yaml
generated
1167
api/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,12 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common'
|
||||
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
|
||||
import { LoginInputDTO, RegisterInputDTO } from './auth.dto'
|
||||
import {
|
||||
LoginInputDTO,
|
||||
RegisterInputDTO,
|
||||
RequestResetPasswordInputDTO,
|
||||
ResetPasswordInputDTO,
|
||||
} from './auth.dto'
|
||||
import { AuthGuard } from './guards/auth.guard'
|
||||
import { AuthService } from './auth.service'
|
||||
import { CanModifyApiKey } from './guards/can-modify-api-key.guard'
|
||||
@@ -92,4 +97,18 @@ export class AuthController {
|
||||
await this.authService.deleteApiKey(params.id)
|
||||
return { message: 'API Key Deleted' }
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Request Password Reset' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/request-password-reset')
|
||||
async requestPasswordReset(@Body() input: RequestResetPasswordInputDTO) {
|
||||
return await this.authService.requestResetPassword(input)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Reset Password' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/reset-password')
|
||||
async resetPassword(@Body() input: ResetPasswordInputDTO) {
|
||||
return await this.authService.resetPassword(input)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,19 @@ export class LoginInputDTO {
|
||||
@ApiProperty({ type: String, required: true })
|
||||
password: string
|
||||
}
|
||||
|
||||
export class RequestResetPasswordInputDTO {
|
||||
@ApiProperty({ type: String, required: true })
|
||||
email: string
|
||||
}
|
||||
|
||||
export class ResetPasswordInputDTO {
|
||||
@ApiProperty({ type: String, required: true })
|
||||
email: string
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
otp: string
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import { AuthController } from './auth.controller'
|
||||
import { AuthService } from './auth.service'
|
||||
import { JwtStrategy } from './jwt.strategy'
|
||||
import { ApiKey, ApiKeySchema } from './schemas/api-key.schema'
|
||||
import { MailModule } from 'src/mail/mail.module'
|
||||
import {
|
||||
PasswordReset,
|
||||
PasswordResetSchema,
|
||||
} from './schemas/password-reset.schema'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,6 +20,10 @@ import { ApiKey, ApiKeySchema } from './schemas/api-key.schema'
|
||||
name: ApiKey.name,
|
||||
schema: ApiKeySchema,
|
||||
},
|
||||
{
|
||||
name: PasswordReset.name,
|
||||
schema: PasswordResetSchema,
|
||||
},
|
||||
]),
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
@@ -22,6 +31,7 @@ import { ApiKey, ApiKeySchema } from './schemas/api-key.schema'
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '180d' },
|
||||
}),
|
||||
MailModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy, MongooseModule],
|
||||
|
||||
@@ -8,12 +8,21 @@ import { ApiKey, ApiKeyDocument } from './schemas/api-key.schema'
|
||||
import { Model } from 'mongoose'
|
||||
import { User } from '../users/schemas/user.schema'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
PasswordReset,
|
||||
PasswordResetDocument,
|
||||
} from './schemas/password-reset.schema'
|
||||
import { MailService } from 'src/mail/mail.service'
|
||||
import { RequestResetPasswordInputDTO, ResetPasswordInputDTO } from './auth.dto'
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
@InjectModel(ApiKey.name) private apiKeyModel: Model<ApiKeyDocument>,
|
||||
@InjectModel(PasswordReset.name)
|
||||
private passwordResetModel: Model<PasswordResetDocument>,
|
||||
private readonly mailService: MailService,
|
||||
) {}
|
||||
|
||||
async login(userData: any) {
|
||||
@@ -88,6 +97,65 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async requestResetPassword({ email }: RequestResetPasswordInputDTO) {
|
||||
const user = await this.usersService.findOne({ email })
|
||||
if (!user) {
|
||||
throw new HttpException({ error: 'User not found' }, HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
const otp = Math.floor(100000 + Math.random() * 900000).toString()
|
||||
const expiresAt = new Date(Date.now() + 20 * 60 * 1000)
|
||||
|
||||
const hashedOtp = await bcrypt.hash(otp, 10)
|
||||
const passwordReset = new this.passwordResetModel({
|
||||
user: user._id,
|
||||
otp: hashedOtp,
|
||||
expiresAt,
|
||||
})
|
||||
passwordReset.save()
|
||||
|
||||
await this.mailService.sendEmailFromTemplate({
|
||||
to: user.email,
|
||||
subject: 'Password Reset',
|
||||
template: 'password-reset-request',
|
||||
context: { name: user.name, otp },
|
||||
})
|
||||
|
||||
return { message: 'Password reset email sent' }
|
||||
}
|
||||
|
||||
async resetPassword({ email, otp, newPassword }: ResetPasswordInputDTO) {
|
||||
const user = await this.usersService.findOne({ email })
|
||||
if (!user) {
|
||||
throw new HttpException({ error: 'User not found' }, HttpStatus.NOT_FOUND)
|
||||
}
|
||||
const passwordReset = await this.passwordResetModel.findOne(
|
||||
{
|
||||
user: user._id,
|
||||
expiresAt: { $gt: new Date() },
|
||||
},
|
||||
null,
|
||||
{ sort: { createdAt: -1 } },
|
||||
)
|
||||
|
||||
if (!passwordReset || !(await bcrypt.compare(otp, passwordReset.otp))) {
|
||||
throw new HttpException({ error: 'Invalid OTP' }, HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10)
|
||||
user.password = hashedPassword
|
||||
await user.save()
|
||||
|
||||
this.mailService.sendEmailFromTemplate({
|
||||
to: user.email,
|
||||
subject: 'Password Reset',
|
||||
template: 'password-reset-success',
|
||||
context: { name: user.name },
|
||||
})
|
||||
|
||||
return { message: 'Password reset successfully' }
|
||||
}
|
||||
|
||||
async generateApiKey(currentUser: User) {
|
||||
const apiKey = uuidv4()
|
||||
const hashedApiKey = await bcrypt.hash(apiKey, 10)
|
||||
|
||||
21
api/src/auth/schemas/password-reset.schema.ts
Normal file
21
api/src/auth/schemas/password-reset.schema.ts
Normal 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)
|
||||
9
api/src/mail/mail.config.ts
Normal file
9
api/src/mail/mail.config.ts
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
26
api/src/mail/mail.module.ts
Normal file
26
api/src/mail/mail.module.ts
Normal file
@@ -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 {}
|
||||
23
api/src/mail/mail.service.ts
Normal file
23
api/src/mail/mail.service.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
api/src/mail/templates/password-reset-request.hbs
Normal file
14
api/src/mail/templates/password-reset-request.hbs
Normal file
@@ -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>
|
||||
8
api/src/mail/templates/password-reset-success.hbs
Normal file
8
api/src/mail/templates/password-reset-success.hbs
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user