start admin api module
This commit is contained in:
parent
bb86a25ad4
commit
266645d08f
@ -93,6 +93,8 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 {}
|
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';
|
import { UserAdapter } from './adapter/user.adapter';
|
||||||
|
|
||||||
const SCOPE_DESCRIPTION: Record<string, string> = {
|
const SCOPE_DESCRIPTION: Record<string, string> = {
|
||||||
|
management: 'Manage Icy Network on your behalf',
|
||||||
email: 'Email address',
|
email: 'Email address',
|
||||||
image: 'Profile picture',
|
image: 'Profile picture',
|
||||||
};
|
};
|
||||||
@ -34,7 +35,7 @@ export class OAuth2Service {
|
|||||||
async (req, res, client, scope) => {
|
async (req, res, client, scope) => {
|
||||||
const fullClient = await this.clientService.getById(client.id as string);
|
const fullClient = await this.clientService.getById(client.id as string);
|
||||||
const allowedScopes = [...ALWAYS_AVAILABLE];
|
const allowedScopes = [...ALWAYS_AVAILABLE];
|
||||||
const disallowedScopes = [...ALWAYS_UNAVAILABLE];
|
let disallowedScopes = [...ALWAYS_UNAVAILABLE];
|
||||||
|
|
||||||
Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
|
Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
|
||||||
if (scope.includes(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', {
|
res.render('authorize', {
|
||||||
csrf: req.csrfToken(),
|
csrf: req.csrfToken(),
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
@ -8,4 +8,23 @@ export class PrivilegeService {
|
|||||||
@Inject('PRIVILEGE_REPOSITORY')
|
@Inject('PRIVILEGE_REPOSITORY')
|
||||||
private privilegeRepository: Repository<Privilege>,
|
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 { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Repository } from 'typeorm';
|
import { ILike, Repository } from 'typeorm';
|
||||||
import { UserTokenType } from '../user-token/user-token.entity';
|
import { UserTokenType } from '../user-token/user-token.entity';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
@ -48,6 +48,56 @@ export class UserService {
|
|||||||
return this.userRepository.findOne({ where: { email }, relations });
|
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(
|
public async getByUsername(
|
||||||
username: string,
|
username: string,
|
||||||
relations?: 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
|
* Include CSRF token, messages and prefilled form values for a template with a form
|
||||||
* @param req Express request
|
* @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 { Global, Module } from '@nestjs/common';
|
||||||
import { FormUtilityService } from './services/form-utility.service';
|
import { FormUtilityService } from './services/form-utility.service';
|
||||||
|
import { PaginationService } from './services/paginate.service';
|
||||||
import { QRCodeService } from './services/qr-code.service';
|
import { QRCodeService } from './services/qr-code.service';
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [TokenService, FormUtilityService, QRCodeService],
|
providers: [
|
||||||
exports: [TokenService, FormUtilityService, QRCodeService],
|
TokenService,
|
||||||
|
FormUtilityService,
|
||||||
|
QRCodeService,
|
||||||
|
PaginationService,
|
||||||
|
],
|
||||||
|
exports: [TokenService, FormUtilityService, QRCodeService, PaginationService],
|
||||||
})
|
})
|
||||||
export class UtilityModule {}
|
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