diff --git a/package-lock.json b/package-lock.json index d0c83c5..5e44c63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", @@ -2279,6 +2280,18 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://mirror-npm.runflare.com/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", @@ -2863,6 +2876,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://mirror-npm.runflare.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/luxon": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", @@ -2876,11 +2898,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://mirror-npm.runflare.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/node": { "version": "22.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4033,6 +4059,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://mirror-npm.runflare.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4841,6 +4872,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://mirror-npm.runflare.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7169,6 +7208,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://mirror-npm.runflare.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://mirror-npm.runflare.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://mirror-npm.runflare.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7281,6 +7360,36 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://mirror-npm.runflare.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://mirror-npm.runflare.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://mirror-npm.runflare.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://mirror-npm.runflare.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://mirror-npm.runflare.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://mirror-npm.runflare.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7295,6 +7404,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://mirror-npm.runflare.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8715,7 +8829,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10130,7 +10243,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { diff --git a/package.json b/package.json index 86e5163..f026aca 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", diff --git a/src/app.module.ts b/src/app.module.ts index 4515cce..bd23b60 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { AuthModule } from './modules/auth/auth.module'; import { OtpModule } from './modules/otp/otp.module'; import { HashingModule } from './common/modules/hashing/hashing.module'; import path from 'path'; +import jwtConfig from './config/jwt.config'; const ENV = process.env.NODE_ENV; @@ -34,7 +35,7 @@ const ENV = process.env.NODE_ENV; ConfigModule.forRoot({ isGlobal: true, envFilePath: !ENV ? '.env' : `.env.${ENV}`, - load: [appConfig, databaseConfig], + load: [appConfig, databaseConfig, jwtConfig], validationSchema: environmentValidation, }), diff --git a/src/config/environment.validation.ts b/src/config/environment.validation.ts index 1206934..36b9324 100644 --- a/src/config/environment.validation.ts +++ b/src/config/environment.validation.ts @@ -11,4 +11,10 @@ export default Joi.object({ DATABASE_NAME: Joi.string().required(), DATABASE_AUTOLOAD: Joi.boolean().default('true'), DATABASE_SYNC: Joi.boolean().default('true'), + JWT_ACCESS_SECRET: Joi.string().required(), + JWT_ACCESS_TTL: Joi.number().required(), + JWT_REFRESH_SECRET: Joi.string().required(), + JWT_REFRESH_TTL: Joi.number().required(), + JWT_ISSUER: Joi.string().required(), + JWT_AUDIENCE: Joi.string().required(), }); diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts new file mode 100644 index 0000000..886d639 --- /dev/null +++ b/src/config/jwt.config.ts @@ -0,0 +1,14 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('jwt', () => ({ + access: { + secret: process.env.JWT_ACCESS_SECRET, + expiresIn: parseInt(process.env.JWT_ACCESS_TTL || '3600'), + }, + refresh: { + secret: process.env.JWT_REFRESH_SECRET, + expiresIn: parseInt(process.env.JWT_REFRESH_TTL || '864000'), + }, + issuer: process.env.JWT_ISSUER, + audience: process.env.JWT_AUDIENCE, +})); diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 3cc9590..616abb0 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -4,9 +4,26 @@ import { AuthService } from './providers/auth.service'; import { UsersModule } from '../users/users.module'; import { OtpModule } from '../otp/otp.module'; import { HashingModule } from '@/common/modules/hashing/hashing.module'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; @Module({ - imports: [UsersModule, OtpModule, HashingModule], + imports: [ + UsersModule, + OtpModule, + HashingModule, + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('jwt.access.secret'), + signOptions: { + expiresIn: configService.get('jwt.access.expiresIn'), + issuer: configService.get('jwt.issuer'), + audience: configService.get('jwt.audience'), + }, + }), + }), + ], controllers: [AuthController], providers: [AuthService], }) diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 95b20c8..9ab8a2e 100644 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -1,8 +1,11 @@ import { UsersService } from '@/modules/users/providers/users.service'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { LoginDTO } from '../dtos/login.dto'; import { OtpService } from '@/modules/otp/providers/otp.service'; import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider'; +import { JwtService } from '@nestjs/jwt'; +import type { ConfigType } from '@nestjs/config'; +import jwtConfig from '@/config/jwt.config'; @Injectable() export class AuthService { @@ -21,6 +24,17 @@ export class AuthService { * Inject Hashing Provider */ private readonly hashingProvider: HashingProvider, + + /** + * Inject Jwt Service + */ + private readonly jwtService: JwtService, + + /** + * Inject Jwt Config + */ + @Inject(jwtConfig.KEY) + private readonly jwtConfiguration: ConfigType, ) {} public async logIn(loginDto: LoginDTO) { @@ -52,9 +66,19 @@ export class AuthService { ); if (isPasswordCorrect) { - // TODO: Create JWT + const tokenPayload = { + sub: user.id, + phone: user.phone, + }; - return { access: 'ACCESS_TOKEN', refresh: 'REFRESH_TOKEN' }; + const accessToken = await this.jwtService.signAsync(tokenPayload); + + const refreshToken = await this.jwtService.signAsync(tokenPayload, { + secret: this.jwtConfiguration.refresh.secret, + expiresIn: this.jwtConfiguration.refresh.expiresIn, + }); + + return { access: accessToken, refresh: refreshToken }; } throw new UnauthorizedException( diff --git a/src/modules/users/user.entity.ts b/src/modules/users/user.entity.ts index 332e181..84309ed 100644 --- a/src/modules/users/user.entity.ts +++ b/src/modules/users/user.entity.ts @@ -39,7 +39,6 @@ export class User { type: 'varchar', length: 96, nullable: true, - select: false, }) password?: string;