diff --git a/src/fe/scss/_authorize.scss b/src/fe/scss/_authorize.scss index da27b57..60f65b1 100644 --- a/src/fe/scss/_authorize.scss +++ b/src/fe/scss/_authorize.scss @@ -82,7 +82,7 @@ max-width: 400px; margin: auto; - .scopes__scope { + &__scope { display: flex; flex-direction: row; align-items: center; diff --git a/src/guards/privileges.guard.ts b/src/guards/privileges.guard.ts index 3535e23..0b89c65 100644 --- a/src/guards/privileges.guard.ts +++ b/src/guards/privileges.guard.ts @@ -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) => - user.privileges.find(({ name }) => name === item), - ) + return privileges.every((item) => + user.privileges.find(({ name }) => name === item), ); } } diff --git a/src/modules/api/admin/admin.module.ts b/src/modules/api/admin/admin.module.ts index 046815f..e69b044 100644 --- a/src/modules/api/admin/admin.module.ts +++ b/src/modules/api/admin/admin.module.ts @@ -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 {} diff --git a/src/modules/api/admin/admin.service.ts b/src/modules/api/admin/admin.service.ts new file mode 100644 index 0000000..6c235bb --- /dev/null +++ b/src/modules/api/admin/admin.service.ts @@ -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 { + return { + ...client, + owner: client.owner + ? this._form.pluckObject(client.owner, ['id', 'uuid', 'username']) + : null, + } as Partial; + } +} diff --git a/src/modules/api/admin/oauth2-admin.controller.ts b/src/modules/api/admin/oauth2-admin.controller.ts new file mode 100644 index 0000000..ce9ee06 --- /dev/null +++ b/src/modules/api/admin/oauth2-admin.controller.ts @@ -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, + ) { + 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, + @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); + } +} diff --git a/src/modules/api/admin/privilege-admin.controller.ts b/src/modules/api/admin/privilege-admin.controller.ts index 968578a..fcecc11 100644 --- a/src/modules/api/admin/privilege-admin.controller.ts +++ b/src/modules/api/admin/privilege-admin.controller.ts @@ -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') diff --git a/src/modules/api/admin/user-admin.controller.ts b/src/modules/api/admin/user-admin.controller.ts index beaf167..4d9a9e5 100644 --- a/src/modules/api/admin/user-admin.controller.ts +++ b/src/modules/api/admin/user-admin.controller.ts @@ -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 }; + } } diff --git a/src/modules/objects/oauth2-client/oauth2-client.module.ts b/src/modules/objects/oauth2-client/oauth2-client.module.ts index 54e04c3..ce538cb 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.module.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.module.ts @@ -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], }) diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index 3bd4066..45e30f9 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -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, @@ -17,6 +28,7 @@ export class OAuth2ClientService { private clientUrlRepository: Repository, @Inject('CLIENT_AUTHORIZATION_REPOSITORY') private clientAuthRepository: Repository, + private _upload: UploadService, ) {} public async hasAuthorized( @@ -94,7 +106,6 @@ export class OAuth2ClientService { public async revokeAuthorization( auth: OAuth2ClientAuthorization, ): Promise { - console.log(auth); return this.clientAuthRepository.remove(auth); } @@ -111,24 +122,67 @@ export class OAuth2ClientService { }); } - public async getById(id: string | number): Promise { + public async getById( + id: string | number, + relations = ['urls', 'picture'], + ): Promise { + 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 { + 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 { + return this.clientUrlRepository.findOne({ + where: { + id, + }, + relations: ['client'], + }); + } + public async checkRedirectURI(id: string, url: string): Promise { return !!(await this.clientUrlRepository.findOne({ where: { @@ -152,4 +215,43 @@ export class OAuth2ClientService { relations: ['client'], })); } + + public async updateClient(client: OAuth2Client): Promise { + await this.clientRepository.save(client); + return client; + } + + public async updateClientURL(url: OAuth2ClientURL): Promise { + await this.clientUrlRepository.save(url); + return url; + } + + public async deleteClientURL(url: OAuth2ClientURL): Promise { + await this.clientUrlRepository.remove(url); + } + + public async updatePicture( + client: OAuth2Client, + upload: Upload, + ): Promise { + if (client.picture) { + await this._upload.delete(client.picture); + } + + client.picture = upload; + await this.updateClient(client); + return client; + } + + public async wipeClientAuthorizations(client: OAuth2Client): Promise { + await this.clientAuthRepository.delete({ + client: { id: client.id }, + }); + } + + public async deletePicture(client: OAuth2Client): Promise { + if (client.picture) { + await this._upload.delete(client.picture); + } + } } diff --git a/src/modules/objects/oauth2-token/oauth2-token.service.ts b/src/modules/objects/oauth2-token/oauth2-token.service.ts index ba07465..f041d66 100644 --- a/src/modules/objects/oauth2-token/oauth2-token.service.ts +++ b/src/modules/objects/oauth2-token/oauth2-token.service.ts @@ -64,6 +64,12 @@ export class OAuth2TokenService { }); } + public async wipeClientTokens(client: OAuth2Client): Promise { + await this.tokenRepository.delete({ + client: { id: client.id }, + }); + } + public async remove(token: OAuth2Token): Promise { await this.tokenRepository.remove(token); } diff --git a/src/modules/objects/privilege/privilege.service.ts b/src/modules/objects/privilege/privilege.service.ts index ce99307..5488c55 100644 --- a/src/modules/objects/privilege/privilege.service.ts +++ b/src/modules/objects/privilege/privilege.service.ts @@ -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 { return this.privilegeRepository.findOne({ where: { id } }); } + + public async getByIDs(id: number[]): Promise { + return this.privilegeRepository.find({ where: { id: In(id) } }); + } } diff --git a/src/modules/objects/upload/upload.service.ts b/src/modules/objects/upload/upload.service.ts index 2f9eec4..fd37157 100644 --- a/src/modules/objects/upload/upload.service.ts +++ b/src/modules/objects/upload/upload.service.ts @@ -31,7 +31,7 @@ export class UploadService { } public async checkImageAspect(file: Express.Multer.File): Promise { - const opened = await readFile(file.path); + const opened = file.buffer || (await readFile(file.path)); return new Promise((resolve) => { const result = imageSize(opened); diff --git a/src/modules/objects/user/user.service.ts b/src/modules/objects/user/user.service.ts index 346c56a..7e02597 100644 --- a/src/modules/objects/user/user.service.ts +++ b/src/modules/objects/user/user.service.ts @@ -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 }); }