comprehensive oauth2 admin api

This commit is contained in:
Evert Prants 2022-08-27 18:52:37 +03:00
parent 13eefb166a
commit e884ce80fc
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
13 changed files with 705 additions and 25 deletions

View File

@ -82,7 +82,7 @@
max-width: 400px;
margin: auto;
.scopes__scope {
&__scope {
display: flex;
flex-direction: row;
align-items: center;

View File

@ -18,11 +18,8 @@ export class PrivilegesGuard implements CanActivate {
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return (
user.privileges.includes('*') ||
privileges.every((item) =>
return privileges.every((item) =>
user.privileges.find(({ name }) => name === item),
)
);
}
}

View File

@ -1,11 +1,59 @@
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import * as multer from 'multer';
import * as mime from 'mime-types';
import { join } from 'path';
import { ConfigurationService } from '../../config/config.service';
import { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
import { ObjectsModule } from 'src/modules/objects/objects.module';
import { OAuth2AdminController } from './oauth2-admin.controller';
import { PrivilegeAdminController } from './privilege-admin.controller';
import { UserAdminController } from './user-admin.controller';
import { ConfigurationModule } from 'src/modules/config/config.module';
import { AdminService } from './admin.service';
@Module({
controllers: [UserAdminController, PrivilegeAdminController],
imports: [ObjectsModule, OAuth2Module],
controllers: [
UserAdminController,
PrivilegeAdminController,
OAuth2AdminController,
],
imports: [
ObjectsModule,
OAuth2Module,
MulterModule.registerAsync({
imports: [ConfigurationModule],
useFactory: async (config: ConfigurationService) => {
return {
storage: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, join(__dirname, '..', '..', '..', '..', 'uploads'));
},
filename: (req, file, cb) => {
const hashTruncate = req.user.uuid.split('-')[0];
const timestamp = Math.floor(Date.now() / 1000);
const ext = mime.extension(file.mimetype);
cb(null, `app-${hashTruncate}-${timestamp}.${ext}`);
},
}),
limits: {
fileSize: 1.049e7, // 10 MiB
},
fileFilter: (req, file, cb) => {
if (
!file.mimetype.startsWith('image/') ||
file.mimetype.includes('svg')
) {
return cb(new Error('Invalid file type.'), false);
}
cb(null, true);
},
};
},
inject: [ConfigurationService],
}),
],
providers: [AdminService],
})
export class AdminApiModule {}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { OAuth2Client } from 'src/modules/objects/oauth2-client/oauth2-client.entity';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
@Injectable()
export class AdminService {
constructor(private _form: FormUtilityService) {}
public stripClientInfo(client: OAuth2Client): Partial<OAuth2Client> {
return {
...client,
owner: client.owner
? this._form.pluckObject(client.owner, ['id', 'uuid', 'username'])
: null,
} as Partial<OAuth2Client>;
}
}

View File

@ -0,0 +1,378 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
Patch,
Post,
Put,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { unlink } from 'fs/promises';
import { Privileges } from 'src/decorators/privileges.decorator';
import { Scopes } from 'src/decorators/scopes.decorator';
import { CurrentUser } from 'src/decorators/user.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { PrivilegesGuard } from 'src/guards/privileges.guard';
import { ScopesGuard } from 'src/guards/scopes.guard';
import { OAuth2Client } from 'src/modules/objects/oauth2-client/oauth2-client.entity';
import {
OAuth2ClientURL,
OAuth2ClientURLType,
} from 'src/modules/objects/oauth2-client/oauth2-client-url.entity';
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
import { UploadService } from 'src/modules/objects/upload/upload.service';
import { User } from 'src/modules/objects/user/user.entity';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { PaginationService } from 'src/modules/utility/services/paginate.service';
import { TokenService } from 'src/modules/utility/services/token.service';
import { PageOptions } from 'src/types/pagination.interfaces';
import { AdminService } from './admin.service';
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
const RELATIONS = ['urls', 'picture', 'owner'];
const SET_CLIENT_FIELDS = [
'title',
'description',
'scope',
'grants',
'activated',
'verified',
];
const URL_TYPES = ['redirect_uri', 'terms', 'privacy', 'website'];
const REQUIRED_CLIENT_FIELDS = ['title', 'scope', 'grants', 'activated'];
@Controller('/api/admin/oauth2')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class OAuth2AdminController {
constructor(
private _oaClient: OAuth2ClientService,
private _oaToken: OAuth2TokenService,
private _service: AdminService,
private _paginate: PaginationService,
private _form: FormUtilityService,
private _token: TokenService,
private _upload: UploadService,
) {}
@Get('scopes')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async oauth2Scopes() {
return this._oaClient.availableScopes;
}
@Get('grants')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async oauth2Grants() {
return this._oaClient.availableGrantTypes;
}
@Get('clients')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async oauth2ClientList(@Query() options: { q?: string } & PageOptions) {
const search = options.q ? decodeURIComponent(options.q) : null;
const resultCount = await this._oaClient.searchClientsCount(
search,
RELATIONS,
);
const pagination = this._paginate.paginate(options, resultCount);
const [list] = await this._oaClient.searchClients(
pagination.pageSize,
pagination.offset,
search,
RELATIONS,
);
return {
pagination,
list: this._form.stripObjectArray(
list.map((item) => this._service.stripClientInfo(item)),
['password'],
),
};
}
@Get('clients/:id')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async oauth2Client(@Param('id') id: string) {
const client = await this._oaClient.getById(parseInt(id, 10), RELATIONS);
if (!client) {
throw new NotFoundException('Client not found');
}
return this._service.stripClientInfo(client);
}
@Patch('clients/:id')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async updateOauth2Client(
@Param('id') id: string,
@Body() setter: Partial<OAuth2Client>,
) {
const client = await this._oaClient.getById(parseInt(id, 10), []);
if (!client) {
throw new NotFoundException('Client not found');
}
const allowedFieldsOnly = this._form.pluckObject(setter, SET_CLIENT_FIELDS);
if (!Object.keys(allowedFieldsOnly).length) {
return this._service.stripClientInfo(client);
}
Object.assign(client, allowedFieldsOnly);
await this._oaClient.updateClient(client);
return this._service.stripClientInfo(client);
}
@Post('clients/:id/new-secret')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async createNewSecret(@Param('id') id: string) {
const client = await this._oaClient.getById(parseInt(id, 10), []);
if (!client) {
throw new NotFoundException('Client not found');
}
client.client_secret = this._token.generateSecret();
await this._oaClient.updateClient(client);
// security
await this._oaToken.wipeClientTokens(client);
await this._oaClient.wipeClientAuthorizations(client);
return this._service.stripClientInfo(client);
}
@Delete('clients/:id/authorizations')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async deleteClientAuthorizations(@Param('id') id: string) {
const client = await this._oaClient.getById(parseInt(id, 10), []);
if (!client) {
throw new NotFoundException('Client not found');
}
await this._oaClient.wipeClientAuthorizations(client);
return this._service.stripClientInfo(client);
}
@Get('clients/:id/urls')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async oauth2ClientURLs(@Param('id') id: string) {
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
if (!client) {
throw new NotFoundException('Client not found');
}
return client.urls;
}
@Delete('clients/:id/urls/:url')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async deleteOAuth2ClientURL(
@Param('id') id: string,
@Param('url') urlId: string,
) {
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
const parsedURLId = parseInt(urlId, 10);
if (!client) {
throw new NotFoundException('Client not found');
}
if (!parsedURLId) {
throw new BadRequestException('Invalid URL ID');
}
const url = await this._oaClient.getClientURLById(parsedURLId);
if (!url) {
throw new BadRequestException('Invalid URL');
}
client.urls = client.urls.filter((url) => url.id !== parsedURLId);
await this._oaClient.deleteClientURL(url);
return client;
}
@Put('clients/:id/urls/:url')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async updateOAuth2ClientURL(
@Param('id') id: string,
@Param('url') urlId: string,
@Body() setter: { url: string; type: string },
) {
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
const plucked = this._form.pluckObject(setter, ['url', 'type']);
const parsedURLId = parseInt(urlId, 10);
if (!client) {
throw new NotFoundException('Client not found');
}
if (!parsedURLId) {
throw new BadRequestException('Invalid URL ID');
}
if (!setter.url || !setter.type || !URL_TYPES.includes(setter.type)) {
throw new NotFoundException('Missing or invalid fields');
}
const url = await this._oaClient.getClientURLById(parsedURLId);
if (!url) {
throw new BadRequestException('Invalid URL');
}
Object.assign(url, plucked);
await this._oaClient.updateClientURL(url);
return url;
}
@Post('clients/:id/urls')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async createOAuth2ClientURL(
@Param('id') id: string,
@Body() setter: { url: string; type: string },
) {
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
if (!client) {
throw new NotFoundException('Client not found');
}
if (!setter.url || !setter.type || !URL_TYPES.includes(setter.type)) {
throw new NotFoundException('Missing or invalid fields');
}
const url = new OAuth2ClientURL();
url.client = client;
url.type = setter.type as OAuth2ClientURLType;
url.url = setter.url;
await this._oaClient.updateClientURL(url);
return url;
}
@Post('clients/:id/picture')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
@UseInterceptors(FileInterceptor('file'))
async uploadClientPictureFile(
@CurrentUser() user: User,
@Param('id') id: string,
@UploadedFile() file: Express.Multer.File,
) {
const client = await this._oaClient.getById(parseInt(id, 10), ['picture']);
try {
if (!client) {
throw new NotFoundException('Client not found');
}
if (!file) {
throw new BadRequestException('Picture upload failed');
}
const matches = await this._upload.checkImageAspect(file);
if (!matches) {
throw new BadRequestException(
'Picture should be with a 1:1 aspect ratio.',
);
}
const upload = await this._upload.registerUploadedFile(file, user);
await this._oaClient.updatePicture(client, upload);
return {
file: upload.file,
};
} catch (e) {
if (!file.buffer) {
await unlink(file.path);
}
throw e;
}
}
@Delete('clients/:id/picture')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async deleteClientPictureFile(@Param('id') id: string) {
const client = await this._oaClient.getById(parseInt(id, 10), ['picture']);
if (!client) {
throw new NotFoundException('Client not found');
}
this._oaClient.deletePicture(client);
client.picture = null;
await this._oaClient.updateClient(client);
return this._service.stripClientInfo(client);
}
// New client
@Post('/clients')
@Scopes('management')
@Privileges('admin', 'admin:oauth2')
async createNewClient(
@Body() setter: Partial<OAuth2Client>,
@CurrentUser() user: User,
) {
const allowedFieldsOnly = this._form.pluckObject(setter, SET_CLIENT_FIELDS);
if (!Object.keys(allowedFieldsOnly).length) {
throw new BadRequestException('Required fields are missing');
}
if (REQUIRED_CLIENT_FIELDS.some((field) => setter[field] === undefined)) {
throw new BadRequestException('Required fields are missing');
}
const splitGrants = allowedFieldsOnly.grants.split(' ');
const splitScopes = allowedFieldsOnly.scope.split(' ');
if (
!splitGrants.every((grant) =>
this._oaClient.availableGrantTypes.includes(grant),
)
) {
throw new BadRequestException('Bad grant types');
}
if (
!splitScopes.every((scope) =>
this._oaClient.availableScopes.includes(scope),
)
) {
throw new BadRequestException('Bad scopes');
}
const client = new OAuth2Client();
Object.assign(client, allowedFieldsOnly);
client.client_id = this._token.createUUID();
client.client_secret = this._token.generateSecret();
client.owner = user;
await this._oaClient.updateClient(client);
return this._service.stripClientInfo(client);
}
}

View File

@ -12,17 +12,11 @@ import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { PrivilegesGuard } from 'src/guards/privileges.guard';
import { ScopesGuard } from 'src/guards/scopes.guard';
import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { PaginationService } from 'src/modules/utility/services/paginate.service';
@Controller('/api/admin/privileges')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class PrivilegeAdminController {
constructor(
private _privilege: PrivilegeService,
private _paginate: PaginationService,
private _form: FormUtilityService,
) {}
constructor(private _privilege: PrivilegeService) {}
@Get('')
@Scopes('management')

View File

@ -1,8 +1,13 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
@ -11,6 +16,7 @@ import { Scopes } from 'src/decorators/scopes.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { PrivilegesGuard } from 'src/guards/privileges.guard';
import { ScopesGuard } from 'src/guards/scopes.guard';
import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service';
import { UserService } from 'src/modules/objects/user/user.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { PaginationService } from 'src/modules/utility/services/paginate.service';
@ -23,10 +29,16 @@ const RELATIONS = ['picture', 'privileges'];
export class UserAdminController {
constructor(
private _user: UserService,
private _privilege: PrivilegeService,
private _paginate: PaginationService,
private _form: FormUtilityService,
) {}
/**
* Get a list of all users or search for a specific user
* @param options Search and pagination options
* @returns Paginated user list
*/
@Get('')
@Scopes('management')
@Privileges('admin', 'admin:user')
@ -49,6 +61,11 @@ export class UserAdminController {
};
}
/**
* Get a single user by ID
* @param id User ID
* @returns User
*/
@Get(':id')
@Scopes('management')
@Privileges('admin', 'admin:user')
@ -60,6 +77,34 @@ export class UserAdminController {
return this._form.stripObject(user, ['password']);
}
/**
* Delete a user avatar from the server
* @param id User ID
* @returns Success
*/
@Delete(':id/avatar')
@Scopes('management')
@Privileges('admin', 'admin:user')
async deleteUserAvatar(@Param('id') id: string) {
const user = await this._user.getById(parseInt(id, 10), ['picture']);
if (!user) {
throw new NotFoundException('User not found');
}
if (user.picture) {
await this._user.deleteAvatar(user);
user.picture = null;
await this._user.updateUser(user);
}
return { success: true };
}
/**
* Unpaginated list of all privileges for user
* @param id User ID
* @returns Privilege list
*/
@Get(':id/privileges')
@Scopes('management')
@Privileges('admin', 'admin:user')
@ -70,4 +115,92 @@ export class UserAdminController {
}
return user.privileges;
}
/**
* Replace user's privileges with the new list
* @param id User ID
* @param body With `privileges`, list of privilege IDs the user should have
* @returns New privileges array
*/
@Put(':id/privileges')
@Scopes('management')
@Privileges('admin', 'admin:user')
async setUserPrivileges(
@Param('id') id: string,
@Body() body: { privileges: number[] },
) {
const user = await this._user.getById(parseInt(id, 10), ['privileges']);
if (!user) {
throw new NotFoundException('User not found');
}
if (!body.privileges) {
throw new BadRequestException('Privileges are required.');
}
if (body.privileges.length) {
const privileges = await this._privilege.getByIDs(body.privileges);
if (!privileges?.length) {
throw new BadRequestException('Privileges not found.');
}
user.privileges = privileges;
} else {
user.privileges.length = 0;
}
await this._user.updateUser(user);
return user.privileges;
}
/**
* Resend activation email to a user
* @param id User ID
* @returns Success or error
*/
@Post(':id/activate')
@Scopes('management')
@Privileges('admin', 'admin:user')
async activateUserEmail(@Param('id') id: string) {
const user = await this._user.getById(parseInt(id, 10));
if (!user) {
throw new NotFoundException('User not found');
}
let error: Error;
try {
await this._user.sendActivationEmail(user);
} catch (e: any) {
error = e as Error;
}
return { success: !error, error: error?.name, message: error?.message };
}
/**
* Send password reset email to a user
* @param id User ID
* @returns Success or error
*/
@Post(':id/password')
@Scopes('management')
@Privileges('admin', 'admin:user')
async resetPasswordEmail(@Param('id') id: string) {
const user = await this._user.getById(parseInt(id, 10));
if (!user) {
throw new NotFoundException('User not found');
}
let error: Error;
try {
await this._user.sendPasswordEmail(user);
} catch (e: any) {
error = e as Error;
}
return { success: !error, error: error?.name, message: error?.message };
}
}

View File

@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UploadModule } from '../upload/upload.module';
import { clientProviders } from './oauth2-client.providers';
import { OAuth2ClientService } from './oauth2-client.service';
@Module({
imports: [DatabaseModule],
imports: [DatabaseModule, UploadModule],
providers: [...clientProviders, OAuth2ClientService],
exports: [OAuth2ClientService],
})

View File

@ -1,5 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { ILike, Repository } from 'typeorm';
import { Upload } from '../upload/upload.entity';
import { UploadService } from '../upload/upload.service';
import { User } from '../user/user.entity';
import { OAuth2ClientAuthorization } from './oauth2-client-authorization.entity';
import {
@ -10,6 +12,15 @@ import { OAuth2Client } from './oauth2-client.entity';
@Injectable()
export class OAuth2ClientService {
public availableGrantTypes = [
'authorization_code',
'refresh_token',
'id_token',
'implicit',
];
public availableScopes = ['image', 'email', 'privileges', 'management'];
constructor(
@Inject('CLIENT_REPOSITORY')
private clientRepository: Repository<OAuth2Client>,
@ -17,6 +28,7 @@ export class OAuth2ClientService {
private clientUrlRepository: Repository<OAuth2ClientURL>,
@Inject('CLIENT_AUTHORIZATION_REPOSITORY')
private clientAuthRepository: Repository<OAuth2ClientAuthorization>,
private _upload: UploadService,
) {}
public async hasAuthorized(
@ -94,7 +106,6 @@ export class OAuth2ClientService {
public async revokeAuthorization(
auth: OAuth2ClientAuthorization,
): Promise<OAuth2ClientAuthorization> {
console.log(auth);
return this.clientAuthRepository.remove(auth);
}
@ -111,24 +122,67 @@ export class OAuth2ClientService {
});
}
public async getById(id: string | number): Promise<OAuth2Client> {
public async getById(
id: string | number,
relations = ['urls', 'picture'],
): Promise<OAuth2Client> {
if (!id) {
return null;
}
let client: OAuth2Client;
if (typeof id === 'string') {
client = await this.clientRepository.findOne({
where: { client_id: id },
relations: ['urls', 'picture'],
relations,
});
} else {
client = await this.clientRepository.findOne({
where: { id },
relations: ['urls', 'picture'],
relations,
});
}
return client;
}
public async searchClients(
limit = 50,
offset = 0,
search?: string,
relations?: string[],
): Promise<[OAuth2Client[], number]> {
return this.clientRepository.findAndCount({
where: search
? [
{
title: ILike(`%${search}%`),
},
]
: undefined,
skip: offset,
take: limit,
relations,
});
}
public async searchClientsCount(
search?: string,
relations?: string[],
): Promise<number> {
return this.clientRepository.count({
where: search
? [
{
title: ILike(`%${search}%`),
},
]
: undefined,
relations,
});
}
public async getClientURLs(
id: string,
type?: OAuth2ClientURLType,
@ -142,6 +196,15 @@ export class OAuth2ClientService {
});
}
public async getClientURLById(id: number): Promise<OAuth2ClientURL> {
return this.clientUrlRepository.findOne({
where: {
id,
},
relations: ['client'],
});
}
public async checkRedirectURI(id: string, url: string): Promise<boolean> {
return !!(await this.clientUrlRepository.findOne({
where: {
@ -152,4 +215,43 @@ export class OAuth2ClientService {
relations: ['client'],
}));
}
public async updateClient(client: OAuth2Client): Promise<OAuth2Client> {
await this.clientRepository.save(client);
return client;
}
public async updateClientURL(url: OAuth2ClientURL): Promise<OAuth2ClientURL> {
await this.clientUrlRepository.save(url);
return url;
}
public async deleteClientURL(url: OAuth2ClientURL): Promise<void> {
await this.clientUrlRepository.remove(url);
}
public async updatePicture(
client: OAuth2Client,
upload: Upload,
): Promise<OAuth2Client> {
if (client.picture) {
await this._upload.delete(client.picture);
}
client.picture = upload;
await this.updateClient(client);
return client;
}
public async wipeClientAuthorizations(client: OAuth2Client): Promise<void> {
await this.clientAuthRepository.delete({
client: { id: client.id },
});
}
public async deletePicture(client: OAuth2Client): Promise<void> {
if (client.picture) {
await this._upload.delete(client.picture);
}
}
}

View File

@ -64,6 +64,12 @@ export class OAuth2TokenService {
});
}
public async wipeClientTokens(client: OAuth2Client): Promise<void> {
await this.tokenRepository.delete({
client: { id: client.id },
});
}
public async remove(token: OAuth2Token): Promise<void> {
await this.tokenRepository.remove(token);
}

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { Privilege } from './privilege.entity';
@Injectable()
@ -27,4 +27,8 @@ export class PrivilegeService {
public async getByID(id: number): Promise<Privilege> {
return this.privilegeRepository.findOne({ where: { id } });
}
public async getByIDs(id: number[]): Promise<Privilege[]> {
return this.privilegeRepository.find({ where: { id: In(id) } });
}
}

View File

@ -31,7 +31,7 @@ export class UploadService {
}
public async checkImageAspect(file: Express.Multer.File): Promise<boolean> {
const opened = await readFile(file.path);
const opened = file.buffer || (await readFile(file.path));
return new Promise((resolve) => {
const result = imageSize(opened);

View File

@ -1,9 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { ILike, Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { UserTokenType } from '../user-token/user-token.entity';
import { User } from './user.entity';
import { TokenService } from 'src/modules/utility/services/token.service';
import * as bcrypt from 'bcrypt';
import { EmailService } from '../email/email.service';
import { RegistrationEmail } from './email/registration.email';
import { ForgotPasswordEmail } from './email/forgot-password.email';
@ -28,7 +29,6 @@ export class UserService {
if (!id) {
return null;
}
return this.userRepository.findOne({ where: { id }, relations });
}