comprehensive oauth2 admin api
This commit is contained in:
parent
13eefb166a
commit
e884ce80fc
@ -82,7 +82,7 @@
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
|
||||
.scopes__scope {
|
||||
&__scope {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
17
src/modules/api/admin/admin.service.ts
Normal file
17
src/modules/api/admin/admin.service.ts
Normal 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>;
|
||||
}
|
||||
}
|
378
src/modules/api/admin/oauth2-admin.controller.ts
Normal file
378
src/modules/api/admin/oauth2-admin.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) } });
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user