diff --git a/package.json b/package.json index fecec4b18..e7feac7cf 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@nestjs/platform-express": "7.6.5", "@nestjs/swagger": "4.7.12", "@nestjs/typeorm": "7.1.5", - "@types/bcrypt": "^3.0.0", "@types/passport-http-bearer": "^1.0.36", + "@types/bcrypt": "^3.0.0", "bcrypt": "^5.0.0", "class-transformer": "0.3.2", "class-validator": "0.13.1", diff --git a/src/api/public/me/me.controller.ts b/src/api/public/me/me.controller.ts index 8efda4437..1fadb6fe1 100644 --- a/src/api/public/me/me.controller.ts +++ b/src/api/public/me/me.controller.ts @@ -13,6 +13,8 @@ import { NotFoundException, Param, Put, + UseGuards, + Request, } from '@nestjs/common'; import { HistoryEntryUpdateDto } from '../../../history/history-entry-update.dto'; import { HistoryEntryDto } from '../../../history/history-entry.dto'; @@ -22,6 +24,7 @@ import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; import { NotesService } from '../../../notes/notes.service'; import { UserInfoDto } from '../../../users/user-info.dto'; import { UsersService } from '../../../users/users.service'; +import { TokenAuthGuard } from '../../../auth/token-auth.guard'; @Controller('me') export class MeController { @@ -34,29 +37,36 @@ export class MeController { this.logger.setContext(MeController.name); } + @UseGuards(TokenAuthGuard) @Get() - async getMe(): Promise { + async getMe(@Request() req): Promise { return this.usersService.toUserDto( - await this.usersService.getUserByUsername('hardcoded'), + await this.usersService.getUserByUsername(req.user.userName), ); } + @UseGuards(TokenAuthGuard) @Get('history') - getUserHistory(): HistoryEntryDto[] { - return this.historyService.getUserHistory('someone'); + getUserHistory(@Request() req): HistoryEntryDto[] { + return this.historyService.getUserHistory(req.user.userName); } + @UseGuards(TokenAuthGuard) @Put('history/:note') updateHistoryEntry( + @Request() req, @Param('note') note: string, @Body() entryUpdateDto: HistoryEntryUpdateDto, ): HistoryEntryDto { + // ToDo: Check if user is allowed to pin this history entry return this.historyService.updateHistoryEntry(note, entryUpdateDto); } + @UseGuards(TokenAuthGuard) @Delete('history/:note') @HttpCode(204) - deleteHistoryEntry(@Param('note') note: string) { + deleteHistoryEntry(@Request() req, @Param('note') note: string) { + // ToDo: Check if user is allowed to delete note try { return this.historyService.deleteHistoryEntry(note); } catch (e) { @@ -64,8 +74,9 @@ export class MeController { } } + @UseGuards(TokenAuthGuard) @Get('notes') - getMyNotes(): NoteMetadataDto[] { - return this.notesService.getUserNotes('someone'); + getMyNotes(@Request() req): NoteMetadataDto[] { + return this.notesService.getUserNotes(req.user.userName); } } diff --git a/src/api/public/media/media.controller.ts b/src/api/public/media/media.controller.ts index 56b84a689..85e740ca1 100644 --- a/src/api/public/media/media.controller.ts +++ b/src/api/public/media/media.controller.ts @@ -12,8 +12,10 @@ import { NotFoundException, Param, Post, + Request, UnauthorizedException, UploadedFile, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -25,6 +27,7 @@ import { import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; +import { TokenAuthGuard } from '../../../auth/token-auth.guard'; @Controller('media') export class MediaController { @@ -35,14 +38,16 @@ export class MediaController { this.logger.setContext(MediaController.name); } + @UseGuards(TokenAuthGuard) @Post() @UseInterceptors(FileInterceptor('file')) async uploadMedia( + @Request() req, @UploadedFile() file: MulterFile, @Headers('HedgeDoc-Note') noteId: string, ) { //TODO: Get user from request - const username = 'hardcoded'; + const username = req.user.userName; this.logger.debug( `Recieved filename '${file.originalname}' for note '${noteId}' from user '${username}'`, 'uploadImage', @@ -64,10 +69,11 @@ export class MediaController { } } + @UseGuards(TokenAuthGuard) @Delete(':filename') - async deleteMedia(@Param('filename') filename: string) { + async deleteMedia(@Request() req, @Param('filename') filename: string) { //TODO: Get user from request - const username = 'hardcoded'; + const username = req.user.userName; try { await this.mediaService.deleteFile(filename, username); } catch (e) { diff --git a/src/api/public/monitoring/monitoring.controller.ts b/src/api/public/monitoring/monitoring.controller.ts index 1ffdb45b5..356ebee51 100644 --- a/src/api/public/monitoring/monitoring.controller.ts +++ b/src/api/public/monitoring/monitoring.controller.ts @@ -4,18 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, UseGuards } from '@nestjs/common'; import { MonitoringService } from '../../../monitoring/monitoring.service'; +import { TokenAuthGuard } from '../../../auth/token-auth.guard'; @Controller('monitoring') export class MonitoringController { constructor(private monitoringService: MonitoringService) {} + @UseGuards(TokenAuthGuard) @Get() getStatus() { return this.monitoringService.getServerStatus(); } + @UseGuards(TokenAuthGuard) @Get('prometheus') getPrometheusStatus() { return ''; diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 8cf74bb4b..d2c63339c 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -14,6 +14,8 @@ import { Param, Post, Put, + Request, + UseGuards, } from '@nestjs/common'; import { NotInDBError } from '../../../errors/errors'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; @@ -21,6 +23,7 @@ import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto'; import { NotesService } from '../../../notes/notes.service'; import { RevisionsService } from '../../../revisions/revisions.service'; import { MarkdownBody } from '../../utils/markdownbody-decorator'; +import { TokenAuthGuard } from '../../../auth/token-auth.guard'; @Controller('notes') export class NotesController { @@ -32,14 +35,18 @@ export class NotesController { this.logger.setContext(NotesController.name); } + @UseGuards(TokenAuthGuard) @Post() - async createNote(@MarkdownBody() text: string) { + async createNote(@Request() req, @MarkdownBody() text: string) { + // ToDo: provide user for createNoteDto this.logger.debug('Got raw markdown:\n' + text); return this.noteService.createNoteDto(text); } + @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias') - async getNote(@Param('noteIdOrAlias') noteIdOrAlias: string) { + async getNote(@Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string) { + // ToDo: check if user is allowed to view this note try { return await this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias); } catch (e) { @@ -50,17 +57,25 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Post(':noteAlias') async createNamedNote( + @Request() req, @Param('noteAlias') noteAlias: string, @MarkdownBody() text: string, ) { + // ToDo: check if user is allowed to view this note this.logger.debug('Got raw markdown:\n' + text); return this.noteService.createNoteDto(text, noteAlias); } + @UseGuards(TokenAuthGuard) @Delete(':noteIdOrAlias') - async deleteNote(@Param('noteIdOrAlias') noteIdOrAlias: string) { + async deleteNote( + @Request() req, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ) { + // ToDo: check if user is allowed to delete this note this.logger.debug('Deleting note: ' + noteIdOrAlias); try { await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias); @@ -74,11 +89,14 @@ export class NotesController { return; } + @UseGuards(TokenAuthGuard) @Put(':noteIdOrAlias') async updateNote( + @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @MarkdownBody() text: string, ) { + // ToDo: check if user is allowed to change this note this.logger.debug('Got raw markdown:\n' + text); try { return await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text); @@ -90,9 +108,14 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/content') @Header('content-type', 'text/markdown') - async getNoteContent(@Param('noteIdOrAlias') noteIdOrAlias: string) { + async getNoteContent( + @Request() req, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ) { + // ToDo: check if user is allowed to view this notes content try { return await this.noteService.getNoteContent(noteIdOrAlias); } catch (e) { @@ -103,8 +126,13 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/metadata') - async getNoteMetadata(@Param('noteIdOrAlias') noteIdOrAlias: string) { + async getNoteMetadata( + @Request() req, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ) { + // ToDo: check if user is allowed to view this notes metadata try { return await this.noteService.getNoteMetadata(noteIdOrAlias); } catch (e) { @@ -115,11 +143,14 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Put(':noteIdOrAlias/metadata/permissions') async updateNotePermissions( + @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @Body() updateDto: NotePermissionsUpdateDto, ) { + // ToDo: check if user is allowed to view this notes permissions try { return await this.noteService.updateNotePermissions( noteIdOrAlias, @@ -133,8 +164,13 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/revisions') - async getNoteRevisions(@Param('noteIdOrAlias') noteIdOrAlias: string) { + async getNoteRevisions( + @Request() req, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ) { + // ToDo: check if user is allowed to view this notes revisions try { return await this.revisionsService.getNoteRevisionMetadatas( noteIdOrAlias, @@ -147,11 +183,14 @@ export class NotesController { } } + @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/revisions/:revisionId') async getNoteRevision( + @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @Param('revisionId') revisionId: number, ) { + // ToDo: check if user is allowed to view this notes revision try { return await this.revisionsService.getNoteRevision( noteIdOrAlias, diff --git a/src/app.module.ts b/src/app.module.ts index 06e5bb281..8b8ae93d6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,7 @@ import { NotesModule } from './notes/notes.module'; import { PermissionsModule } from './permissions/permissions.module'; import { RevisionsModule } from './revisions/revisions.module'; import { UsersModule } from './users/users.module'; +import { AuthModule } from './auth/auth.module'; import appConfig from './config/app.config'; import mediaConfig from './config/media.config'; import hstsConfig from './config/hsts.config'; @@ -55,6 +56,7 @@ import authConfig from './config/auth.config'; GroupsModule, LoggerModule, MediaModule, + AuthModule, ], controllers: [], providers: [], diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 000000000..95ae873b6 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { UsersModule } from '../users/users.module'; +import { PassportModule } from '@nestjs/passport'; +import { TokenStrategy } from './token.strategy'; + +@Module({ + imports: [UsersModule, PassportModule], + providers: [AuthService, TokenStrategy], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 000000000..df9d1a1de --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,31 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; +import { UsersModule } from '../users/users.module'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { User } from '../users/user.entity'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: getRepositoryToken(User), + useValue: {}, + }, + ], + imports: [UsersModule], + }) + .overrideProvider(getRepositoryToken(User)) + .useValue({}) + .compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 000000000..af7df69f5 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; +import { User } from '../users/user.entity'; + +@Injectable() +export class AuthService { + constructor(private usersService: UsersService) {} + + async validateToken(token: string): Promise { + const user = await this.usersService.getUserByAuthToken(token); + if (user) { + return user; + } + return null; + } +} diff --git a/src/auth/token-auth.guard.ts b/src/auth/token-auth.guard.ts new file mode 100644 index 000000000..e6b691d3b --- /dev/null +++ b/src/auth/token-auth.guard.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class TokenAuthGuard extends AuthGuard('token') {} diff --git a/src/auth/token.strategy.ts b/src/auth/token.strategy.ts new file mode 100644 index 000000000..317b255f4 --- /dev/null +++ b/src/auth/token.strategy.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Strategy } from 'passport-http-bearer'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { User } from '../users/user.entity'; + +@Injectable() +export class TokenStrategy extends PassportStrategy(Strategy, 'token') { + constructor(private authService: AuthService) { + super(); + } + + async validate(token: string): Promise { + const user = await this.authService.validateToken(token); + if (!user) { + throw new UnauthorizedException(); + } + return user; + } +}