570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Put,
|
|
Query,
|
|
UnauthorizedException,
|
|
UploadedFile,
|
|
UseGuards,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
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';
|
|
import { Throttle } from '@nestjs/throttler';
|
|
|
|
const RELATIONS = ['urls', 'picture', 'owner'];
|
|
const SET_CLIENT_FIELDS = [
|
|
'title',
|
|
'description',
|
|
'scope',
|
|
'grants',
|
|
'activated',
|
|
'verified',
|
|
'urls',
|
|
];
|
|
|
|
const URL_TYPES = ['redirect_uri', 'terms', 'privacy', 'website'];
|
|
|
|
const REQUIRED_CLIENT_FIELDS = ['title', 'grants', 'activated'];
|
|
|
|
@ApiBearerAuth()
|
|
@ApiTags('admin')
|
|
@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'], 'self:oauth2')
|
|
async oauth2Scopes(@CurrentUser() user: User) {
|
|
const reducedPermissions = !this._service.userHasPrivilege(
|
|
user,
|
|
'admin:oauth2',
|
|
);
|
|
|
|
return reducedPermissions
|
|
? this._service.removeUnprivileged(this._oaClient.availableScopes)
|
|
: this._oaClient.availableScopes;
|
|
}
|
|
|
|
@Get('grants')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async oauth2Grants(@CurrentUser() user: User) {
|
|
const reducedPermissions = !this._service.userHasPrivilege(
|
|
user,
|
|
'admin:oauth2',
|
|
);
|
|
|
|
return reducedPermissions
|
|
? this._service.removeUnprivileged(this._oaClient.availableGrantTypes)
|
|
: this._oaClient.availableGrantTypes;
|
|
}
|
|
|
|
@Get('clients')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async oauth2ClientList(
|
|
@Query() options: { q?: string } & PageOptions,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
if (!this._service.userHasPrivilege(user, 'admin:oauth2')) {
|
|
const unprivileged = await this._oaClient.getClientsByOwner(
|
|
user,
|
|
RELATIONS,
|
|
);
|
|
|
|
return {
|
|
pagination: null,
|
|
list: unprivileged.map((item) => this._oaClient.stripClientInfo(item)),
|
|
};
|
|
}
|
|
|
|
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._oaClient.stripClientInfo(item)),
|
|
['password'],
|
|
),
|
|
};
|
|
}
|
|
|
|
// New client
|
|
@Post('clients')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async createNewClient(
|
|
@Body() setter: Partial<OAuth2Client>,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const allowedFieldsOnly = this._form.pluckObject(setter, SET_CLIENT_FIELDS);
|
|
const reducedPermissions = !this._service.userHasPrivilege(
|
|
user,
|
|
'admin:oauth2',
|
|
);
|
|
|
|
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 || '')
|
|
.trim()
|
|
.split(' ')
|
|
.filter((item) => item);
|
|
const splitScopes = (allowedFieldsOnly.scope || '')
|
|
.trim()
|
|
.split(' ')
|
|
.filter((item) => item);
|
|
let availableGrantTypes = this._oaClient.availableGrantTypes;
|
|
let availableScopes = this._oaClient.availableScopes;
|
|
|
|
if (reducedPermissions) {
|
|
availableGrantTypes =
|
|
this._service.removeUnprivileged(availableGrantTypes);
|
|
availableScopes = this._service.removeUnprivileged(availableScopes);
|
|
allowedFieldsOnly.activated = true;
|
|
}
|
|
|
|
if (!splitGrants.every((grant) => availableGrantTypes.includes(grant))) {
|
|
throw new BadRequestException('Bad grant types');
|
|
}
|
|
|
|
if (!splitScopes.every((scope) => availableScopes.includes(scope))) {
|
|
throw new BadRequestException('Bad scopes');
|
|
}
|
|
|
|
const urls = setter.urls?.slice();
|
|
delete allowedFieldsOnly.urls;
|
|
|
|
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);
|
|
|
|
if (urls?.length) {
|
|
await this._oaClient.upsertURLs(client, urls);
|
|
}
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
@Get('clients/:id')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async oauth2Client(@Param('id') id: string, @CurrentUser() user: User) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), RELATIONS);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to view this client',
|
|
);
|
|
}
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
@Patch('clients/:id')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async updateOauth2Client(
|
|
@Param('id') id: string,
|
|
@Body() setter: Partial<OAuth2Client>,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), []);
|
|
const reducedPermissions = !this._service.userHasPrivilege(
|
|
user,
|
|
'admin:oauth2',
|
|
);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
const allowedFieldsOnly = this._form.pluckObject(setter, SET_CLIENT_FIELDS);
|
|
const urls = allowedFieldsOnly.urls?.slice();
|
|
delete allowedFieldsOnly.urls;
|
|
|
|
// Non-admin cannot change the activated or verified status
|
|
if (reducedPermissions) {
|
|
delete allowedFieldsOnly.activated;
|
|
delete allowedFieldsOnly.verified;
|
|
}
|
|
|
|
if (!Object.keys(allowedFieldsOnly).length) {
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
const splitGrants = (allowedFieldsOnly.grants || '')
|
|
.trim()
|
|
.split(' ')
|
|
.filter((item) => item);
|
|
const splitScopes = (allowedFieldsOnly.scope || '')
|
|
.trim()
|
|
.split(' ')
|
|
.filter((item) => item);
|
|
let availableGrantTypes = this._oaClient.availableGrantTypes;
|
|
let availableScopes = this._oaClient.availableScopes;
|
|
|
|
if (reducedPermissions) {
|
|
availableGrantTypes =
|
|
this._service.removeUnprivileged(availableGrantTypes);
|
|
availableScopes = this._service.removeUnprivileged(availableScopes);
|
|
allowedFieldsOnly.activated = true;
|
|
}
|
|
|
|
if (!splitGrants.every((grant) => availableGrantTypes.includes(grant))) {
|
|
throw new BadRequestException('Bad grant types');
|
|
}
|
|
|
|
if (!splitScopes.every((scope) => availableScopes.includes(scope))) {
|
|
throw new BadRequestException('Bad scopes');
|
|
}
|
|
|
|
Object.assign(client, allowedFieldsOnly);
|
|
await this._oaClient.updateClient(client);
|
|
|
|
if (urls?.length) {
|
|
await this._oaClient.upsertURLs(client, urls);
|
|
}
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
@Delete('clients/:id')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async deleteOauth2Client(@Param('id') id: string, @CurrentUser() user: User) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), []);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
if (client.activated) {
|
|
throw new BadRequestException('Please deactivate the client first.');
|
|
}
|
|
|
|
await this._oaClient.deleteClient(client);
|
|
return { success: true };
|
|
}
|
|
|
|
@Post('clients/:id/new-secret')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async createNewSecret(@Param('id') id: string, @CurrentUser() user: User) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), []);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
client.client_secret = this._token.generateSecret();
|
|
await this._oaClient.updateClient(client);
|
|
|
|
// security
|
|
await this._oaToken.wipeClientTokens(client);
|
|
await this._oaClient.wipeClientAuthorizations(client);
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
@Delete('clients/:id/authorizations')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async deleteClientAuthorizations(
|
|
@Param('id') id: string,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), []);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
await this._oaClient.wipeClientAuthorizations(client);
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
|
|
@Get('clients/:id/urls')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async oauth2ClientURLs(@Param('id') id: string, @CurrentUser() user: User) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
return client.urls;
|
|
}
|
|
|
|
@Delete('clients/:id/urls/:url')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
|
async deleteOAuth2ClientURL(
|
|
@Param('id') id: string,
|
|
@Param('url') urlId: string,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
|
|
const parsedURLId = parseInt(urlId, 10);
|
|
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
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'], 'self:oauth2')
|
|
async updateOAuth2ClientURL(
|
|
@Param('id') id: string,
|
|
@Param('url') urlId: string,
|
|
@Body() setter: { url: string; type: string },
|
|
@CurrentUser() user: User,
|
|
) {
|
|
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 (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
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'], 'self:oauth2')
|
|
async createOAuth2ClientURL(
|
|
@Param('id') id: string,
|
|
@Body() setter: { url: string; type: string },
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), ['urls']);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
@Throttle(3, 60)
|
|
@Post('clients/:id/picture')
|
|
@Scopes('management')
|
|
@Privileges(['admin', 'admin:oauth2'], 'self: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 (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
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'], 'self:oauth2')
|
|
async deleteClientPictureFile(
|
|
@Param('id') id: string,
|
|
@CurrentUser() user: User,
|
|
) {
|
|
const client = await this._oaClient.getById(parseInt(id, 10), ['picture']);
|
|
if (!client) {
|
|
throw new NotFoundException('Client not found');
|
|
}
|
|
|
|
if (!this._service.userCanEditClient(user, client)) {
|
|
throw new UnauthorizedException(
|
|
'You do not have permission to edit this client',
|
|
);
|
|
}
|
|
|
|
this._oaClient.deletePicture(client);
|
|
client.picture = null;
|
|
await this._oaClient.updateClient(client);
|
|
|
|
return this._oaClient.stripClientInfo(client);
|
|
}
|
|
}
|