diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d895bd..39437e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.4.2] - 2022-12-06 + +## Added + +- Uploading video from url source (YouTube, Vimeo, .mp4, etc..) +- Added student and teacher to seeders + +## Fix + +- Removing lesson files from drive when deleting course, category or user. +- Fixed lesson display ordering when seeding database +- Refactored bouncer actions + ## [1.3.2] - 2022-06-06 ## Added diff --git a/app/Controllers/Http/Api/v1/UsersController.ts b/app/Controllers/Http/Api/v1/UsersController.ts index 3f03c28..3219071 100644 --- a/app/Controllers/Http/Api/v1/UsersController.ts +++ b/app/Controllers/Http/Api/v1/UsersController.ts @@ -93,7 +93,7 @@ export default class UsersController extends BaseController { */ 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) { throw new Exception(result.message, result.status, result.error.code); diff --git a/app/Helpers/CourseHelper.ts b/app/Helpers/CourseHelper.ts new file mode 100644 index 0000000..369b0e6 --- /dev/null +++ b/app/Helpers/CourseHelper.ts @@ -0,0 +1,28 @@ +import Course from 'App/Models/Course'; + +export type FileEntry = { + path: string; + name: string; +}; + +export default class CourseHelper { + public static getVideoFileNames(course: Course): Array { + const data = course.lessons.map(l => { + if (!l.video) return null; + return { path: 'videos/', name: l.video.name }; + }); + + return data; + } + + public static getMaterialFileNames(course: Course): FileEntry[] { + const data = course.lessons.flatMap(l => l.materials.map(m => ({ path: 'materials/', name: m.name }))); + return data; + } + + public static getImageFileName(course: Course): FileEntry | null { + if (!course.image) return null; + const data = { path: 'images/courses/', name: course.image.name }; + return data; + } +} diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 973d17a..94ff146 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -37,6 +37,9 @@ export default class Lesson extends BaseModel { @column() public description: string; + @column() + public linked_video_url: string | null; + @column() public duration: string; diff --git a/app/Policies/LessonPolicy.ts b/app/Policies/LessonPolicy.ts new file mode 100644 index 0000000..16cb1fd --- /dev/null +++ b/app/Policies/LessonPolicy.ts @@ -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); + } +} diff --git a/app/Policies/RolePolicy.ts b/app/Policies/RolePolicy.ts new file mode 100644 index 0000000..10840a8 --- /dev/null +++ b/app/Policies/RolePolicy.ts @@ -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) { + 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; + } + } +} diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index a18020f..b78caab 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -32,6 +32,32 @@ export default class CourseRepository { this.Course = Course; } + /** + * Get list of courses by teacher id + * + * @param id Teacher id + * @returns Array of courses + */ + public async getByTeacherId(id: string | number): Promise { + const data = await this.Course.query() + .preload('lessons', q => q.preload('video').preload('materials')) + .where('teacher_id', id); + return data; + } + + /** + * Get list of courses by category id + * + * @param id Category id + * @returns Array of courses + */ + public async getByCategoryId(id: string | number): Promise { + const data = await this.Course.query() + .preload('lessons', q => q.preload('video').preload('materials')) + .where('category_id', id); + return data; + } + /** * Set status on course * diff --git a/app/Repositories/LessonRepository.ts b/app/Repositories/LessonRepository.ts index a4c8688..275b2b6 100644 --- a/app/Repositories/LessonRepository.ts +++ b/app/Repositories/LessonRepository.ts @@ -106,6 +106,7 @@ export default class LessonRepository { duration: data.duration.toFormat('HH:mm:ss'), display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1, course_id: course.id, + linked_video_url: !data.video ? data.linked_video_url : null, }); /** @@ -116,7 +117,9 @@ export default class LessonRepository { /** * Move video file to disk and save to database */ - await this.createVideo(lesson, data.video); + if (data.video) { + await this.createVideo(lesson, data.video); + } /** * Create materials @@ -154,22 +157,28 @@ export default class LessonRepository { }); /** - * Update video + * Update linked video url */ - if (data.video) { - /** - * Delete video from drive and database - */ + if (data.linked_video_url && !data.video) { if (lesson.video) { await this.Drive.delete(`videos/${lesson.video.name}`); } await lesson.related('video').query().delete(); + lesson.merge({ linked_video_url: data.linked_video_url }); + } - /** - * Upload new video to drive and save to database - */ + /** + * Update file video + */ + if (data.video) { + if (lesson.video) { + await this.Drive.delete(`videos/${lesson.video.name}`); + } + + await lesson.related('video').query().delete(); await this.createVideo(lesson, data.video); + lesson.merge({ linked_video_url: null }); } /** @@ -202,7 +211,10 @@ export default class LessonRepository { /** * Delete files from disk */ - await this.Drive.delete(`videos/${lesson.video.name}`); + if (lesson.video) { + await this.Drive.delete(`videos/${lesson.video.name}`); + } + await this.deleteMaterials(lesson); /** diff --git a/app/Services/CategoryService.ts b/app/Services/CategoryService.ts index cfe78b0..9df57fd 100644 --- a/app/Services/CategoryService.ts +++ b/app/Services/CategoryService.ts @@ -10,6 +10,7 @@ import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; * Repositories */ import CategoryRepository from 'App/Repositories/CategoryRepository'; +import CourseRepository from 'App/Repositories/CourseRepository'; /** * Validators @@ -17,12 +18,27 @@ import CategoryRepository from 'App/Repositories/CategoryRepository'; import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator'; import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator'; +/** + * Services + */ +import CourseService from 'App/Services/CourseService'; + @inject() export default class CategoryService { private categoryRepository: CategoryRepository; - constructor(categoryRepository: CategoryRepository) { + private courseRepository: CourseRepository; + + private courseService: CourseService; + + constructor( + categoryRepository: CategoryRepository, + courseRepository: CourseRepository, + courseService: CourseService + ) { this.categoryRepository = categoryRepository; + this.courseRepository = courseRepository; + this.courseService = courseService; } /** @@ -94,7 +110,7 @@ export default class CategoryService { * @returns Deleted category */ public async deleteCategory(id: string | number): Promise { - const data = await this.categoryRepository.delete(id); + const data = await this.categoryRepository.getById(id); if (!data) { return { @@ -108,6 +124,10 @@ export default class CategoryService { }; } + const courses = await this.courseRepository.getByCategoryId(id); + await this.courseService.deleteAllFiles(courses); + await this.categoryRepository.delete(id); + return { success: true, status: HttpStatusEnum.OK, diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts index 9e7903e..66b1f86 100644 --- a/app/Services/CourseService.ts +++ b/app/Services/CourseService.ts @@ -26,6 +26,7 @@ import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator'; import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator'; import Course from 'App/Models/Course'; import Drive from '@ioc:Adonis/Core/Drive'; +import CourseHelper, { FileEntry } from 'App/Helpers/CourseHelper'; @inject() export default class CourseService { @@ -234,9 +235,9 @@ export default class CourseService { /** * 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 { success: false, status: HttpStatusEnum.NOT_FOUND, @@ -248,13 +249,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 { success: false, 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: {}, error: { code: 'E_BAD_REQUEST', @@ -311,23 +312,7 @@ export default class CourseService { }; } - /** - * Collect all file names from lessons in course and delete files - */ - const videos: string[] = course.lessons.map(lesson => lesson.video.name); - const materials: string[] = course.lessons.map(lesson => lesson.materials.map(m => m.name)).flat(); - - await Promise.all( - videos.map(async name => { - await Drive.delete(`videos/${name}`); - }) - ); - - await Promise.all( - materials.map(async name => { - await Drive.delete(`materials/${name}`); - }) - ); + await this.deleteAllFiles([course]); return { success: true, @@ -345,7 +330,7 @@ export default class CourseService { * @returns Response */ public async updateCourse(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise { - const course = await this.courseRepository.update(id, data); + const course = await this.courseRepository.getById(id); if (!course) { return { @@ -359,11 +344,45 @@ export default class CourseService { }; } + if (data.teacher_id) { + const teacher = await this.userRepository.getById(data.teacher_id); + + if (!teacher) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Teacher not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + await teacher.load('roles'); + + const isTeacherOrAdmin = RoleHelper.userContainRoles(teacher.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]); + + if (!isTeacherOrAdmin) { + return { + success: false, + status: HttpStatusEnum.BAD_REQUEST, + message: 'Author is not a teacher or admin.', + data: {}, + error: { + code: 'E_BAD_REQUEST', + }, + }; + } + } + + const updated = await this.courseRepository.update(id, data); + return { success: true, status: HttpStatusEnum.OK, message: 'Course updated.', - data: course, + data: updated || {}, }; } @@ -686,6 +705,29 @@ export default class CourseService { data: { count }, }; } + + /** + * This function will delete all associated course files on disk + * + * @param courses List of courses + */ + public async deleteAllFiles(courses: Course[]): Promise { + /** + * Collect file names + */ + const images = courses.map(course => CourseHelper.getImageFileName(course)).filter(Boolean) as FileEntry[]; + const videos = courses.flatMap(course => CourseHelper.getVideoFileNames(course)).filter(Boolean) as FileEntry[]; + const materials = courses.flatMap(course => CourseHelper.getMaterialFileNames(course)); + + /** + * Remove files from disk + */ + const promises = videos + .concat(images, materials) + .map(file => Drive.delete(`${file.path}${file.name.substring(file.name.lastIndexOf('/') + 1)}`)); + + await Promise.all(promises); + } } new Ioc().make(CourseService); diff --git a/app/Services/LessonService.ts b/app/Services/LessonService.ts index cf0fe3b..4f3062e 100644 --- a/app/Services/LessonService.ts +++ b/app/Services/LessonService.ts @@ -78,20 +78,7 @@ export default class LessonService { }; } - /** - * 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', - }, - }; - } + await ctx.bouncer.with('LessonPolicy').authorize('view', lesson); /** * Load lesson with materials @@ -109,7 +96,8 @@ export default class LessonService { /** * Fetch lesson material by file name * - * @param fileName Name of file + * @param ctx Http context + * @param name Filename * @returns Response */ public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise> { @@ -131,18 +119,7 @@ export default class LessonService { * Allow user to view lesson material */ await material.load('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', - }, - }; - } + await ctx.bouncer.with('LessonPolicy').authorize('view', material.lesson); return { success: true, @@ -168,18 +145,7 @@ export default class LessonService { } await video.load('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', - }, - }; - } + await ctx.bouncer.with('LessonPolicy').authorize('view', video.lesson); return { success: true, @@ -211,17 +177,7 @@ export default class LessonService { }; } - 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', - }, - }; - } + await ctx.bouncer.with('LessonPolicy').authorize('view', lesson); const progress = await this.lessonProgressRepository.get(user.id, lesson.id); @@ -354,20 +310,7 @@ export default class LessonService { }; } - /** - * 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', - }, - }; - } + await ctx.bouncer.with('LessonPolicy').authorize('view', lesson); /** * Load lesson with materials diff --git a/app/Services/UserService.ts b/app/Services/UserService.ts index df95a08..7348766 100644 --- a/app/Services/UserService.ts +++ b/app/Services/UserService.ts @@ -22,21 +22,48 @@ import Role from 'App/Models/Role'; import RoleRepository from 'App/Repositories/RoleRepository'; import UserRepository from 'App/Repositories/UserRepository'; +/** + * Services + */ +import CourseService from './CourseService'; + /** * Validators */ import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; +/** + * Datatypes + */ +import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; + +/** + * Helpers + */ +import RoleHelper from 'App/Helpers/RoleHelper'; +import CourseRepository from 'App/Repositories/CourseRepository'; + @inject() export default class UserService { private userRepository: UserRepository; private roleRepository: RoleRepository; - constructor(userRepository: UserRepository, roleRepository: RoleRepository) { + private courseRepository: CourseRepository; + + private courseService: CourseService; + + constructor( + userRepository: UserRepository, + roleRepository: RoleRepository, + courseRepository: CourseRepository, + courseService: CourseService + ) { this.userRepository = userRepository; this.roleRepository = roleRepository; + this.courseRepository = courseRepository; + this.courseService = courseService; } /** @@ -120,20 +147,7 @@ export default class UserService { }; } - /** - * 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', - }, - }; - } + await ctx.bouncer.with('RolePolicy').authorize('manage', [role.slug as RoleEnum]); /** * Create new user @@ -166,7 +180,7 @@ export default class UserService { data: UpdateUserValidator['schema']['props'], ctx: HttpContextContract ): Promise { - const user = await this.userRepository.update(id, data); + const user = await this.userRepository.getById(id); if (!user) { return { @@ -180,6 +194,8 @@ export default class UserService { }; } + await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]); + /** * Update user role */ @@ -187,27 +203,18 @@ export default class UserService { const role = await this.roleRepository.getBySlug(data.role); if (role) { - 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', - }, - }; - } - + await ctx.bouncer.with('RolePolicy').authorize('manage', [data.role]); await this.userRepository.updateRoles(user, [role]); } } + const updated = await this.userRepository.update(id, data); + return { success: true, status: HttpStatusEnum.OK, message: 'User updated.', - data: user, + data: updated || {}, }; } @@ -217,8 +224,8 @@ export default class UserService { * @param id User id * @returns Response */ - public async deleteUser(id: string | number): Promise { - const user = await this.userRepository.delete(id); + public async deleteUser(id: string | number, ctx: HttpContextContract): Promise { + const user = await this.userRepository.getById(id); if (!user) { return { @@ -232,6 +239,15 @@ export default class UserService { }; } + await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]); + + if (RoleHelper.userContainRoles(user.roles, [RoleEnum.TEACHER, RoleEnum.ADMIN])) { + const courses = await this.courseRepository.getByTeacherId(id); + await this.courseService.deleteAllFiles(courses); + } + + await this.userRepository.delete(id); + return { success: true, status: HttpStatusEnum.OK, diff --git a/app/Validators/Lesson/CreateLessonValidator.ts b/app/Validators/Lesson/CreateLessonValidator.ts index 976de89..ca92e7d 100644 --- a/app/Validators/Lesson/CreateLessonValidator.ts +++ b/app/Validators/Lesson/CreateLessonValidator.ts @@ -9,10 +9,11 @@ export default class CreateLessonValidator { title: schema.string({}, [rules.maxLength(255)]), description: schema.string(), duration: schema.date({ format: 'HH:mm:ss' }), - video: schema.file({ + video: schema.file.optional({ size: '5000mb', extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], }), + linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]), materials: schema.array.optional().members( schema.file({ size: '100mb', diff --git a/app/Validators/Lesson/UpdateLessonValidator.ts b/app/Validators/Lesson/UpdateLessonValidator.ts index 909fc7d..4f5b03d 100644 --- a/app/Validators/Lesson/UpdateLessonValidator.ts +++ b/app/Validators/Lesson/UpdateLessonValidator.ts @@ -13,6 +13,7 @@ export default class UpdateLessonValidator { size: '5000mb', extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], }), + linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]), materials: schema.array.nullableAndOptional().members( schema.file({ size: '100mb', diff --git a/database/migrations/1628016919501_lessons.ts b/database/migrations/1628016919501_lessons.ts index 6c65853..e3fb51f 100644 --- a/database/migrations/1628016919501_lessons.ts +++ b/database/migrations/1628016919501_lessons.ts @@ -11,6 +11,7 @@ export default class Lessons extends BaseSchema { table.integer('display_order').notNullable(); table.integer('color_id').unsigned().references('colors.id'); table.string('description'); + table.text('linked_video_url').nullable(); table.time('duration').notNullable(); table.timestamp('created_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }); diff --git a/database/seeders/Course.ts b/database/seeders/Course.ts index 78ca18d..f27b1df 100644 --- a/database/seeders/Course.ts +++ b/database/seeders/Course.ts @@ -7,7 +7,13 @@ export default class CourseSeeder extends BaseSeeder { public async run() { this.CourseFactory = CourseFactory; - await this.CourseFactory.with('lessons', 10, lessonFactory => lessonFactory.with('content')) + const LESSONS_COUNT = 10; + + await this.CourseFactory.with('lessons', LESSONS_COUNT, lessonFactory => + lessonFactory + .with('content') + .merge(new Array(LESSONS_COUNT).fill(LESSONS_COUNT).map((_, i) => ({ display_order: i + 1 }))) + ) .with('category') .with('teacher') .createMany(3); diff --git a/database/seeders/User.ts b/database/seeders/User.ts index 07d19d2..21fec2b 100644 --- a/database/seeders/User.ts +++ b/database/seeders/User.ts @@ -11,23 +11,48 @@ export default class UserSeeder extends BaseSeeder { */ const roles = { admin: await Role.findByOrFail('slug', 'admin'), + teacher: await Role.findByOrFail('slug', 'teacher'), student: await Role.findByOrFail('slug', 'student'), }; /** * Administrator */ - const userAdmin = await User.create({ + const administrator = await User.create({ first_name: 'John', last_name: 'Doe', login: 'admin', password: '123456', email: 'administrator@example.com', }); - await userAdmin.related('roles').attach([roles.admin.id]); + await administrator.related('roles').attach([roles.admin.id]); /** - * Students + * Teacher + */ + const teacher = await User.create({ + first_name: 'Hanna', + last_name: 'Liv', + login: 'teacher', + password: '123456', + email: 'teacher@example.com', + }); + await teacher.related('roles').attach([roles.teacher.id]); + + /** + * Student + */ + const student = await User.create({ + first_name: 'Ylfa', + last_name: 'Erna', + login: 'student', + password: '123456', + email: 'student@example.com', + }); + await student.related('roles').attach([roles.student.id]); + + /** + * Student list */ await StudentFactory.with('contacts').createMany(20); } diff --git a/package.json b/package.json index d569578..565b343 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "1.3.2", + "version": "1.4.2", "private": true, "scripts": { "build": "node ace build --production", diff --git a/start/bouncer.ts b/start/bouncer.ts index 6493865..0755d17 100644 --- a/start/bouncer.ts +++ b/start/bouncer.ts @@ -6,11 +6,6 @@ */ 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 |**************************************************************** */ -export const { actions } = Bouncer.define('manageUserRole', async (user: User, role: Role) => { - 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; -}); +export const { actions } = Bouncer; /* |-------------------------------------------------------------------------- @@ -74,4 +54,7 @@ export const { actions } = Bouncer.define('manageUserRole', async (user: User, r | 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'), +}); diff --git a/start/routes/apis/v1/users.ts b/start/routes/apis/v1/users.ts index a82f47d..0ea165d 100644 --- a/start/routes/apis/v1/users.ts +++ b/start/routes/apis/v1/users.ts @@ -7,7 +7,7 @@ Route.group(() => { 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.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles') - .middleware('role:admin,teacher') + .middleware('role:admin') .as('users.attach-role'); Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles') .middleware('role:admin')