Compare commits

..

No commits in common. "develop" and "v1.3.2" have entirely different histories.

23 changed files with 164 additions and 355 deletions

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ coverage
.env .env
tmp tmp
uploads uploads
npm-debug.log

View File

@ -1,18 +1,5 @@
# Changelog # 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 ## [1.3.2] - 2022-06-06
## Added ## Added

View File

@ -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, ctx); const result = await this.userService.deleteUser(ctx.params.id);
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);

View File

@ -1,28 +0,0 @@
import Course from 'App/Models/Course';
export type FileEntry = {
path: string;
name: string;
};
export default class CourseHelper {
public static getVideoFileNames(course: Course): Array<FileEntry | null> {
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;
}
}

View File

@ -37,9 +37,6 @@ export default class Lesson extends BaseModel {
@column() @column()
public description: string; public description: string;
@column()
public linked_video_url: string | null;
@column() @column()
public duration: string; public duration: string;

View File

@ -1,35 +0,0 @@
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);
}
}

View File

@ -1,36 +0,0 @@
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;
}
}
}

View File

@ -32,32 +32,6 @@ export default class CourseRepository {
this.Course = Course; 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<Course[]> {
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<Course[]> {
const data = await this.Course.query()
.preload('lessons', q => q.preload('video').preload('materials'))
.where('category_id', id);
return data;
}
/** /**
* Set status on course * Set status on course
* *

View File

@ -106,7 +106,6 @@ export default class LessonRepository {
duration: data.duration.toFormat('HH:mm:ss'), duration: data.duration.toFormat('HH:mm:ss'),
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1, display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
course_id: course.id, course_id: course.id,
linked_video_url: !data.video ? data.linked_video_url : null,
}); });
/** /**
@ -117,9 +116,7 @@ export default class LessonRepository {
/** /**
* Move video file to disk and save to database * Move video file to disk and save to database
*/ */
if (data.video) { await this.createVideo(lesson, data.video);
await this.createVideo(lesson, data.video);
}
/** /**
* Create materials * Create materials
@ -157,28 +154,22 @@ export default class LessonRepository {
}); });
/** /**
* Update linked video url * Update video
*/
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 });
}
/**
* Update file video
*/ */
if (data.video) { if (data.video) {
/**
* Delete video from drive and database
*/
if (lesson.video) { if (lesson.video) {
await this.Drive.delete(`videos/${lesson.video.name}`); await this.Drive.delete(`videos/${lesson.video.name}`);
} }
await lesson.related('video').query().delete(); await lesson.related('video').query().delete();
/**
* Upload new video to drive and save to database
*/
await this.createVideo(lesson, data.video); await this.createVideo(lesson, data.video);
lesson.merge({ linked_video_url: null });
} }
/** /**
@ -211,10 +202,7 @@ export default class LessonRepository {
/** /**
* Delete files from disk * Delete files from disk
*/ */
if (lesson.video) { await this.Drive.delete(`videos/${lesson.video.name}`);
await this.Drive.delete(`videos/${lesson.video.name}`);
}
await this.deleteMaterials(lesson); await this.deleteMaterials(lesson);
/** /**

View File

@ -10,7 +10,6 @@ import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
* Repositories * Repositories
*/ */
import CategoryRepository from 'App/Repositories/CategoryRepository'; import CategoryRepository from 'App/Repositories/CategoryRepository';
import CourseRepository from 'App/Repositories/CourseRepository';
/** /**
* Validators * Validators
@ -18,27 +17,12 @@ import CourseRepository from 'App/Repositories/CourseRepository';
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator'; import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator'; import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
/**
* Services
*/
import CourseService from 'App/Services/CourseService';
@inject() @inject()
export default class CategoryService { export default class CategoryService {
private categoryRepository: CategoryRepository; private categoryRepository: CategoryRepository;
private courseRepository: CourseRepository; constructor(categoryRepository: CategoryRepository) {
private courseService: CourseService;
constructor(
categoryRepository: CategoryRepository,
courseRepository: CourseRepository,
courseService: CourseService
) {
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.courseRepository = courseRepository;
this.courseService = courseService;
} }
/** /**
@ -110,7 +94,7 @@ export default class CategoryService {
* @returns Deleted category * @returns Deleted category
*/ */
public async deleteCategory(id: string | number): Promise<IResponse> { public async deleteCategory(id: string | number): Promise<IResponse> {
const data = await this.categoryRepository.getById(id); const data = await this.categoryRepository.delete(id);
if (!data) { if (!data) {
return { return {
@ -124,10 +108,6 @@ export default class CategoryService {
}; };
} }
const courses = await this.courseRepository.getByCategoryId(id);
await this.courseService.deleteAllFiles(courses);
await this.categoryRepository.delete(id);
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,

View File

@ -26,7 +26,6 @@ import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator';
import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator'; import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator';
import Course from 'App/Models/Course'; import Course from 'App/Models/Course';
import Drive from '@ioc:Adonis/Core/Drive'; import Drive from '@ioc:Adonis/Core/Drive';
import CourseHelper, { FileEntry } from 'App/Helpers/CourseHelper';
@inject() @inject()
export default class CourseService { export default class CourseService {
@ -235,9 +234,9 @@ export default class CourseService {
/** /**
* Find course teacher * Find course teacher
*/ */
const user = await this.userRepository.getById(data.teacher_id); const teacher = await this.userRepository.getById(data.teacher_id);
if (!user) { if (!teacher) {
return { return {
success: false, success: false,
status: HttpStatusEnum.NOT_FOUND, status: HttpStatusEnum.NOT_FOUND,
@ -249,13 +248,13 @@ export default class CourseService {
}; };
} }
const isHasPermissions = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]); const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]);
if (!isHasPermissions) { if (!isTeacher) {
return { return {
success: false, success: false,
status: HttpStatusEnum.BAD_REQUEST, status: HttpStatusEnum.BAD_REQUEST,
message: `User with id "${user.id}" does not have sufficient permissions.`, message: `User with id "${teacher.id}" not a teacher.`,
data: {}, data: {},
error: { error: {
code: 'E_BAD_REQUEST', code: 'E_BAD_REQUEST',
@ -312,7 +311,23 @@ export default class CourseService {
}; };
} }
await this.deleteAllFiles([course]); /**
* 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}`);
})
);
return { return {
success: true, success: true,
@ -330,7 +345,7 @@ export default class CourseService {
* @returns Response * @returns Response
*/ */
public async updateCourse(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise<IResponse> { public async updateCourse(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise<IResponse> {
const course = await this.courseRepository.getById(id); const course = await this.courseRepository.update(id, data);
if (!course) { if (!course) {
return { return {
@ -344,45 +359,11 @@ 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 { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,
message: 'Course updated.', message: 'Course updated.',
data: updated || {}, data: course,
}; };
} }
@ -705,29 +686,6 @@ export default class CourseService {
data: { count }, data: { count },
}; };
} }
/**
* This function will delete all associated course files on disk
*
* @param courses List of courses
*/
public async deleteAllFiles(courses: Course[]): Promise<void> {
/**
* 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); new Ioc().make(CourseService);

View File

@ -78,7 +78,20 @@ 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
@ -96,8 +109,7 @@ export default class LessonService {
/** /**
* Fetch lesson material by file name * Fetch lesson material by file name
* *
* @param ctx Http context * @param fileName Name of file
* @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>> {
@ -119,7 +131,18 @@ 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,
@ -145,7 +168,18 @@ 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,
@ -177,7 +211,17 @@ export default class LessonService {
}; };
} }
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson); 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',
},
};
}
const progress = await this.lessonProgressRepository.get(user.id, lesson.id); const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
@ -310,7 +354,20 @@ 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

View File

@ -22,48 +22,21 @@ import Role from 'App/Models/Role';
import RoleRepository from 'App/Repositories/RoleRepository'; import RoleRepository from 'App/Repositories/RoleRepository';
import UserRepository from 'App/Repositories/UserRepository'; import UserRepository from 'App/Repositories/UserRepository';
/**
* Services
*/
import CourseService from './CourseService';
/** /**
* Validators * Validators
*/ */
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';
/**
* Datatypes
*/
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
/**
* Helpers
*/
import RoleHelper from 'App/Helpers/RoleHelper';
import CourseRepository from 'App/Repositories/CourseRepository';
@inject() @inject()
export default class UserService { export default class UserService {
private userRepository: UserRepository; private userRepository: UserRepository;
private roleRepository: RoleRepository; private roleRepository: RoleRepository;
private courseRepository: CourseRepository; constructor(userRepository: UserRepository, roleRepository: RoleRepository) {
private courseService: CourseService;
constructor(
userRepository: UserRepository,
roleRepository: RoleRepository,
courseRepository: CourseRepository,
courseService: CourseService
) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.roleRepository = roleRepository; this.roleRepository = roleRepository;
this.courseRepository = courseRepository;
this.courseService = courseService;
} }
/** /**
@ -147,7 +120,20 @@ 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
@ -180,7 +166,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.getById(id); const user = await this.userRepository.update(id, data);
if (!user) { if (!user) {
return { return {
@ -194,8 +180,6 @@ export default class UserService {
}; };
} }
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
/** /**
* Update user role * Update user role
*/ */
@ -203,18 +187,27 @@ export default class UserService {
const role = await this.roleRepository.getBySlug(data.role); const role = await this.roleRepository.getBySlug(data.role);
if (role) { if (role) {
await ctx.bouncer.with('RolePolicy').authorize('manage', [data.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 this.userRepository.updateRoles(user, [role]); await this.userRepository.updateRoles(user, [role]);
} }
} }
const updated = await this.userRepository.update(id, data);
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,
message: 'User updated.', message: 'User updated.',
data: updated || {}, data: user,
}; };
} }
@ -224,8 +217,8 @@ export default class UserService {
* @param id User id * @param id User id
* @returns Response * @returns Response
*/ */
public async deleteUser(id: string | number, ctx: HttpContextContract): Promise<IResponse> { public async deleteUser(id: string | number): Promise<IResponse> {
const user = await this.userRepository.getById(id); const user = await this.userRepository.delete(id);
if (!user) { if (!user) {
return { return {
@ -239,15 +232,6 @@ 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 { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,

View File

@ -9,11 +9,10 @@ export default class CreateLessonValidator {
title: schema.string({}, [rules.maxLength(255)]), title: schema.string({}, [rules.maxLength(255)]),
description: schema.string(), description: schema.string(),
duration: schema.date({ format: 'HH:mm:ss' }), duration: schema.date({ format: 'HH:mm:ss' }),
video: schema.file.optional({ video: schema.file({
size: '5000mb', size: '5000mb',
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], 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( materials: schema.array.optional().members(
schema.file({ schema.file({
size: '100mb', size: '100mb',

View File

@ -13,7 +13,6 @@ export default class UpdateLessonValidator {
size: '5000mb', size: '5000mb',
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], 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( materials: schema.array.nullableAndOptional().members(
schema.file({ schema.file({
size: '100mb', size: '100mb',

View File

@ -11,7 +11,6 @@ export default class Lessons extends BaseSchema {
table.integer('display_order').notNullable(); table.integer('display_order').notNullable();
table.integer('color_id').unsigned().references('colors.id'); table.integer('color_id').unsigned().references('colors.id');
table.string('description'); table.string('description');
table.text('linked_video_url').nullable();
table.time('duration').notNullable(); table.time('duration').notNullable();
table.timestamp('created_at', { useTz: true }); table.timestamp('created_at', { useTz: true });
table.timestamp('updated_at', { useTz: true }); table.timestamp('updated_at', { useTz: true });

View File

@ -7,13 +7,7 @@ export default class CourseSeeder extends BaseSeeder {
public async run() { public async run() {
this.CourseFactory = CourseFactory; this.CourseFactory = CourseFactory;
const LESSONS_COUNT = 10; await this.CourseFactory.with('lessons', 10, lessonFactory => lessonFactory.with('content'))
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('category')
.with('teacher') .with('teacher')
.createMany(3); .createMany(3);

View File

@ -11,48 +11,23 @@ export default class UserSeeder extends BaseSeeder {
*/ */
const roles = { const roles = {
admin: await Role.findByOrFail('slug', 'admin'), admin: await Role.findByOrFail('slug', 'admin'),
teacher: await Role.findByOrFail('slug', 'teacher'),
student: await Role.findByOrFail('slug', 'student'), student: await Role.findByOrFail('slug', 'student'),
}; };
/** /**
* Administrator * Administrator
*/ */
const administrator = await User.create({ const userAdmin = await User.create({
first_name: 'John', first_name: 'John',
last_name: 'Doe', last_name: 'Doe',
login: 'admin', login: 'admin',
password: '123456', password: '123456',
email: 'administrator@example.com', email: 'administrator@example.com',
}); });
await administrator.related('roles').attach([roles.admin.id]); await userAdmin.related('roles').attach([roles.admin.id]);
/** /**
* Teacher * Students
*/
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); await StudentFactory.with('contacts').createMany(20);
} }

1
env.ts
View File

@ -15,6 +15,7 @@
import Env from '@ioc:Adonis/Core/Env'; import Env from '@ioc:Adonis/Core/Env';
export default Env.rules({ export default Env.rules({
HOST: Env.schema.string({ format: 'host' }),
PORT: Env.schema.number(), PORT: Env.schema.number(),
APP_KEY: Env.schema.string(), APP_KEY: Env.schema.string(),
APP_NAME: Env.schema.string(), APP_NAME: Env.schema.string(),

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "1.4.2", "version": "1.3.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "1.4.2", "version": "1.3.1",
"dependencies": { "dependencies": {
"@adonisjs/application": "^5.2.5", "@adonisjs/application": "^5.2.5",
"@adonisjs/attachment-lite": "^1.0.7", "@adonisjs/attachment-lite": "^1.0.7",

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "1.4.5", "version": "1.3.2",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "node ace build --production", "build": "node ace build --production",

View File

@ -6,6 +6,11 @@
*/ */
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';
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -29,7 +34,22 @@ import Bouncer from '@ioc:Adonis/Addons/Bouncer';
| NOTE: Always export the "actions" const from this file | NOTE: Always export the "actions" const from this file
|**************************************************************** |****************************************************************
*/ */
export const { actions } = Bouncer; 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;
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -54,7 +74,4 @@ export const { actions } = Bouncer;
| 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'),
});

View File

@ -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') .middleware('role:admin,teacher')
.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')