mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
refactor: moved bouncer actions to policies
This commit is contained in:
parent
06e8acd0b2
commit
b6217a4007
@ -93,7 +93,7 @@ export default class UsersController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public async delete(ctx: HttpContextContract) {
|
public async delete(ctx: HttpContextContract) {
|
||||||
const result = await this.userService.deleteUser(ctx.params.id);
|
const result = await this.userService.deleteUser(ctx.params.id, ctx);
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
throw new Exception(result.message, result.status, result.error.code);
|
||||||
|
|||||||
35
app/Policies/LessonPolicy.ts
Normal file
35
app/Policies/LessonPolicy.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers
|
||||||
|
*/
|
||||||
|
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models
|
||||||
|
*/
|
||||||
|
import Lesson from 'App/Models/Lesson';
|
||||||
|
import User from 'App/Models/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datatypes
|
||||||
|
*/
|
||||||
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
|
|
||||||
|
export default class LessonPolicy extends BasePolicy {
|
||||||
|
public async view(user: User, lesson: Lesson) {
|
||||||
|
/**
|
||||||
|
* Load required data to check permissions
|
||||||
|
*/
|
||||||
|
await user.load(loader => loader.load('roles').load('courses'));
|
||||||
|
await lesson.load('course');
|
||||||
|
|
||||||
|
const isAdminOrTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
||||||
|
|
||||||
|
if (isAdminOrTeacher) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!user.courses.find(course => course.id === lesson.course.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Policies/RolePolicy.ts
Normal file
36
app/Policies/RolePolicy.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models
|
||||||
|
*/
|
||||||
|
import User from 'App/Models/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers
|
||||||
|
*/
|
||||||
|
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datatypes
|
||||||
|
*/
|
||||||
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
|
|
||||||
|
export default class RolePolicy extends BasePolicy {
|
||||||
|
public async manage(user: User, roles: Array<RoleEnum>) {
|
||||||
|
await user.load('roles');
|
||||||
|
|
||||||
|
const isAdmin = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN]);
|
||||||
|
const isTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.TEACHER]);
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User with role `TEACHER` can manage only user with `STUDENT` role
|
||||||
|
*/
|
||||||
|
if (!(roles.includes(RoleEnum.TEACHER) || roles.includes(RoleEnum.ADMIN)) && isTeacher) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -234,9 +234,9 @@ export default class CourseService {
|
|||||||
/**
|
/**
|
||||||
* Find course teacher
|
* Find course teacher
|
||||||
*/
|
*/
|
||||||
const teacher = await this.userRepository.getById(data.teacher_id);
|
const user = await this.userRepository.getById(data.teacher_id);
|
||||||
|
|
||||||
if (!teacher) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
status: HttpStatusEnum.NOT_FOUND,
|
||||||
@ -248,13 +248,13 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]);
|
const isHasPermissions = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
||||||
|
|
||||||
if (!isTeacher) {
|
if (!isHasPermissions) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.BAD_REQUEST,
|
status: HttpStatusEnum.BAD_REQUEST,
|
||||||
message: `User with id "${teacher.id}" not a teacher.`,
|
message: `User with id "${user.id}" does not have sufficient permissions.`,
|
||||||
data: {},
|
data: {},
|
||||||
error: {
|
error: {
|
||||||
code: 'E_BAD_REQUEST',
|
code: 'E_BAD_REQUEST',
|
||||||
@ -359,6 +359,10 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (data.teacher_id) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
|
|||||||
@ -78,20 +78,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
* Allow user to view lesson content
|
|
||||||
*/
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load lesson with materials
|
* Load lesson with materials
|
||||||
@ -109,7 +96,8 @@ export default class LessonService {
|
|||||||
/**
|
/**
|
||||||
* Fetch lesson material by file name
|
* Fetch lesson material by file name
|
||||||
*
|
*
|
||||||
* @param fileName Name of file
|
* @param ctx Http context
|
||||||
|
* @param name Filename
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
||||||
@ -131,18 +119,7 @@ export default class LessonService {
|
|||||||
* Allow user to view lesson material
|
* Allow user to view lesson material
|
||||||
*/
|
*/
|
||||||
await material.load('lesson');
|
await material.load('lesson');
|
||||||
|
await ctx.bouncer.with('LessonPolicy').authorize('view', material.lesson);
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', material.lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You are not able to view this file.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -168,18 +145,7 @@ export default class LessonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await video.load('lesson');
|
await video.load('lesson');
|
||||||
|
await ctx.bouncer.with('LessonPolicy').authorize('view', video.lesson);
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', video.lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You are not able to view this file.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -211,17 +177,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
|
const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
|
||||||
|
|
||||||
@ -354,20 +310,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
* Allow user to view lesson content
|
|
||||||
*/
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load lesson with materials
|
* Load lesson with materials
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import UserRepository from 'App/Repositories/UserRepository';
|
|||||||
*/
|
*/
|
||||||
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
||||||
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
||||||
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@ -120,20 +121,7 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await ctx.bouncer.with('RolePolicy').authorize('manage', [role.slug as RoleEnum]);
|
||||||
* Check user permissions
|
|
||||||
*/
|
|
||||||
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You dont have permissions to permorm that action',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new user
|
* Create new user
|
||||||
@ -166,7 +154,7 @@ export default class UserService {
|
|||||||
data: UpdateUserValidator['schema']['props'],
|
data: UpdateUserValidator['schema']['props'],
|
||||||
ctx: HttpContextContract
|
ctx: HttpContextContract
|
||||||
): Promise<IResponse> {
|
): Promise<IResponse> {
|
||||||
const user = await this.userRepository.update(id, data);
|
const user = await this.userRepository.getById(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -180,6 +168,9 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
|
||||||
|
await this.userRepository.update(id, data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user role
|
* Update user role
|
||||||
*/
|
*/
|
||||||
@ -187,18 +178,7 @@ export default class UserService {
|
|||||||
const role = await this.roleRepository.getBySlug(data.role);
|
const role = await this.roleRepository.getBySlug(data.role);
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
await ctx.bouncer.with('RolePolicy').authorize('manage', [data.role]);
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You dont have permissions to permorm that action',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userRepository.updateRoles(user, [role]);
|
await this.userRepository.updateRoles(user, [role]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,8 +197,8 @@ export default class UserService {
|
|||||||
* @param id User id
|
* @param id User id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async deleteUser(id: string | number): Promise<IResponse> {
|
public async deleteUser(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
|
||||||
const user = await this.userRepository.delete(id);
|
const user = await this.userRepository.getById(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -232,6 +212,9 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
|
||||||
|
await this.userRepository.delete(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
|
|||||||
@ -6,11 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
import Lesson from 'App/Models/Lesson';
|
|
||||||
import Role from 'App/Models/Role';
|
|
||||||
import User from 'App/Models/User';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -34,22 +29,7 @@ import User from 'App/Models/User';
|
|||||||
| NOTE: Always export the "actions" const from this file
|
| NOTE: Always export the "actions" const from this file
|
||||||
|****************************************************************
|
|****************************************************************
|
||||||
*/
|
*/
|
||||||
export const { actions } = Bouncer.define('manageUserRole', async (user: User, role: Role) => {
|
export const { actions } = Bouncer;
|
||||||
await user.load('roles');
|
|
||||||
return !(
|
|
||||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
|
||||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
|
||||||
);
|
|
||||||
}).define('viewLessonContent', async (user: User, lesson: Lesson) => {
|
|
||||||
await user.load(loader => loader.load('roles').load('courses'));
|
|
||||||
await lesson.load('course');
|
|
||||||
|
|
||||||
if (!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER])) {
|
|
||||||
return !!user.courses.find(course => course.id === lesson.course.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -74,4 +54,7 @@ export const { actions } = Bouncer.define('manageUserRole', async (user: User, r
|
|||||||
| NOTE: Always export the "policies" const from this file
|
| NOTE: Always export the "policies" const from this file
|
||||||
|****************************************************************
|
|****************************************************************
|
||||||
*/
|
*/
|
||||||
export const { policies } = Bouncer.registerPolicies({});
|
export const { policies } = Bouncer.registerPolicies({
|
||||||
|
LessonPolicy: () => import('App/Policies/LessonPolicy'),
|
||||||
|
RolePolicy: () => import('App/Policies/RolePolicy'),
|
||||||
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Route.group(() => {
|
|||||||
Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update');
|
Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update');
|
||||||
Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete');
|
Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete');
|
||||||
Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles')
|
Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles')
|
||||||
.middleware('role:admin,teacher')
|
.middleware('role:admin')
|
||||||
.as('users.attach-role');
|
.as('users.attach-role');
|
||||||
Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles')
|
Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles')
|
||||||
.middleware('role:admin')
|
.middleware('role:admin')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user