Merge pull request #738 from hedgedoc/private/tokens

This commit is contained in:
Yannick Bungers 2021-01-25 21:38:53 +01:00 committed by GitHub
commit ca04856425
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1056 additions and 77 deletions

View file

@ -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

View file

@ -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",

View 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 {}

View 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();
});
});

View 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);
}
}

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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) {

View file

@ -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 '';

View file

@ -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';

View file

@ -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,

View file

@ -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: [],

View 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;
}

View 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;
}

View 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
View 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 {}

View 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
View 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.`,
);
}
}

View 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;
}
}

View 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') {}

View 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;
}
}

View file

@ -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';
}

View file

@ -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 {

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -12,7 +12,7 @@ import {
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm/index';
} from 'typeorm';
import { User } from '../users/user.entity';
import { Revision } from './revision.entity';

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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;
}

View file

@ -11,7 +11,7 @@ import {
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm/index';
} from 'typeorm';
import { User } from './user.entity';
@Entity()

View file

@ -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 {

View file

@ -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;

View file

@ -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],
})

View file

@ -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
View 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;

View file

@ -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',

View file

@ -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 () => {

View file

@ -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
View file

@ -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==