From b043c75f925bffdba66da880819c9decb0068a4b Mon Sep 17 00:00:00 2001 From: nicekid1 <86746988+nicekid1@users.noreply.github.com> Date: Sat, 11 Jan 2025 10:37:05 +0330 Subject: [PATCH] Add refresh token endpoint for admin and user --- migrations/20250104074851-create-user.js | 44 +++++++------ migrations/20250104094702-create-admin.js | 13 ++-- src/admin/admin.controller.ts | 10 ++- src/admin/admin.service.ts | 48 +++++++++++++- src/admin/entities/admin.entity.ts | 5 ++ src/users/entities/user.entity.ts | 7 +++ src/users/users.controller.ts | 20 +++--- src/users/users.service.ts | 77 +++++++++++++++++------ 8 files changed, 169 insertions(+), 55 deletions(-) diff --git a/migrations/20250104074851-create-user.js b/migrations/20250104074851-create-user.js index f88aca3..74f9dec 100644 --- a/migrations/20250104074851-create-user.js +++ b/migrations/20250104074851-create-user.js @@ -1,63 +1,69 @@ 'use strict'; -/** @type {import('sequelize-cli').Migration} */ module.exports = { - async up(queryInterface, Sequelize) { - await queryInterface.dropTable('Users',{cascade:true}) + up: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Users', { cascade: true }); await queryInterface.createTable('Users', { id: { - allowNull: false, + type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, - type: Sequelize.INTEGER + allowNull: false, }, email: { type: Sequelize.STRING, unique: true, - allowNull: false + allowNull: false, }, password: { type: Sequelize.STRING, - allowNull: false + allowNull: false, }, role: { type: Sequelize.STRING, - defaultValue: 'user' + allowNull: false, + defaultValue: 'user', }, firstName: { type: Sequelize.STRING, - allowNull: false + allowNull: true, }, lastName: { type: Sequelize.STRING, - allowNull: false + allowNull: true, }, username: { type: Sequelize.STRING, unique: true, - allowNull: false + allowNull: false, }, phoneNumber: { type: Sequelize.STRING, unique: true, - allowNull: false + allowNull: false, }, gender: { - type: Sequelize.ENUM("male", "female"), - allowNull: false + type: Sequelize.ENUM('male', 'female'), + allowNull: false, + }, + refreshToken: { + type: Sequelize.STRING, + allowNull: true, }, createdAt: { + type: Sequelize.DATE, allowNull: false, - type: Sequelize.DATE + defaultValue: Sequelize.fn('NOW'), }, updatedAt: { + type: Sequelize.DATE, allowNull: false, - type: Sequelize.DATE - } + defaultValue: Sequelize.fn('NOW'), + }, }); }, - async down(queryInterface, Sequelize) { + down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('Users'); - } + }, }; diff --git a/migrations/20250104094702-create-admin.js b/migrations/20250104094702-create-admin.js index e48a8a4..0c55c0b 100644 --- a/migrations/20250104094702-create-admin.js +++ b/migrations/20250104094702-create-admin.js @@ -3,13 +3,12 @@ module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.dropTable('Admins', { cascade: true }); - await queryInterface.createTable('Admins', { id: { - allowNull: false, + type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, - type: Sequelize.INTEGER + allowNull: false, }, email: { type: Sequelize.STRING, @@ -42,6 +41,10 @@ module.exports = { allowNull: false, unique: true, }, + refreshToken: { + type: Sequelize.STRING, + allowNull: true, + }, gender: { type: Sequelize.ENUM('male', 'female'), allowNull: false, @@ -49,12 +52,12 @@ module.exports = { createdAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.NOW, + defaultValue: Sequelize.fn('NOW'), }, updatedAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.NOW, + defaultValue: Sequelize.fn('NOW'), }, }); }, diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 3172954..a79a2d8 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -8,14 +8,22 @@ import { RoleGuard } from "src/guard/role.guard"; @Controller("admin") export class AdminController { constructor(private readonly adminService: AdminService) {} + // register as a admin @Post("register") async register(@Body() createAdminDto: CreateAdminDto): Promise<{message}> { return this.adminService.register(createAdminDto); } + //login as admin @Post("login") - async login(@Body() loginAdminDto: LoginAdminDto): Promise<{ token: string }> { + async login(@Body() loginAdminDto: LoginAdminDto): Promise<{ accessToken, refreshToken }> { return this.adminService.login(loginAdminDto); } + //get a new access token + @Post("new-token") + async newAccessToken(@Body("refreshToken") refreshToken: string) { + return this.adminService.newAccessToken(refreshToken); + } + //edit admin profile @UseGuards(RoleGuard) @Put() async editAdminProfile(@Request() req, @Body() updateAdminDto: UpdateUserDto): Promise<{message}>{ diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 5e571a4..f066479 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -34,7 +34,7 @@ export class AdminService { } } //login method - async login(loginAdminDto: LoginAdminDto): Promise<{ token: string }> { + async login(loginAdminDto: LoginAdminDto): Promise<{ accessToken: string; refreshToken: string }> { try { const admin = await this.adminModel.findOne({ where: { email: loginAdminDto.email }, @@ -49,7 +49,7 @@ export class AdminService { throw new HttpException("Invalid email or password.", HttpStatus.UNAUTHORIZED); } - const token = this.jwtService.sign( + const accessToken = this.jwtService.sign( { id: admin.id, role: admin.role }, { secret: this.configService.get("JWT_SECRET"), @@ -57,7 +57,15 @@ export class AdminService { }, ); - return { token }; + const refreshToken = this.jwtService.sign( + { id: admin.id, role: admin.role }, + { + secret: this.configService.get("JWT_REFRESH_SECRET"), + expiresIn: this.configService.get("JWT_REFRESH_EXPIRES") || "7d", + }, + ); + + return { accessToken, refreshToken }; } catch (error) { if (error instanceof HttpException) { throw error; @@ -65,6 +73,40 @@ export class AdminService { throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); } } + //getting new access token + async newAccessToken(refreshToken: string) { + if (!refreshToken) { + throw new HttpException({ message: "Refresh token is required." }, HttpStatus.BAD_REQUEST); + } + + let decoded; + try { + decoded = this.jwtService.verify(refreshToken, { secret: process.env.JWT_REFRESH_SECRET }); + } catch (error) { + throw new HttpException({ message: "Invalid or expired token." }, HttpStatus.UNAUTHORIZED); + } + + const user = await this.adminModel.findOne({ + where: { id: decoded.id }, + }); + if (!user) { + throw new HttpException({ message: "User not found." }, HttpStatus.NOT_FOUND); + } + + if (user.role !== "admin") { + throw new HttpException({ message: "You are not authorized to get an admin token." }, HttpStatus.FORBIDDEN); + } + + const accessToken = this.jwtService.sign( + { id: user.id, role: user.role }, + { + secret: this.configService.get("JWT_SECRET"), + expiresIn: this.configService.get("JWT_EXPIRES") || "1h", + }, + ); + + return { accessToken }; + } //edit admin profile method async editAdminProfile(userId: number, updateAdminDto: UpdateUserDto) { try { diff --git a/src/admin/entities/admin.entity.ts b/src/admin/entities/admin.entity.ts index c92a01e..20c308e 100644 --- a/src/admin/entities/admin.entity.ts +++ b/src/admin/entities/admin.entity.ts @@ -22,6 +22,11 @@ export class Admin extends Model { @Column({ unique: true }) phoneNumber: string; + @Column({ + type: DataType.STRING, + allowNull: true, + }) + refreshToken: string; @Column({ type: DataType.ENUM("male", "female"), diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index 64948eb..47af507 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -28,6 +28,13 @@ export class User extends Model { allowNull: false, }) gender: string; + + @Column({ + type: DataType.STRING, + allowNull: true, + }) + refreshToken: string; + } export enum Gender { Male = 'male', diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index ee851a8..b0dd800 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -12,14 +12,19 @@ export class UsersController { constructor(private readonly usersService: UsersService) {} //register as user @Post("register") - async register(@Body() createUserDto: CreateUserDto): Promise<{message}> { + async register(@Body() createUserDto: CreateUserDto): Promise<{ message }> { return this.usersService.register(createUserDto); } //login as user @Post("login") - async login(@Body() loginUserDto: LoginUserDto): Promise<{ token }> { + async login(@Body() loginUserDto: LoginUserDto): Promise<{ accessToken; refreshToken }> { return this.usersService.login(loginUserDto); } + //get access token + @Post("new-token") + async newAccessToken(@Body("refreshToken") refreshToken: string) { + return this.usersService.newAccessToken(refreshToken); + } //retrieve a user information @UseGuards(JwtAuthGuard) @@ -32,18 +37,19 @@ export class UsersController { @UseGuards(JwtAuthGuard) @Put() async editProfile(@Request() req, @Body() updateUserDto: UpdateUserDto) { - const userId = req.user.id; + const userId = req.user.id; return this.usersService.editProfile(userId, updateUserDto); } //get users list (admin) - @UseGuards(RoleGuard) + @UseGuards(RoleGuard) @Get("users") async findAll(): Promise { - return this.usersService.findAll(); + return this.usersService.findAll(); } - @UseGuards(RoleGuard) + // get a specific user info by admin + @UseGuards(RoleGuard) @Get("users/:id") async findSpecificUserInfoByUser(@Param("id") id): Promise { - return this.usersService.findSpecificUserInfoByUser(id); + return this.usersService.findSpecificUserInfoByUser(id); } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index eac7538..558f9ce 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -36,7 +36,7 @@ export class UsersService { } } // Login method - async login(loginUserDto: LoginUserDto): Promise<{ token: string }> { + async login(loginUserDto: LoginUserDto): Promise<{ accessToken: string; refreshToken: string }> { try { const user = await this.userModel.findOne({ where: { email: loginUserDto.email }, @@ -45,18 +45,34 @@ export class UsersService { if (!user) { throw new UnauthorizedException("Invalid email or password."); } + const passwordMatch = await bcrypt.compare(loginUserDto.password, user.password); if (!passwordMatch) { throw new UnauthorizedException("Invalid email or password."); } - const token = this.jwtService.sign( + + const accessToken = this.jwtService.sign( { id: user.id, role: user.role }, { secret: this.configService.get("JWT_SECRET"), expiresIn: this.configService.get("JWT_EXPIRES") || "1h", }, ); - return { token }; + + const refreshToken = this.jwtService.sign( + { id: user.id }, + { + secret: this.configService.get("JWT_REFRESH_SECRET"), + expiresIn: this.configService.get("JWT_REFRESH_EXPIRES") || "7d", + }, + ); + + await this.userModel.update( + { refreshToken }, // + { where: { id: user.id } }, + ); + + return { accessToken: accessToken, refreshToken: refreshToken }; } catch (error) { if (error instanceof HttpException) { throw error; @@ -64,6 +80,36 @@ export class UsersService { throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); } } + // getting access token + async newAccessToken(refreshToken: string) { + if (!refreshToken) { + throw new HttpException({ message: "Refresh token is required." }, HttpStatus.BAD_REQUEST); + } + + let decoded; + try { + decoded = this.jwtService.verify(refreshToken, { secret: process.env.JWT_REFRESH_SECRET }); + } catch (error) { + throw new HttpException({ message: "Invalid or expired token." }, HttpStatus.UNAUTHORIZED); + } + + const user = await this.userModel.findOne({ + where: { id: decoded.id }, + }); + if (!user) { + throw new HttpException({ message: "User not found." }, HttpStatus.NOT_FOUND); + } + + const accessToken = this.jwtService.sign( + { id: user.id, role: user.role }, + { + secret: this.configService.get("JWT_SECRET"), + expiresIn: this.configService.get("JWT_EXPIRES") || "1h", + }, + ); + + return { accessToken }; + } //get information user method async getProfile(userId: number): Promise { try { @@ -84,7 +130,7 @@ export class UsersService { try { let user = await this.userModel.findOne({ where: { id: userId } }); if (!user) { - throw new NotFoundException('User not found.'); + throw new NotFoundException("User not found."); } if (updateUserDto.password) { updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10); @@ -92,14 +138,11 @@ export class UsersService { await user.update(updateUserDto); user = await this.userModel.findOne({ where: { id: userId }, - attributes: { exclude: ['password'] }, + attributes: { exclude: ["password"] }, }); - return { message: 'User account updated successfully.', user }; - } catch (error) { - throw new HttpException( - 'An unexpected error occurred. Please try again later.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + return { message: "User account updated successfully.", user }; + } catch (error) { + throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); } } //get users list @@ -107,10 +150,7 @@ export class UsersService { try { return await this.userModel.findAll(); } catch (error) { - throw new HttpException( - 'An unexpected error occurred while fetching users.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException("An unexpected error occurred while fetching users.", HttpStatus.INTERNAL_SERVER_ERROR); } } //get users list @@ -118,14 +158,11 @@ export class UsersService { try { const user = await this.userModel.findByPk(userId); if (!user) { - throw new HttpException('User not found.', HttpStatus.NOT_FOUND); + throw new HttpException("User not found.", HttpStatus.NOT_FOUND); } return user; } catch (error) { - throw new HttpException( - 'An unexpected error occurred while fetching user information.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException("An unexpected error occurred while fetching user information.", HttpStatus.INTERNAL_SERVER_ERROR); } } }