mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-10-19 22:00:15 -04:00
Merge pull request #738 from hedgedoc/private/tokens
This commit is contained in:
commit
ca04856425
45 changed files with 1056 additions and 77 deletions
|
@ -28,7 +28,12 @@ entity "auth_token"{
|
||||||
*id : number <<generated>>
|
*id : number <<generated>>
|
||||||
--
|
--
|
||||||
*userId : uuid
|
*userId : uuid
|
||||||
|
*keyId: text
|
||||||
*accessToken : text
|
*accessToken : text
|
||||||
|
*label: text
|
||||||
|
*createdAt: date
|
||||||
|
lastUsed: number
|
||||||
|
validUntil: number
|
||||||
}
|
}
|
||||||
|
|
||||||
entity "identity" {
|
entity "identity" {
|
||||||
|
@ -131,7 +136,7 @@ entity "media_upload" {
|
||||||
|
|
||||||
user "1" -- "0..*" note: owner
|
user "1" -- "0..*" note: owner
|
||||||
user "1" -u- "1..*" identity
|
user "1" -u- "1..*" identity
|
||||||
user "1" - "1..*" auth_token
|
user "1" - "1..*" auth_token: authTokens
|
||||||
user "1" -l- "1..*" session
|
user "1" -l- "1..*" session
|
||||||
user "1" - "0..*" media_upload
|
user "1" - "0..*" media_upload
|
||||||
user "0..*" -- "0..*" note
|
user "0..*" -- "0..*" note
|
||||||
|
|
|
@ -27,15 +27,23 @@
|
||||||
"@nestjs/common": "7.6.5",
|
"@nestjs/common": "7.6.5",
|
||||||
"@nestjs/config": "0.6.2",
|
"@nestjs/config": "0.6.2",
|
||||||
"@nestjs/core": "7.6.5",
|
"@nestjs/core": "7.6.5",
|
||||||
|
"@nestjs/passport": "^7.1.5",
|
||||||
"@nestjs/platform-express": "7.6.5",
|
"@nestjs/platform-express": "7.6.5",
|
||||||
"@nestjs/swagger": "4.7.12",
|
"@nestjs/swagger": "4.7.12",
|
||||||
|
"@nestjs/schedule": "^0.4.2",
|
||||||
"@nestjs/typeorm": "7.1.5",
|
"@nestjs/typeorm": "7.1.5",
|
||||||
|
"@types/cron": "^1.7.2",
|
||||||
|
"@types/passport-http-bearer": "^1.0.36",
|
||||||
|
"@types/bcrypt": "^3.0.0",
|
||||||
|
"bcrypt": "^5.0.0",
|
||||||
"class-transformer": "0.3.2",
|
"class-transformer": "0.3.2",
|
||||||
"class-validator": "0.13.1",
|
"class-validator": "0.13.1",
|
||||||
"cli-color": "2.0.0",
|
"cli-color": "2.0.0",
|
||||||
"connect-typeorm": "1.1.4",
|
"connect-typeorm": "1.1.4",
|
||||||
"file-type": "16.2.0",
|
"file-type": "16.2.0",
|
||||||
"joi": "17.3.0",
|
"joi": "17.3.0",
|
||||||
|
"passport": "^0.4.1",
|
||||||
|
"passport-http-bearer": "^1.0.1",
|
||||||
"raw-body": "2.4.1",
|
"raw-body": "2.4.1",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
|
|
17
src/api/private/private-api.module.ts
Normal file
17
src/api/private/private-api.module.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TokensController } from './tokens/tokens.controller';
|
||||||
|
import { LoggerModule } from '../../logger/logger.module';
|
||||||
|
import { UsersModule } from '../../users/users.module';
|
||||||
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [LoggerModule, UsersModule, AuthModule],
|
||||||
|
controllers: [TokensController],
|
||||||
|
})
|
||||||
|
export class PrivateApiModule {}
|
38
src/api/private/tokens/tokens.controller.spec.ts
Normal file
38
src/api/private/tokens/tokens.controller.spec.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { TokensController } from './tokens.controller';
|
||||||
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Identity } from '../../../users/identity.entity';
|
||||||
|
import { User } from '../../../users/user.entity';
|
||||||
|
import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
|
import { AuthModule } from '../../../auth/auth.module';
|
||||||
|
|
||||||
|
describe('TokensController', () => {
|
||||||
|
let controller: TokensController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [TokensController],
|
||||||
|
imports: [LoggerModule, AuthModule],
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(AuthToken))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
controller = module.get<TokensController>(TokensController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
53
src/api/private/tokens/tokens.controller.ts
Normal file
53
src/api/private/tokens/tokens.controller.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
||||||
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
import { TimestampMillis } from '../../../utils/timestamp';
|
||||||
|
import { AuthTokenDto } from '../../../auth/auth-token.dto';
|
||||||
|
import { AuthTokenWithSecretDto } from '../../../auth/auth-token-with-secret.dto';
|
||||||
|
|
||||||
|
@Controller('tokens')
|
||||||
|
export class TokensController {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(TokensController.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getUserTokens(): Promise<AuthTokenDto[]> {
|
||||||
|
// ToDo: Get real userName
|
||||||
|
return (
|
||||||
|
await this.authService.getTokensByUsername('hardcoded')
|
||||||
|
).map((token) => this.authService.toAuthTokenDto(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async postTokenRequest(
|
||||||
|
@Body('label') label: string,
|
||||||
|
@Body('validUntil') validUntil: TimestampMillis,
|
||||||
|
): Promise<AuthTokenWithSecretDto> {
|
||||||
|
// ToDo: Get real userName
|
||||||
|
return this.authService.createTokenForUser('hardcoded', label, validUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:keyId')
|
||||||
|
@HttpCode(204)
|
||||||
|
async deleteToken(@Param('keyId') keyId: string) {
|
||||||
|
return this.authService.removeToken(keyId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import { NotesModule } from '../../../notes/notes.module';
|
||||||
import { Tag } from '../../../notes/tag.entity';
|
import { Tag } from '../../../notes/tag.entity';
|
||||||
import { Authorship } from '../../../revisions/authorship.entity';
|
import { Authorship } from '../../../revisions/authorship.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { AuthToken } from '../../../users/auth-token.entity';
|
import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
import { Identity } from '../../../users/identity.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { UsersModule } from '../../../users/users.module';
|
import { UsersModule } from '../../../users/users.module';
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
Put,
|
Put,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { HistoryEntryUpdateDto } from '../../../history/history-entry-update.dto';
|
import { HistoryEntryUpdateDto } from '../../../history/history-entry-update.dto';
|
||||||
import { HistoryEntryDto } from '../../../history/history-entry.dto';
|
import { HistoryEntryDto } from '../../../history/history-entry.dto';
|
||||||
|
@ -22,7 +24,10 @@ import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
|
||||||
import { NotesService } from '../../../notes/notes.service';
|
import { NotesService } from '../../../notes/notes.service';
|
||||||
import { UserInfoDto } from '../../../users/user-info.dto';
|
import { UserInfoDto } from '../../../users/user-info.dto';
|
||||||
import { UsersService } from '../../../users/users.service';
|
import { UsersService } from '../../../users/users.service';
|
||||||
|
import { TokenAuthGuard } from '../../../auth/token-auth.guard';
|
||||||
|
import { ApiSecurity } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiSecurity('token')
|
||||||
@Controller('me')
|
@Controller('me')
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -34,29 +39,36 @@ export class MeController {
|
||||||
this.logger.setContext(MeController.name);
|
this.logger.setContext(MeController.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
async getMe(): Promise<UserInfoDto> {
|
async getMe(@Request() req): Promise<UserInfoDto> {
|
||||||
return this.usersService.toUserDto(
|
return this.usersService.toUserDto(
|
||||||
await this.usersService.getUserByUsername('hardcoded'),
|
await this.usersService.getUserByUsername(req.user.userName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get('history')
|
@Get('history')
|
||||||
getUserHistory(): HistoryEntryDto[] {
|
getUserHistory(@Request() req): HistoryEntryDto[] {
|
||||||
return this.historyService.getUserHistory('someone');
|
return this.historyService.getUserHistory(req.user.userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Put('history/:note')
|
@Put('history/:note')
|
||||||
updateHistoryEntry(
|
updateHistoryEntry(
|
||||||
|
@Request() req,
|
||||||
@Param('note') note: string,
|
@Param('note') note: string,
|
||||||
@Body() entryUpdateDto: HistoryEntryUpdateDto,
|
@Body() entryUpdateDto: HistoryEntryUpdateDto,
|
||||||
): HistoryEntryDto {
|
): HistoryEntryDto {
|
||||||
|
// ToDo: Check if user is allowed to pin this history entry
|
||||||
return this.historyService.updateHistoryEntry(note, entryUpdateDto);
|
return this.historyService.updateHistoryEntry(note, entryUpdateDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Delete('history/:note')
|
@Delete('history/:note')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
deleteHistoryEntry(@Param('note') note: string) {
|
deleteHistoryEntry(@Request() req, @Param('note') note: string) {
|
||||||
|
// ToDo: Check if user is allowed to delete note
|
||||||
try {
|
try {
|
||||||
return this.historyService.deleteHistoryEntry(note);
|
return this.historyService.deleteHistoryEntry(note);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -64,8 +76,9 @@ export class MeController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get('notes')
|
@Get('notes')
|
||||||
getMyNotes(): NoteMetadataDto[] {
|
getMyNotes(@Request() req): NoteMetadataDto[] {
|
||||||
return this.notesService.getUserNotes('someone');
|
return this.notesService.getUserNotes(req.user.userName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { NotesModule } from '../../../notes/notes.module';
|
||||||
import { Tag } from '../../../notes/tag.entity';
|
import { Tag } from '../../../notes/tag.entity';
|
||||||
import { Authorship } from '../../../revisions/authorship.entity';
|
import { Authorship } from '../../../revisions/authorship.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { AuthToken } from '../../../users/auth-token.entity';
|
import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
import { Identity } from '../../../users/identity.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { MediaController } from './media.controller';
|
import { MediaController } from './media.controller';
|
||||||
|
|
|
@ -12,8 +12,10 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
|
Request,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
|
UseGuards,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
@ -25,7 +27,10 @@ import {
|
||||||
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
||||||
import { MediaService } from '../../../media/media.service';
|
import { MediaService } from '../../../media/media.service';
|
||||||
import { MulterFile } from '../../../media/multer-file.interface';
|
import { MulterFile } from '../../../media/multer-file.interface';
|
||||||
|
import { TokenAuthGuard } from '../../../auth/token-auth.guard';
|
||||||
|
import { ApiSecurity } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiSecurity('token')
|
||||||
@Controller('media')
|
@Controller('media')
|
||||||
export class MediaController {
|
export class MediaController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -35,14 +40,15 @@ export class MediaController {
|
||||||
this.logger.setContext(MediaController.name);
|
this.logger.setContext(MediaController.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Post()
|
@Post()
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadMedia(
|
async uploadMedia(
|
||||||
|
@Request() req,
|
||||||
@UploadedFile() file: MulterFile,
|
@UploadedFile() file: MulterFile,
|
||||||
@Headers('HedgeDoc-Note') noteId: string,
|
@Headers('HedgeDoc-Note') noteId: string,
|
||||||
) {
|
) {
|
||||||
//TODO: Get user from request
|
const username = req.user.userName;
|
||||||
const username = 'hardcoded';
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Recieved filename '${file.originalname}' for note '${noteId}' from user '${username}'`,
|
`Recieved filename '${file.originalname}' for note '${noteId}' from user '${username}'`,
|
||||||
'uploadImage',
|
'uploadImage',
|
||||||
|
@ -64,10 +70,10 @@ export class MediaController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Delete(':filename')
|
@Delete(':filename')
|
||||||
async deleteMedia(@Param('filename') filename: string) {
|
async deleteMedia(@Request() req, @Param('filename') filename: string) {
|
||||||
//TODO: Get user from request
|
const username = req.user.userName;
|
||||||
const username = 'hardcoded';
|
|
||||||
try {
|
try {
|
||||||
await this.mediaService.deleteFile(filename, username);
|
await this.mediaService.deleteFile(filename, username);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -4,18 +4,23 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { MonitoringService } from '../../../monitoring/monitoring.service';
|
||||||
|
import { TokenAuthGuard } from '../../../auth/token-auth.guard';
|
||||||
|
import { ApiSecurity } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiSecurity('token')
|
||||||
@Controller('monitoring')
|
@Controller('monitoring')
|
||||||
export class MonitoringController {
|
export class MonitoringController {
|
||||||
constructor(private monitoringService: MonitoringService) {}
|
constructor(private monitoringService: MonitoringService) {}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return this.monitoringService.getServerStatus();
|
return this.monitoringService.getServerStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get('prometheus')
|
@Get('prometheus')
|
||||||
getPrometheusStatus() {
|
getPrometheusStatus() {
|
||||||
return '';
|
return '';
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Tag } from '../../../notes/tag.entity';
|
||||||
import { Authorship } from '../../../revisions/authorship.entity';
|
import { Authorship } from '../../../revisions/authorship.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { RevisionsModule } from '../../../revisions/revisions.module';
|
import { RevisionsModule } from '../../../revisions/revisions.module';
|
||||||
import { AuthToken } from '../../../users/auth-token.entity';
|
import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
import { Identity } from '../../../users/identity.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { UsersModule } from '../../../users/users.module';
|
import { UsersModule } from '../../../users/users.module';
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Request,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { NotInDBError } from '../../../errors/errors';
|
import { NotInDBError } from '../../../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
||||||
|
@ -21,7 +23,10 @@ import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto';
|
||||||
import { NotesService } from '../../../notes/notes.service';
|
import { NotesService } from '../../../notes/notes.service';
|
||||||
import { RevisionsService } from '../../../revisions/revisions.service';
|
import { RevisionsService } from '../../../revisions/revisions.service';
|
||||||
import { MarkdownBody } from '../../utils/markdownbody-decorator';
|
import { MarkdownBody } from '../../utils/markdownbody-decorator';
|
||||||
|
import { TokenAuthGuard } from '../../../auth/token-auth.guard';
|
||||||
|
import { ApiSecurity } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiSecurity('token')
|
||||||
@Controller('notes')
|
@Controller('notes')
|
||||||
export class NotesController {
|
export class NotesController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -32,14 +37,18 @@ export class NotesController {
|
||||||
this.logger.setContext(NotesController.name);
|
this.logger.setContext(NotesController.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Post()
|
@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);
|
this.logger.debug('Got raw markdown:\n' + text);
|
||||||
return this.noteService.createNoteDto(text);
|
return this.noteService.createNoteDto(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias')
|
@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 {
|
try {
|
||||||
return await this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias);
|
return await this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -50,17 +59,25 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Post(':noteAlias')
|
@Post(':noteAlias')
|
||||||
async createNamedNote(
|
async createNamedNote(
|
||||||
|
@Request() req,
|
||||||
@Param('noteAlias') noteAlias: string,
|
@Param('noteAlias') noteAlias: string,
|
||||||
@MarkdownBody() text: string,
|
@MarkdownBody() text: string,
|
||||||
) {
|
) {
|
||||||
|
// ToDo: check if user is allowed to view this note
|
||||||
this.logger.debug('Got raw markdown:\n' + text);
|
this.logger.debug('Got raw markdown:\n' + text);
|
||||||
return this.noteService.createNoteDto(text, noteAlias);
|
return this.noteService.createNoteDto(text, noteAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Delete(':noteIdOrAlias')
|
@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);
|
this.logger.debug('Deleting note: ' + noteIdOrAlias);
|
||||||
try {
|
try {
|
||||||
await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias);
|
await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias);
|
||||||
|
@ -74,11 +91,14 @@ export class NotesController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Put(':noteIdOrAlias')
|
@Put(':noteIdOrAlias')
|
||||||
async updateNote(
|
async updateNote(
|
||||||
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@MarkdownBody() text: string,
|
@MarkdownBody() text: string,
|
||||||
) {
|
) {
|
||||||
|
// ToDo: check if user is allowed to change this note
|
||||||
this.logger.debug('Got raw markdown:\n' + text);
|
this.logger.debug('Got raw markdown:\n' + text);
|
||||||
try {
|
try {
|
||||||
return await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text);
|
return await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text);
|
||||||
|
@ -90,9 +110,14 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias/content')
|
@Get(':noteIdOrAlias/content')
|
||||||
@Header('content-type', 'text/markdown')
|
@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 {
|
try {
|
||||||
return await this.noteService.getNoteContent(noteIdOrAlias);
|
return await this.noteService.getNoteContent(noteIdOrAlias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -103,8 +128,13 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias/metadata')
|
@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 {
|
try {
|
||||||
return await this.noteService.getNoteMetadata(noteIdOrAlias);
|
return await this.noteService.getNoteMetadata(noteIdOrAlias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -115,11 +145,14 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Put(':noteIdOrAlias/metadata/permissions')
|
@Put(':noteIdOrAlias/metadata/permissions')
|
||||||
async updateNotePermissions(
|
async updateNotePermissions(
|
||||||
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@Body() updateDto: NotePermissionsUpdateDto,
|
@Body() updateDto: NotePermissionsUpdateDto,
|
||||||
) {
|
) {
|
||||||
|
// ToDo: check if user is allowed to view this notes permissions
|
||||||
try {
|
try {
|
||||||
return await this.noteService.updateNotePermissions(
|
return await this.noteService.updateNotePermissions(
|
||||||
noteIdOrAlias,
|
noteIdOrAlias,
|
||||||
|
@ -133,8 +166,13 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias/revisions')
|
@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 {
|
try {
|
||||||
return await this.revisionsService.getNoteRevisionMetadatas(
|
return await this.revisionsService.getNoteRevisionMetadatas(
|
||||||
noteIdOrAlias,
|
noteIdOrAlias,
|
||||||
|
@ -147,11 +185,14 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias/revisions/:revisionId')
|
@Get(':noteIdOrAlias/revisions/:revisionId')
|
||||||
async getNoteRevision(
|
async getNoteRevision(
|
||||||
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@Param('revisionId') revisionId: number,
|
@Param('revisionId') revisionId: number,
|
||||||
) {
|
) {
|
||||||
|
// ToDo: check if user is allowed to view this notes revision
|
||||||
try {
|
try {
|
||||||
return await this.revisionsService.getNoteRevision(
|
return await this.revisionsService.getNoteRevision(
|
||||||
noteIdOrAlias,
|
noteIdOrAlias,
|
||||||
|
|
|
@ -18,12 +18,15 @@ import { NotesModule } from './notes/notes.module';
|
||||||
import { PermissionsModule } from './permissions/permissions.module';
|
import { PermissionsModule } from './permissions/permissions.module';
|
||||||
import { RevisionsModule } from './revisions/revisions.module';
|
import { RevisionsModule } from './revisions/revisions.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
import appConfig from './config/app.config';
|
import appConfig from './config/app.config';
|
||||||
import mediaConfig from './config/media.config';
|
import mediaConfig from './config/media.config';
|
||||||
import hstsConfig from './config/hsts.config';
|
import hstsConfig from './config/hsts.config';
|
||||||
import cspConfig from './config/csp.config';
|
import cspConfig from './config/csp.config';
|
||||||
import databaseConfig from './config/database.config';
|
import databaseConfig from './config/database.config';
|
||||||
import authConfig from './config/auth.config';
|
import authConfig from './config/auth.config';
|
||||||
|
import { PrivateApiModule } from './api/private/private-api.module';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -44,17 +47,20 @@ import authConfig from './config/auth.config';
|
||||||
],
|
],
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
NotesModule,
|
NotesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RevisionsModule,
|
RevisionsModule,
|
||||||
AuthorsModule,
|
AuthorsModule,
|
||||||
PublicApiModule,
|
PublicApiModule,
|
||||||
|
PrivateApiModule,
|
||||||
HistoryModule,
|
HistoryModule,
|
||||||
MonitoringModule,
|
MonitoringModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
GroupsModule,
|
GroupsModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
MediaModule,
|
MediaModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
13
src/auth/auth-token-with-secret.dto.ts
Normal file
13
src/auth/auth-token-with-secret.dto.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { AuthTokenDto } from './auth-token.dto';
|
||||||
|
|
||||||
|
export class AuthTokenWithSecretDto extends AuthTokenDto {
|
||||||
|
@IsString()
|
||||||
|
secret: string;
|
||||||
|
}
|
22
src/auth/auth-token.dto.ts
Normal file
22
src/auth/auth-token.dto.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IsDate, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class AuthTokenDto {
|
||||||
|
@IsString()
|
||||||
|
label: string;
|
||||||
|
@IsString()
|
||||||
|
keyId: string;
|
||||||
|
@IsDate()
|
||||||
|
createdAt: Date;
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
validUntil: Date;
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
lastUsed: Date;
|
||||||
|
}
|
65
src/auth/auth-token.entity.ts
Normal file
65
src/auth/auth-token.entity.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class AuthToken {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
keyId: string;
|
||||||
|
|
||||||
|
@ManyToOne((_) => User, (user) => user.authTokens)
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
accessTokenHash: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
validUntil: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
lastUsed: Date;
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
user: User,
|
||||||
|
label: string,
|
||||||
|
keyId: string,
|
||||||
|
accessToken: string,
|
||||||
|
validUntil: Date,
|
||||||
|
): Pick<
|
||||||
|
AuthToken,
|
||||||
|
'user' | 'label' | 'keyId' | 'accessTokenHash' | 'createdAt' | 'validUntil'
|
||||||
|
> {
|
||||||
|
const newToken = new AuthToken();
|
||||||
|
newToken.user = user;
|
||||||
|
newToken.label = label;
|
||||||
|
newToken.keyId = keyId;
|
||||||
|
newToken.accessTokenHash = accessToken;
|
||||||
|
newToken.createdAt = new Date();
|
||||||
|
newToken.validUntil = validUntil;
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
}
|
26
src/auth/auth.module.ts
Normal file
26
src/auth/auth.module.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
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';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthToken } from './auth-token.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UsersModule,
|
||||||
|
PassportModule,
|
||||||
|
LoggerModule,
|
||||||
|
TypeOrmModule.forFeature([AuthToken]),
|
||||||
|
],
|
||||||
|
providers: [AuthService, TokenStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
199
src/auth/auth.service.spec.ts
Normal file
199
src/auth/auth.service.spec.ts
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { Identity } from '../users/identity.entity';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { AuthToken } from './auth-token.entity';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
let user: User;
|
||||||
|
let authToken: AuthToken;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = {
|
||||||
|
authTokens: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
displayName: 'hardcoded',
|
||||||
|
id: '1',
|
||||||
|
identities: [],
|
||||||
|
ownedNotes: [],
|
||||||
|
updatedAt: new Date(),
|
||||||
|
userName: 'Testy',
|
||||||
|
};
|
||||||
|
|
||||||
|
authToken = {
|
||||||
|
accessTokenHash: '',
|
||||||
|
createdAt: new Date(),
|
||||||
|
id: 1,
|
||||||
|
label: 'testIdentifier',
|
||||||
|
keyId: 'abc',
|
||||||
|
lastUsed: null,
|
||||||
|
user: null,
|
||||||
|
validUntil: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(AuthToken),
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: [PassportModule, UsersModule, LoggerModule],
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(AuthToken))
|
||||||
|
.useValue({
|
||||||
|
findOne: (): AuthToken => {
|
||||||
|
return {
|
||||||
|
...authToken,
|
||||||
|
user: user,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
save: async (entity: AuthToken) => {
|
||||||
|
if (entity.lastUsed === undefined) {
|
||||||
|
expect(entity.lastUsed).toBeUndefined();
|
||||||
|
} else {
|
||||||
|
expect(entity.lastUsed.getTime()).toBeLessThanOrEqual(
|
||||||
|
new Date().getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
},
|
||||||
|
remove: async (entity: AuthToken) => {
|
||||||
|
expect(entity).toEqual({
|
||||||
|
...authToken,
|
||||||
|
user: user,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useValue({
|
||||||
|
findOne: (): User => {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
authTokens: [authToken],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
service = module.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkPassword', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const testPassword = 'thisIsATestPassword';
|
||||||
|
const hash = await service.hashPassword(testPassword);
|
||||||
|
service
|
||||||
|
.checkPassword(testPassword, hash)
|
||||||
|
.then((result) => expect(result).toBeTruthy());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTokensByUsername', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const tokens = await service.getTokensByUsername(user.userName);
|
||||||
|
expect(tokens).toHaveLength(1);
|
||||||
|
expect(tokens).toEqual([authToken]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAuthToken', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const token = 'testToken';
|
||||||
|
authToken.accessTokenHash = await service.hashPassword(token);
|
||||||
|
const authTokenFromCall = await service.getAuthTokenAndValidate(
|
||||||
|
authToken.keyId,
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
expect(authTokenFromCall).toEqual({
|
||||||
|
...authToken,
|
||||||
|
user: user,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLastUsedToken', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
await service.setLastUsedToken(authToken.keyId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateToken', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const token = 'testToken';
|
||||||
|
authToken.accessTokenHash = await service.hashPassword(token);
|
||||||
|
const userByToken = await service.validateToken(
|
||||||
|
`${authToken.keyId}.${token}`,
|
||||||
|
);
|
||||||
|
expect(userByToken).toEqual({
|
||||||
|
...user,
|
||||||
|
authTokens: [authToken],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeToken', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
await service.removeToken(authToken.keyId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTokenForUser', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const identifier = 'identifier2';
|
||||||
|
const token = await service.createTokenForUser(
|
||||||
|
user.userName,
|
||||||
|
identifier,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(token.label).toEqual(identifier);
|
||||||
|
expect(
|
||||||
|
token.validUntil.getTime() -
|
||||||
|
(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000),
|
||||||
|
).toBeLessThanOrEqual(10000);
|
||||||
|
expect(token.lastUsed).toBeNull();
|
||||||
|
expect(token.secret.startsWith(token.keyId)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BufferToBase64Url', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(
|
||||||
|
service.BufferToBase64Url(
|
||||||
|
Buffer.from('testsentence is a test sentence'),
|
||||||
|
),
|
||||||
|
).toEqual('dGVzdHNlbnRlbmNlIGlzIGEgdGVzdCBzZW50ZW5jZQ');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toAuthTokenDto', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const tokenDto = await service.toAuthTokenDto(authToken);
|
||||||
|
expect(tokenDto.keyId).toEqual(authToken.keyId);
|
||||||
|
expect(tokenDto.lastUsed).toBeNull();
|
||||||
|
expect(tokenDto.label).toEqual(authToken.label);
|
||||||
|
expect(tokenDto.validUntil).toBeNull();
|
||||||
|
expect(tokenDto.createdAt.getTime()).toEqual(
|
||||||
|
authToken.createdAt.getTime(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
232
src/auth/auth.service.ts
Normal file
232
src/auth/auth.service.ts
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { AuthToken } from './auth-token.entity';
|
||||||
|
import { AuthTokenDto } from './auth-token.dto';
|
||||||
|
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
|
||||||
|
import { compare, hash } from 'bcrypt';
|
||||||
|
import {
|
||||||
|
NotInDBError,
|
||||||
|
TokenNotValidError,
|
||||||
|
TooManyTokensError,
|
||||||
|
} from '../errors/errors';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { TimestampMillis } from '../utils/timestamp';
|
||||||
|
import { Cron, Timeout } from '@nestjs/schedule';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private usersService: UsersService,
|
||||||
|
@InjectRepository(AuthToken)
|
||||||
|
private authTokenRepository: Repository<AuthToken>,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(AuthService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string): Promise<User> {
|
||||||
|
const [keyId, secret] = token.split('.');
|
||||||
|
const accessToken = await this.getAuthTokenAndValidate(keyId, secret);
|
||||||
|
await this.setLastUsedToken(keyId);
|
||||||
|
const user = await this.usersService.getUserByUsername(
|
||||||
|
accessToken.user.userName,
|
||||||
|
);
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hashPassword(cleartext: string): Promise<string> {
|
||||||
|
// hash the password with bcrypt and 2^12 iterations
|
||||||
|
// this was decided on the basis of https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#bcrypt
|
||||||
|
return hash(cleartext, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPassword(cleartext: string, password: string): Promise<boolean> {
|
||||||
|
return compare(cleartext, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async randomString(length: number): Promise<Buffer> {
|
||||||
|
if (length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return randomBytes(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferToBase64Url(text: Buffer): string {
|
||||||
|
// This is necessary as the is no base64url encoding in the toString method
|
||||||
|
// but as can be seen on https://tools.ietf.org/html/rfc4648#page-7
|
||||||
|
// base64url is quite easy buildable from base64
|
||||||
|
return text
|
||||||
|
.toString('base64')
|
||||||
|
.replace('+', '-')
|
||||||
|
.replace('/', '_')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTokenForUser(
|
||||||
|
userName: string,
|
||||||
|
identifier: string,
|
||||||
|
validUntil: TimestampMillis,
|
||||||
|
): Promise<AuthTokenWithSecretDto> {
|
||||||
|
const user = await this.usersService.getUserByUsername(userName, true);
|
||||||
|
if (user.authTokens.length >= 200) {
|
||||||
|
// This is a very high ceiling unlikely to hinder legitimate usage,
|
||||||
|
// but should prevent possible attack vectors
|
||||||
|
throw new TooManyTokensError(
|
||||||
|
`User '${user.userName}' has already 200 tokens and can't have anymore`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const secret = await this.randomString(64);
|
||||||
|
const keyId = this.BufferToBase64Url(await this.randomString(8));
|
||||||
|
const accessTokenString = await this.hashPassword(secret.toString());
|
||||||
|
const accessToken = this.BufferToBase64Url(Buffer.from(accessTokenString));
|
||||||
|
let token;
|
||||||
|
// Tokens can only be valid for a maximum of 2 years
|
||||||
|
const maximumTokenValidity =
|
||||||
|
new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000;
|
||||||
|
if (validUntil === 0 || validUntil > maximumTokenValidity) {
|
||||||
|
token = AuthToken.create(
|
||||||
|
user,
|
||||||
|
identifier,
|
||||||
|
keyId,
|
||||||
|
accessToken,
|
||||||
|
new Date(maximumTokenValidity),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
token = AuthToken.create(
|
||||||
|
user,
|
||||||
|
identifier,
|
||||||
|
keyId,
|
||||||
|
accessToken,
|
||||||
|
new Date(validUntil),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const createdToken = await this.authTokenRepository.save(token);
|
||||||
|
return this.toAuthTokenWithSecretDto(createdToken, `${keyId}.${secret}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastUsedToken(keyId: string) {
|
||||||
|
const accessToken = await this.authTokenRepository.findOne({
|
||||||
|
where: { keyId: keyId },
|
||||||
|
});
|
||||||
|
accessToken.lastUsed = new Date();
|
||||||
|
await this.authTokenRepository.save(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthTokenAndValidate(
|
||||||
|
keyId: string,
|
||||||
|
token: string,
|
||||||
|
): Promise<AuthToken> {
|
||||||
|
const accessToken = await this.authTokenRepository.findOne({
|
||||||
|
where: { keyId: keyId },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
if (accessToken === undefined) {
|
||||||
|
throw new NotInDBError(`AuthToken '${token}' not found`);
|
||||||
|
}
|
||||||
|
if (!(await this.checkPassword(token, accessToken.accessTokenHash))) {
|
||||||
|
// hashes are not the same
|
||||||
|
throw new TokenNotValidError(`AuthToken '${token}' is not valid.`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
accessToken.validUntil &&
|
||||||
|
accessToken.validUntil.getTime() < new Date().getTime()
|
||||||
|
) {
|
||||||
|
// tokens validUntil Date lies in the past
|
||||||
|
throw new TokenNotValidError(
|
||||||
|
`AuthToken '${token}' is not valid since ${accessToken.validUntil}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
|
||||||
|
const user = await this.usersService.getUserByUsername(userName, true);
|
||||||
|
if (user.authTokens === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return user.authTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeToken(keyId: string) {
|
||||||
|
const token = await this.authTokenRepository.findOne({
|
||||||
|
where: { keyId: keyId },
|
||||||
|
});
|
||||||
|
await this.authTokenRepository.remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
toAuthTokenDto(authToken: AuthToken): AuthTokenDto | null {
|
||||||
|
if (!authToken) {
|
||||||
|
this.logger.warn(`Recieved ${authToken} argument!`, 'toAuthTokenDto');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tokenDto: AuthTokenDto = {
|
||||||
|
lastUsed: null,
|
||||||
|
validUntil: null,
|
||||||
|
label: authToken.label,
|
||||||
|
keyId: authToken.keyId,
|
||||||
|
createdAt: authToken.createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authToken.validUntil) {
|
||||||
|
tokenDto.validUntil = new Date(authToken.validUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authToken.lastUsed) {
|
||||||
|
tokenDto.lastUsed = new Date(authToken.lastUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
toAuthTokenWithSecretDto(
|
||||||
|
authToken: AuthToken | null | undefined,
|
||||||
|
secret: string,
|
||||||
|
): AuthTokenWithSecretDto | null {
|
||||||
|
const tokenDto = this.toAuthTokenDto(authToken);
|
||||||
|
return {
|
||||||
|
...tokenDto,
|
||||||
|
secret: secret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all non valid tokens every sunday on 3:00 AM
|
||||||
|
@Cron('0 0 3 * * 0')
|
||||||
|
async handleCron() {
|
||||||
|
return this.removeInvalidTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all non valid tokens 5 sec after startup
|
||||||
|
@Timeout(5000)
|
||||||
|
async handleTimeout() {
|
||||||
|
return this.removeInvalidTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeInvalidTokens() {
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const tokens: AuthToken[] = await this.authTokenRepository.find();
|
||||||
|
let removedTokens = 0;
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.validUntil && token.validUntil.getTime() <= currentTime) {
|
||||||
|
this.logger.debug(`AuthToken '${token.keyId}' was removed`);
|
||||||
|
await this.authTokenRepository.remove(token);
|
||||||
|
removedTokens++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.log(
|
||||||
|
`${removedTokens} invalid AuthTokens were purged from the DB.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/auth/mock-auth.guard.ts
Normal file
18
src/auth/mock-auth.guard.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MockAuthGuard {
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
req.user = {
|
||||||
|
userName: 'hardcoded',
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
11
src/auth/token-auth.guard.ts
Normal file
11
src/auth/token-auth.guard.ts
Normal file
|
@ -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') {}
|
26
src/auth/token.strategy.ts
Normal file
26
src/auth/token.strategy.ts
Normal file
|
@ -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<User> {
|
||||||
|
const user = await this.authService.validateToken(token);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,3 +15,11 @@ export class ClientError extends Error {
|
||||||
export class PermissionError extends Error {
|
export class PermissionError extends Error {
|
||||||
name = 'PermissionError';
|
name = 'PermissionError';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TokenNotValidError extends Error {
|
||||||
|
name = 'TokenNotValidError';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TooManyTokensError extends Error {
|
||||||
|
name = 'TooManyTokensError';
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Group {
|
export class Group {
|
||||||
|
|
|
@ -26,6 +26,10 @@ async function bootstrap() {
|
||||||
const swaggerOptions = new DocumentBuilder()
|
const swaggerOptions = new DocumentBuilder()
|
||||||
.setTitle('HedgeDoc')
|
.setTitle('HedgeDoc')
|
||||||
.setVersion('2.0-dev')
|
.setVersion('2.0-dev')
|
||||||
|
.addSecurity('token', {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
})
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, swaggerOptions);
|
const document = SwaggerModule.createDocument(app, swaggerOptions);
|
||||||
SwaggerModule.setup('apidoc', app, document);
|
SwaggerModule.setup('apidoc', app, document);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { NotesModule } from '../notes/notes.module';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { Authorship } from '../revisions/authorship.entity';
|
import { Authorship } from '../revisions/authorship.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { AuthToken } from '../users/auth-token.entity';
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
import { Identity } from '../users/identity.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Column, Entity, ManyToOne } from 'typeorm/index';
|
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { Note } from './note.entity';
|
import { Note } from './note.entity';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Authorship } from '../revisions/authorship.entity';
|
import { Authorship } from '../revisions/authorship.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
import { AuthToken } from '../users/auth-token.entity';
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
import { Identity } from '../users/identity.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Column, Entity, ManyToOne } from 'typeorm/index';
|
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Column, Entity, ManyToOne } from 'typeorm/index';
|
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm/index';
|
} from 'typeorm';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IsDate, IsNumber, IsString } from 'class-validator';
|
import { IsDate, IsNumber } from 'class-validator';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
|
|
||||||
export class RevisionMetadataDto {
|
export class RevisionMetadataDto {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { JoinTable, ManyToMany } from 'typeorm/index';
|
import { JoinTable, ManyToMany } from 'typeorm';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { Authorship } from './authorship.entity';
|
import { Authorship } from './authorship.entity';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
|
||||||
import { AuthorColor } from '../notes/author-color.entity';
|
import { AuthorColor } from '../notes/author-color.entity';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { NotesModule } from '../notes/notes.module';
|
import { NotesModule } from '../notes/notes.module';
|
||||||
import { AuthToken } from '../users/auth-token.entity';
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
import { Identity } from '../users/identity.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { Authorship } from './authorship.entity';
|
import { Authorship } from './authorship.entity';
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
} from 'typeorm/index';
|
|
||||||
import { User } from './user.entity';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class AuthToken {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ManyToOne((_) => User, (user) => user.authToken)
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
accessToken: string;
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm/index';
|
} from 'typeorm';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISession } from 'connect-typeorm';
|
import { ISession } from 'connect-typeorm';
|
||||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm/index';
|
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Session implements ISession {
|
export class Session implements ISession {
|
||||||
|
|
|
@ -10,9 +10,9 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Column, OneToMany } from 'typeorm/index';
|
import { Column, OneToMany } from 'typeorm';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { AuthToken } from './auth-token.entity';
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Identity } from './identity.entity';
|
import { Identity } from './identity.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -46,7 +46,7 @@ export class User {
|
||||||
ownedNotes: Note[];
|
ownedNotes: Note[];
|
||||||
|
|
||||||
@OneToMany((_) => AuthToken, (authToken) => authToken.user)
|
@OneToMany((_) => AuthToken, (authToken) => authToken.user)
|
||||||
authToken: AuthToken[];
|
authTokens: AuthToken[];
|
||||||
|
|
||||||
@OneToMany((_) => Identity, (identity) => identity.user)
|
@OneToMany((_) => Identity, (identity) => identity.user)
|
||||||
identities: Identity[];
|
identities: Identity[];
|
||||||
|
@ -59,7 +59,7 @@ export class User {
|
||||||
displayName: string,
|
displayName: string,
|
||||||
): Pick<
|
): Pick<
|
||||||
User,
|
User,
|
||||||
'userName' | 'displayName' | 'ownedNotes' | 'authToken' | 'identities'
|
'userName' | 'displayName' | 'ownedNotes' | 'authTokens' | 'identities'
|
||||||
> {
|
> {
|
||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
newUser.userName = userName;
|
newUser.userName = userName;
|
||||||
|
|
|
@ -7,16 +7,12 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { AuthToken } from './auth-token.entity';
|
|
||||||
import { Identity } from './identity.entity';
|
import { Identity } from './identity.entity';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([User, Identity]), LoggerModule],
|
||||||
TypeOrmModule.forFeature([User, AuthToken, Identity]),
|
|
||||||
LoggerModule,
|
|
||||||
],
|
|
||||||
providers: [UsersService],
|
providers: [UsersService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,16 +27,17 @@ export class UsersService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(userName: string) {
|
async deleteUser(userName: string) {
|
||||||
//TOOD: Handle owned notes and edits
|
// TODO: Handle owned notes and edits
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userName: userName },
|
where: { userName: userName },
|
||||||
});
|
});
|
||||||
await this.userRepository.delete(user);
|
await this.userRepository.delete(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByUsername(userName: string): Promise<User> {
|
async getUserByUsername(userName: string, withTokens = false): Promise<User> {
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userName: userName },
|
where: { userName: userName },
|
||||||
|
relations: withTokens ? ['authTokens'] : null,
|
||||||
});
|
});
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
throw new NotInDBError(`User with username '${userName}' not found`);
|
throw new NotInDBError(`User with username '${userName}' not found`);
|
||||||
|
|
7
src/utils/timestamp.ts
Normal file
7
src/utils/timestamp.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TimestampMillis = number;
|
|
@ -21,6 +21,9 @@ import { NotesModule } from '../../src/notes/notes.module';
|
||||||
import { NotesService } from '../../src/notes/notes.service';
|
import { NotesService } from '../../src/notes/notes.service';
|
||||||
import { PermissionsModule } from '../../src/permissions/permissions.module';
|
import { PermissionsModule } from '../../src/permissions/permissions.module';
|
||||||
import { UsersService } from '../../src/users/users.service';
|
import { UsersService } from '../../src/users/users.service';
|
||||||
|
import { AuthModule } from '../../src/auth/auth.module';
|
||||||
|
import { TokenAuthGuard } from '../../src/auth/token-auth.guard';
|
||||||
|
import { MockAuthGuard } from '../../src/auth/mock-auth.guard';
|
||||||
|
|
||||||
describe('Notes', () => {
|
describe('Notes', () => {
|
||||||
let app: NestExpressApplication;
|
let app: NestExpressApplication;
|
||||||
|
@ -46,8 +49,12 @@ describe('Notes', () => {
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
GroupsModule,
|
GroupsModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideGuard(TokenAuthGuard)
|
||||||
|
.useClass(MockAuthGuard)
|
||||||
|
.compile();
|
||||||
app = moduleRef.createNestApplication<NestExpressApplication>();
|
app = moduleRef.createNestApplication<NestExpressApplication>();
|
||||||
app.useStaticAssets('uploads', {
|
app.useStaticAssets('uploads', {
|
||||||
prefix: '/uploads',
|
prefix: '/uploads',
|
||||||
|
|
|
@ -17,6 +17,10 @@ import { LoggerModule } from '../../src/logger/logger.module';
|
||||||
import { NotesModule } from '../../src/notes/notes.module';
|
import { NotesModule } from '../../src/notes/notes.module';
|
||||||
import { NotesService } from '../../src/notes/notes.service';
|
import { NotesService } from '../../src/notes/notes.service';
|
||||||
import { PermissionsModule } from '../../src/permissions/permissions.module';
|
import { PermissionsModule } from '../../src/permissions/permissions.module';
|
||||||
|
import { AuthModule } from '../../src/auth/auth.module';
|
||||||
|
import { TokenAuthGuard } from '../../src/auth/token-auth.guard';
|
||||||
|
import { MockAuthGuard } from '../../src/auth/mock-auth.guard';
|
||||||
|
import { UsersService } from '../../src/users/users.service';
|
||||||
|
|
||||||
describe('Notes', () => {
|
describe('Notes', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
@ -41,12 +45,18 @@ describe('Notes', () => {
|
||||||
dropSchema: true,
|
dropSchema: true,
|
||||||
}),
|
}),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideGuard(TokenAuthGuard)
|
||||||
|
.useClass(MockAuthGuard)
|
||||||
|
.compile();
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
notesService = moduleRef.get(NotesService);
|
notesService = moduleRef.get(NotesService);
|
||||||
|
const usersService: UsersService = moduleRef.get('UsersService');
|
||||||
|
await usersService.createUser('testy', 'Testy McTestFace');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`POST /notes`, async () => {
|
it(`POST /notes`, async () => {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { INestApplication } from '@nestjs/common';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { AppModule } from '../../src/app.module';
|
import { AppModule } from '../../src/app.module';
|
||||||
//import { UsersService } from '../../src/users/users.service';
|
|
||||||
import { UserInfoDto } from '../../src/users/user-info.dto';
|
import { UserInfoDto } from '../../src/users/user-info.dto';
|
||||||
import { HistoryService } from '../../src/history/history.service';
|
import { HistoryService } from '../../src/history/history.service';
|
||||||
import { NotesService } from '../../src/notes/notes.service';
|
import { NotesService } from '../../src/notes/notes.service';
|
||||||
|
|
174
yarn.lock
174
yarn.lock
|
@ -614,6 +614,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-0.3.0.tgz#1dcf178c198e948c548ca803850e2eba639900d4"
|
resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-0.3.0.tgz#1dcf178c198e948c548ca803850e2eba639900d4"
|
||||||
integrity sha512-AdWVTOg3AhAEcVhPGgUJiLbLXb7L5Pe7vc20YQ0oOXP/KD/nJj0I3BcytVdBhzmgepol67BdivNUvo27Hx3Ndw==
|
integrity sha512-AdWVTOg3AhAEcVhPGgUJiLbLXb7L5Pe7vc20YQ0oOXP/KD/nJj0I3BcytVdBhzmgepol67BdivNUvo27Hx3Ndw==
|
||||||
|
|
||||||
|
"@nestjs/passport@^7.1.5":
|
||||||
|
version "7.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-7.1.5.tgz#b32fc0492008d73ebae4327fbc0012a738a83664"
|
||||||
|
integrity sha512-Hu9hPxTdBZA0C4GrWTsSflzwsJ99oAk9jqAwpcszdFNqfjMjkPGuCM9QsVZbBP2bE8fxrVrPsNOILS6puY8e/A==
|
||||||
|
|
||||||
"@nestjs/platform-express@7.6.5":
|
"@nestjs/platform-express@7.6.5":
|
||||||
version "7.6.5"
|
version "7.6.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-7.6.5.tgz#7ee3df2c104aadac766af8b310bb4d04f4039d4a"
|
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-7.6.5.tgz#7ee3df2c104aadac766af8b310bb4d04f4039d4a"
|
||||||
|
@ -625,6 +630,14 @@
|
||||||
multer "1.4.2"
|
multer "1.4.2"
|
||||||
tslib "2.0.3"
|
tslib "2.0.3"
|
||||||
|
|
||||||
|
"@nestjs/schedule@^0.4.2":
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-0.4.2.tgz#7503545586b3a2ba82e46241e9fdbe2c3e33cf9f"
|
||||||
|
integrity sha512-TLfGTe7YT6FofE6MJmmf0i73OvB0k9EWGulbz3gRnNVtMiyvnY8RaRwwDXlO5873p5wfDFWz+7PDOzdI+lLN7w==
|
||||||
|
dependencies:
|
||||||
|
cron "1.7.2"
|
||||||
|
uuid "8.3.2"
|
||||||
|
|
||||||
"@nestjs/schematics@7.2.7", "@nestjs/schematics@^7.1.0":
|
"@nestjs/schematics@7.2.7", "@nestjs/schematics@^7.1.0":
|
||||||
version "7.2.7"
|
version "7.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-7.2.7.tgz#22cd9d687afbbce068a7d20df02806c6f85832a8"
|
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-7.2.7.tgz#22cd9d687afbbce068a7d20df02806c6f85832a8"
|
||||||
|
@ -738,6 +751,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
|
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
|
||||||
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
|
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
|
||||||
|
|
||||||
|
"@types/accepts@*":
|
||||||
|
version "1.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
|
||||||
|
integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/anymatch@*":
|
"@types/anymatch@*":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||||
|
@ -776,6 +796,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
|
"@types/bcrypt@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-3.0.0.tgz#851489a9065a067cb7f3c9cbe4ce9bed8bba0876"
|
||||||
|
integrity sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.0"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||||
|
@ -791,11 +816,34 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/content-disposition@*":
|
||||||
|
version "0.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.3.tgz#0aa116701955c2faa0717fc69cd1596095e49d96"
|
||||||
|
integrity sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==
|
||||||
|
|
||||||
"@types/cookiejar@*":
|
"@types/cookiejar@*":
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
|
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
|
||||||
integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
|
integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
|
||||||
|
|
||||||
|
"@types/cookies@*":
|
||||||
|
version "0.7.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.6.tgz#71212c5391a976d3bae57d4b09fac20fc6bda504"
|
||||||
|
integrity sha512-FK4U5Qyn7/Sc5ih233OuHO0qAkOpEcD/eG6584yEiLKizTFRny86qHLe/rej3HFQrkBuUjF4whFliAdODbVN/w==
|
||||||
|
dependencies:
|
||||||
|
"@types/connect" "*"
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/keygrip" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/cron@^1.7.2":
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cron/-/cron-1.7.2.tgz#e9fb420da616920dae82d13adfca53282ffaab6e"
|
||||||
|
integrity sha512-AEpNLRcsVSc5AdseJKNHpz0d4e8+ow+abTaC0fKDbAU86rF1evoFF0oC2fV9FdqtfVXkG2LKshpLTJCFOpyvTg==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
moment ">=2.14.0"
|
||||||
|
|
||||||
"@types/debug@0.0.31":
|
"@types/debug@0.0.31":
|
||||||
version "0.0.31"
|
version "0.0.31"
|
||||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
|
||||||
|
@ -865,6 +913,16 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/http-assert@*":
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
|
||||||
|
integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==
|
||||||
|
|
||||||
|
"@types/http-errors@*":
|
||||||
|
version "1.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69"
|
||||||
|
integrity sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
||||||
|
@ -902,6 +960,32 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||||
|
|
||||||
|
"@types/keygrip@*":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||||
|
integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
|
||||||
|
|
||||||
|
"@types/koa-compose@*":
|
||||||
|
version "3.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
|
||||||
|
integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/koa" "*"
|
||||||
|
|
||||||
|
"@types/koa@*":
|
||||||
|
version "2.11.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.6.tgz#b7030caa6b44af801c2aea13ba77d74aff7484d5"
|
||||||
|
integrity sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A==
|
||||||
|
dependencies:
|
||||||
|
"@types/accepts" "*"
|
||||||
|
"@types/content-disposition" "*"
|
||||||
|
"@types/cookies" "*"
|
||||||
|
"@types/http-assert" "*"
|
||||||
|
"@types/http-errors" "*"
|
||||||
|
"@types/keygrip" "*"
|
||||||
|
"@types/koa-compose" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/mime@^1":
|
"@types/mime@^1":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
|
@ -927,6 +1011,22 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/passport-http-bearer@^1.0.36":
|
||||||
|
version "1.0.36"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/passport-http-bearer/-/passport-http-bearer-1.0.36.tgz#c48e3040441de10b140bf8e5cec1a73df9c07172"
|
||||||
|
integrity sha512-D6yFiojv/JSxuQY2FcT/dzFHw+ypVOkKN4QzTdt6xZyrmMQBI7p1wr5F3+clCNUgxRgoNaBVRuzlwu5NSV530w==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/koa" "*"
|
||||||
|
"@types/passport" "*"
|
||||||
|
|
||||||
|
"@types/passport@*":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.5.tgz#1ff54ec3e30fa6480c5e8b8de949c6dc40ddfa2a"
|
||||||
|
integrity sha512-wNL4kT/5rnZgyGkqX7V2qH/R/te+bklv+nXcvHzyX99vNggx9DGN+F8CEOW3P/gRi7Cjm991uidRgTHsYkSuMg==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
|
||||||
"@types/prettier@^2.0.0":
|
"@types/prettier@^2.0.0":
|
||||||
version "2.1.6"
|
version "2.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03"
|
||||||
|
@ -1627,6 +1727,14 @@ bcrypt-pbkdf@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
tweetnacl "^0.14.3"
|
||||||
|
|
||||||
|
bcrypt@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.0.tgz#051407c7cd5ffbfb773d541ca3760ea0754e37e2"
|
||||||
|
integrity sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==
|
||||||
|
dependencies:
|
||||||
|
node-addon-api "^3.0.0"
|
||||||
|
node-pre-gyp "0.15.0"
|
||||||
|
|
||||||
big.js@^5.2.2:
|
big.js@^5.2.2:
|
||||||
version "5.2.2"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||||
|
@ -2194,6 +2302,13 @@ create-require@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
|
cron@1.7.2:
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cron/-/cron-1.7.2.tgz#2ea1f35c138a07edac2ac5af5084ed6fee5723db"
|
||||||
|
integrity sha512-+SaJ2OfeRvfQqwXQ2kgr0Y5pzBR/lijf5OpnnaruwWnmI799JfWr2jN2ItOV9s3A/+TFOt6mxvKzQq5F0Jp6VQ==
|
||||||
|
dependencies:
|
||||||
|
moment-timezone "^0.5.x"
|
||||||
|
|
||||||
cross-spawn@^6.0.0:
|
cross-spawn@^6.0.0:
|
||||||
version "6.0.5"
|
version "6.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||||
|
@ -4738,13 +4853,25 @@ mkdirp@1.x, mkdirp@^1.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
|
"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3:
|
||||||
version "0.5.5"
|
version "0.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
|
||||||
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
|
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
moment-timezone@^0.5.x:
|
||||||
|
version "0.5.32"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2"
|
||||||
|
integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==
|
||||||
|
dependencies:
|
||||||
|
moment ">= 2.9.0"
|
||||||
|
|
||||||
|
"moment@>= 2.9.0", moment@>=2.14.0:
|
||||||
|
version "2.29.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
|
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
@ -4820,7 +4947,7 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
needle@^2.2.1:
|
needle@^2.2.1, needle@^2.5.0:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
|
resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
|
||||||
integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
|
integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
|
||||||
|
@ -4911,6 +5038,22 @@ node-notifier@^8.0.0:
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
which "^2.0.2"
|
which "^2.0.2"
|
||||||
|
|
||||||
|
node-pre-gyp@0.15.0:
|
||||||
|
version "0.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087"
|
||||||
|
integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==
|
||||||
|
dependencies:
|
||||||
|
detect-libc "^1.0.2"
|
||||||
|
mkdirp "^0.5.3"
|
||||||
|
needle "^2.5.0"
|
||||||
|
nopt "^4.0.1"
|
||||||
|
npm-packlist "^1.1.6"
|
||||||
|
npmlog "^4.0.2"
|
||||||
|
rc "^1.2.7"
|
||||||
|
rimraf "^2.6.1"
|
||||||
|
semver "^5.3.0"
|
||||||
|
tar "^4.4.2"
|
||||||
|
|
||||||
node-pre-gyp@^0.11.0:
|
node-pre-gyp@^0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
|
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
|
||||||
|
@ -5312,6 +5455,26 @@ pascalcase@^0.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||||
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
|
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
|
||||||
|
|
||||||
|
passport-http-bearer@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz#147469ea3669e2a84c6167ef99dbb77e1f0098a8"
|
||||||
|
integrity sha1-FHRp6jZp4qhMYWfvmdu3fh8AmKg=
|
||||||
|
dependencies:
|
||||||
|
passport-strategy "1.x.x"
|
||||||
|
|
||||||
|
passport-strategy@1.x.x:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
|
||||||
|
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
|
||||||
|
|
||||||
|
passport@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
|
||||||
|
integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
|
||||||
|
dependencies:
|
||||||
|
passport-strategy "1.x.x"
|
||||||
|
pause "0.0.1"
|
||||||
|
|
||||||
path-exists@^3.0.0:
|
path-exists@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
||||||
|
@ -5364,6 +5527,11 @@ path-type@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||||
|
|
||||||
|
pause@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
||||||
|
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
|
||||||
|
|
||||||
peek-readable@^3.1.3:
|
peek-readable@^3.1.3:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-3.1.3.tgz#932480d46cf6aa553c46c68566c4fb69a82cd2b1"
|
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-3.1.3.tgz#932480d46cf6aa553c46c68566c4fb69a82cd2b1"
|
||||||
|
@ -6451,7 +6619,7 @@ tar@^2.0.0:
|
||||||
fstream "^1.0.12"
|
fstream "^1.0.12"
|
||||||
inherits "2"
|
inherits "2"
|
||||||
|
|
||||||
tar@^4:
|
tar@^4, tar@^4.4.2:
|
||||||
version "4.4.13"
|
version "4.4.13"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||||
|
|
Loading…
Reference in a new issue