diff --git a/package.json b/package.json index 97343c5..c75e603 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "jest", "paths": "resolve-tspaths --out \"dist\"", "start": "nest start", - "dev": "nest start --debug --watch", + "dev": "nest start --debug=0.0.0.0 --watch", "spa-dev": "vite --config=src/frontend/vite.config.ts --host", "spa-build": "vite --config=src/frontend/vite.config.ts build", "spa-preview": "vite --config=src/frontend/vite.config.ts preview" diff --git a/src/backend/auth/auth.controller.ts b/src/backend/auth/auth.controller.ts index be2d857..5cb2d53 100644 --- a/src/backend/auth/auth.controller.ts +++ b/src/backend/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Query, Res, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Query, Res } from '@nestjs/common'; import { ApiExcludeController } from '@nestjs/swagger'; import { Response } from 'express'; import Joi from 'joi'; @@ -7,8 +7,7 @@ import { JoiPipe } from 'nestjs-joi'; import logger from '../logger'; import { UserDocument } from '../user/user.model'; -import { User } from './auth.decorator'; -import { AuthGuard } from './auth.guard'; +import { Auth, User } from './auth.decorator'; import { AuthService } from './auth.service'; export const COOKIE_NAME_VACDM_TOKEN = 'vacdm_token'; @@ -48,7 +47,7 @@ export class AuthController { } @Get('/profile') - @UseGuards(AuthGuard) + @Auth('any') getProfile(@User() user: UserDocument) { return user; } diff --git a/src/backend/auth/auth.decorator.ts b/src/backend/auth/auth.decorator.ts index 1f6dc09..df1f537 100644 --- a/src/backend/auth/auth.decorator.ts +++ b/src/backend/auth/auth.decorator.ts @@ -1,11 +1,30 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { ExecutionContext, SetMetadata, UseGuards, applyDecorators, createParamDecorator } from '@nestjs/common'; +import { Request } from 'express'; import { UserDocument } from '../user/user.model'; -export const User = createParamDecorator( - function getRequestUserDecorator(data: void, ctx: ExecutionContext): UserDocument { - const request = ctx.switchToHttp().getRequest(); +import { AuthGuard } from './auth.guard'; - return request.user; +export type AcceptedUserTypes = 'web' | 'plugin' | 'any'; + +export function Auth(usertype: AcceptedUserTypes = 'any') { + return applyDecorators( + SetMetadata('usertype', usertype), + UseGuards(AuthGuard), + ); +} + +export const User = createParamDecorator( + function getRequestUserDecorator(data: AcceptedUserTypes | undefined, ctx: ExecutionContext): UserDocument | undefined { + const request = ctx.switchToHttp().getRequest(); + + if (data === 'web') { + return request.webUser; + } + if (data === 'plugin') { + return request.pluginUser; + } + + return request.user || request.webUser || request.pluginUser; }, ); diff --git a/src/backend/auth/auth.guard.ts b/src/backend/auth/auth.guard.ts index d718516..373aefc 100644 --- a/src/backend/auth/auth.guard.ts +++ b/src/backend/auth/auth.guard.ts @@ -2,6 +2,8 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Request } from 'express'; +import { AcceptedUserTypes } from './auth.decorator'; + @Injectable() export class AuthGuard implements CanActivate { constructor(private reflector: Reflector) {} @@ -9,8 +11,25 @@ export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); - const { user } = request; + const usertype = this.reflector.get('usertype', context.getHandler()) || 'any'; + + switch (usertype) { + case 'web': { + request.user = request.webUser; + break; + } + case 'plugin': { + request.user = request.pluginUser; + break; + } + case 'any': { + request.user = request.webUser || request.pluginUser; + break; + } + } + + - return !!user; + return !!request.user; } } diff --git a/src/backend/auth/auth.middleware.ts b/src/backend/auth/auth.middleware.ts index 6757727..6bc05fd 100644 --- a/src/backend/auth/auth.middleware.ts +++ b/src/backend/auth/auth.middleware.ts @@ -9,15 +9,29 @@ import { COOKIE_NAME_VACDM_TOKEN } from './auth.controller'; export class AuthMiddleware implements NestMiddleware { constructor(private userService: UserService) {} - async use(request: Request, response: Response, next: NextFunction) { - const token = request.cookies[COOKIE_NAME_VACDM_TOKEN]; + private headerTokenRegex = /^Bearer (.+)$/; - if (!token) return next(); + async use(request: Request, response: Response, next: NextFunction) { + const cookieToken = request.cookies[COOKIE_NAME_VACDM_TOKEN]; - try { - request.user = await this.userService.getUserFromToken(token); - } catch (_) { - // do nothing + if (cookieToken) { + try { + request.webUser = await this.userService.getUserFromToken(cookieToken); + } catch (_) { + // do nothing + } + } + + const pluginToken = request.headers.authorization; + + const res = this.headerTokenRegex.exec(pluginToken || ''); + + if (res) { + try { + request.pluginUser = await this.userService.getUserFromPluginToken(res[1]); + } catch (_) { + // do nothing + } } next(); diff --git a/src/backend/custom.d.ts b/src/backend/custom.d.ts index aafcc95..4438d08 100644 --- a/src/backend/custom.d.ts +++ b/src/backend/custom.d.ts @@ -1,9 +1,11 @@ -import User from '@/shared/interfaces/user.interface'; +import { UserDocument } from './user/user.model'; declare global { namespace Express { interface Request { - user?: User; + webUser?: UserDocument; + pluginUser?: UserDocument; + user?: UserDocument; } } } diff --git a/src/backend/plugin-token/plugin-token.controller.ts b/src/backend/plugin-token/plugin-token.controller.ts index 0bf367e..d878d0b 100644 --- a/src/backend/plugin-token/plugin-token.controller.ts +++ b/src/backend/plugin-token/plugin-token.controller.ts @@ -1,11 +1,10 @@ -import { BadRequestException, Body, Controller, Delete, Get, HttpCode, NotFoundException, Param, Post, Query, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, HttpCode, NotFoundException, Param, Post, Query, Res, UnauthorizedException } from '@nestjs/common'; import { ApiExcludeController } from '@nestjs/swagger'; import { Response } from 'express'; import { FilterQuery } from 'mongoose'; import PluginToken from '../../shared/interfaces/plugin-token.interface'; -import { User } from '../auth/auth.decorator'; -import { AuthGuard } from '../auth/auth.guard'; +import { Auth, User } from '../auth/auth.decorator'; import getAppConfig from '../config'; import logger from '../logger'; import { UserDocument } from '../user/user.model'; @@ -40,6 +39,7 @@ export class PluginTokenController { */ @HttpCode(200) @Post('/authorize/:id') + @Auth('web') async authorizePluginToken(@Param('id') tokenId: string, @User() user: UserDocument, @Body('confirm') confirmation: string, @Body('label') label: string) { if (!user) { throw new UnauthorizedException(); @@ -101,7 +101,7 @@ export class PluginTokenController { } @Get('') - @UseGuards(AuthGuard) + @Auth('web') async getAllTokens(@User() user: UserDocument, @Query('scope') scope = 'own') { let filter: FilterQuery = { user: user._id }; @@ -115,7 +115,7 @@ export class PluginTokenController { } @Delete('/:id') - @UseGuards(AuthGuard) + @Auth('web') async revokeToken(@User() user: UserDocument, @Param('id') id: string) { const token = await this.pluginTokenService.findToken({ user: user._id, _id: id }); diff --git a/src/backend/user/user.module.ts b/src/backend/user/user.module.ts index 2f50e70..ce97f22 100644 --- a/src/backend/user/user.module.ts +++ b/src/backend/user/user.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { DatabaseModule } from '../database.module'; +import { PluginTokenModule } from '../plugin-token/plugin-token.module'; import { ScheduleModule } from '../schedule.module'; import { UserController } from './user.controller'; @@ -11,6 +12,7 @@ import { UserService } from './user.service'; imports: [ DatabaseModule, ScheduleModule, + PluginTokenModule, ], providers: [UserService, UserProvider], exports: [UserService], diff --git a/src/backend/user/user.service.ts b/src/backend/user/user.service.ts index 6cc7628..a7398cc 100644 --- a/src/backend/user/user.service.ts +++ b/src/backend/user/user.service.ts @@ -5,6 +5,7 @@ import { FilterQuery } from 'mongoose'; import getAppConfig from '../config'; import logger from '../logger'; +import { PluginTokenService } from '../plugin-token/plugin-token.service'; import { AGENDA_PROVIDER } from '../schedule.module'; import { USER_MODEL, UserDocument, UserModel } from './user.model'; @@ -17,6 +18,7 @@ export class UserService { constructor( @Inject(USER_MODEL) private userModel: UserModel, @Inject(AGENDA_PROVIDER) private agenda: Agenda, + private pluginTokenService: PluginTokenService, ) { this.agenda.define('USER_cleanupUsers', this.cleanupUsers.bind(this)); this.agenda.every('10 minutes', 'USER_cleanupUsers'); @@ -94,6 +96,18 @@ export class UserService { return this.getUserFromCid(tokenData.cid); } + async getUserFromPluginToken(token: string): Promise { + const userId = await this.pluginTokenService.attemptPluginAuthentication(token); + + if (typeof userId === 'undefined') { + throw new NotFoundException(); + } + + const user = await this.getUserFromId(userId); + + return user; + } + async cleanupUsers() { const inactiveUsers = await this.getUsers({ admin: false,