diff --git a/.env b/.env index 8c99d18..d08fa71 100644 --- a/.env +++ b/.env @@ -25,3 +25,6 @@ TWITTER_CLIENT_SECRET= # Bright Data Configuration BRIGHT_DATA_TOKEN= + +# JWT Configuration +JWT_SECRET= diff --git a/package-lock.json b/package-lock.json index 42eaaef..0de3581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,19 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "helmet": "^8.0.0", "http-status-codes": "^2.3.0", "mongoose": "^8.7.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -30,6 +36,8 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -1843,6 +1851,18 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mongoose": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.1.0.tgz", @@ -1854,6 +1874,15 @@ "rxjs": "^7.0.0" } }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.6", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.6.tgz", @@ -2253,6 +2282,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2269,11 +2306,50 @@ "version": "20.17.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.0.tgz", "integrity": "sha512-a7zRo0f0eLo9K5X9Wp5cAqTUNGzuFLDG2R7C4HY2BhcMAsxgSPuRvAC1ZB6QkuUQXf0YZAgfOX2ZyrBa2n4nHQ==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", @@ -3246,6 +3322,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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", @@ -3903,6 +3984,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/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", @@ -5037,6 +5126,14 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", + "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/hexoid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", @@ -6249,6 +6346,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "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": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -6337,6 +6474,36 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "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==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/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", @@ -6349,6 +6516,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/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", @@ -7005,6 +7177,51 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7074,6 +7291,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7643,7 +7865,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -8550,8 +8771,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "2.0.1", diff --git a/package.json b/package.json index 5d11dc4..3fb8001 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,19 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "helmet": "^8.0.0", "http-status-codes": "^2.3.0", "mongoose": "^8.7.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -44,6 +50,8 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -79,4 +87,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/src/app.module.ts b/src/app.module.ts index b340465..3d328fa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,9 +5,11 @@ import { AppService } from './app.service'; import { CommonModule } from './common/common.module'; import { DriversModule } from './drivers/drivers.module'; import { SocialMediaContentModule } from './social-media-content/social-media-content.module'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; @Module({ - imports: [CommonModule, DriversModule, SocialMediaContentModule, CoreModule], + imports: [CommonModule, DriversModule, SocialMediaContentModule, CoreModule, AuthModule, UsersModule], controllers: [AppController], providers: [AppService], }) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..1b7cb28 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,42 @@ +import { EnvironmentVariables } from '@common/config/validate-env'; +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { UsersModule } from '@users/users.module'; +import { AuthController } from './controllers/auth.controller'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { AuthService } from './services/auth.service'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +const exported = [AuthService]; + +// TODO: declare auth guard globally : https://docs.nestjs.com/security/authentication#enable-authentication-globally + +@Module({ + imports: [ + UsersModule, + PassportModule, + JwtModule.registerAsync({ + useFactory: async ( + configService: ConfigService, + ) => ({ + global: true, + secret: configService.get('JWT_SECRET'), + }), + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [ + AuthService, + JwtStrategy, + /** Declare auth guard globally */ + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], +}) +export class AuthModule {} diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts new file mode 100644 index 0000000..6e4767f --- /dev/null +++ b/src/auth/controllers/auth.controller.ts @@ -0,0 +1,36 @@ +import { Public } from '@auth/decorators/is-public.decorator'; +import { AuthService } from '@auth/services/auth.service'; +import { + Controller, + Get, + HttpCode, + HttpStatus, + Post, + Request, + UseGuards, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Public() + @HttpCode(HttpStatus.OK) + @UseGuards(AuthGuard('local')) + @Post('login') + public async login(@Request() req) { + console.log(req); + return this.authService.login(req.user); + } + + @Post('logout') + public async logout(@Request() req) { + return req.logout(); + } + + @Get('profile') + public async profile(@Request() req) { + return req.user; + } +} diff --git a/src/auth/decorators/is-public.decorator.ts b/src/auth/decorators/is-public.decorator.ts new file mode 100644 index 0000000..a57bdf3 --- /dev/null +++ b/src/auth/decorators/is-public.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +/** Decorator to mark a route as public (not protected by auth guard) */ +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..c49835a --- /dev/null +++ b/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,22 @@ +import { IS_PUBLIC_KEY } from '@auth/decorators/is-public.decorator'; +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + return super.canActivate(context); + } +} diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts new file mode 100644 index 0000000..205453d --- /dev/null +++ b/src/auth/services/auth.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { UserService } from '@users/services/user.service'; + +@Injectable() +export class AuthService { + constructor( + private jwtService: JwtService, + private usersService: UserService, + ) {} + + public async validateUser(username: string, pass: string): Promise { + const user = await this.usersService.findOne(username); + if (user && user.password === pass) { + const { password, ...result } = user; + return result; + } + return null; + } + + public async login(user: any) { + const payload = { username: user.username, sub: user.userId }; + return { + access_token: this.jwtService.sign(payload), + }; + } +} diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..edad0bc --- /dev/null +++ b/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,20 @@ +import { EnvironmentVariables } from '@common/config/validate-env'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + }); + } + + async validate(payload: any) { + return { userId: payload.sub, username: payload.username }; + } +} diff --git a/src/common/config/validate-env.ts b/src/common/config/validate-env.ts index 3bff2e8..078c4c2 100644 --- a/src/common/config/validate-env.ts +++ b/src/common/config/validate-env.ts @@ -68,6 +68,9 @@ export class EnvironmentVariables { @IsString() TWITTER_BEARER_TOKEN: string; + + @IsString() + JWT_SECRET: string; } export function validate(config: Record) { diff --git a/src/drivers/brightdata/modules/brightdata-monitor/schemas/brightdata-monitor.schema.ts b/src/drivers/brightdata/modules/brightdata-monitor/schemas/brightdata-monitor.schema.ts index f8743e6..6ffb9af 100644 --- a/src/drivers/brightdata/modules/brightdata-monitor/schemas/brightdata-monitor.schema.ts +++ b/src/drivers/brightdata/modules/brightdata-monitor/schemas/brightdata-monitor.schema.ts @@ -13,6 +13,7 @@ export class BrightDataMonitorModel { @Prop() public dataset_id!: string; + /** Called api endpoint */ @Prop() public url!: string; diff --git a/src/main.ts b/src/main.ts index 862a22a..ce8d9be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,15 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import helmet from 'helmet'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn', 'log'], }); + app.use(helmet()); + // https://docs.nestjs.com/techniques/validation#stripping-properties app.useGlobalPipes( new ValidationPipe({ diff --git a/src/users/schema/user.schema.ts b/src/users/schema/user.schema.ts new file mode 100644 index 0000000..6551ee5 --- /dev/null +++ b/src/users/schema/user.schema.ts @@ -0,0 +1,10 @@ +import { Prop, Schema } from '@nestjs/mongoose'; + +@Schema() +export class UserSchema { + @Prop({ required: true }) + username: string; + + @Prop({ required: true }) + password: string; +} diff --git a/src/users/services/user.service.ts b/src/users/services/user.service.ts new file mode 100644 index 0000000..77801a0 --- /dev/null +++ b/src/users/services/user.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; + +// This should be a real class/interface representing a user entity +export type User = any; + +@Injectable() +export class UserService { + // TODO: replace with a real database + private readonly users = [ + { + userId: 1, + username: 'john', + password: 'changeme', + }, + { + userId: 2, + username: 'maria', + password: 'guess', + }, + ]; + + async findOne(username: string): Promise { + return this.users.find((user) => user.username === username); + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts new file mode 100644 index 0000000..957f6ff --- /dev/null +++ b/src/users/users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './services/user.service'; + +const exported = [UserService]; + +@Module({ + providers: exported, + exports: exported, +}) +export class UsersModule {} diff --git a/tsconfig.json b/tsconfig.json index 5d827d2..daabf64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,9 @@ "@common/*": ["./src/common/*"], "@drivers/*": ["./src/drivers/*"], "@social-media-content/*": ["./src/social-media-content/*"], - "@core/*": ["./src/core/*"] + "@core/*": ["./src/core/*"], + "@auth/*": ["./src/auth/*"], + "@users/*": ["./src/users/*"] } } }