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:
Philip Molares 2021-01-16 23:53:46 +01:00 committed by David Mehren
parent 1c7452d066
commit 80c7ae2fa9
No known key found for this signature in database
GPG key ID: 185982BA4C42B7C3
10 changed files with 248 additions and 12 deletions

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

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

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

View file

@ -15,3 +15,7 @@ export class ClientError extends Error {
export class PermissionError extends Error { export class PermissionError extends Error {
name = 'PermissionError'; name = 'PermissionError';
} }
export class RandomnessError extends Error {
name = 'RandomnessError';
}

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

View file

@ -4,22 +4,38 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
Column,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm/index';
import { User } from './user.entity'; import { User } from './user.entity';
import { Type } from 'class-transformer';
@Entity() @Entity()
export class AuthToken { export class AuthToken {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ManyToOne((_) => User, (user) => user.authToken) @ManyToOne((_) => User, (user) => user.authTokens)
user: User; user: User;
@Column()
identifier: string;
@Type(() => Date)
@Column('text')
createdAt: Date;
@Column() @Column()
accessToken: string; 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;
}
} }

View file

@ -10,7 +10,7 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { Column, OneToMany } from 'typeorm/index'; import { Column, OneToMany } from 'typeorm';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { AuthToken } from './auth-token.entity'; import { AuthToken } from './auth-token.entity';
import { Identity } from './identity.entity'; import { Identity } from './identity.entity';
@ -46,7 +46,7 @@ export class User {
ownedNotes: Note[]; ownedNotes: Note[];
@OneToMany((_) => AuthToken, (authToken) => authToken.user) @OneToMany((_) => AuthToken, (authToken) => authToken.user)
authToken: AuthToken[]; authTokens: AuthToken[];
@OneToMany((_) => Identity, (identity) => identity.user) @OneToMany((_) => Identity, (identity) => identity.user)
identities: Identity[]; identities: Identity[];
@ -59,7 +59,7 @@ export class User {
displayName: string, displayName: string,
): Pick< ): Pick<
User, User,
'userName' | 'displayName' | 'ownedNotes' | 'authToken' | 'identities' 'userName' | 'displayName' | 'ownedNotes' | 'authTokens' | 'identities'
> { > {
const newUser = new User(); const newUser = new User();
newUser.userName = userName; newUser.userName = userName;

View file

@ -9,6 +9,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { User } from './user.entity'; import { User } from './user.entity';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { AuthToken } from './auth-token.entity';
describe('UsersService', () => { describe('UsersService', () => {
let service: UsersService; let service: UsersService;
@ -21,11 +22,17 @@ describe('UsersService', () => {
provide: getRepositoryToken(User), provide: getRepositoryToken(User),
useValue: {}, useValue: {},
}, },
{
provide: getRepositoryToken(AuthToken),
useValue: {},
},
], ],
imports: [LoggerModule], imports: [LoggerModule],
}) })
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
.useValue({})
.compile(); .compile();
service = module.get<UsersService>(UsersService); service = module.get<UsersService>(UsersService);

View file

@ -7,16 +7,22 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { NotInDBError } from '../errors/errors'; import { NotInDBError, RandomnessError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UserInfoDto } from './user-info.dto'; import { UserInfoDto } from './user-info.dto';
import { User } from './user.entity'; 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() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
@InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(AuthToken)
private authTokenRepository: Repository<AuthToken>,
) { ) {
this.logger.setContext(UsersService.name); this.logger.setContext(UsersService.name);
} }
@ -26,8 +32,29 @@ export class UsersService {
return this.userRepository.save(user); 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) { async deleteUser(userName: string) {
//TOOD: Handle owned notes and edits // TODO: Handle owned notes and edits
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userName: userName }, where: { userName: userName },
}); });
@ -44,6 +71,16 @@ export class UsersService {
return user; 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 { getPhotoUrl(user: User): string {
if (user.photo) { if (user.photo) {
return 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 { toUserDto(user: User | null | undefined): UserInfoDto | null {
if (!user) { if (!user) {
this.logger.warn(`Recieved ${user} argument!`, 'toUserDto'); this.logger.warn(`Recieved ${user} argument!`, 'toUserDto');