mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-17 12:39:01 +00: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
docs/content/dev
package.jsonsrc
api
private
public
auth
auth-token-with-secret.dto.tsauth-token.dto.tsauth-token.entity.tsauth.module.tsauth.service.spec.tsauth.service.tsmock-auth.guard.tstoken-auth.guard.tstoken.strategy.ts
errors
groups
main.tsmedia
notes
permissions
revisions
users
auth-token.entity.tsidentity.entity.tssession.entity.tsuser.entity.tsusers.module.tsusers.service.ts
utils
test/public-api
yarn.lock
|
@ -28,7 +28,12 @@ entity "auth_token"{
|
|||
*id : number <<generated>>
|
||||
--
|
||||
*userId : uuid
|
||||
*keyId: text
|
||||
*accessToken : text
|
||||
*label: text
|
||||
*createdAt: date
|
||||
lastUsed: number
|
||||
validUntil: number
|
||||
}
|
||||
|
||||
entity "identity" {
|
||||
|
@ -131,7 +136,7 @@ entity "media_upload" {
|
|||
|
||||
user "1" -- "0..*" note: owner
|
||||
user "1" -u- "1..*" identity
|
||||
user "1" - "1..*" auth_token
|
||||
user "1" - "1..*" auth_token: authTokens
|
||||
user "1" -l- "1..*" session
|
||||
user "1" - "0..*" media_upload
|
||||
user "0..*" -- "0..*" note
|
||||
|
|
|
@ -27,15 +27,23 @@
|
|||
"@nestjs/common": "7.6.5",
|
||||
"@nestjs/config": "0.6.2",
|
||||
"@nestjs/core": "7.6.5",
|
||||
"@nestjs/passport": "^7.1.5",
|
||||
"@nestjs/platform-express": "7.6.5",
|
||||
"@nestjs/swagger": "4.7.12",
|
||||
"@nestjs/schedule": "^0.4.2",
|
||||
"@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-validator": "0.13.1",
|
||||
"cli-color": "2.0.0",
|
||||
"connect-typeorm": "1.1.4",
|
||||
"file-type": "16.2.0",
|
||||
"joi": "17.3.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"raw-body": "2.4.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"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 { Authorship } from '../../../revisions/authorship.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 { User } from '../../../users/user.entity';
|
||||
import { UsersModule } from '../../../users/users.module';
|
||||
|
|
|
@ -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,7 +24,10 @@ 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';
|
||||
import { ApiSecurity } from '@nestjs/swagger';
|
||||
|
||||
@ApiSecurity('token')
|
||||
@Controller('me')
|
||||
export class MeController {
|
||||
constructor(
|
||||
|
@ -34,29 +39,36 @@ export class MeController {
|
|||
this.logger.setContext(MeController.name);
|
||||
}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
@Get()
|
||||
async getMe(): Promise<UserInfoDto> {
|
||||
async getMe(@Request() req): Promise<UserInfoDto> {
|
||||
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 +76,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { NotesModule } from '../../../notes/notes.module';
|
|||
import { Tag } from '../../../notes/tag.entity';
|
||||
import { Authorship } from '../../../revisions/authorship.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 { User } from '../../../users/user.entity';
|
||||
import { MediaController } from './media.controller';
|
||||
|
|
|
@ -12,8 +12,10 @@ import {
|
|||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Request,
|
||||
UnauthorizedException,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
|
@ -25,7 +27,10 @@ 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';
|
||||
import { ApiSecurity } from '@nestjs/swagger';
|
||||
|
||||
@ApiSecurity('token')
|
||||
@Controller('media')
|
||||
export class MediaController {
|
||||
constructor(
|
||||
|
@ -35,14 +40,15 @@ 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 +70,10 @@ export class MediaController {
|
|||
}
|
||||
}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
@Delete(':filename')
|
||||
async deleteMedia(@Param('filename') filename: string) {
|
||||
//TODO: Get user from request
|
||||
const username = 'hardcoded';
|
||||
async deleteMedia(@Request() req, @Param('filename') filename: string) {
|
||||
const username = req.user.userName;
|
||||
try {
|
||||
await this.mediaService.deleteFile(filename, username);
|
||||
} catch (e) {
|
||||
|
|
|
@ -4,18 +4,23 @@
|
|||
* 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';
|
||||
import { ApiSecurity } from '@nestjs/swagger';
|
||||
|
||||
@ApiSecurity('token')
|
||||
@Controller('monitoring')
|
||||
export class MonitoringController {
|
||||
constructor(private monitoringService: MonitoringService) {}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
@Get()
|
||||
getStatus() {
|
||||
return this.monitoringService.getServerStatus();
|
||||
}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
@Get('prometheus')
|
||||
getPrometheusStatus() {
|
||||
return '';
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Tag } from '../../../notes/tag.entity';
|
|||
import { Authorship } from '../../../revisions/authorship.entity';
|
||||
import { Revision } from '../../../revisions/revision.entity';
|
||||
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 { User } from '../../../users/user.entity';
|
||||
import { UsersModule } from '../../../users/users.module';
|
||||
|
|
|
@ -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,7 +23,10 @@ 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';
|
||||
import { ApiSecurity } from '@nestjs/swagger';
|
||||
|
||||
@ApiSecurity('token')
|
||||
@Controller('notes')
|
||||
export class NotesController {
|
||||
constructor(
|
||||
|
@ -32,14 +37,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 +59,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 +91,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 +110,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 +128,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 +145,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 +166,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 +185,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,
|
||||
|
|
|
@ -18,12 +18,15 @@ 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';
|
||||
import cspConfig from './config/csp.config';
|
||||
import databaseConfig from './config/database.config';
|
||||
import authConfig from './config/auth.config';
|
||||
import { PrivateApiModule } from './api/private/private-api.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -44,17 +47,20 @@ import authConfig from './config/auth.config';
|
|||
],
|
||||
isGlobal: true,
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
NotesModule,
|
||||
UsersModule,
|
||||
RevisionsModule,
|
||||
AuthorsModule,
|
||||
PublicApiModule,
|
||||
PrivateApiModule,
|
||||
HistoryModule,
|
||||
MonitoringModule,
|
||||
PermissionsModule,
|
||||
GroupsModule,
|
||||
LoggerModule,
|
||||
MediaModule,
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [],
|
||||
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 {
|
||||
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
|
||||
*/
|
||||
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Group {
|
||||
|
|
|
@ -26,6 +26,10 @@ async function bootstrap() {
|
|||
const swaggerOptions = new DocumentBuilder()
|
||||
.setTitle('HedgeDoc')
|
||||
.setVersion('2.0-dev')
|
||||
.addSecurity('token', {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
})
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, swaggerOptions);
|
||||
SwaggerModule.setup('apidoc', app, document);
|
||||
|
|
|
@ -15,7 +15,7 @@ import { NotesModule } from '../notes/notes.module';
|
|||
import { Tag } from '../notes/tag.entity';
|
||||
import { Authorship } from '../revisions/authorship.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 { User } from '../users/user.entity';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 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 { Note } from './note.entity';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
|
|||
import { Authorship } from '../revisions/authorship.entity';
|
||||
import { Revision } from '../revisions/revision.entity';
|
||||
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 { User } from '../users/user.entity';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 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 { Note } from '../notes/note.entity';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 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 { User } from '../users/user.entity';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm/index';
|
||||
} from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
import { Revision } from './revision.entity';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 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';
|
||||
|
||||
export class RevisionMetadataDto {
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { JoinTable, ManyToMany } from 'typeorm/index';
|
||||
import { JoinTable, ManyToMany } from 'typeorm';
|
||||
import { Note } from '../notes/note.entity';
|
||||
import { Authorship } from './authorship.entity';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
|
|||
import { AuthorColor } from '../notes/author-color.entity';
|
||||
import { Note } from '../notes/note.entity';
|
||||
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 { User } from '../users/user.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,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm/index';
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity()
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { ISession } from 'connect-typeorm';
|
||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm/index';
|
||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Session implements ISession {
|
||||
|
|
|
@ -10,9 +10,9 @@ import {
|
|||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Column, OneToMany } from 'typeorm/index';
|
||||
import { Column, OneToMany } from 'typeorm';
|
||||
import { Note } from '../notes/note.entity';
|
||||
import { AuthToken } from './auth-token.entity';
|
||||
import { AuthToken } from '../auth/auth-token.entity';
|
||||
import { Identity } from './identity.entity';
|
||||
|
||||
@Entity()
|
||||
|
@ -46,7 +46,7 @@ export class User {
|
|||
ownedNotes: Note[];
|
||||
|
||||
@OneToMany((_) => AuthToken, (authToken) => authToken.user)
|
||||
authToken: AuthToken[];
|
||||
authTokens: AuthToken[];
|
||||
|
||||
@OneToMany((_) => Identity, (identity) => identity.user)
|
||||
identities: Identity[];
|
||||
|
@ -59,7 +59,7 @@ export class User {
|
|||
displayName: string,
|
||||
): Pick<
|
||||
User,
|
||||
'userName' | 'displayName' | 'ownedNotes' | 'authToken' | 'identities'
|
||||
'userName' | 'displayName' | 'ownedNotes' | 'authTokens' | 'identities'
|
||||
> {
|
||||
const newUser = new User();
|
||||
newUser.userName = userName;
|
||||
|
|
|
@ -7,16 +7,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { AuthToken } from './auth-token.entity';
|
||||
import { Identity } from './identity.entity';
|
||||
import { User } from './user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, AuthToken, Identity]),
|
||||
LoggerModule,
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([User, Identity]), LoggerModule],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
|
|
|
@ -27,16 +27,17 @@ export class UsersService {
|
|||
}
|
||||
|
||||
async deleteUser(userName: string) {
|
||||
//TOOD: Handle owned notes and edits
|
||||
// TODO: Handle owned notes and edits
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userName: userName },
|
||||
});
|
||||
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({
|
||||
where: { userName: userName },
|
||||
relations: withTokens ? ['authTokens'] : null,
|
||||
});
|
||||
if (user === undefined) {
|
||||
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 { PermissionsModule } from '../../src/permissions/permissions.module';
|
||||
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', () => {
|
||||
let app: NestExpressApplication;
|
||||
|
@ -46,8 +49,12 @@ describe('Notes', () => {
|
|||
PermissionsModule,
|
||||
GroupsModule,
|
||||
LoggerModule,
|
||||
AuthModule,
|
||||
],
|
||||
}).compile();
|
||||
})
|
||||
.overrideGuard(TokenAuthGuard)
|
||||
.useClass(MockAuthGuard)
|
||||
.compile();
|
||||
app = moduleRef.createNestApplication<NestExpressApplication>();
|
||||
app.useStaticAssets('uploads', {
|
||||
prefix: '/uploads',
|
||||
|
|
|
@ -17,6 +17,10 @@ import { LoggerModule } from '../../src/logger/logger.module';
|
|||
import { NotesModule } from '../../src/notes/notes.module';
|
||||
import { NotesService } from '../../src/notes/notes.service';
|
||||
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', () => {
|
||||
let app: INestApplication;
|
||||
|
@ -41,12 +45,18 @@ describe('Notes', () => {
|
|||
dropSchema: true,
|
||||
}),
|
||||
LoggerModule,
|
||||
AuthModule,
|
||||
],
|
||||
}).compile();
|
||||
})
|
||||
.overrideGuard(TokenAuthGuard)
|
||||
.useClass(MockAuthGuard)
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
notesService = moduleRef.get(NotesService);
|
||||
const usersService: UsersService = moduleRef.get('UsersService');
|
||||
await usersService.createUser('testy', 'Testy McTestFace');
|
||||
});
|
||||
|
||||
it(`POST /notes`, async () => {
|
||||
|
|
|
@ -8,7 +8,6 @@ import { INestApplication } from '@nestjs/common';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../../src/app.module';
|
||||
//import { UsersService } from '../../src/users/users.service';
|
||||
import { UserInfoDto } from '../../src/users/user-info.dto';
|
||||
import { HistoryService } from '../../src/history/history.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"
|
||||
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":
|
||||
version "7.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-7.6.5.tgz#7ee3df2c104aadac766af8b310bb4d04f4039d4a"
|
||||
|
@ -625,6 +630,14 @@
|
|||
multer "1.4.2"
|
||||
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":
|
||||
version "7.2.7"
|
||||
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"
|
||||
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@*":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||
|
@ -776,6 +796,11 @@
|
|||
dependencies:
|
||||
"@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@*":
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||
|
@ -791,11 +816,34 @@
|
|||
dependencies:
|
||||
"@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@*":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
|
||||
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":
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
|
||||
|
@ -865,6 +913,16 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "2.0.3"
|
||||
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"
|
||||
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":
|
||||
version "1.3.2"
|
||||
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"
|
||||
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":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03"
|
||||
|
@ -1627,6 +1727,14 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "5.2.2"
|
||||
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"
|
||||
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:
|
||||
version "6.0.5"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
|
||||
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
needle@^2.2.1:
|
||||
needle@^2.2.1, needle@^2.5.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
|
||||
integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
|
||||
|
@ -4911,6 +5038,22 @@ node-notifier@^8.0.0:
|
|||
uuid "^8.3.0"
|
||||
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:
|
||||
version "0.11.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "3.1.3"
|
||||
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"
|
||||
inherits "2"
|
||||
|
||||
tar@^4:
|
||||
tar@^4, tar@^4.4.2:
|
||||
version "4.4.13"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||
|
|
Loading…
Add table
Reference in a new issue