diff --git a/app/Controllers/Http/Api/v1/CoursesController.ts b/app/Controllers/Http/Api/v1/CoursesController.ts index a0b4ad2..0aaa60f 100644 --- a/app/Controllers/Http/Api/v1/CoursesController.ts +++ b/app/Controllers/Http/Api/v1/CoursesController.ts @@ -1,6 +1,11 @@ import { Exception } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +/** + * Datatype + */ +import Role from 'App/Datatype/Role'; + /** * Models */ @@ -10,10 +15,10 @@ import User from 'App/Models/User'; /** * Validators */ -import AddCourseToUserValidator from 'App/Validators/AddCourseToUserValidator'; -import CreateCourseValidator from 'App/Validators/CreateCourseValidator'; -import DelCourseFromUserValidator from 'App/Validators/DelCourseFromUserValidator'; -import UpdateCourseValidator from 'App/Validators/UpdateCourseValidator'; +import AddCourseToUserValidator from 'App/Validators/Course/AddCourseToUserValidator'; +import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator'; +import DelCourseFromUserValidator from 'App/Validators/Course/DelCourseFromUserValidator'; +import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator'; export default class CoursesController { private readonly Course: typeof Course; @@ -130,7 +135,7 @@ export default class CoursesController { * Check user on role Teacher */ const roles = teacher.roles.map(r => r.slug); - if (!roles.includes('teacher')) { + if (!roles.includes(Role.TEACHER)) { throw new Exception(`User with id "${teacher.id}" not teacher.`, 400, 'ERR_USER_NOT_TEACHER'); } @@ -212,7 +217,7 @@ export default class CoursesController { const studentRoles = student.roles.map(r => r.slug); - if (!studentRoles.includes('student')) { + if (!studentRoles.includes(Role.STUDENT)) { throw new Exception(`User cannot be attached to the course without "student" role.`, 400, 'E_USER_ROLE'); } diff --git a/app/Controllers/Http/Api/v1/LessonsController.ts b/app/Controllers/Http/Api/v1/LessonsController.ts new file mode 100644 index 0000000..8b535cc --- /dev/null +++ b/app/Controllers/Http/Api/v1/LessonsController.ts @@ -0,0 +1,110 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +/** + * Models + */ +import Lesson from 'App/Models/Lesson'; +import Course from 'App/Models/Course'; + +/** + * Validators + */ +import CreateLessonValidator from 'App/Validators/Lesson/CreateLessonValidator'; +import UpdateLessonValidator from 'App/Validators/Lesson/UpdateLessonValidator'; + +import BaseController from '../../BaseController'; + +export default class LessonsController extends BaseController { + private readonly Lesson: typeof Lesson; + + private readonly Course: typeof Course; + + constructor() { + super(); + this.Lesson = Lesson; + this.Course = Course; + } + + /** + * List of all lessons + * GET /lessons + */ + public async list(ctx: HttpContextContract) { + const lessons = await this.Lesson.query(); + + return this.sendResponse(ctx, lessons, 'Fetched list lessons.'); + } + + /** + * Get one by id + * GET /lessons/:id + */ + public async show(ctx: HttpContextContract) { + const lesson = await this.Lesson.findOrFail(ctx.params.id); + + return this.sendResponse(ctx, lesson, 'Fetched lesson by id.'); + } + + /** + * Create new lesson + * POST /lessons + */ + public async create(ctx: HttpContextContract) { + const payload = await ctx.request.validate(CreateLessonValidator); + const course = await this.Course.findOrFail(payload.course_id); + const lesson = await this.Lesson.create({ + title: payload.title, + description: payload.description, + }); + + await lesson.related('course').associate(course); + await lesson.related('content').create({ + video_url: payload.video_url, + }); + + await lesson.load('content'); + + return this.sendResponse(ctx, lesson, 'Created new lesson.'); + } + + /** + * Delete lesson by id. + * DELETE /lessons/:id + */ + public async delete(ctx: HttpContextContract) { + const lesson = await this.Lesson.findOrFail(ctx.params.id); + + await lesson.delete(); + + return this.sendResponse(ctx, lesson, 'Lesson deleted.'); + } + + /** + * Update lesson by id. + * PUT /lessons/:id + */ + public async update(ctx: HttpContextContract) { + const payload = await ctx.request.validate(UpdateLessonValidator); + const lesson = await this.Lesson.firstOrFail(ctx.params.id); + + await lesson + .merge({ + course_id: payload.course_id, + title: payload.title, + description: payload.description, + }) + .save(); + + return this.sendResponse(ctx, lesson, 'Lesson updated.'); + } + + /** + * Get lesson content by id + * GET /lessons/:id/content + */ + public async getContent(ctx: HttpContextContract) { + const lesson = await this.Lesson.query().preload('content').where('id', ctx.params.id).firstOrFail(); + + return this.sendResponse(ctx, lesson.content, 'Lesson content fetched.'); + } +} diff --git a/app/Controllers/Http/Api/v1/MeController.ts b/app/Controllers/Http/Api/v1/MeController.ts index e91237d..10ac882 100644 --- a/app/Controllers/Http/Api/v1/MeController.ts +++ b/app/Controllers/Http/Api/v1/MeController.ts @@ -11,8 +11,8 @@ import Contact from 'App/Models/Contact'; /** * Validators */ -import ChangePasswordValidator from 'App/Validators/ChangePasswordValidator'; -import UpdateContactsValidator from 'App/Validators/UpdateContactsValidator'; +import ChangePasswordValidator from 'App/Validators/Password/ChangePasswordValidator'; +import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator'; export default class MeController { private readonly guard: 'api'; diff --git a/app/Controllers/Http/Api/v1/UsersController.ts b/app/Controllers/Http/Api/v1/UsersController.ts index 70c9d19..d5e1efc 100644 --- a/app/Controllers/Http/Api/v1/UsersController.ts +++ b/app/Controllers/Http/Api/v1/UsersController.ts @@ -10,17 +10,20 @@ import User from 'App/Models/User'; /** * Validators */ -import AddRoleToUserValidator from 'App/Validators/AddRoleToUserValidator'; -import CreateUserValidator from 'App/Validators/CreateUserValidator'; -import DelRoleFromUserValidator from 'App/Validators/DelRoleFromUserValidator'; -import UpdateUserValidator from 'App/Validators/UpdateUserValidator'; +import AddRoleToUserValidator from 'App/Validators/User/AddRoleToUserValidator'; +import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; +import DelRoleFromUserValidator from 'App/Validators/User/DelRoleFromUserValidator'; +import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; -export default class UsersController { +import BaseController from '../../BaseController'; + +export default class UsersController extends BaseController { private readonly User: typeof User; private readonly Role: typeof Role; constructor() { + super(); this.User = User; this.Role = Role; } diff --git a/app/Controllers/Http/BaseController.ts b/app/Controllers/Http/BaseController.ts new file mode 100644 index 0000000..82cd977 --- /dev/null +++ b/app/Controllers/Http/BaseController.ts @@ -0,0 +1,14 @@ +/* eslint-disable class-methods-use-this */ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class BaseController { + public sendResponse(ctx: HttpContextContract, data: object, message: string, status = 200): void { + const response = { + status, + message, + data, + }; + + return ctx.response.status(response.status).send(response); + } +} diff --git a/app/Datatype/Role.ts b/app/Datatype/Role.ts new file mode 100644 index 0000000..4e37fd7 --- /dev/null +++ b/app/Datatype/Role.ts @@ -0,0 +1,7 @@ +enum Role { + ADMIN = 'admin', + TEACHER = 'teacher', + STUDENT = 'student', +} + +export default Role; diff --git a/app/Middleware/Role.ts b/app/Middleware/Role.ts index 4844bf2..ac9b78b 100644 --- a/app/Middleware/Role.ts +++ b/app/Middleware/Role.ts @@ -5,14 +5,14 @@ export default class Role { error: { message: string; status: number; - errorCode: string; + code: string; }; constructor() { this.error = { message: 'You dont have permission to perform that action.', status: 403, - errorCode: 'E_ACCESS_DENIED', + code: 'E_ACCESS_DENIED', }; } @@ -28,7 +28,7 @@ export default class Role { /** * Failed to verify user rights */ - throw new Exception(this.error.message, this.error.status, this.error.errorCode); + throw new Exception(this.error.message, this.error.status, this.error.code); } } diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 97c08ed..ca29d85 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -1,7 +1,9 @@ /* eslint-disable no-param-reassign */ import { DateTime } from 'luxon'; import { nanoid } from 'nanoid'; -import { BaseModel, beforeCreate, column } from '@ioc:Adonis/Lucid/Orm'; +import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasOne, hasOne } from '@ioc:Adonis/Lucid/Orm'; +import Course from './Course'; +import LessonContent from './LessonContent'; export default class Lesson extends BaseModel { @column({ isPrimary: true }) @@ -16,6 +18,16 @@ export default class Lesson extends BaseModel { @column() public description: string; + @belongsTo(() => Course, { + foreignKey: 'course_id', + }) + public course: BelongsTo; + + @hasOne(() => LessonContent, { + foreignKey: 'lesson_id', + }) + public content: HasOne; + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; diff --git a/app/Models/LessonContent.ts b/app/Models/LessonContent.ts new file mode 100644 index 0000000..940e66e --- /dev/null +++ b/app/Models/LessonContent.ts @@ -0,0 +1,19 @@ +import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'; +import { DateTime } from 'luxon'; + +export default class LessonContent extends BaseModel { + @column({ isPrimary: true }) + public id: string; + + @column() + public lesson_id: string; + + @column() + public video_url: string; + + @column.dateTime({ autoCreate: true, serializeAs: null }) + public createdAt: DateTime; + + @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) + public updatedAt: DateTime; +} diff --git a/app/Models/User.ts b/app/Models/User.ts index f63ce36..dd9cf3d 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -73,7 +73,7 @@ export default class User extends BaseModel { }); await user.related('roles').attach(inputRoles.map(r => r.id)); - await user.load('roles'); // load updated roles + await user.load('roles'); } /** diff --git a/app/Validators/UpdateContactsValidator.ts b/app/Validators/Contacts/UpdateContactsValidator.ts similarity index 100% rename from app/Validators/UpdateContactsValidator.ts rename to app/Validators/Contacts/UpdateContactsValidator.ts diff --git a/app/Validators/AddCourseToUserValidator.ts b/app/Validators/Course/AddCourseToUserValidator.ts similarity index 100% rename from app/Validators/AddCourseToUserValidator.ts rename to app/Validators/Course/AddCourseToUserValidator.ts diff --git a/app/Validators/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts similarity index 100% rename from app/Validators/CreateCourseValidator.ts rename to app/Validators/Course/CreateCourseValidator.ts diff --git a/app/Validators/DelCourseFromUserValidator.ts b/app/Validators/Course/DelCourseFromUserValidator.ts similarity index 100% rename from app/Validators/DelCourseFromUserValidator.ts rename to app/Validators/Course/DelCourseFromUserValidator.ts diff --git a/app/Validators/UpdateCourseValidator.ts b/app/Validators/Course/UpdateCourseValidator.ts similarity index 100% rename from app/Validators/UpdateCourseValidator.ts rename to app/Validators/Course/UpdateCourseValidator.ts diff --git a/app/Validators/Lesson/CreateLessonValidator.ts b/app/Validators/Lesson/CreateLessonValidator.ts new file mode 100644 index 0000000..1827c2a --- /dev/null +++ b/app/Validators/Lesson/CreateLessonValidator.ts @@ -0,0 +1,15 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class CreateLessonValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + course_id: schema.string({}, [rules.exists({ table: 'courses', column: 'id' })]), + title: schema.string({}, [rules.maxLength(255)]), + description: schema.string(), + video_url: schema.string(), + }); + + public messages = {}; +} diff --git a/app/Validators/Lesson/UpdateLessonValidator.ts b/app/Validators/Lesson/UpdateLessonValidator.ts new file mode 100644 index 0000000..6dca473 --- /dev/null +++ b/app/Validators/Lesson/UpdateLessonValidator.ts @@ -0,0 +1,14 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class UpdateLessonValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + course_id: schema.string({}, [rules.exists({ table: 'courses', column: 'id' })]), + title: schema.string({}, [rules.maxLength(255)]), + description: schema.string(), + }); + + public messages = {}; +} diff --git a/app/Validators/ChangePasswordValidator.ts b/app/Validators/Password/ChangePasswordValidator.ts similarity index 100% rename from app/Validators/ChangePasswordValidator.ts rename to app/Validators/Password/ChangePasswordValidator.ts diff --git a/app/Validators/AddRoleToUserValidator.ts b/app/Validators/User/AddRoleToUserValidator.ts similarity index 100% rename from app/Validators/AddRoleToUserValidator.ts rename to app/Validators/User/AddRoleToUserValidator.ts diff --git a/app/Validators/CreateUserValidator.ts b/app/Validators/User/CreateUserValidator.ts similarity index 100% rename from app/Validators/CreateUserValidator.ts rename to app/Validators/User/CreateUserValidator.ts diff --git a/app/Validators/DelRoleFromUserValidator.ts b/app/Validators/User/DelRoleFromUserValidator.ts similarity index 100% rename from app/Validators/DelRoleFromUserValidator.ts rename to app/Validators/User/DelRoleFromUserValidator.ts diff --git a/app/Validators/UpdateUserValidator.ts b/app/Validators/User/UpdateUserValidator.ts similarity index 100% rename from app/Validators/UpdateUserValidator.ts rename to app/Validators/User/UpdateUserValidator.ts diff --git a/database/migrations/1627310102207_lessons.ts b/database/migrations/1628016919501_lessons.ts similarity index 100% rename from database/migrations/1627310102207_lessons.ts rename to database/migrations/1628016919501_lessons.ts diff --git a/database/migrations/1628017319337_lesson_contents.ts b/database/migrations/1628017319337_lesson_contents.ts new file mode 100644 index 0000000..ea5c422 --- /dev/null +++ b/database/migrations/1628017319337_lesson_contents.ts @@ -0,0 +1,19 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class LessonContents extends BaseSchema { + protected tableName = 'lesson_contents'; + + public async up() { + this.schema.createTable(this.tableName, table => { + table.increments('id'); + table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE'); + table.string('video_url').notNullable(); + table.timestamp('created_at', { useTz: true }); + table.timestamp('updated_at', { useTz: true }); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index edbe4c0..fac334f 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -24,7 +24,6 @@ Route.group(() => { * Me module * */ - require('./me'); /** @@ -32,8 +31,14 @@ Route.group(() => { * Courses module * */ - require('./courses'); + + /** + * + * Lessons module + * + */ + require('./lessons'); }).middleware('auth'); }) .prefix('v1') diff --git a/routes/apis/v1/lessons.ts b/routes/apis/v1/lessons.ts new file mode 100644 index 0000000..b18edb1 --- /dev/null +++ b/routes/apis/v1/lessons.ts @@ -0,0 +1,12 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.group(() => { + Route.get('/', 'Api/v1/LessonsController.list').middleware('role:admin,teacher,student').as('lessons.list'); + Route.post('/', 'Api/v1/LessonsController.create').middleware('role:admin,teacher').as('lessons.create'); + Route.get('/:id', 'Api/v1/LessonsController.show').middleware('role:admin,teacher,student').as('lessons.show'); + Route.delete('/:id', 'Api/v1/LessonsController.delete').middleware('role:admin,teacher').as('lessons.delete'); + Route.put('/:id', 'Api/v1/LessonsController.update').middleware('role:admin,teacher').as('lessons.update'); + Route.get('/:id/content', 'Api/v1/LessonsController.getContent') + .middleware('role:admin,teacher,student') + .as('lessons.get-content'); +}).prefix('lessons');