Implement authentication guard with JWT
parent
797a8bbfda
commit
8a590b42f2
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
@ -2368,6 +2369,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz",
|
||||
"integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==",
|
||||
"license": "MIT",
|
||||
"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/platform-express": {
|
||||
"version": "11.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz",
|
||||
|
|
@ -2903,6 +2917,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
|
|
@ -2917,11 +2941,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
|
|
@ -4302,6 +4331,12 @@
|
|||
"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==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -5015,6 +5050,15 @@
|
|||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "Apache-2.0",
|
||||
"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",
|
||||
|
|
@ -7428,6 +7472,49 @@
|
|||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"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://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"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://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
|
|
@ -7525,6 +7612,42 @@
|
|||
"dev": true,
|
||||
"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.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
|
@ -7539,6 +7662,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
|
|
@ -8897,7 +9026,6 @@
|
|||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
|
@ -10369,7 +10497,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": {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import { Session } from './dto/session';
|
|||
import { TokensController } from './controller/tokens.controller';
|
||||
import { TokenService } from './service/token.service';
|
||||
import { Token } from './dto/token';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { AuthService } from './service/auth.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -28,6 +31,11 @@ import { Token } from './dto/token';
|
|||
synchronize: true,
|
||||
}),
|
||||
TypeOrmModule.forFeature([User, Purchase, Session, Token]),
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
secret: process.env.JWT_SECRET || 'jwt-key',
|
||||
signOptions: { expiresIn: '4 WEEKS' },
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
AccessController,
|
||||
|
|
@ -41,6 +49,11 @@ import { Token } from './dto/token';
|
|||
StripeService,
|
||||
SessionService,
|
||||
TokenService,
|
||||
AuthService,
|
||||
{
|
||||
provide: 'APP_GUARD',
|
||||
useClass: AuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
private readonly jwtSecret = process.env.JWT_SECRET || 'jwt-key';
|
||||
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private reflector: Reflector,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.jwtSecret,
|
||||
});
|
||||
|
||||
request['user'] = payload;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,37 +6,36 @@ import {
|
|||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from '../service/user.service';
|
||||
import { AuthService } from 'src/service/auth.service';
|
||||
import { Public } from 'src/auth.guard';
|
||||
|
||||
@Controller('/access')
|
||||
export class AccessController {
|
||||
private readonly logger = new Logger(AccessController.name);
|
||||
|
||||
constructor(private readonly userService: UserService) {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Post('/login')
|
||||
@Public()
|
||||
public async login(
|
||||
@Body('name') name: string,
|
||||
@Body('password') password: string,
|
||||
) {
|
||||
this.logger.debug('Received login request');
|
||||
const user = await this.userService.getUserByName(name);
|
||||
if (user.password !== password) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return {
|
||||
userId: user.id,
|
||||
};
|
||||
return await this.authService.signIn(name, password);
|
||||
}
|
||||
|
||||
@Post('/register')
|
||||
@Public()
|
||||
public async new(
|
||||
@Body('name') name: string,
|
||||
@Body('password') password: string,
|
||||
) {
|
||||
this.logger.debug('Received register request');
|
||||
const user = await this.userService.createUser(name, password);
|
||||
return {
|
||||
userId: user.id,
|
||||
};
|
||||
await this.userService.createUser(name, password);
|
||||
return await this.authService.signIn(name, password);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Logger, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Logger, Post, Req } from '@nestjs/common';
|
||||
import { PurchaseService } from '../service/purchase.service';
|
||||
import { UserService } from '../service/user.service';
|
||||
import { PurchaseItem } from '../dto/purchase-item';
|
||||
|
|
@ -23,9 +23,10 @@ export class BuyController {
|
|||
|
||||
@Post('/start')
|
||||
public async startPurchaseFlow(
|
||||
@Body('userId') userId: string,
|
||||
@Req() request,
|
||||
@Body('quantity') quantity: number,
|
||||
) {
|
||||
const userId = request.userId;
|
||||
const user = await this.userService.getUserById(userId);
|
||||
const purchase = await this.purchaseService.createPurchase(
|
||||
user,
|
||||
|
|
@ -41,7 +42,8 @@ export class BuyController {
|
|||
}
|
||||
|
||||
@Post('/complete')
|
||||
public async complete(@Body('userId') userId: string) {
|
||||
public async complete(@Req() request) {
|
||||
const userId = request.userId;
|
||||
const user = await this.userService.getUserById(userId);
|
||||
const purchases = await this.purchaseService.getPurchasesForUser(user);
|
||||
purchases
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, Query, Req } from '@nestjs/common';
|
||||
import { SessionService } from 'src/service/session.service';
|
||||
import { UserService } from 'src/service/user.service';
|
||||
|
||||
|
|
@ -18,7 +18,8 @@ export class SessionController {
|
|||
}
|
||||
|
||||
@Get()
|
||||
public async getAllSessions(@Query('userId') userId: string) {
|
||||
public async getAllSessions(@Req() request) {
|
||||
const userId = request.userId;
|
||||
const sessions = await this.sessionService.getAllSessions();
|
||||
const user = await this.userService.getUserById(userId);
|
||||
return sessions.map((session) => ({
|
||||
|
|
@ -33,18 +34,20 @@ export class SessionController {
|
|||
|
||||
@Post('/join')
|
||||
public async joinSession(
|
||||
@Body('userId') userId: string,
|
||||
@Req() request,
|
||||
@Body('sessionId') sessionId: string,
|
||||
) {
|
||||
const userId = request.userId;
|
||||
const user = await this.userService.getUserById(userId);
|
||||
await this.sessionService.joinSession(user, sessionId);
|
||||
}
|
||||
|
||||
@Post('/leave')
|
||||
public async leaveSession(
|
||||
@Body('userId') userId: string,
|
||||
@Req() request,
|
||||
@Body('sessionId') sessionId: string,
|
||||
) {
|
||||
const userId = request.userId;
|
||||
const user = await this.userService.getUserById(userId);
|
||||
await this.sessionService.leaveSession(user, sessionId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Logger, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Logger, Query, Req } from '@nestjs/common';
|
||||
import { TokenService } from 'src/service/token.service';
|
||||
import { UserService } from 'src/service/user.service';
|
||||
|
||||
|
|
@ -12,7 +12,8 @@ export class TokensController {
|
|||
) {}
|
||||
|
||||
@Get()
|
||||
public async getTokenCount(@Query('userId') userId: string) {
|
||||
public async getTokenCount(@Req() request) {
|
||||
const userId = request.userId;
|
||||
this.logger.debug(`Finding tokens for user: ${userId}`);
|
||||
const user = await this.userService.getUserById(userId);
|
||||
const token = await this.tokenService.getTokens(user);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async signIn(name: string, password: string) {
|
||||
const user = await this.usersService.getUserByName(name);
|
||||
if (user?.password !== password) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const payload = { userId: user.id };
|
||||
return {
|
||||
jwtToken: await this.jwtService.signAsync(payload),
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue