Skip to content

Commit

Permalink
private: adds tokens controller
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
DerMolly committed Jan 16, 2021
1 parent 9bc6867 commit 573927f
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 12 deletions.
15 changes: 15 additions & 0 deletions src/api/private/private-api.module.ts
@@ -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 {}
32 changes: 32 additions & 0 deletions src/api/private/tokens/tokens.controller.spec.ts
@@ -0,0 +1,32 @@
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();
});
});
48 changes: 48 additions & 0 deletions src/api/private/tokens/tokens.controller.ts
@@ -0,0 +1,48 @@
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);
}
}
4 changes: 4 additions & 0 deletions src/errors/errors.ts
Expand Up @@ -15,3 +15,7 @@ export class ClientError extends Error {
export class PermissionError extends Error {
name = 'PermissionError';
}

export class RandomnessError extends Error {
name = 'RandomnessError';
}
13 changes: 13 additions & 0 deletions src/users/auth-token-with-secret.dto.ts
@@ -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 changes: 14 additions & 0 deletions src/users/auth-token.dto.ts
@@ -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;
}
30 changes: 23 additions & 7 deletions src/users/auth-token.entity.ts
Expand Up @@ -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;
}
}
6 changes: 3 additions & 3 deletions src/users/user.entity.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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[];
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/users/users.service.spec.ts
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
79 changes: 77 additions & 2 deletions src/users/users.service.ts
Expand Up @@ -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);
}
Expand All @@ -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 },
});
Expand All @@ -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;
Expand All @@ -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');
Expand Down

0 comments on commit 573927f

Please sign in to comment.