Compare commits

...

2 Commits

  1. 4
      migrations/20250104074851-create-user.js
  2. 110
      package-lock.json
  3. 2
      package.json
  4. 32
      src/redis/redis.service.ts
  5. 4
      src/shop/shop.module.ts
  6. 5
      src/users/entities/user.entity.ts
  7. 20
      src/users/guard/auth.guard.ts
  8. 4
      src/users/guard/auth.module.ts
  9. 26
      src/users/guard/jwt.strategy.ts
  10. 6
      src/users/guard/role.guard.ts
  11. 4
      src/users/users.controller.ts
  12. 33
      src/users/users.module.ts
  13. 64
      src/users/users.service.ts

@ -46,10 +46,6 @@ module.exports = {
type: Sequelize.ENUM('male', 'female'),
allowNull: false,
},
refreshToken: {
type: Sequelize.STRING,
allowNull: true,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,

110
package-lock.json generated

@ -21,6 +21,7 @@
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.4.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.13.1",
@ -30,6 +31,7 @@
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
"umzug": "^3.8.2",
"uuid": "^11.0.5",
"zarinpal-checkout": "^0.3.0"
},
"devDependencies": {
@ -936,6 +938,12 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1857,6 +1865,19 @@
"sequelize-typescript": "^2.0.0"
}
},
"node_modules/@nestjs/sequelize/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nestjs/testing": {
"version": "10.4.15",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.15.tgz",
@ -3980,6 +4001,15 @@
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -4367,6 +4397,15 @@
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -6103,6 +6142,30 @@
"node": ">=12.0.0"
}
},
"node_modules/ioredis": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -7409,12 +7472,24 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@ -8805,6 +8880,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@ -9681,6 +9777,12 @@
"node": ">=8"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -10619,16 +10721,16 @@
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {

@ -32,6 +32,7 @@
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.4.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.13.1",
@ -41,6 +42,7 @@
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
"umzug": "^3.8.2",
"uuid": "^11.0.5",
"zarinpal-checkout": "^0.3.0"
},
"devDependencies": {

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisService {
private readonly redis: Redis;
constructor() {
this.redis = new Redis({
host: 'localhost',
});
}
// Add a userId and JTI pair to the whitelist with TTL (Time-To-Live)
async addToWhitelist(userId: string, jti: string, ttl: number): Promise<void> {
const key = `${userId}:${jti}`;
await this.redis.set(key, 'valid', 'EX', ttl);
}
// Check if the userId and JTI pair exists in the whitelist
async isWhitelisted(userId: string, jti: string): Promise<boolean> {
const key = `${userId}:${jti}`;
const value = await this.redis.get(key);
return value === 'valid';
}
// Delete a userId and JTI pair from the whitelist
async deleteFromWhitelist(userId: string, jti: string): Promise<void> {
const key = `${userId}:${jti}`;
await this.redis.del(key);
}
}

@ -11,6 +11,8 @@ import { Wallet } from "./entities/wallet.entity";
import { Transaction } from "./entities/transaction.entity";
import { Payment } from "./entities/payment.entity";
import { Invoice } from "./entities/invoice.entity";
import { RedisService } from "src/redis/redis.service";
import { JwtStrategy } from "src/users/guard/jwt.strategy";
@Module({
imports: [
@ -21,6 +23,6 @@ import { Invoice } from "./entities/invoice.entity";
}),
],
controllers: [ShopController],
providers: [ShopService, RoleGuard, JwtAuthGuard],
providers: [ShopService, RoleGuard, JwtAuthGuard,RedisService,JwtStrategy],
})
export class ShopModule {}

@ -33,11 +33,6 @@ export class User extends Model<User> {
})
gender: string;
@Column({
type: DataType.STRING,
allowNull: true,
})
refreshToken: string;
}
export enum Gender {
Male = "male",

@ -1,13 +1,16 @@
import { Injectable } from "@nestjs/common";
import { CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Observable } from "rxjs";
import { JwtStrategy } from './jwt.strategy';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
constructor(
private readonly jwtService: JwtService,
private readonly jwtStrategy: JwtStrategy
) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers["authorization"]?.split(" ")[1];
@ -16,10 +19,15 @@ export class JwtAuthGuard implements CanActivate {
}
try {
const decoded = this.jwtService.verify(token,{secret:process.env.JWT_SECRET});
request.user = decoded;
return true;
const decoded = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET,
});
await this.jwtStrategy.validate(decoded);
request.user = decoded;
return true;
} catch (error) {
console.log(error)
throw new UnauthorizedException("Invalid or expired token");
}
}

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtAuthGuard } from './auth.guard';
import { RedisService } from 'src/redis/redis.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
providers: [JwtService, JwtAuthGuard],
providers: [JwtService, JwtAuthGuard,RedisService,JwtStrategy],
exports: [JwtService, JwtAuthGuard],
})
export class AuthModule {}

@ -0,0 +1,26 @@
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { RedisService } from 'src/redis/redis.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly redisService: RedisService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any): Promise<any> {
const { jti, id } = payload;
const response = await this.redisService.isWhitelisted(id, jti)
console.log(response)
if (!response) {
throw new UnauthorizedException('Token is not whitelisted');
}
return payload;
}
}

@ -1,4 +1,4 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { CanActivate, ExecutionContext, HttpException, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
@Injectable()
@ -23,7 +23,9 @@ export class RoleGuard implements CanActivate {
request.user = decoded;
return true;
} catch (error) {
console.log(error);
if(error instanceof HttpException){
throw error
}
throw new UnauthorizedException(error.response);
}
}

@ -29,8 +29,8 @@ export class UsersController {
@UseGuards(JwtAuthGuard)
@Get("logout")
async logout(@Request() req) {
const userId = req.user.id;
return this.usersService.logout(userId);
const user = req.user;
return this.usersService.logout(user);
}
//retrieve a user information
@UseGuards(JwtAuthGuard)

@ -1,30 +1,33 @@
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';
import { JwtAuthGuard } from 'src/users/guard/auth.guard';
import { Module } from "@nestjs/common";
import { SequelizeModule } from "@nestjs/sequelize";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { UsersService } from "./users.service";
import { UsersController } from "./users.controller";
import { User } from "./entities/user.entity";
import { JwtAuthGuard } from "src/users/guard/auth.guard";
import { JwtStrategy } from "./guard/jwt.strategy";
import { RedisService } from "src/redis/redis.service";
@Module({
imports: [SequelizeModule.forFeature([User]),
PassportModule.register({ defaultStrategy: 'jwt' }),
imports: [
SequelizeModule.forFeature([User]),
PassportModule.register({ defaultStrategy: "jwt" }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
secret: config.get<string>('JWT_SECRET'),
secret: config.get<string>("JWT_SECRET"),
signOptions: {
expiresIn: config.get<string | number>('JWT_EXPIRES', '1h'),
expiresIn: config.get<string | number>("JWT_EXPIRES", "1h"),
},
};
},
}),
],
],
controllers: [UsersController],
providers: [UsersService,JwtAuthGuard],
providers: [UsersService, JwtAuthGuard, RedisService, JwtStrategy],
})
export class UsersModule {}

@ -7,6 +7,8 @@ import { ConfigService } from "@nestjs/config";
import { CreateUserDto } from "./dto/create-user.dto";
import { LoginUserDto } from "./dto/login-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { v4 as uuidv4 } from "uuid";
import { RedisService } from "src/redis/redis.service";
@Injectable()
export class UsersService {
@ -14,6 +16,7 @@ export class UsersService {
@InjectModel(User) private readonly userModel: typeof User,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly redisService: RedisService,
) {}
// Register method
@ -46,7 +49,6 @@ export class UsersService {
const user = await this.userModel.findOne({
where: { email: createUserDto.email },
});
const refreshToken = this.jwtService.sign(
{ id: user.id },
{
@ -54,9 +56,6 @@ export class UsersService {
expiresIn: this.configService.get<string | number>("JWT_REFRESH_EXPIRES") || "7d",
},
);
await this.userModel.update({ refreshToken }, { where: { id: user.id } });
return { message: "User registered successfully." };
} catch (error) {
if (error instanceof HttpException) {
@ -73,7 +72,7 @@ export class UsersService {
});
if (!user) {
throw new UnauthorizedException("Invalid email , username or password.");
throw new UnauthorizedException("Invalid email, username or password.");
}
const passwordMatch = await bcrypt.compare(loginUserDto.password, user.password);
@ -81,8 +80,9 @@ export class UsersService {
throw new UnauthorizedException("Invalid email or password.");
}
const jti = uuidv4();
const accessToken = this.jwtService.sign(
{ id: user.id, role: user.role },
{ id: user.id, role: user.role, jti },
{
secret: this.configService.get<string>("JWT_SECRET"),
expiresIn: this.configService.get<string | number>("JWT_EXPIRES") || "1h",
@ -90,17 +90,13 @@ export class UsersService {
);
const refreshToken = this.jwtService.sign(
{ id: user.id },
{ id: user.id, jti },
{
secret: this.configService.get<string>("JWT_REFRESH_SECRET"),
expiresIn: this.configService.get<string | number>("JWT_REFRESH_EXPIRES") || "7d",
},
);
await this.userModel.update(
{ refreshToken }, //
{ where: { id: user.id } },
);
await this.redisService.addToWhitelist(user.id, jti, 3600);
return { accessToken, refreshToken };
} catch (error) {
@ -127,38 +123,30 @@ export class UsersService {
if (!user) {
throw new HttpException("User not found.", HttpStatus.NOT_FOUND);
}
if (user.refreshToken !== refreshToken) {
throw new HttpException("Invalid refresh token.", HttpStatus.FORBIDDEN);
}
const jti = uuidv4();
const accessToken = this.jwtService.sign(
{ id: user.id, role: user.role },
{ id: user.id, role: user.role, jti },
{
secret: this.configService.get<string>("JWT_SECRET"),
expiresIn: this.configService.get<string | number>("JWT_EXPIRES") || "1h",
},
);
await this.redisService.addToWhitelist(user.id, jti, 3600);
return { accessToken };
}
//logout (delete refresh token from database)
async logout(userId: number) {
if (!userId) {
throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST);
}
async logout(user: any) {
try {
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
throw new HttpException("User not found.", HttpStatus.NOT_FOUND);
}
const jti = user.jti;
const userId = user.id;
await this.userModel.update({ refreshToken: null }, { where: { id: userId } });
await this.redisService.deleteFromWhitelist(userId, jti);
return { message: "Logout is successful." };
return { message: "Logged out successfully." };
} catch (error) {
throw new HttpException("An error occurred while logging out.", HttpStatus.INTERNAL_SERVER_ERROR);
throw new UnauthorizedException("An error occurred during logout.");
}
}
//get information user method
@ -166,7 +154,7 @@ export class UsersService {
try {
const user = await this.userModel.findOne({
where: { id: userId },
attributes: { exclude: ["password","refreshToken"] },
attributes: { exclude: ["password", "refreshToken"] },
});
if (!user) {
throw new HttpException("User not found.", HttpStatus.NOT_FOUND);
@ -252,14 +240,14 @@ export class UsersService {
return users;
} catch (error) {
if(error instanceof HttpException){
throw error
if (error instanceof HttpException) {
throw error;
}
throw new HttpException("An unexpected error occurred while fetching users.", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
//get users list
async findSpecificUserInfoByUser(userId: number){
async findSpecificUserInfoByUser(userId: number) {
try {
const user = await this.userModel.findByPk(userId, {
attributes: { exclude: ["refreshToken", "password"] },
@ -269,8 +257,8 @@ export class UsersService {
}
return user;
} catch (error) {
if(error instanceof HttpException){
throw error
if (error instanceof HttpException) {
throw error;
}
throw new HttpException("An unexpected error occurred while fetching user information.", HttpStatus.INTERNAL_SERVER_ERROR);
}
@ -292,8 +280,8 @@ export class UsersService {
return { message: "User deleted successfully." };
} catch (error) {
if(error instanceof HttpException){
throw error
if (error instanceof HttpException) {
throw error;
}
throw new HttpException("An error occurred while deleting the user.", HttpStatus.INTERNAL_SERVER_ERROR);
}

Loading…
Cancel
Save