start admin api module
This commit is contained in:
parent
bb86a25ad4
commit
266645d08f
@ -93,6 +93,8 @@
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OAuth2Module } from 'src/modules/oauth2/oauth2.module';
|
||||
import { ObjectsModule } from 'src/modules/objects/objects.module';
|
||||
import { PrivilegeAdminController } from './privilege-admin.controller';
|
||||
import { UserAdminController } from './user-admin.controller';
|
||||
|
||||
@Module({})
|
||||
@Module({
|
||||
controllers: [UserAdminController, PrivilegeAdminController],
|
||||
imports: [ObjectsModule, OAuth2Module],
|
||||
})
|
||||
export class AdminApiModule {}
|
||||
|
44
src/modules/api/admin/privilege-admin.controller.ts
Normal file
44
src/modules/api/admin/privilege-admin.controller.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Privileges } from 'src/decorators/privileges.decorator';
|
||||
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 { 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,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user', 'admin:user:privilege')
|
||||
async privilegeList() {
|
||||
return this._privilege.getAllPrivileges();
|
||||
}
|
||||
|
||||
@Post('')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user', 'admin:user:privilege')
|
||||
async newPrivilege(@Body() body: { privilege: string }) {
|
||||
if (!body.privilege) {
|
||||
throw new BadRequestException('Privilege is required');
|
||||
}
|
||||
|
||||
return this._privilege.createPrivilege(body.privilege);
|
||||
}
|
||||
}
|
73
src/modules/api/admin/user-admin.controller.ts
Normal file
73
src/modules/api/admin/user-admin.controller.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Privileges } from 'src/decorators/privileges.decorator';
|
||||
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 { 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';
|
||||
import { PageOptions } from 'src/types/pagination.interfaces';
|
||||
|
||||
const RELATIONS = ['picture', 'privileges'];
|
||||
|
||||
@Controller('/api/admin/users')
|
||||
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
|
||||
export class UserAdminController {
|
||||
constructor(
|
||||
private _user: UserService,
|
||||
private _paginate: PaginationService,
|
||||
private _form: FormUtilityService,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user')
|
||||
async userList(@Query() options: { q?: string } & PageOptions) {
|
||||
const search = options.q ? decodeURIComponent(options.q) : null;
|
||||
const resultCount = await this._user.searchUsersCount(search, RELATIONS);
|
||||
|
||||
const pagination = this._paginate.paginate(options, resultCount);
|
||||
|
||||
const [list] = await this._user.searchUsers(
|
||||
pagination.pageSize,
|
||||
pagination.offset,
|
||||
search,
|
||||
RELATIONS,
|
||||
);
|
||||
|
||||
return {
|
||||
pagination,
|
||||
list: this._form.stripObjectArray(list, ['password']),
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user')
|
||||
async user(@Param('id') id: string) {
|
||||
const user = await this._user.getById(parseInt(id, 10), RELATIONS);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return this._form.stripObject(user, ['password']);
|
||||
}
|
||||
|
||||
@Get(':id/privileges')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user')
|
||||
async userPrivileges(@Param('id') id: string) {
|
||||
const user = await this._user.getById(parseInt(id, 10), ['privileges']);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return user.privileges;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||
import { UserAdapter } from './adapter/user.adapter';
|
||||
|
||||
const SCOPE_DESCRIPTION: Record<string, string> = {
|
||||
management: 'Manage Icy Network on your behalf',
|
||||
email: 'Email address',
|
||||
image: 'Profile picture',
|
||||
};
|
||||
@ -34,7 +35,7 @@ export class OAuth2Service {
|
||||
async (req, res, client, scope) => {
|
||||
const fullClient = await this.clientService.getById(client.id as string);
|
||||
const allowedScopes = [...ALWAYS_AVAILABLE];
|
||||
const disallowedScopes = [...ALWAYS_UNAVAILABLE];
|
||||
let disallowedScopes = [...ALWAYS_UNAVAILABLE];
|
||||
|
||||
Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
|
||||
if (scope.includes(item)) {
|
||||
@ -44,6 +45,13 @@ export class OAuth2Service {
|
||||
}
|
||||
});
|
||||
|
||||
if (scope.includes('management')) {
|
||||
disallowedScopes = [
|
||||
'THIS APPLICATION COULD ACCESS SENSITIVE INFORMATION!',
|
||||
'MAKE SURE YOU TRUST THE DEVELOPERS OF THIS APPLICATION',
|
||||
];
|
||||
}
|
||||
|
||||
res.render('authorize', {
|
||||
csrf: req.csrfToken(),
|
||||
user: req.user,
|
||||
|
@ -8,4 +8,23 @@ export class PrivilegeService {
|
||||
@Inject('PRIVILEGE_REPOSITORY')
|
||||
private privilegeRepository: Repository<Privilege>,
|
||||
) {}
|
||||
|
||||
public getAllPrivileges(): Promise<Privilege[]> {
|
||||
return this.privilegeRepository.find();
|
||||
}
|
||||
|
||||
public async createPrivilege(name: string): Promise<Privilege> {
|
||||
const privilege = new Privilege();
|
||||
privilege.name = name;
|
||||
await this.privilegeRepository.save(privilege);
|
||||
return privilege;
|
||||
}
|
||||
|
||||
public async getByName(name: string): Promise<Privilege> {
|
||||
return this.privilegeRepository.findOne({ where: { name } });
|
||||
}
|
||||
|
||||
public async getByID(id: number): Promise<Privilege> {
|
||||
return this.privilegeRepository.findOne({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ILike, Repository } from 'typeorm';
|
||||
import { UserTokenType } from '../user-token/user-token.entity';
|
||||
import { User } from './user.entity';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
@ -48,6 +48,56 @@ export class UserService {
|
||||
return this.userRepository.findOne({ where: { email }, relations });
|
||||
}
|
||||
|
||||
public async searchUsers(
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
search?: string,
|
||||
relations?: string[],
|
||||
): Promise<[User[], number]> {
|
||||
const searchTerm = `%${search}%`;
|
||||
return this.userRepository.findAndCount({
|
||||
where: search
|
||||
? [
|
||||
{
|
||||
display_name: ILike(searchTerm),
|
||||
},
|
||||
{
|
||||
username: ILike(searchTerm),
|
||||
},
|
||||
{
|
||||
email: ILike(searchTerm),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
public async searchUsersCount(
|
||||
search?: string,
|
||||
relations?: string[],
|
||||
): Promise<number> {
|
||||
const searchTerm = `%${search}%`;
|
||||
return this.userRepository.count({
|
||||
where: search
|
||||
? [
|
||||
{
|
||||
display_name: ILike(searchTerm),
|
||||
},
|
||||
{
|
||||
username: ILike(searchTerm),
|
||||
},
|
||||
{
|
||||
email: ILike(searchTerm),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
public async getByUsername(
|
||||
username: string,
|
||||
relations?: string[],
|
||||
|
@ -33,6 +33,54 @@ export class FormUtilityService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip keys from an object
|
||||
* @param object Object to strip
|
||||
* @param keys Keys to strip from the object
|
||||
* @returns Stripped object
|
||||
*/
|
||||
public stripObject<T>(object: T, keys: string[]): T {
|
||||
return keys.reduce<T>((obj, field) => {
|
||||
delete obj[field];
|
||||
return obj;
|
||||
}, object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluck keys from an object
|
||||
* @param object Object to pluck
|
||||
* @param keys Keys to pluck from the object
|
||||
* @returns Plucked object
|
||||
*/
|
||||
public pluckObject<T>(object: T, keys: string[]): Partial<T> {
|
||||
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
|
||||
if (keys.includes(field)) {
|
||||
obj[field] = object[field];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip keys from an object array
|
||||
* @param array Object array to strip
|
||||
* @param keys Keys to strip from the object array
|
||||
* @returns Stripped object
|
||||
*/
|
||||
public stripObjectArray<T>(array: T[], keys: string[]): T[] {
|
||||
return array.map((object) => this.stripObject(object, keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluck keys from an object array
|
||||
* @param array Object array to pluck
|
||||
* @param keys Keys to pluck from the object array
|
||||
* @returns Plucked object
|
||||
*/
|
||||
public pluckObjectArray<T>(array: T[], keys: string[]): Partial<T>[] {
|
||||
return array.map((object) => this.pluckObject(object, keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Include CSRF token, messages and prefilled form values for a template with a form
|
||||
* @param req Express request
|
||||
|
33
src/modules/utility/services/paginate.service.ts
Normal file
33
src/modules/utility/services/paginate.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
PageOptions,
|
||||
PaginationOptions,
|
||||
} from 'src/types/pagination.interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class PaginationService {
|
||||
public paginate(options: PageOptions, rowCount: number): PaginationOptions {
|
||||
const paginationOptions: PaginationOptions = {
|
||||
page: options.page || 1,
|
||||
pageSize: options.pageSize || 50,
|
||||
rowCount,
|
||||
};
|
||||
|
||||
// Calculate page count from row count and page size
|
||||
paginationOptions.pageCount = Math.ceil(
|
||||
rowCount / paginationOptions.pageSize,
|
||||
);
|
||||
|
||||
// Limit the page number to 1..pageCount
|
||||
paginationOptions.page = Math.max(
|
||||
Math.min(paginationOptions.page, paginationOptions.pageCount),
|
||||
1,
|
||||
);
|
||||
|
||||
// Calculate the offset for the query
|
||||
paginationOptions.offset =
|
||||
(paginationOptions.page - 1) * paginationOptions.pageSize;
|
||||
|
||||
return paginationOptions;
|
||||
}
|
||||
}
|
@ -1,11 +1,17 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { FormUtilityService } from './services/form-utility.service';
|
||||
import { PaginationService } from './services/paginate.service';
|
||||
import { QRCodeService } from './services/qr-code.service';
|
||||
import { TokenService } from './services/token.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [TokenService, FormUtilityService, QRCodeService],
|
||||
exports: [TokenService, FormUtilityService, QRCodeService],
|
||||
providers: [
|
||||
TokenService,
|
||||
FormUtilityService,
|
||||
QRCodeService,
|
||||
PaginationService,
|
||||
],
|
||||
exports: [TokenService, FormUtilityService, QRCodeService, PaginationService],
|
||||
})
|
||||
export class UtilityModule {}
|
||||
|
12
src/types/pagination.interfaces.ts
Normal file
12
src/types/pagination.interfaces.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface PageOptions {
|
||||
pageSize?: number;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export interface PaginationOptions {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
rowCount: number;
|
||||
pageCount?: number;
|
||||
offset?: number;
|
||||
}
|
Reference in New Issue
Block a user