diff --git a/app/Controllers/Http/Api/v1/LessonsController.ts b/app/Controllers/Http/Api/v1/LessonsController.ts index 1cd2e82..3ac125c 100644 --- a/app/Controllers/Http/Api/v1/LessonsController.ts +++ b/app/Controllers/Http/Api/v1/LessonsController.ts @@ -1,3 +1,4 @@ +import Application from '@ioc:Adonis/Core/Application'; import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; @@ -6,6 +7,11 @@ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; */ import LessonService from 'App/Services/LessonService'; +/** + * Models + */ +import LessonMaterial from 'App/Models/LessonMaterial'; + /** * Validators */ @@ -96,7 +102,7 @@ export default class LessonsController extends BaseController { } /** - * Get lesson content by id + * Get lesson content by lesson id * GET /lessons/:id/content */ public async getContent(ctx: HttpContextContract) { @@ -108,6 +114,29 @@ export default class LessonsController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + + /** + * Get lesson materials by file name + * GET /lessons/materials/:file + */ + public async getMaterial(ctx: HttpContextContract) { + const { file } = ctx.request.params(); + const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, file); + + if (!success && error) { + throw new Exception(message, status, error.code); + } + + /** + * Send file to user + */ + if (data instanceof LessonMaterial) { + const path = Application.makePath(data.url); + return this.sendFile(ctx, path, data.clientName); + } + + return this.sendResponse(ctx, data, message, status); + } } new Ioc().make(LessonsController); diff --git a/app/Controllers/Http/BaseController.ts b/app/Controllers/Http/BaseController.ts index 7acac39..ea36c24 100644 --- a/app/Controllers/Http/BaseController.ts +++ b/app/Controllers/Http/BaseController.ts @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this */ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import { ResponseContract } from '@ioc:Adonis/Core/Response'; export default class BaseController { public sendResponse(ctx: HttpContextContract, data: object, message: string, status = 200, meta?: any): void { @@ -13,7 +14,7 @@ export default class BaseController { return ctx.response.status(response.status).send(response); } - public sendFile(ctx: HttpContextContract, path: string) { - return ctx.response.download(path); + public sendFile(ctx: HttpContextContract, ...args: Parameters) { + return ctx.response.attachment(...args); } } diff --git a/app/Datatypes/Interfaces/IResponse.ts b/app/Datatypes/Interfaces/IResponse.ts index 6299338..7af5254 100644 --- a/app/Datatypes/Interfaces/IResponse.ts +++ b/app/Datatypes/Interfaces/IResponse.ts @@ -1,8 +1,8 @@ -export default interface IResponse { +export default interface IResponse { success: boolean; status: number; message: string; - data: object; + data: T | {}; meta?: { pagination?: any; }; diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 686db69..dc81f31 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -3,6 +3,7 @@ import { DateTime } from 'luxon'; import { nanoid } from 'nanoid'; import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasOne, hasOne } from '@ioc:Adonis/Lucid/Orm'; import Course from './Course'; +// eslint-disable-next-line import/no-cycle import LessonContent from './LessonContent'; export default class Lesson extends BaseModel { diff --git a/app/Models/LessonContent.ts b/app/Models/LessonContent.ts index 8cd7dc2..70e0cc2 100644 --- a/app/Models/LessonContent.ts +++ b/app/Models/LessonContent.ts @@ -1,5 +1,7 @@ -import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'; +/* eslint-disable import/no-cycle */ +import { BaseModel, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'; import { DateTime } from 'luxon'; +import Lesson from './Lesson'; import LessonMaterial from './LessonMaterial'; export default class LessonContent extends BaseModel { @@ -15,6 +17,9 @@ export default class LessonContent extends BaseModel { @hasMany(() => LessonMaterial, { foreignKey: 'lesson_content_id' }) public materials: HasMany; + @belongsTo(() => Lesson, { foreignKey: 'lesson_id' }) + public lesson: BelongsTo; + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; diff --git a/app/Models/LessonMaterial.ts b/app/Models/LessonMaterial.ts index c0c01a1..ea75db8 100644 --- a/app/Models/LessonMaterial.ts +++ b/app/Models/LessonMaterial.ts @@ -1,5 +1,7 @@ -import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'; +import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'; import { DateTime } from 'luxon'; +// eslint-disable-next-line import/no-cycle +import LessonContent from './LessonContent'; export default class LessonMaterial extends BaseModel { @column({ isPrimary: true }) @@ -11,12 +13,20 @@ export default class LessonMaterial extends BaseModel { @column() public name: string; + @column() + public clientName: string; + @column() public ext: string; @column() public lesson_content_id: number; + @belongsTo(() => LessonContent, { + foreignKey: 'lesson_content_id', + }) + public content: BelongsTo; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Repositories/LessonRepository.ts b/app/Repositories/LessonRepository.ts index 33bd427..19c7d5f 100644 --- a/app/Repositories/LessonRepository.ts +++ b/app/Repositories/LessonRepository.ts @@ -1,8 +1,10 @@ /** * Models */ +import Drive from '@ioc:Adonis/Core/Drive'; import Course from 'App/Models/Course'; import Lesson from 'App/Models/Lesson'; +import LessonMaterial from 'App/Models/LessonMaterial'; /** * Validators @@ -13,8 +15,11 @@ import UpdateLessonValidator from 'App/Validators/Lesson/UpdateLessonValidator'; export default class LessonRepository { private Lesson: typeof Lesson; + private LessonMaterial: typeof LessonMaterial; + constructor() { this.Lesson = Lesson; + this.LessonMaterial = LessonMaterial; } /** @@ -49,6 +54,17 @@ export default class LessonRepository { return lesson && lesson.content; } + /** + * Get lesson material + * + * @param fileName Name of file + * @returns Lesson material file or null + */ + public async getMaterialFileByName(fileName: string): Promise { + const material = await this.LessonMaterial.query().where('name', fileName).first(); + return material; + } + /** * Create lesson * @@ -79,10 +95,12 @@ export default class LessonRepository { */ await file.moveToDisk('materials'); if (file.state === 'moved') { + const url = await Drive.getUrl(`materials/${file.fileName}`); await content.related('materials').create({ - name: file.clientName, + name: file.fileName, + clientName: file.clientName, ext: file.extname, - url: file.filePath, + url, }); } }) diff --git a/app/Services/LessonService.ts b/app/Services/LessonService.ts index 5446e0e..b3401cd 100644 --- a/app/Services/LessonService.ts +++ b/app/Services/LessonService.ts @@ -1,14 +1,13 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; -// import { AuthContract } from '@ioc:Adonis/Addons/Auth'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; /** * Datatypes */ import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; -// import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; import IResponse from 'App/Datatypes/Interfaces/IResponse'; -// import RoleHelper from 'App/Helpers/RoleHelper'; +import LessonMaterial from 'App/Models/LessonMaterial'; + /** * Repositories */ @@ -77,6 +76,55 @@ export default class LessonService { }; } + /** + * Fetch lesson material by file name + * + * @param fileName Name of file + * @returns Response + */ + public async fetchMaterialFile(ctx: HttpContextContract, fileName: string): Promise> { + const material = await this.lessonRepository.getMaterialFileByName(fileName); + + if (!material) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + data: {}, + message: 'Material not found.', + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + /** + * Allow user to view lesson material + */ + await material.load('content', q => q.preload('lesson')); + + const { + content: { lesson }, + } = material; + if (await ctx.bouncer.denies('viewLessonContent', lesson)) { + return { + success: false, + status: HttpStatusEnum.FORBIDDEN, + message: 'You are not able to view this file.', + data: {}, + error: { + code: 'E_FORBIDDEN', + }, + }; + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched lesson material.', + data: material, + }; + } + /** * Create lesson * diff --git a/database/migrations/1637080400230_lesson_materials.ts b/database/migrations/1637080400230_lesson_materials.ts index dbf2eda..90b82a3 100644 --- a/database/migrations/1637080400230_lesson_materials.ts +++ b/database/migrations/1637080400230_lesson_materials.ts @@ -8,6 +8,7 @@ export default class LessonMaterials extends BaseSchema { table.increments('id').primary(); table.string('url').notNullable(); table.string('name').notNullable(); + table.string('client_name').notNullable(); table.string('ext').notNullable(); table.integer('lesson_content_id').unsigned().references('lesson_contents.id').onDelete('CASCADE'); table.timestamp('created_at', { useTz: true }); diff --git a/routes/apis/v1/lessons.ts b/routes/apis/v1/lessons.ts index b18edb1..e8d14ab 100644 --- a/routes/apis/v1/lessons.ts +++ b/routes/apis/v1/lessons.ts @@ -9,4 +9,7 @@ Route.group(() => { Route.get('/:id/content', 'Api/v1/LessonsController.getContent') .middleware('role:admin,teacher,student') .as('lessons.get-content'); + Route.get('/materials/:file', 'Api/v1/LessonsController.getMaterial') + .middleware('role:admin,teacher,student') + .as('lessons.get-material'); }).prefix('lessons');