import { Inject, Injectable } from '@nestjs/common'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; 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 { OAuth2ClientURL, OAuth2ClientURLType, } from './oauth2-client-url.entity'; import { OAuth2Client } from './oauth2-client.entity'; @Injectable() export class OAuth2ClientService { public availableGrantTypes = [ 'authorization_code', 'refresh_token', 'id_token', 'implicit', ]; public availableScopes = [ 'picture', 'profile', 'email', 'privileges', 'management', 'openid', ]; constructor( @Inject('CLIENT_REPOSITORY') private clientRepository: Repository, @Inject('CLIENT_URL_REPOSITORY') private clientUrlRepository: Repository, @Inject('CLIENT_AUTHORIZATION_REPOSITORY') private clientAuthRepository: Repository, private _upload: UploadService, private _form: FormUtilityService, ) {} public async hasAuthorized( userId: number, clientId: string, scope: string[], ): Promise { const authorization = await this.clientAuthRepository.findOne({ where: { user: { id: userId, }, client: { client_id: clientId, }, }, relations: ['user', 'client'], }); if (!authorization) { return false; } // Scopes must have been allowed const splitScope = authorization.scope.split(' '); if (scope.every((item) => splitScope.includes(item))) { return true; } return false; } public async createAuthorization( user: User, client: OAuth2Client, scope: string[], ): Promise { const existing = await this.clientAuthRepository.findOne({ where: { user: { id: user.id }, client: { id: client.id }, }, relations: ['user', 'client'], }); if (existing) { const splitScope = existing.scope.split(' '); scope.forEach((item) => { if (!splitScope.includes(item)) { splitScope.push(item); } }); existing.scope = splitScope.join(' '); await this.clientAuthRepository.save(existing); return existing; } const authorization = new OAuth2ClientAuthorization(); authorization.user = user; authorization.client = client; authorization.scope = scope.join(' '); await this.clientAuthRepository.insert(authorization); return authorization; } public async getAuthorizations( user: User, ): Promise { return this.clientAuthRepository.find({ relations: ['user', 'client', 'client.urls'], where: { user: { id: user.id } }, }); } public async revokeAuthorization( auth: OAuth2ClientAuthorization, ): Promise { return this.clientAuthRepository.remove(auth); } public async getAuthorization( user: User, authId: number, ): Promise { return this.clientAuthRepository.findOne({ where: { user: { id: user.id }, id: authId, }, relations: ['user', 'client'], }); } 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, }); } else { client = await this.clientRepository.findOne({ where: { id }, 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}%`), }, { client_id: search, }, ] : undefined, skip: offset, take: limit, order: { id: 'asc' }, relations, }); } public async searchClientsCount( search?: string, relations?: string[], ): Promise { return this.clientRepository.count({ where: search ? [ { title: ILike(`%${search}%`), }, { client_id: search, }, ] : undefined, relations, }); } public async getClientsByOwner( owner: User, relations?: string[], ): Promise { return this.clientRepository.find({ where: { owner: { id: owner.id } }, order: { id: 'asc' }, relations, }); } public async getClientURLs( id: string, type?: OAuth2ClientURLType, ): Promise { return this.clientUrlRepository.find({ where: { client: { client_id: id }, type, }, relations: ['client'], }); } 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: { client: { client_id: id }, url, type: OAuth2ClientURLType.REDIRECT_URI, }, relations: ['client'], })); } public async upsertURLs( client: OAuth2Client, urls: OAuth2ClientURL[], ): Promise { const existingURLs = await this.getClientURLs(client.client_id); const removed = []; for (const existing of existingURLs) { const alsoProvided = urls.find(({ id }) => id === existing.id); if (alsoProvided && !!alsoProvided.url) { Object.assign(existing, alsoProvided); await this.updateClientURL(existing); } else { await this.deleteClientURL(existing); removed.push(existing); } } for (const newUrl of urls.filter((url) => !url.id)) { const newUrlObject = this.reobjectifyURL(newUrl, client); await this.updateClientURL(newUrlObject); existingURLs.push(newUrlObject); } client.urls = existingURLs .filter(({ id }) => !removed.some((removed) => removed.id === id)) .map((itm) => ({ ...itm, client: undefined })); return 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 deleteClient(client: OAuth2Client): Promise { await this.clientRepository.remove(client); } 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); } } public stripClientInfo(client: OAuth2Client): Partial { return { ...client, owner: client.owner ? this._form.pluckObject(client.owner, ['id', 'uuid', 'username']) : null, picture: client.picture ? this._form.pluckObject(client.picture, [ 'id', 'mimetype', 'file', 'created_at', ]) : null, } as Partial; } private reobjectifyURL( input: Partial, client: OAuth2Client, ): OAuth2ClientURL { const reObjectifyURL = new OAuth2ClientURL(); Object.assign(reObjectifyURL, input); reObjectifyURL.client = client; return reObjectifyURL; } }