mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-04 22:47:11 +00:00
private: adds tokens controller
adds private api adds AuthTokenDto and AuthTokenWithSecretDto adds necessary methods in the users service adds RandomnessError Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
1c7452d066
commit
80c7ae2fa9
10 changed files with 248 additions and 12 deletions
15
src/api/private/private-api.module.ts
Normal file
15
src/api/private/private-api.module.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersModule } from '../../users/users.module';
|
||||
import { TokensController } from './tokens/tokens.controller';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
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 { UsersModule } from '../../../users/users.module';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Identity } from '../../../users/identity.entity';
|
||||
import { User } from '../../../users/user.entity';
|
||||
import { AuthToken } from '../../../users/auth-token.entity';
|
||||
|
||||
describe('TokensController', () => {
|
||||
let controller: TokensController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TokensController],
|
||||
imports: [LoggerModule, UsersModule],
|
||||
})
|
||||
.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();
|
||||
});
|
||||
});
|
54
src/api/private/tokens/tokens.controller.ts
Normal file
54
src/api/private/tokens/tokens.controller.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { UsersService } from '../../../users/users.service';
|
||||
import { AuthTokenDto } from '../../../users/auth-token.dto';
|
||||
import { AuthTokenWithSecretDto } from '../../../users/auth-token-with-secret.dto';
|
||||
|
||||
@Controller('tokens')
|
||||
export class TokensController {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
private usersService: UsersService,
|
||||
) {
|
||||
this.logger.setContext(TokensController.name);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async getUserTokens(): Promise<AuthTokenDto[]> {
|
||||
// ToDo: Get real userName
|
||||
return (await this.usersService.getTokensByUsername('molly')).map((token) =>
|
||||
this.usersService.toAuthTokenDto(token),
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async postToken(@Body() label: string): Promise<AuthTokenWithSecretDto> {
|
||||
// ToDo: Get real userName
|
||||
const authToken = await this.usersService.createTokenForUser(
|
||||
'hardcoded',
|
||||
label,
|
||||
);
|
||||
return this.usersService.toAuthTokenWithSecretDto(authToken);
|
||||
}
|
||||
|
||||
@Delete('/:timestamp')
|
||||
@HttpCode(204)
|
||||
async deleteToken(@Param('timestamp') timestamp: number) {
|
||||
// ToDo: Get real userName
|
||||
return this.usersService.removeToken('hardcoded', timestamp);
|
||||
}
|
||||
}
|
|
@ -15,3 +15,7 @@ export class ClientError extends Error {
|
|||
export class PermissionError extends Error {
|
||||
name = 'PermissionError';
|
||||
}
|
||||
|
||||
export class RandomnessError extends Error {
|
||||
name = 'RandomnessError';
|
||||
}
|
||||
|
|
13
src/users/auth-token-with-secret.dto.ts
Normal file
13
src/users/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;
|
||||
}
|
14
src/users/auth-token.dto.ts
Normal file
14
src/users/auth-token.dto.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IsNumber, IsString } from 'class-validator';
|
||||
|
||||
export class AuthTokenDto {
|
||||
@IsString()
|
||||
label: string;
|
||||
@IsNumber()
|
||||
created: number;
|
||||
}
|
|
@ -4,22 +4,38 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm/index';
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@Entity()
|
||||
export class AuthToken {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ManyToOne((_) => User, (user) => user.authToken)
|
||||
@ManyToOne((_) => User, (user) => user.authTokens)
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
identifier: string;
|
||||
|
||||
@Type(() => Date)
|
||||
@Column('text')
|
||||
createdAt: Date;
|
||||
|
||||
@Column()
|
||||
accessToken: string;
|
||||
|
||||
public static create(
|
||||
user: User,
|
||||
identifier: string,
|
||||
accessToken: string,
|
||||
): Pick<AuthToken, 'user' | 'accessToken'> {
|
||||
const newToken = new AuthToken();
|
||||
newToken.user = user;
|
||||
newToken.identifier = identifier;
|
||||
newToken.accessToken = accessToken;
|
||||
newToken.createdAt = new Date();
|
||||
return newToken;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ 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 { Identity } from './identity.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;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { User } from './user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { AuthToken } from './auth-token.entity';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
|
@ -21,11 +22,17 @@ describe('UsersService', () => {
|
|||
provide: getRepositoryToken(User),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AuthToken),
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
imports: [LoggerModule],
|
||||
})
|
||||
.overrideProvider(getRepositoryToken(User))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(AuthToken))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
|
|
|
@ -7,16 +7,22 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotInDBError } from '../errors/errors';
|
||||
import { NotInDBError, RandomnessError } from '../errors/errors';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { UserInfoDto } from './user-info.dto';
|
||||
import { User } from './user.entity';
|
||||
import { AuthToken } from './auth-token.entity';
|
||||
import crypt from 'crypto';
|
||||
import { AuthTokenDto } from './auth-token.dto';
|
||||
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@InjectRepository(User) private userRepository: Repository<User>,
|
||||
@InjectRepository(AuthToken)
|
||||
private authTokenRepository: Repository<AuthToken>,
|
||||
) {
|
||||
this.logger.setContext(UsersService.name);
|
||||
}
|
||||
|
@ -26,8 +32,29 @@ export class UsersService {
|
|||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async createTokenForUser(
|
||||
userName: string,
|
||||
identifier: string,
|
||||
): Promise<AuthToken> {
|
||||
const user = await this.getUserByUsername(userName);
|
||||
let accessToken = '';
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
accessToken = crypt.randomBytes(64).toString();
|
||||
await this.getUserByAuthToken(accessToken);
|
||||
} catch (NotInDBError) {
|
||||
const token = AuthToken.create(user, identifier, accessToken);
|
||||
return this.authTokenRepository.save(token);
|
||||
}
|
||||
}
|
||||
// This should never happen
|
||||
throw new RandomnessError(
|
||||
'You machine is not able to generate not-in-use tokens. This should never happen.',
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
@ -44,6 +71,16 @@ export class UsersService {
|
|||
return user;
|
||||
}
|
||||
|
||||
async getUserByAuthToken(token: string): Promise<User> {
|
||||
const accessToken = await this.authTokenRepository.findOne({
|
||||
where: { accessToken: token },
|
||||
});
|
||||
if (accessToken === undefined) {
|
||||
throw new NotInDBError(`AuthToken '${token}' not found`);
|
||||
}
|
||||
return this.getUserByUsername(accessToken.user.userName);
|
||||
}
|
||||
|
||||
getPhotoUrl(user: User): string {
|
||||
if (user.photo) {
|
||||
return user.photo;
|
||||
|
@ -53,6 +90,44 @@ export class UsersService {
|
|||
}
|
||||
}
|
||||
|
||||
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
|
||||
const user = await this.getUserByUsername(userName);
|
||||
return user.authTokens;
|
||||
}
|
||||
|
||||
async removeToken(userName: string, timestamp: number) {
|
||||
const user = await this.getUserByUsername(userName);
|
||||
const token = await this.authTokenRepository.findOne({
|
||||
where: { createdAt: new Date(timestamp), user: user },
|
||||
});
|
||||
await this.authTokenRepository.remove(token);
|
||||
}
|
||||
|
||||
toAuthTokenDto(authToken: AuthToken | null | undefined): AuthTokenDto | null {
|
||||
if (!authToken) {
|
||||
this.logger.warn(`Recieved ${authToken} argument!`, 'toAuthTokenDto');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: authToken.identifier,
|
||||
created: authToken.createdAt.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
toAuthTokenWithSecretDto(
|
||||
authToken: AuthToken | null | undefined,
|
||||
): AuthTokenWithSecretDto | null {
|
||||
if (!authToken) {
|
||||
this.logger.warn(`Recieved ${authToken} argument!`, 'toAuthTokenDto');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: authToken.identifier,
|
||||
created: authToken.createdAt.getTime(),
|
||||
secret: authToken.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
toUserDto(user: User | null | undefined): UserInfoDto | null {
|
||||
if (!user) {
|
||||
this.logger.warn(`Recieved ${user} argument!`, 'toUserDto');
|
||||
|
|
Loading…
Reference in a new issue