mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04: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 {
|
export class PermissionError extends Error {
|
||||||
name = 'PermissionError';
|
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
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue