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