diff --git a/.adonisrc.json b/.adonisrc.json index e25a497..e444ba7 100644 --- a/.adonisrc.json +++ b/.adonisrc.json @@ -24,7 +24,12 @@ "Database": "database", "Contracts": "contracts" }, - "preloads": ["./routes/index", "./start/kernel", "./start/event", "./start/bouncer"], + "preloads": [ + "./routes/index", + "./start/kernel", + "./start/event", + "./start/bouncer" + ], "providers": [ "./providers/AppProvider", "@adonisjs/core", @@ -34,9 +39,12 @@ "@adonisjs/mail", "@adonisjs/view", "@adonisjs/attachment-lite", - "@adonisjs/bouncer" + "@adonisjs/bouncer", + "@adonisjs/drive-s3" + ], + "aceProviders": [ + "@adonisjs/repl" ], - "aceProviders": ["@adonisjs/repl"], "metaFiles": [ { "pattern": "resources/views/**/*.edge", diff --git a/.env.example b/.env.example index 2dbe8bf..930d2db 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,8 @@ SMTP_PASSWORD= MAILGUN_API_KEY= MAILGUN_DOMAIN= CACHE_VIEWS=false +S3_KEY=dummyKey +S3_SECRET=dummySecret +S3_BUCKET=dummyBucket +S3_REGION=dummyRegion +S3_ENDPOINT=dummyEndpoint diff --git a/CHANGELOG.md b/CHANGELOG.md index bb780b7..b259d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,24 @@ - API deployment with Docker and Nginx web server. -## [1.0.1] - 2021-11-25 +## [1.1.0] - 2022-01-31 + +### Added + +- Cloud storate support (AWS S3) +- Websocket server with redis session storage (online counter) +- Videos controller in api (video viewing rights) +- Categories updating operations +- Statistics controller in api (online count, lessonc count, courses count) +- Materials count for each lessons +- Users pagination +- Batch actions with students (detaching from course) ### Fix - Creating materials for lesson - Contacts updating -### Added - -- Materials count for each lessons - ## [1.0.0] - 2021-11-25 ### Added diff --git a/ace-manifest.json b/ace-manifest.json index 0b1c0f2..d8e5080 100644 --- a/ace-manifest.json +++ b/ace-manifest.json @@ -83,7 +83,9 @@ ] }, "make:model": { - "settings": {}, + "settings": { + "loadApp": true + }, "commandPath": "@adonisjs/lucid/build/commands/MakeModel", "commandName": "make:model", "description": "Make a new Lucid model", diff --git a/app/Controllers/Http/Api/v1/CategoriesController.ts b/app/Controllers/Http/Api/v1/CategoriesController.ts index 7d778f5..7173ada 100644 --- a/app/Controllers/Http/Api/v1/CategoriesController.ts +++ b/app/Controllers/Http/Api/v1/CategoriesController.ts @@ -7,9 +7,10 @@ import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import CategoryService from 'App/Services/CategoryService'; /** - * Validatoes + * Validators */ import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator'; +import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator'; import BaseController from '../../BaseController'; @@ -78,6 +79,21 @@ export default class CategoriesController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + + /** + * Update category by id + * PATCH /categories/:id + */ + public async update(ctx: HttpContextContract) { + const payload = await ctx.request.validate(UpdateCategoryValidator); + const result = await this.categoryService.updateCategory(ctx.params.id, payload); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } } new Ioc().make(CategoriesController); diff --git a/app/Controllers/Http/Api/v1/CoursesController.ts b/app/Controllers/Http/Api/v1/CoursesController.ts index e39c128..c3bda0e 100644 --- a/app/Controllers/Http/Api/v1/CoursesController.ts +++ b/app/Controllers/Http/Api/v1/CoursesController.ts @@ -10,6 +10,8 @@ import CourseService from 'App/Services/CourseService'; * Validators */ import AddCourseToUserValidator from 'App/Validators/Course/AddCourseToUserValidator'; +import AttachStudentsValitator from 'App/Validators/Course/AttachStudentsValitator'; +import DetachStudentstValitator from 'App/Validators/Course/DetachStudentstValitator'; import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator'; import DelCourseFromUserValidator from 'App/Validators/Course/DelCourseFromUserValidator'; import SetCourseStatusValidator from 'App/Validators/Course/SetCourseStatusValidator'; @@ -71,7 +73,7 @@ export default class CoursesController extends BaseController { } /** - * Show Category of Course with "id" + * Show Category by course id * GET /courses/:id/category */ public async showCategory(ctx: HttpContextContract) { @@ -85,11 +87,11 @@ export default class CoursesController extends BaseController { } /** - * Show Lessons of ourse with "id" + * Show Lessons by course id * GET /courses/:id/lessons */ public async showLessons(ctx: HttpContextContract) { - const result = await this.courseService.fetchCourseLessons(ctx.params.id); + const result = await this.courseService.fetchCourseLessons(ctx.params.id, ctx); if (!result.success && result.error) { throw new Exception(result.message, result.status, result.error.code); @@ -171,6 +173,21 @@ export default class CoursesController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + /** + * Attach list of students to course + * POST /courses/:id/attach-student-list + */ + public async attachStudentList(ctx: HttpContextContract) { + const payload = await ctx.request.validate(AttachStudentsValitator); + const result = await this.courseService.attachStudentList(ctx.params.id, payload.students); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } + /** * Detach student from course * DELETE /courses/:id/detach-student @@ -186,6 +203,21 @@ export default class CoursesController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + /** + * Detach list of students to course + * POST /courses/:id/detach-student-list + */ + public async detachStudentList(ctx: HttpContextContract) { + const payload = await ctx.request.validate(DetachStudentstValitator); + const result = await this.courseService.detachStudentList(ctx.params.id, payload.students); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } + /** * Fetch count of students of course * GET /courses/:id/students/count diff --git a/app/Controllers/Http/Api/v1/LessonsController.ts b/app/Controllers/Http/Api/v1/LessonsController.ts index 90a9101..c7a6e1c 100644 --- a/app/Controllers/Http/Api/v1/LessonsController.ts +++ b/app/Controllers/Http/Api/v1/LessonsController.ts @@ -1,4 +1,3 @@ -import Application from '@ioc:Adonis/Core/Application'; import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { schema } from '@ioc:Adonis/Core/Validator'; @@ -49,7 +48,7 @@ export default class LessonsController extends BaseController { * GET /lessons/:id */ public async show(ctx: HttpContextContract) { - const result = await this.lessonService.fetchLesson(ctx.params.id); + const result = await this.lessonService.fetchLesson(ctx.params.id, ctx); if (!result.success && result.error) { throw new Exception(result.message, result.status, result.error.code); @@ -89,7 +88,7 @@ export default class LessonsController extends BaseController { /** * Update lesson by id. - * PUT /lessons/:id + * PATCH /lessons/:id */ public async update(ctx: HttpContextContract) { const payload = await ctx.request.validate(UpdateLessonValidator); @@ -118,22 +117,18 @@ export default class LessonsController extends BaseController { /** * Get lesson materials by file name - * GET /lessons/materials/:file + * GET /lessons/materials/:fileName */ public async getMaterial(ctx: HttpContextContract) { - const { file } = ctx.request.params(); - const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, file); + const { fileName } = ctx.request.params(); + const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, fileName); 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.sendFileFromDrive(`materials/${data.name}`, ctx); } return this.sendResponse(ctx, data, message, status); @@ -157,6 +152,20 @@ export default class LessonsController extends BaseController { return this.sendResponse(ctx, data, message, status); } + + /** + * Get information about progress of video + * GET /lessons/:id/progress + */ + public async getVideoProgress(ctx: HttpContextContract) { + const { data, message, status, success, error } = await this.lessonService.fetchLessonProgress(ctx.params.id, ctx); + + if (!success && error) { + throw new Exception(message, status, error.code); + } + + return this.sendResponse(ctx, data, message, status); + } } new Ioc().make(LessonsController); diff --git a/app/Controllers/Http/Api/v1/StatController.ts b/app/Controllers/Http/Api/v1/StatController.ts new file mode 100644 index 0000000..92291f7 --- /dev/null +++ b/app/Controllers/Http/Api/v1/StatController.ts @@ -0,0 +1,31 @@ +import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import StatService from 'App/Services/StatService'; + +import BaseController from '../../BaseController'; + +@inject() +export default class StatController extends BaseController { + private statService: StatService; + + constructor(statService: StatService) { + super(); + this.statService = statService; + } + + /** + * Return current stat about system + * GET /stat + */ + public async index(ctx: HttpContextContract) { + const result = await this.statService.fetchStatData(); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } +} + +new Ioc().make(StatController); diff --git a/app/Controllers/Http/BaseController.ts b/app/Controllers/Http/BaseController.ts index ea36c24..233532e 100644 --- a/app/Controllers/Http/BaseController.ts +++ b/app/Controllers/Http/BaseController.ts @@ -1,6 +1,8 @@ /* eslint-disable class-methods-use-this */ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { ResponseContract } from '@ioc:Adonis/Core/Response'; +import Drive from '@ioc:Adonis/Core/Drive'; +import { extname } from 'path'; export default class BaseController { public sendResponse(ctx: HttpContextContract, data: object, message: string, status = 200, meta?: any): void { @@ -14,7 +16,27 @@ export default class BaseController { return ctx.response.status(response.status).send(response); } + public sendStream(ctx: HttpContextContract, ...args: Parameters) { + return ctx.response.stream(...args); + } + public sendFile(ctx: HttpContextContract, ...args: Parameters) { return ctx.response.attachment(...args); } + + public async sendFileFromDrive(pathname: string, ctx: HttpContextContract) { + /** + * File not found + */ + if (!(await Drive.exists(pathname))) return ctx.response.notFound(); + + const { size } = await Drive.getStats(pathname); + + ctx.response.type(extname(pathname)); + ctx.response.header('content-length', size); + + const stream = await Drive.getStream(pathname); + + return this.sendStream(ctx, stream); + } } diff --git a/app/Controllers/Http/VideosController.ts b/app/Controllers/Http/VideosController.ts new file mode 100644 index 0000000..d4ab112 --- /dev/null +++ b/app/Controllers/Http/VideosController.ts @@ -0,0 +1,53 @@ +import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +/** + * Models + */ +import LessonVideo from 'App/Models/LessonVideo'; + +/** + * Services + */ +import LessonService from 'App/Services/LessonService'; + +import BaseController from './BaseController'; + +@inject() +export default class VideosController extends BaseController { + private lessonService: LessonService; + + constructor(lessonService: LessonService) { + super(); + this.lessonService = lessonService; + } + + /** + * Get video file of lesson by file name + * GET /video/:fileName + */ + public async getVideo(ctx: HttpContextContract) { + const { fileName } = ctx.request.params(); + const { data, message, status, success, error } = await this.lessonService.fetchVideoFile(ctx, fileName); + + if (!success && error) { + throw new Exception(message, status, error.code); + } + + if (data instanceof LessonVideo) { + /** + * Allow user to control video + */ + ctx.response.header('Accept-Ranges', 'bytes'); + + /** + * Send video + */ + return this.sendFileFromDrive(`videos/${data.name}`, ctx); + } + + return this.sendResponse(ctx, data, message, status); + } +} + +new Ioc().make(VideosController); diff --git a/app/Controllers/Ws/OnlineController.ts b/app/Controllers/Ws/OnlineController.ts new file mode 100644 index 0000000..9f59b86 --- /dev/null +++ b/app/Controllers/Ws/OnlineController.ts @@ -0,0 +1,92 @@ +import { Server, Socket } from 'socket.io'; +import { ClientToServerEvents, InterServerEvents, ServerToClientEvents, SocketData } from 'App/Services/WsService'; +import RedisSessionStore from 'App/Store/SessionStore'; + +export default class OnlineController { + public socket: Socket; + + public io: Server; + + public sessionStore: RedisSessionStore; + + constructor( + socket: Socket, + io: Server, + sessionStore: RedisSessionStore + ) { + this.socket = socket; + this.io = io; + this.sessionStore = sessionStore; + + this.onConnected(socket); + + this.socket.on('disconnect', reason => this.onDisconnected(reason)); + } + + public sendOnlineCount(online: number) { + this.socket.emit('user:online', online); + this.socket.broadcast.emit('user:online', online); + } + + public setUserOnline() { + const { sessionId, userId, userName } = this.socket.data; + + if (sessionId && userId && userName) { + /** + * Set session and send online count + */ + this.sessionStore + .saveSession(sessionId, { userId, userName, connected: true }) + .then(() => this.sessionStore.getOnlineSessionsCount()) + .then(online => this.sendOnlineCount(online)); + } + } + + public async setUserOffline() { + if (this.socket.data.userName && this.socket.data.sessionId && this.socket.data.userId) { + const matchingSockets = await this.io.in(this.socket.data.userId).allSockets(); + const isDisconnected = matchingSockets.size === 0; + + if (isDisconnected) { + const { sessionId, userId, userName } = this.socket.data; + /** + * Update connection flag of socket to flase and send online count + */ + this.sessionStore + .saveSession(sessionId, { userId, userName, connected: false }) + .then(() => this.sessionStore.getOnlineSessionsCount()) + .then(online => this.socket.broadcast.emit('user:online', online)); + } + } + } + + /** + * On socket connected + */ + public async onConnected(socket: Socket) { + /** + * Set user online on connected + */ + this.setUserOnline(); + + /** + * Broadcast to all clients about new connection + */ + if (this.socket.data.userName && this.socket.data.sessionId && this.socket.data.userId) { + socket.broadcast.emit('user:connected', { userId: this.socket.data.userId, userName: this.socket.data.userName }); + } + + /** + * Watch status of user and set session + */ + socket.on('user:status', () => this.setUserOnline()); + } + + /** + * On socket disconnected + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public onDisconnected(_reason: string) { + this.setUserOffline(); + } +} diff --git a/app/Datatypes/Enums/ColorEnum.ts b/app/Datatypes/Enums/ColorEnum.ts index 4ee5616..f66d33e 100644 --- a/app/Datatypes/Enums/ColorEnum.ts +++ b/app/Datatypes/Enums/ColorEnum.ts @@ -1,16 +1,22 @@ enum ColorEnum { - Beige = '#F5F5DC', IndianRed = '#CD5C5C', FireBrick = '#B22222', - Pink = '#FFC0CB', Tomato = '#FF6347', Orange = '#FFA500', Gold = '#FFD700', - Moccasin = '#FFE4B5', - Khaki = '#F0E68C', Purple = '#800080', Indigo = '#4B0082', Teal = '#008080', + Cyan = '#0BC5EA', + Crimson = '#DC143C', + Chocolate = '#D2691E', + Salmon = '#FA8072', + Violet = '#EE82EE', + Blue = '#0000FF', + SteelBlue = '#4682B4', + RosyBrown = '#BC8F8F', + DarkSlateGray = '#2F4F4F', + DimGray = '#696969', MediumVioletRed = '#C71585', MediumOrchid = '#BA55D3', MediumPurple = '#9370DB', diff --git a/app/Helpers/CookieHelper.ts b/app/Helpers/CookieHelper.ts new file mode 100644 index 0000000..2540ffc --- /dev/null +++ b/app/Helpers/CookieHelper.ts @@ -0,0 +1,14 @@ +export default class CookieHelper { + public static parseCookieString(string: string): { [key: string]: string } { + const values = string + .split(';') + .map(v => v.split('=')) + .reduce((acc, value) => { + const [key, ...rest] = value; + acc[decodeURIComponent(key.trim())] = decodeURIComponent(rest.join('=').trim()); + return acc; + }, {}); + + return values; + } +} diff --git a/app/Helpers/OpaqueTokenHelper.ts b/app/Helpers/OpaqueTokenHelper.ts new file mode 100644 index 0000000..34fb587 --- /dev/null +++ b/app/Helpers/OpaqueTokenHelper.ts @@ -0,0 +1,30 @@ +import crypto from 'crypto'; + +export default class OpaqueTokenHelper { + /** + * Parse token + * + * @param token token + * @returns Hashed token with id + */ + public static parseToken(token: string) { + const parts = token.split('.'); + + if (parts.length !== 2) { + throw new Error('E_INVALID_API_TOKEN'); + } + + const id = Buffer.from(parts[0], 'base64').toString('utf-8'); + + if (!id) { + throw new Error('E_INVALID_API_TOKEN'); + } + + const hashed = crypto.createHash('sha256').update(parts[1]).digest('hex'); + + return { + hashed, + id, + }; + } +} diff --git a/app/Middleware/Auth.ts b/app/Middleware/Auth.ts index 14f7874..b2d027b 100644 --- a/app/Middleware/Auth.ts +++ b/app/Middleware/Auth.ts @@ -31,8 +31,7 @@ export default class AuthMiddleware { * driver */ let guardLastAttempted: string | undefined; - const authPromises: Promise[] = guards.map(guard => auth.use(guard).check()); - const authResults: boolean[] = await Promise.all(authPromises); + const authResults: boolean[] = await Promise.all(guards.map(guard => auth.use(guard).check())); for (let i = 0; i < authResults.length; i += 1) { /** @@ -54,7 +53,7 @@ export default class AuthMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise, customGuards: (keyof GuardsList)[]) { /** - * Verify token from cookie and set the authorization header + * Set the authorization header for token */ const token = ctx.request.cookie('token'); if (token) { diff --git a/app/Middleware/Ws/Auth.ts b/app/Middleware/Ws/Auth.ts new file mode 100644 index 0000000..9ea6786 --- /dev/null +++ b/app/Middleware/Ws/Auth.ts @@ -0,0 +1,77 @@ +/* eslint-disable no-param-reassign */ +import { nanoid } from 'nanoid'; +import Redis from '@ioc:Adonis/Addons/Redis'; +import CookieHelper from 'App/Helpers/CookieHelper'; +import OpaqueTokenHelper from 'App/Helpers/OpaqueTokenHelper'; +import Ws, { ClientToServerEvents, InterServerEvents, ServerToClientEvents, SocketData } from 'App/Services/WsService'; +import { Socket } from 'socket.io'; +import { ExtendedError } from 'socket.io/dist/namespace'; + +export default class AuthMiddleware { + /** + * Checks the cookie token in headers and + * validates the opaque token + * + * @param socket + * @param next + * @returns void + */ + public static async authenticate( + socket: Socket, + next: (err?: ExtendedError | undefined) => void + ) { + /** + * Parse and validate token from cookie + */ + const cookie = socket.handshake.headers?.cookie; + + if (!cookie) { + return next(new Error('SocketErrors.MissingParameter')); + } + + const token = CookieHelper.parseCookieString(cookie)?.token; + + if (!token) { + return next(new Error('SocketErrors.MissingParameter')); + } + + const tokenData = JSON.parse(Buffer.from(token.split('.')[0].split(':')[1], 'base64').toString('utf-8')); + const parsed = OpaqueTokenHelper.parseToken(tokenData.message); + const data = await Redis.get(`api:${parsed.id}`); + + if (!data) { + return next(new Error('SocketErrors.BadCredentials')); + } + + /** + * Assign existing session to socket data + */ + const sessionId = socket.handshake.auth.sessionId as string | undefined; + + if (sessionId) { + const session = await Ws.sessionStore.findSession(sessionId); + + if (session) { + socket.data.sessionId = sessionId; + socket.data.userId = session.userId; + socket.data.userName = session.userName; + + return next(); + } + } + + /** + * Parse data from redis string and create new session + */ + const parsedData = JSON.parse(data); + + /** + * Set custom data to socket + */ + socket.data.sessionId = nanoid(); + socket.data.userId = parsedData.user_id as string; + socket.data.userName = parsedData.userName as string; + + return next(); + } +} diff --git a/app/Models/Category.ts b/app/Models/Category.ts index ee832c8..9bd6696 100644 --- a/app/Models/Category.ts +++ b/app/Models/Category.ts @@ -14,7 +14,7 @@ export default class Category extends BaseModel { public title: string; @column() - public description: string; + public description: string | null; @column({ serializeAs: null }) public color_id: number | null; @@ -24,10 +24,10 @@ export default class Category extends BaseModel { }) public color: BelongsTo; - @column.dateTime({ autoCreate: true, serializeAs: null }) + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; - @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) + @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime; /** diff --git a/app/Models/Course.ts b/app/Models/Course.ts index e8f0a9b..ff271e6 100644 --- a/app/Models/Course.ts +++ b/app/Models/Course.ts @@ -34,6 +34,9 @@ export default class Course extends BaseModel { @column() public description: string; + @column() + public education_description: string | null; + @column({ serializeAs: null }) public teacher_id: string; diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 3fb3dbd..973d17a 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -17,6 +17,8 @@ import ColorHelper from 'App/Helpers/ColorHelper'; import Course from './Course'; import LessonMaterial from './LessonMaterial'; import LessonContent from './LessonContent'; +import LessonProgress from './LessonProgress'; +import LessonVideo from './LessonVideo'; import Color from './Color'; export default class Lesson extends BaseModel { @@ -56,9 +58,19 @@ export default class Lesson extends BaseModel { }) public content: HasOne; + @hasOne(() => LessonVideo, { + foreignKey: 'lesson_id', + }) + public video: HasOne; + @hasMany(() => LessonMaterial, { foreignKey: 'lesson_id' }) public materials: HasMany; + @hasOne(() => LessonProgress, { + foreignKey: 'lesson_id', + }) + public progress: HasOne; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Models/LessonContent.ts b/app/Models/LessonContent.ts index 5a1edd1..17d7662 100644 --- a/app/Models/LessonContent.ts +++ b/app/Models/LessonContent.ts @@ -11,7 +11,7 @@ export default class LessonContent extends BaseModel { public lesson_id: string; @column() - public video_url: string; + public body: string; @belongsTo(() => Lesson, { foreignKey: 'lesson_id' }) public lesson: BelongsTo; diff --git a/app/Models/LessonMaterial.ts b/app/Models/LessonMaterial.ts index 44c3db5..87f37a8 100644 --- a/app/Models/LessonMaterial.ts +++ b/app/Models/LessonMaterial.ts @@ -8,10 +8,10 @@ export default class LessonMaterial extends BaseModel { public id: number; @column() - public url: string; + public name: string; @column() - public name: string; + public size: number; @column() public clientName: string; diff --git a/app/Models/LessonProgress.ts b/app/Models/LessonProgress.ts new file mode 100644 index 0000000..745e502 --- /dev/null +++ b/app/Models/LessonProgress.ts @@ -0,0 +1,28 @@ +/* eslint-disable import/no-cycle */ +import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'; +import Lesson from './Lesson'; +import User from './User'; + +export default class LessonProgress extends BaseModel { + @column({ isPrimary: true, serializeAs: null }) + public id: number; + + @column({ serializeAs: null }) + public lesson_id: string; + + @column({ serializeAs: null }) + public user_id: string; + + @column() + public is_watched: boolean; + + @belongsTo(() => Lesson, { + foreignKey: 'lesson_id', + }) + public lesson: BelongsTo; + + @belongsTo(() => User, { + foreignKey: 'user_id', + }) + public user: BelongsTo; +} diff --git a/app/Models/LessonVideo.ts b/app/Models/LessonVideo.ts new file mode 100644 index 0000000..87975fb --- /dev/null +++ b/app/Models/LessonVideo.ts @@ -0,0 +1,38 @@ +/* eslint-disable import/no-cycle */ +import { DateTime } from 'luxon'; +import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'; +import Lesson from './Lesson'; + +export default class LessonVideo extends BaseModel { + @column({ isPrimary: true }) + public id: number; + + @column({ serializeAs: null }) + public lesson_id: string; + + @column() + public name: string; + + @column() + public size: number; + + @column() + public clientName: string; + + @column() + public ext: string; + + @column() + public url: string; + + @belongsTo(() => Lesson, { + foreignKey: 'lesson_id', + }) + public lesson: BelongsTo; + + @column.dateTime({ autoCreate: true }) + public createdAt: DateTime; + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updatedAt: DateTime; +} diff --git a/app/Models/User.ts b/app/Models/User.ts index 4eb9bae..695fa6b 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -7,6 +7,8 @@ import { beforeSave, column, computed, + HasMany, + hasMany, HasOne, hasOne, ManyToMany, @@ -17,6 +19,7 @@ import { nanoid } from 'nanoid'; import Role from 'App/Models/Role'; import Contact from 'App/Models/Contact'; import Course from 'App/Models/Course'; +import LessonProgress from './LessonProgress'; export default class User extends BaseModel { @column({ isPrimary: true }) @@ -62,6 +65,11 @@ export default class User extends BaseModel { }) public likes: ManyToMany; + @hasMany(() => LessonProgress, { + foreignKey: 'user_id', + }) + public lessons_progress: HasMany; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Repositories/CategoryRepository.ts b/app/Repositories/CategoryRepository.ts index fe2c83a..3a16555 100644 --- a/app/Repositories/CategoryRepository.ts +++ b/app/Repositories/CategoryRepository.ts @@ -7,6 +7,7 @@ import Category from 'App/Models/Category'; * Validators */ import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator'; +import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator'; export default class CategoryRepository { private Category: typeof Category; @@ -67,4 +68,22 @@ export default class CategoryRepository { return null; } + + /** + * Update category by id + * + * @param id Category id + * @param data + * @returns + */ + public async update(id: string | number, data: UpdateCategoryValidator['schema']['props']): Promise { + const category = await this.Category.query().where('id', id).first(); + + if (category) { + await category.merge(data).save(); + return category; + } + + return null; + } } diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index 3d2ed62..138d1cf 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -49,7 +49,7 @@ export default class CourseRepository { * @returns List of courses */ public async getAll(params?: FetchCoursesValidator['schema']['props']): Promise { - const { status, category_id } = params || {}; + const { status, category_id, limit } = params || {}; const query = this.Course.query(); @@ -61,12 +61,17 @@ export default class CourseRepository { query.andWhere('status', status); } + if (limit) { + query.limit(limit); + } + const courses = await query .preload('color') .preload('category') .withCount('students') .withCount('likes') - .withCount('lessons'); + .withCount('lessons') + .orderBy('created_at', 'desc'); return courses; } @@ -81,9 +86,12 @@ export default class CourseRepository { const course = await this.Course.query() .preload('teacher') .preload('color') - .preload('category') + .preload('category', q => q.preload('color')) .preload('lessons', q => q.preload('color').orderBy('display_order', 'asc').withCount('materials')) - .preload('students') + .preload('students', q => q.preload('roles').preload('contacts')) + .withCount('likes') + .withCount('lessons') + .withCount('students') .where('id', id) .first(); @@ -153,8 +161,17 @@ export default class CourseRepository { * @param id Course id * @returns List of lessons or null */ - public async getLessons(id: string | number): Promise { - const course = await this.Course.query().select('id').preload('lessons').where('id', id).first(); + public async getLessons(id: string | number, userId: string): Promise { + const course = await this.Course.query() + .select('id') + .preload('lessons', q => + q + .preload('progress', p => p.where('user_id', userId)) + .preload('color') + .orderBy('display_order', 'asc') + ) + .where('id', id) + .first(); return course && course.lessons; } @@ -194,6 +211,7 @@ export default class CourseRepository { course.title = data.title; course.description = data.description; + course.education_description = data.education_description; course.status = data.status; course.teacher_id = data.teacher_id; course.category_id = data.category_id; @@ -255,6 +273,7 @@ export default class CourseRepository { course.merge({ title: data.title, description: data.description, + education_description: data.education_description, teacher_id: data.teacher_id, category_id: data.category_id, }); @@ -269,10 +288,11 @@ export default class CourseRepository { /** * Load other updated data */ - course.load('category'); - course.load('teacher'); + await course.load('category'); + await course.load('teacher'); await course.save(); + return course; } diff --git a/app/Repositories/LessonProgressRepository.ts b/app/Repositories/LessonProgressRepository.ts new file mode 100644 index 0000000..5dfab41 --- /dev/null +++ b/app/Repositories/LessonProgressRepository.ts @@ -0,0 +1,23 @@ +import LessonProgress from 'App/Models/LessonProgress'; + +export default class LessonProgressRepository { + private LessonProgress: typeof LessonProgress; + + constructor() { + this.LessonProgress = LessonProgress; + } + + public async get(user_id: string, lesson_id: string): Promise { + const progress = await this.LessonProgress.query() + .where('user_id', user_id) + .andWhere('lesson_id', lesson_id) + .first(); + + return progress; + } + + public async create(data: { user_id: string; lesson_id: string; is_watched: boolean }): Promise { + const progress = await this.LessonProgress.create(data); + return progress; + } +} diff --git a/app/Repositories/LessonRepository.ts b/app/Repositories/LessonRepository.ts index e93662b..934ee6e 100644 --- a/app/Repositories/LessonRepository.ts +++ b/app/Repositories/LessonRepository.ts @@ -1,4 +1,6 @@ import Drive from '@ioc:Adonis/Core/Drive'; +import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; +import { cuid } from '@ioc:Adonis/Core/Helpers'; /** * Models @@ -6,6 +8,7 @@ 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'; +import LessonVideo from 'App/Models/LessonVideo'; /** * Validators @@ -18,11 +21,14 @@ export default class LessonRepository { private LessonMaterial: typeof LessonMaterial; + private LessonVideo: typeof LessonVideo; + private Drive: typeof Drive; constructor() { this.Lesson = Lesson; this.LessonMaterial = LessonMaterial; + this.LessonVideo = LessonVideo; this.Drive = Drive; } @@ -43,7 +49,7 @@ export default class LessonRepository { * @returns Lesson or null */ public async getById(id: string | number): Promise { - const lesson = await this.Lesson.query().where('id', id).first(); + const lesson = await this.Lesson.query().where('id', id).preload('color').withCount('materials').first(); return lesson; } @@ -59,16 +65,32 @@ export default class LessonRepository { } /** - * Get lesson material + * Get lesson material by name * * @param fileName Name of file - * @returns Lesson material file or null + * @returns Lesson material or null */ public async getMaterialFileByName(fileName: string): Promise { const material = await this.LessonMaterial.query().where('name', fileName).first(); return material; } + /** + * Get lesson material by id + * + * @param id Material id + * @returns Lesson material or null + */ + public async getMaterialFileById(id: number): Promise { + const material = await this.LessonMaterial.query().where('id', id).first(); + return material; + } + + public async getVideoFileByName(fileName: string): Promise { + const video = await this.LessonVideo.query().where('name', fileName).first(); + return video; + } + /** * Create lesson * @@ -83,62 +105,30 @@ export default class LessonRepository { description: data.description, duration: data.duration.toFormat('HH:mm:ss'), display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1, + course_id: course.id, }); - /** - * Attach lesson to course - */ - await lesson.related('course').associate(course); - /** * Create content */ - await lesson.related('content').create({ video_url: data.video_url }); + await lesson.related('content').create({ body: '' }); + + /** + * Move video file to disk and save to database + */ + await this.createVideo(lesson, data.video); /** * Create materials */ if (data.materials) { - /** - * Move each file to disk - */ - const files = await Promise.all( - data.materials.map(async file => { - await file.moveToDisk('materials'); - return file; - }) - ); - - /** - * Create lesson material - */ - const materials = await Promise.all( - files.map(async file => { - if (file.state === 'moved' && file.fileName && file.extname) { - const url = await this.Drive.getUrl(`materials/${file.fileName}`); - const material = new LessonMaterial(); - - material.name = file.fileName; - material.clientName = file.clientName; - material.ext = file.extname; - material.url = url; - - return material; - } - - return null; - }) - ); - - /** - * Save lesson materials to database - */ - await lesson.related('materials').createMany(materials.filter((l): l is LessonMaterial => l !== null)); + await this.createMaterials(lesson, data.materials); } + /** * Load data */ - await lesson.load(loader => loader.load('content').load('materials')); + await lesson.load(loader => loader.load('content').load('materials').load('video').load('progress')); return lesson; } @@ -150,19 +140,51 @@ export default class LessonRepository { * @returns Updated lesson */ public async update(id: string | number, data: UpdateLessonValidator['schema']['props']): Promise { - const lesson = await this.Lesson.query().where('id', id).first(); + const lesson = await this.Lesson.query().preload('video').preload('color').where('id', id).first(); if (lesson) { - await lesson - .merge({ - course_id: data.course_id, - title: data.title, - description: data.description, - }) - .save(); + /** + * Update files + */ + lesson.merge({ + title: data.title, + description: data.description, + course_id: data.course_id, + }); + + /** + * Update video + */ + if (data.video) { + /** + * Delete video from drive and database + */ + if (lesson.video) { + await this.Drive.delete(`videos/${lesson.video.name}`); + } + + await lesson.related('video').query().delete(); + + /** + * Upload new video to drive and save to database + */ + await this.createVideo(lesson, data.video); + } + + /** + * Update materials + */ + if (data.materials && data.materials !== null) { + await this.deleteMaterials(lesson); + await this.createMaterials(lesson, data.materials); + } else if (data.materials === null) await this.deleteMaterials(lesson); + + await lesson.load(loader => loader.load('video').load('materials')); + await lesson.save(); return lesson; } + return null; } @@ -173,17 +195,14 @@ export default class LessonRepository { * @returns Deleted lesson or null */ public async delete(id: string | number): Promise { - const lesson = await this.Lesson.query().where('id', id).preload('materials').preload('color').first(); + const lesson = await this.Lesson.query().where('id', id).preload('video').preload('color').first(); if (lesson) { /** * Delete files from disk */ - await Promise.all( - lesson.materials.map(async file => { - await this.Drive.delete(`materials/${file.name}`); - }) - ); + await this.Drive.delete(`videos/${lesson.video.name}`); + await this.deleteMaterials(lesson); /** * Delete lesson from database @@ -194,4 +213,68 @@ export default class LessonRepository { return null; } + + // eslint-disable-next-line class-methods-use-this + public async createVideo(lesson: Lesson, video: MultipartFileContract): Promise { + const videoName = `${cuid()}.${video.extname}`; + await video.moveToDisk('videos', { name: videoName }); + if (video.state === 'moved') { + await lesson.related('video').create({ + name: videoName, + clientName: video.clientName, + ext: video.extname, + size: video.size, + url: `/video/${videoName}`, + }); + } + } + + // eslint-disable-next-line class-methods-use-this + public async createMaterials(lesson: Lesson, materials: MultipartFileContract[]): Promise { + /** + * Move each file to disk + */ + const files = await Promise.all( + materials.map(async file => { + const name = `${cuid()}.${file.extname}`; + await file.moveToDisk('materials', { name }); + return { file, name }; + }) + ); + + /** + * Create lesson material + */ + const promises = await Promise.all( + files.map(async ({ file, name }) => { + if (file.state === 'moved' && file.fileName && file.extname) { + const material = new LessonMaterial(); + + material.name = name; + material.size = file.size; + material.clientName = file.clientName; + material.ext = file.extname; + + return material; + } + + return null; + }) + ); + + /** + * Save lesson materials to database + */ + await lesson.related('materials').createMany(promises.filter((l): l is LessonMaterial => l !== null)); + } + + public async deleteMaterials(lesson: Lesson) { + await lesson.load('materials'); + await Promise.all( + lesson.materials.map(async material => { + await this.Drive.delete(`materials/${material.name}`); + await material.delete(); + }) + ); + } } diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts index 2ff2e3f..ed9b30e 100644 --- a/app/Repositories/UserRepository.ts +++ b/app/Repositories/UserRepository.ts @@ -30,6 +30,11 @@ export default class UserRepository { return data; } + public async getByIds(ids: Array): Promise> { + const data = await this.User.query().preload('contacts').preload('roles').whereIn('id', ids); + return data; + } + /** * Get user by column * @@ -64,7 +69,7 @@ export default class UserRepository { */ public async getAll(params?: any) { // eslint-disable-next-line @typescript-eslint/naming-convention - const { search, email, login, first_name, last_name, role, page = 1, limit = 10 }: any = params || {}; + const { search, email, login, first_name, last_name, role, page, limit }: any = params || {}; const query = this.User.query(); @@ -97,8 +102,21 @@ export default class UserRepository { query.whereHas('roles', q => q.where('slug', role)); } - const data = await query.preload('contacts').preload('roles').orderBy('created_at', 'desc').paginate(page, limit); - return data.toJSON(); + query.preload('contacts').preload('roles').orderBy('created_at', 'desc'); + + /** + * Return paginated users if provided params + */ + if (limit && page) { + const data = await query.paginate(page, limit); + return data.toJSON(); + } + + /** + * Return all users + */ + const data = await query; + return data; } /** diff --git a/app/Services/AuthService.ts b/app/Services/AuthService.ts index 226533f..6b6e47d 100644 --- a/app/Services/AuthService.ts +++ b/app/Services/AuthService.ts @@ -1,7 +1,6 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import Hash from '@ioc:Adonis/Core/Hash'; -import Logger from '@ioc:Adonis/Core/Logger'; /** * Datatypes @@ -68,17 +67,19 @@ export default class AuthService { const token = await ctx.auth.use(this.authGuard).generate(user, { expiresIn: '1d', userRoles: user.roles.map(role => role.slug), + userName: user.fullname, }); ctx.response.cookie('token', token.token); - Logger.info(`A token for user with id: "${token.user.id}" was successfully generated.`); - return { success: true, status: HttpStatusEnum.OK, message: 'Login success.', - data: token.toJSON(), + data: { + userId: user.id, + ...token.toJSON(), + }, }; } diff --git a/app/Services/CategoryService.ts b/app/Services/CategoryService.ts index 3b2454b..cfe78b0 100644 --- a/app/Services/CategoryService.ts +++ b/app/Services/CategoryService.ts @@ -15,6 +15,7 @@ import CategoryRepository from 'App/Repositories/CategoryRepository'; * Validators */ import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator'; +import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator'; @inject() export default class CategoryService { @@ -114,6 +115,38 @@ export default class CategoryService { data, }; } + + /** + * Update category + * + * @param id Category id + * @returns Updated category + */ + public async updateCategory( + id: string | number, + data: UpdateCategoryValidator['schema']['props'] + ): Promise { + const category = await this.categoryRepository.update(id, data); + + if (!category) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Category not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Category updated.', + data: category, + }; + } } new Ioc().make(CategoryService); diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts index 0b6623d..24fcfa7 100644 --- a/app/Services/CourseService.ts +++ b/app/Services/CourseService.ts @@ -1,5 +1,6 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { AuthContract } from '@ioc:Adonis/Addons/Auth'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; /** * Datatypes @@ -23,6 +24,7 @@ import CategoryRepository from 'App/Repositories/CategoryRepository'; import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator'; import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator'; import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator'; +import Course from 'App/Models/Course'; @inject() export default class CourseService { @@ -64,7 +66,7 @@ export default class CourseService { * @param id Course id * @returns Response */ - public async fetchCourse(id: string | number): Promise { + public async fetchCourse(id: string | number): Promise> { const data = await this.courseRepository.getById(id); if (!data) { @@ -151,8 +153,9 @@ export default class CourseService { * @param id Course id * @returns Response */ - public async fetchCourseLessons(id: string | number): Promise { - const data = await this.courseRepository.getLessons(id); + public async fetchCourseLessons(id: string | number, ctx: HttpContextContract): Promise { + const user = await ctx.auth.use('api').authenticate(); + const data = await this.courseRepository.getLessons(id, user.id); if (!data) { return { @@ -420,6 +423,93 @@ export default class CourseService { }; } + /** + * Attach list of students from course + * + * @param id Course id + * @param ids Student ids + * @returns Response + */ + public async attachStudentList(id: string | number, ids: Array): Promise { + const course = await this.courseRepository.getById(id); + + if (!course) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Course not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + const students = await this.userRepository.getByIds(ids); + + try { + await course.related('students').attach(students.map(s => s.id)); + } catch (error) { + if (error.code === '23505') { + return { + success: false, + status: HttpStatusEnum.BAD_REQUEST, + message: 'Some student already attached to that course.', + data: {}, + error: { + code: 'E_BAD_REQUEST', + }, + }; + } + throw new Error(error); + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Student list successfully attached.', + data: {}, + }; + } + + /** + * Detach list of students from course + * + * @param id Course id + * @param ids Student ids + * @returns Response + */ + public async detachStudentList(id: string | number, ids: Array): Promise { + const course = await this.courseRepository.getById(id); + + if (!course) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Course not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + const students = await this.userRepository.getByIds(ids); + + try { + await course.related('students').detach(students.map(s => s.id)); + } catch (error) { + throw new Error(error); + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Student list successfully detached from course.', + data: {}, + }; + } + public async detachUserCourse(id: string | number, studentId: string | number): Promise { const course = await this.courseRepository.getById(id); diff --git a/app/Services/LessonService.ts b/app/Services/LessonService.ts index dcba84c..cf0fe3b 100644 --- a/app/Services/LessonService.ts +++ b/app/Services/LessonService.ts @@ -8,11 +8,13 @@ import Database from '@ioc:Adonis/Lucid/Database'; import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; import IResponse from 'App/Datatypes/Interfaces/IResponse'; import LessonMaterial from 'App/Models/LessonMaterial'; +import LessonVideo from 'App/Models/LessonVideo'; /** * Repositories */ import CourseRepository from 'App/Repositories/CourseRepository'; +import LessonProgressRepository from 'App/Repositories/LessonProgressRepository'; import LessonRepository from 'App/Repositories/LessonRepository'; /** @@ -27,9 +29,16 @@ export default class LessonService { private lessonRepository: LessonRepository; - constructor(courseRepository: CourseRepository, lessonRepository: LessonRepository) { + private lessonProgressRepository: LessonProgressRepository; + + constructor( + courseRepository: CourseRepository, + lessonRepository: LessonRepository, + lessonProgressRepository: LessonProgressRepository + ) { this.courseRepository = courseRepository; this.lessonRepository = lessonRepository; + this.lessonProgressRepository = lessonProgressRepository; } /** @@ -54,7 +63,7 @@ export default class LessonService { * @param id Lesson id * @returns Response */ - public async fetchLesson(id: string | number): Promise { + public async fetchLesson(id: string | number, ctx: HttpContextContract) { const lesson = await this.lessonRepository.getById(id); if (!lesson) { @@ -69,6 +78,26 @@ 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', + }, + }; + } + + /** + * Load lesson with materials + */ + await lesson.load(loader => loader.load('content').load('materials').load('video')); + return { success: true, status: HttpStatusEnum.OK, @@ -83,8 +112,8 @@ export default class LessonService { * @param fileName Name of file * @returns Response */ - public async fetchMaterialFile(ctx: HttpContextContract, fileName: string): Promise> { - const material = await this.lessonRepository.getMaterialFileByName(fileName); + public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise> { + const material = await this.lessonRepository.getMaterialFileByName(name); if (!material) { return { @@ -123,6 +152,93 @@ export default class LessonService { }; } + public async fetchVideoFile(ctx: HttpContextContract, name: string): Promise> { + const video = await this.lessonRepository.getVideoFileByName(name); + + if (!video) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + data: {}, + message: 'Video not found.', + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + 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', + }, + }; + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched lesson video.', + data: video, + }; + } + + /** + * Fetch progress of lesson by lesson id + * + * @param id Lesson id + * @param ctx Http context + */ + public async fetchLessonProgress(id: string, ctx: HttpContextContract): Promise { + const user = await ctx.auth.use('api').authenticate(); + const lesson = await this.lessonRepository.getById(id); + + if (!lesson) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Lesson not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + 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); + + if (!progress) { + await this.lessonProgressRepository.create({ user_id: user.id, lesson_id: lesson.id, is_watched: true }); + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched lesson progress', + data: { + progress, + }, + }; + } + /** * Create lesson * @@ -144,6 +260,9 @@ export default class LessonService { }; } + /** + * Create new lesson + */ const lesson = await this.lessonRepository.create(course, data); return { diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index f10032e..5fb3854 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -37,7 +37,10 @@ export default class MeService { const user = await auth.use(this.authGuard).authenticate(); await user.load(loader => { - loader.load('roles').load('contacts'); + loader + .load('roles') + .load('contacts') + .load('likes', q => q.select('id')); }); /** diff --git a/app/Services/StatService.ts b/app/Services/StatService.ts new file mode 100644 index 0000000..d50ff32 --- /dev/null +++ b/app/Services/StatService.ts @@ -0,0 +1,37 @@ +import { inject, Ioc } from '@adonisjs/core/build/standalone'; +import IResponse from 'App/Datatypes/Interfaces/IResponse'; +import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; +import Database from '@ioc:Adonis/Lucid/Database'; +import Ws from './WsService'; + +@inject() +export default class StatService { + /** + * Get basic statistics about the system + * + * @returns Response + */ + // eslint-disable-next-line class-methods-use-this + public async fetchStatData(): Promise { + const count = await Database.rawQuery( + ` + select (select count(*) from courses) as courses_count, + (select count(*) from lessons) as lessons_count + ` + ); + + const online = await Ws.sessionStore.getOnlineSessionsCount(); + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched stat.', + data: { + ...count.rows[0], + online, + }, + }; + } +} + +new Ioc().make(StatService); diff --git a/app/Services/UserService.ts b/app/Services/UserService.ts index 97b11b6..df95a08 100644 --- a/app/Services/UserService.ts +++ b/app/Services/UserService.ts @@ -76,16 +76,24 @@ export default class UserService { */ public async fetchUsers(params?: any): Promise { const users = await this.userRepository.getAll(params); - - return { + const result: IResponse = { success: true, status: HttpStatusEnum.OK, message: 'Fetched all users.', - data: users.data, - meta: { - pagination: users.meta, - }, + data: {}, }; + + if (users instanceof Array) { + result.data = users; + return result; + } + + result.data = users.data; + result.meta = { + pagination: users.meta, + }; + + return result; } /** diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts new file mode 100644 index 0000000..2cc313f --- /dev/null +++ b/app/Services/WsService.ts @@ -0,0 +1,95 @@ +import { Server } from 'socket.io'; +import Redis from '@ioc:Adonis/Addons/Redis'; +import AdonisServer from '@ioc:Adonis/Core/Server'; + +/** + * Controllers + */ +import OnlineController from 'App/Controllers/Ws/OnlineController'; + +/** + * Middlewares + */ +import AuthMiddleware from 'App/Middleware/Ws/Auth'; +import RedisSessionStore from 'App/Store/SessionStore'; + +export interface ServerToClientEvents { + /** + * User + */ + 'user:session': (data: { sessionId: string | undefined; userId: string | undefined }) => void; + 'user:connected': (data: { userId: string; userName: string }) => void; + 'user:online': (data: number) => void; +} + +export interface ClientToServerEvents { + /** + * User + */ + 'user:logout': () => void; + 'user:status': () => void; +} + +export interface InterServerEvents {} + +export interface SocketData { + sessionId: string; + userId: string; + userName: string; +} + +class WsService { + public io: Server; + + public sessionStore: RedisSessionStore; + + public booted = false; + + public boot() { + if (this.booted) { + return; + } + + this.booted = true; + this.io = new Server(AdonisServer.instance!, { + cors: { + origin: 'http://localhost:3000', + credentials: true, + }, + }); + this.sessionStore = new RedisSessionStore(Redis.connection('session')); + + this.setupMiddlewares(); + this.listen(); + } + + private listen() { + this.io.on('connection', socket => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const onlineController = new OnlineController(socket, this.io, this.sessionStore); + + const { sessionId, userId, userName } = socket.data; + const isExistSocketData = !!(sessionId && userId && userName); + + /** + * Send session to client + */ + socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId }); + + /** + * Destroy session on user logout request + */ + socket.on('user:logout', async () => { + if (isExistSocketData) { + await this.sessionStore.destroySession(sessionId); + } + }); + }); + } + + private setupMiddlewares() { + this.io.use(AuthMiddleware.authenticate); + } +} + +export default new WsService(); diff --git a/app/Store/SessionStore.ts b/app/Store/SessionStore.ts new file mode 100644 index 0000000..12e43c6 --- /dev/null +++ b/app/Store/SessionStore.ts @@ -0,0 +1,108 @@ +/* eslint-disable max-classes-per-file */ +import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis'; + +type SessionDataType = { userId: string; userName: string; connected: boolean }; + +const SESSION_TTL = 24 * 60 * 60; // 1 day + +class RedisSessionStore { + public redisClient: RedisConnectionContract; + + constructor(redisClient: RedisConnectionContract) { + this.redisClient = redisClient; + } + + // eslint-disable-next-line class-methods-use-this + private transform(values: (string | null)[]) { + const [userId, userName, connected] = values; + + if (!userId || !userName || !connected) return undefined; + + return { + userId, + userName, + connected: connected === 'true', + }; + } + + /** + * Return session data by session id + * + * @param id Session id + * @returns Session data + */ + public async findSession(id: string): Promise { + const values = await this.redisClient.hmget(id, 'userId', 'userName', 'connected'); + return this.transform(values); + } + + /** + * Returns the all sessions from store + */ + public async getSessions(): Promise> { + const keys = new Set(); + let nextIndex = 0; + + do { + // eslint-disable-next-line no-await-in-loop + const [nextIndexAsStr, result] = await this.redisClient.scan(nextIndex, 'MATCH', '*', 'COUNT', 100); + nextIndex = parseInt(nextIndexAsStr, 10); + result.forEach(s => keys.add(s)); + } while (nextIndex !== 0); + const commands = Array.from(keys).map(key => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [prefix, ...id] = key.split('-'); + return ['hmget', id.join('-'), 'userId', 'userName', 'connected']; + }); + + return this.redisClient + .multi(commands) + .exec() + .then(results => results.map(([err, session]) => (err ? undefined : this.transform(session)))); + } + + /** + * Returns the unique sessions with connected flag `true` + */ + public async getOnlineSessionsCount() { + const sessions = await this.getSessions(); + const online = new Set(sessions.filter(s => s?.connected === true).map(s => s?.userId)); + + return online.size; + } + + /** + * Set session data to redis store + * + * @param id Session id + * @param data Session data + */ + public async saveSession(id: string, data: SessionDataType): Promise { + await this.redisClient + .multi() + .hmset(id, { + userId: data.userId, + userName: data.userName, + connected: data.connected, + }) + .expire(id, SESSION_TTL) + .exec(); + } + + /** + * Remove session from redis store + * + * @param id Session id + */ + public async destroySession(id: string): Promise { + const session = await this.findSession(id); + + if (session) { + return this.redisClient.del(id); + } + + return 0; + } +} + +export default RedisSessionStore; diff --git a/app/Validators/Category/CreateCategoryValidator.ts b/app/Validators/Category/CreateCategoryValidator.ts index 1895f46..3281ea1 100644 --- a/app/Validators/Category/CreateCategoryValidator.ts +++ b/app/Validators/Category/CreateCategoryValidator.ts @@ -6,7 +6,7 @@ export default class CreateCategoryValidator { public schema = schema.create({ title: schema.string(), - description: schema.string(), + description: schema.string.optional(), }); public messages = {}; diff --git a/app/Validators/Category/UpdateCategoryValidator.ts b/app/Validators/Category/UpdateCategoryValidator.ts new file mode 100644 index 0000000..6dff15e --- /dev/null +++ b/app/Validators/Category/UpdateCategoryValidator.ts @@ -0,0 +1,13 @@ +import { schema } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class UpdateCategoryValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + title: schema.string.optional(), + description: schema.string.nullableAndOptional(), + }); + + public messages = {}; +} diff --git a/app/Validators/Contacts/CreateContactsValidator.ts b/app/Validators/Contacts/CreateContactsValidator.ts index 999a5bb..e41d565 100644 --- a/app/Validators/Contacts/CreateContactsValidator.ts +++ b/app/Validators/Contacts/CreateContactsValidator.ts @@ -5,15 +5,15 @@ export default class CreateContactsValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - phone_number: schema.string.optional({}, [ + phone_number: schema.string.nullableAndOptional({}, [ rules.mobile({ locales: ['ru-RU', 'en-US'], strict: true, }), ]), - vk_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), - twitter_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), - telegram_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), + vk_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), + twitter_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), + telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]), }); public messages = {}; diff --git a/app/Validators/Course/AttachStudentsValitator.ts b/app/Validators/Course/AttachStudentsValitator.ts new file mode 100644 index 0000000..3a07d08 --- /dev/null +++ b/app/Validators/Course/AttachStudentsValitator.ts @@ -0,0 +1,23 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class AttachStudentsValitator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + students: schema.array().members( + schema.string({}, [ + rules.exists({ table: 'users', column: 'id' }), + rules.unique({ + table: 'users_courses', + column: 'user_id', + where: { + course_id: this.ctx.params.id, + }, + }), + ]) + ), + }); + + public messages = {}; +} diff --git a/app/Validators/Course/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts index 23d8ce1..337a186 100644 --- a/app/Validators/Course/CreateCourseValidator.ts +++ b/app/Validators/Course/CreateCourseValidator.ts @@ -8,6 +8,7 @@ export default class CreateCourseValidator { public schema = schema.create({ title: schema.string(), description: schema.string(), + education_description: schema.string.nullable(), image: schema.file.optional({ size: '10mb', extnames: ['jpg', 'jpeg', 'png'], diff --git a/app/Validators/Course/DetachStudentstValitator.ts b/app/Validators/Course/DetachStudentstValitator.ts new file mode 100644 index 0000000..27c89ff --- /dev/null +++ b/app/Validators/Course/DetachStudentstValitator.ts @@ -0,0 +1,12 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class DetachStudentstValitator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + students: schema.array().members(schema.string({}, [rules.exists({ table: 'users', column: 'id' })])), + }); + + public messages = {}; +} diff --git a/app/Validators/Course/FetchCoursesValidator.ts b/app/Validators/Course/FetchCoursesValidator.ts index aa12618..3b02f36 100644 --- a/app/Validators/Course/FetchCoursesValidator.ts +++ b/app/Validators/Course/FetchCoursesValidator.ts @@ -8,6 +8,7 @@ export default class FetchCoursesValidator { public schema = schema.create({ status: schema.enum.optional(Object.values(CourseStatusEnum)), category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]), + limit: schema.number.optional(), }); public messages = {}; diff --git a/app/Validators/Course/UpdateCourseValidator.ts b/app/Validators/Course/UpdateCourseValidator.ts index a5096c1..c8c8a8d 100644 --- a/app/Validators/Course/UpdateCourseValidator.ts +++ b/app/Validators/Course/UpdateCourseValidator.ts @@ -7,6 +7,7 @@ export default class UpdateCourseValidator { public schema = schema.create({ title: schema.string.optional(), description: schema.string.optional(), + education_description: schema.string.nullable(), image: schema.file.optional({ size: '10mb', extnames: ['jpg', 'jpeg', 'png'], diff --git a/app/Validators/Lesson/CreateLessonValidator.ts b/app/Validators/Lesson/CreateLessonValidator.ts index 5a82dba..976de89 100644 --- a/app/Validators/Lesson/CreateLessonValidator.ts +++ b/app/Validators/Lesson/CreateLessonValidator.ts @@ -9,10 +9,13 @@ export default class CreateLessonValidator { title: schema.string({}, [rules.maxLength(255)]), description: schema.string(), duration: schema.date({ format: 'HH:mm:ss' }), - video_url: schema.string({}, [rules.url()]), + video: schema.file({ + size: '5000mb', + extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], + }), materials: schema.array.optional().members( schema.file({ - size: '10mb', + size: '100mb', extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'], }) ), diff --git a/app/Validators/Lesson/UpdateLessonValidator.ts b/app/Validators/Lesson/UpdateLessonValidator.ts index 6dca473..909fc7d 100644 --- a/app/Validators/Lesson/UpdateLessonValidator.ts +++ b/app/Validators/Lesson/UpdateLessonValidator.ts @@ -5,9 +5,20 @@ 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(), + course_id: schema.string.optional({}, [rules.exists({ table: 'courses', column: 'id' })]), + title: schema.string.optional({}, [rules.maxLength(255)]), + description: schema.string.optional(), + duration: schema.date.optional({ format: 'HH:mm:ss' }), + video: schema.file.optional({ + size: '5000mb', + extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'], + }), + materials: schema.array.nullableAndOptional().members( + schema.file({ + size: '100mb', + extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'], + }) + ), }); public messages = {}; diff --git a/app/Validators/Validator.ts b/app/Validators/Validator.ts new file mode 100644 index 0000000..5eeef80 --- /dev/null +++ b/app/Validators/Validator.ts @@ -0,0 +1,31 @@ +import { validator, ParsedTypedSchema } from '@ioc:Adonis/Core/Validator'; + +export default abstract class Validator { + /** + * Object for schemas. + * Each key in object will correspong to a type of validation. + * Each values will be a valid Adonis schema, made using schema.create + */ + protected abstract schemas(type: string): ParsedTypedSchema; + + /** + * Object of error message objects used to pass custom error messages. + * Each key will correspond to a valid validation type defined in schemas + */ + protected errorMessages = {}; + + /** + * Cache key, to be used if validator schema caching is needed. + * Refer https://preview.adonisjs.com/guides/validator/schema-caching + */ + protected cacheKey: string; + + public async fire(data: { [key: string]: any }, type: string): Promise { + return validator.validate({ + schema: this.schemas(type), + data, + ...(this.errorMessages[type] ? { messages: this.errorMessages[type] } : {}), + ...(this.cacheKey ? { cacheKey: this.cacheKey } : {}), + }); + } +} diff --git a/config/database.ts b/config/database.ts index 6ca9598..56d9e86 100644 --- a/config/database.ts +++ b/config/database.ts @@ -50,7 +50,7 @@ const databaseConfig: DatabaseConfig = { paths: ['./database/seeders/MainSeeder'], }, healthCheck: true, - debug: true, + debug: false, }, }, }; diff --git a/config/drive.ts b/config/drive.ts index e957be5..74330f2 100644 --- a/config/drive.ts +++ b/config/drive.ts @@ -93,15 +93,15 @@ const driveConfig: DriveConfig = { |************************************************************************** | */ - // s3: { - // driver: 's3', - // visibility: 'public', - // key: Env.get('S3_KEY'), - // secret: Env.get('S3_SECRET'), - // region: Env.get('S3_REGION'), - // bucket: Env.get('S3_BUCKET'), - // endpoint: Env.get('S3_ENDPOINT'), - // }, + s3: { + driver: 's3', + visibility: 'private', + key: Env.get('S3_KEY'), + secret: Env.get('S3_SECRET'), + region: Env.get('S3_REGION'), + bucket: Env.get('S3_BUCKET'), + endpoint: Env.get('S3_ENDPOINT'), + }, /* |-------------------------------------------------------------------------- diff --git a/config/redis.ts b/config/redis.ts index 80e0bad..a19f00e 100644 --- a/config/redis.ts +++ b/config/redis.ts @@ -43,6 +43,14 @@ const redisConfig: RedisConfig = { keyPrefix: '', healthCheck: true, }, + session: { + host: Env.get('REDIS_HOST'), + port: Env.get('REDIS_PORT'), + password: Env.get('REDIS_PASSWORD', ''), + db: 1, + keyPrefix: 'session-', + healthCheck: true, + }, }, }; diff --git a/contracts/drive.ts b/contracts/drive.ts index 2d64030..26f8333 100644 --- a/contracts/drive.ts +++ b/contracts/drive.ts @@ -4,10 +4,10 @@ declare module '@ioc:Adonis/Core/Drive' { config: LocalDriverConfig; implementation: LocalDriverContract; }; - // s3: { - // config: S3DriverConfig - // implementation: S3DriverContract - // } + s3: { + config: S3DriverConfig; + implementation: S3DriverContract; + }; // gcs: { // config: GcsDriverConfig // implementation: GcsDriverContract diff --git a/contracts/redis.ts b/contracts/redis.ts index 1f2a08c..55f2fec 100644 --- a/contracts/redis.ts +++ b/contracts/redis.ts @@ -8,5 +8,6 @@ declare module '@ioc:Adonis/Addons/Redis' { interface RedisConnectionsList { local: RedisConnectionConfig; + session: RedisConnectionConfig; } } diff --git a/database/factories/index.ts b/database/factories/index.ts index b1c2c90..a602976 100644 --- a/database/factories/index.ts +++ b/database/factories/index.ts @@ -56,7 +56,7 @@ export const StudentFactory = Factory.define(User, ({ faker }) => makeFakeUser(f export const LessonContentFactory = Factory.define(LessonContent, ({ faker }) => { return { - video_url: `https://www.youtube.com/embed/${faker.datatype.string(10)}`, + body: `${faker.lorem.sentences(40)}`, }; }).build(); @@ -87,6 +87,7 @@ export const CourseFactory = Factory.define(Course, ({ faker }) => { return { title: faker.lorem.sentence(8), description: faker.lorem.sentence(20), + education_description: faker.lorem.sentence(15), status: CourseStatusEnum.PUBLISHED, }; }) diff --git a/database/migrations/1627300211260_courses.ts b/database/migrations/1627300211260_courses.ts index 9657c28..0040390 100644 --- a/database/migrations/1627300211260_courses.ts +++ b/database/migrations/1627300211260_courses.ts @@ -14,6 +14,7 @@ export default class Courses extends BaseSchema { table.json('image'); table.string('title').notNullable(); table.string('description').notNullable(); + table.string('education_description', 250); table.string('teacher_id', 21).unsigned().references('users.id').notNullable().onDelete('CASCADE'); table.string('category_id', 21).unsigned().references('categories.id').notNullable().onDelete('CASCADE'); table.integer('color_id').unsigned().references('colors.id'); diff --git a/database/migrations/1628017319337_lesson_contents.ts b/database/migrations/1628017319337_lesson_contents.ts index 569f178..da14cff 100644 --- a/database/migrations/1628017319337_lesson_contents.ts +++ b/database/migrations/1628017319337_lesson_contents.ts @@ -7,7 +7,7 @@ export default class LessonContents extends BaseSchema { this.schema.createTable(this.tableName, table => { table.increments('id').primary(); table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE'); - table.string('video_url').notNullable(); + table.text('body'); table.timestamp('created_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }); }); diff --git a/database/migrations/1637080400230_lesson_materials.ts b/database/migrations/1637080400230_lesson_materials.ts index 56ddb25..2763388 100644 --- a/database/migrations/1637080400230_lesson_materials.ts +++ b/database/migrations/1637080400230_lesson_materials.ts @@ -6,8 +6,8 @@ export default class LessonMaterials extends BaseSchema { public async up() { this.schema.createTable(this.tableName, table => { table.increments('id').primary(); - table.string('url').notNullable(); table.string('name').notNullable(); + table.integer('size').notNullable(); table.string('client_name').notNullable(); table.string('ext').notNullable(); table.string('lesson_id', 21).unsigned().references('lessons.id').onDelete('CASCADE'); diff --git a/database/migrations/1640112055569_lesson_progresses.ts b/database/migrations/1640112055569_lesson_progresses.ts new file mode 100644 index 0000000..9b3ad8a --- /dev/null +++ b/database/migrations/1640112055569_lesson_progresses.ts @@ -0,0 +1,19 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class LessonProgresses extends BaseSchema { + protected tableName = 'lesson_progresses'; + + public async up() { + this.schema.createTable(this.tableName, table => { + table.increments('id'); + table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE'); + table.string('lesson_id', 21).unsigned().references('lessons.id').onDelete('CASCADE'); + table.boolean('is_watched').notNullable(); + table.unique(['user_id', 'lesson_id']); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/database/migrations/1640897230275_lesson_videos.ts b/database/migrations/1640897230275_lesson_videos.ts new file mode 100644 index 0000000..dcd7118 --- /dev/null +++ b/database/migrations/1640897230275_lesson_videos.ts @@ -0,0 +1,24 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class LessonVideos extends BaseSchema { + protected tableName = 'lesson_videos'; + + public async up() { + this.schema.createTable(this.tableName, table => { + table.increments('id'); + table.string('lesson_id', 21).unsigned().references('lessons.id').onDelete('CASCADE').notNullable(); + table.string('name').notNullable(); + table.integer('size').notNullable(); + table.string('client_name').notNullable(); + table.string('ext').notNullable(); + table.string('url').notNullable(); + table.timestamp('created_at', { useTz: true }); + table.timestamp('updated_at', { useTz: true }); + table.unique(['lesson_id']); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/env.ts b/env.ts index aed92d2..4dd1fe7 100644 --- a/env.ts +++ b/env.ts @@ -20,13 +20,13 @@ export default Env.rules({ APP_KEY: Env.schema.string(), APP_NAME: Env.schema.string(), NODE_ENV: Env.schema.enum(['development', 'production', 'testing'] as const), - DRIVE_DISK: Env.schema.enum(['local'] as const), + DRIVE_DISK: Env.schema.enum(['local', 's3'] as const), PG_HOST: Env.schema.string({ format: 'host' }), PG_PORT: Env.schema.number(), PG_USER: Env.schema.string(), PG_PASSWORD: Env.schema.string.optional(), PG_DB_NAME: Env.schema.string(), - REDIS_CONNECTION: Env.schema.enum(['local'] as const), + REDIS_CONNECTION: Env.schema.enum(['local', 'session'] as const), REDIS_HOST: Env.schema.string({ format: 'host' }), REDIS_PORT: Env.schema.number(), REDIS_PASSWORD: Env.schema.string.optional(), @@ -36,4 +36,9 @@ export default Env.rules({ SMTP_PASSWORD: Env.schema.string(), MAILGUN_API_KEY: Env.schema.string(), MAILGUN_DOMAIN: Env.schema.string(), + S3_KEY: Env.schema.string(), + S3_SECRET: Env.schema.string(), + S3_BUCKET: Env.schema.string(), + S3_REGION: Env.schema.string(), + S3_ENDPOINT: Env.schema.string.optional(), }); diff --git a/package-lock.json b/package-lock.json index c08db4f..86a0dc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -508,6 +508,16 @@ } } }, + "@adonisjs/drive-s3": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@adonisjs/drive-s3/-/drive-s3-1.0.5.tgz", + "integrity": "sha512-Jny0CH0K02vt6fo4CNfInc/kh2FefZmjTSjWLWEl6Gyg/+2GIy6+/PBgRsk8hpTR+T4tGT4V5Cgs71aVZfbzeg==", + "requires": { + "@aws-sdk/client-s3": "^3.28.0", + "@aws-sdk/s3-request-presigner": "^3.28.0", + "get-stream": "^6.0.1" + } + }, "@adonisjs/encryption": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@adonisjs/encryption/-/encryption-4.0.5.tgz", @@ -1146,6 +1156,947 @@ "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==" }, + "@aws-crypto/crc32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-2.0.0.tgz", + "integrity": "sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA==", + "requires": { + "@aws-crypto/util": "^2.0.0", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/ie11-detection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", + "integrity": "sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz", + "integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==", + "requires": { + "@aws-crypto/ie11-detection": "^2.0.0", + "@aws-crypto/sha256-js": "^2.0.0", + "@aws-crypto/supports-web-crypto": "^2.0.0", + "@aws-crypto/util": "^2.0.0", + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz", + "integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==", + "requires": { + "@aws-crypto/util": "^2.0.0", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz", + "integrity": "sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.1.tgz", + "integrity": "sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ==", + "requires": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-sdk/abort-controller": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.40.0.tgz", + "integrity": "sha512-S7LzLvNuwuf0q7r4q7zqGzxd/W2xYsn8cpZ90MMb3ObolhbkLySrikUJujmXae8k+2/KFCOr+FVC0YLrATSUgQ==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/chunked-blob-reader": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.37.0.tgz", + "integrity": "sha512-uDacnFaczeO962RnVttwAQddS4rgDfI7nfeY8NV6iZkDv5uxGzHTfH4jT7WvPDM1pSMcOMDx8RJ+Tmtsd1VTsA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/chunked-blob-reader-native": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.37.0.tgz", + "integrity": "sha512-h9OYq6EvDrpb7SKod+Kow+d3aRNFVBYR1a8G8ahEDDQe3AtmA2Smyvni4kt/ABTiKvYdof2//Pq3BL/IUV9n9Q==", + "requires": { + "@aws-sdk/util-base64-browser": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/client-s3": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.44.0.tgz", + "integrity": "sha512-vv3w5RhAZnT/LfiV0SICvvzv5SAeH61erbwm6FpKC9W7lGdcA6o7F2/Bls44fyMW41awYIRZKoPGZApYCrFR6A==", + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.43.0", + "@aws-sdk/config-resolver": "3.40.0", + "@aws-sdk/credential-provider-node": "3.41.0", + "@aws-sdk/eventstream-serde-browser": "3.40.0", + "@aws-sdk/eventstream-serde-config-resolver": "3.40.0", + "@aws-sdk/eventstream-serde-node": "3.40.0", + "@aws-sdk/fetch-http-handler": "3.40.0", + "@aws-sdk/hash-blob-browser": "3.40.0", + "@aws-sdk/hash-node": "3.40.0", + "@aws-sdk/hash-stream-node": "3.40.0", + "@aws-sdk/invalid-dependency": "3.40.0", + "@aws-sdk/md5-js": "3.40.0", + "@aws-sdk/middleware-apply-body-checksum": "3.40.0", + "@aws-sdk/middleware-bucket-endpoint": "3.41.0", + "@aws-sdk/middleware-content-length": "3.40.0", + "@aws-sdk/middleware-expect-continue": "3.40.0", + "@aws-sdk/middleware-host-header": "3.40.0", + "@aws-sdk/middleware-location-constraint": "3.40.0", + "@aws-sdk/middleware-logger": "3.40.0", + "@aws-sdk/middleware-retry": "3.40.0", + "@aws-sdk/middleware-sdk-s3": "3.41.0", + "@aws-sdk/middleware-serde": "3.40.0", + "@aws-sdk/middleware-signing": "3.40.0", + "@aws-sdk/middleware-ssec": "3.40.0", + "@aws-sdk/middleware-stack": "3.40.0", + "@aws-sdk/middleware-user-agent": "3.40.0", + "@aws-sdk/node-config-provider": "3.40.0", + "@aws-sdk/node-http-handler": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/smithy-client": "3.41.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/url-parser": "3.40.0", + "@aws-sdk/util-base64-browser": "3.37.0", + "@aws-sdk/util-base64-node": "3.37.0", + "@aws-sdk/util-body-length-browser": "3.37.0", + "@aws-sdk/util-body-length-node": "3.37.0", + "@aws-sdk/util-user-agent-browser": "3.40.0", + "@aws-sdk/util-user-agent-node": "3.40.0", + "@aws-sdk/util-utf8-browser": "3.37.0", + "@aws-sdk/util-utf8-node": "3.37.0", + "@aws-sdk/util-waiter": "3.40.0", + "@aws-sdk/xml-builder": "3.37.0", + "entities": "2.2.0", + "fast-xml-parser": "3.19.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/client-sso": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.41.0.tgz", + "integrity": "sha512-xDvcy7wv3KdHhOpl5fZN+Ydw+dHBmsCZwMFI1ZdJVCSGO+ZKgl5KVWi1LCif6vjZP1pUuGl44oDOZz1ACqOzTg==", + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.40.0", + "@aws-sdk/fetch-http-handler": "3.40.0", + "@aws-sdk/hash-node": "3.40.0", + "@aws-sdk/invalid-dependency": "3.40.0", + "@aws-sdk/middleware-content-length": "3.40.0", + "@aws-sdk/middleware-host-header": "3.40.0", + "@aws-sdk/middleware-logger": "3.40.0", + "@aws-sdk/middleware-retry": "3.40.0", + "@aws-sdk/middleware-serde": "3.40.0", + "@aws-sdk/middleware-stack": "3.40.0", + "@aws-sdk/middleware-user-agent": "3.40.0", + "@aws-sdk/node-config-provider": "3.40.0", + "@aws-sdk/node-http-handler": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/smithy-client": "3.41.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/url-parser": "3.40.0", + "@aws-sdk/util-base64-browser": "3.37.0", + "@aws-sdk/util-base64-node": "3.37.0", + "@aws-sdk/util-body-length-browser": "3.37.0", + "@aws-sdk/util-body-length-node": "3.37.0", + "@aws-sdk/util-user-agent-browser": "3.40.0", + "@aws-sdk/util-user-agent-node": "3.40.0", + "@aws-sdk/util-utf8-browser": "3.37.0", + "@aws-sdk/util-utf8-node": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/client-sts": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.43.0.tgz", + "integrity": "sha512-4CKYimjhIEixVtJH0Y8FR5FXc7zIepZtfScy8QHgH+DERXm/YL5cuUbkJiL6ZRTpek0vztVvE+mNSQU0z1eXag==", + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.40.0", + "@aws-sdk/credential-provider-node": "3.41.0", + "@aws-sdk/fetch-http-handler": "3.40.0", + "@aws-sdk/hash-node": "3.40.0", + "@aws-sdk/invalid-dependency": "3.40.0", + "@aws-sdk/middleware-content-length": "3.40.0", + "@aws-sdk/middleware-host-header": "3.40.0", + "@aws-sdk/middleware-logger": "3.40.0", + "@aws-sdk/middleware-retry": "3.40.0", + "@aws-sdk/middleware-sdk-sts": "3.40.0", + "@aws-sdk/middleware-serde": "3.40.0", + "@aws-sdk/middleware-signing": "3.40.0", + "@aws-sdk/middleware-stack": "3.40.0", + "@aws-sdk/middleware-user-agent": "3.40.0", + "@aws-sdk/node-config-provider": "3.40.0", + "@aws-sdk/node-http-handler": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/smithy-client": "3.41.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/url-parser": "3.40.0", + "@aws-sdk/util-base64-browser": "3.37.0", + "@aws-sdk/util-base64-node": "3.37.0", + "@aws-sdk/util-body-length-browser": "3.37.0", + "@aws-sdk/util-body-length-node": "3.37.0", + "@aws-sdk/util-user-agent-browser": "3.40.0", + "@aws-sdk/util-user-agent-node": "3.40.0", + "@aws-sdk/util-utf8-browser": "3.37.0", + "@aws-sdk/util-utf8-node": "3.37.0", + "entities": "2.2.0", + "fast-xml-parser": "3.19.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/config-resolver": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.40.0.tgz", + "integrity": "sha512-QYy6J2k31QL6J74hPBfptnLW1kQYdN+xjwH4UQ1mv7EUhRoJN9ZY2soStJowFy4at6IIOOVWbyG5dyqvrbEovg==", + "requires": { + "@aws-sdk/signature-v4": "3.40.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-config-provider": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.40.0.tgz", + "integrity": "sha512-qHZdf2vxhzZkSygjw2I4SEYFL2dMZxxYvO4QlkqQouKY81OVxs/j69oiNCjPasQzGz5jaZZKI8xEAIfkSyr1lg==", + "requires": { + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-imds": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.40.0.tgz", + "integrity": "sha512-Ty/wVa+BQrCFrP06AGl5S1CeLifDt68YrlYXUnkRn603SX4DvxBgVO7XFeDH58G8ziDCiqxfmVl4yjbncPPeSw==", + "requires": { + "@aws-sdk/node-config-provider": "3.40.0", + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/url-parser": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.41.0.tgz", + "integrity": "sha512-98CGEHg7Tb6HxK5ZIdbAcijvD3IpLe0ddse1xMe/Ilhjz770FS/L2UNprOP6PZTqrSfBffiMrvfThUSuUaTlIQ==", + "requires": { + "@aws-sdk/credential-provider-env": "3.40.0", + "@aws-sdk/credential-provider-imds": "3.40.0", + "@aws-sdk/credential-provider-sso": "3.41.0", + "@aws-sdk/credential-provider-web-identity": "3.41.0", + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-credentials": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.41.0.tgz", + "integrity": "sha512-5FW6+wNJgyDCsbAd+mLm/1DBTDkyIYOMVzcxbr6Vi3pM4UrMFdeLdAP62edYW8usg78Xg+c6vaAoEv/M3zkS0Q==", + "requires": { + "@aws-sdk/credential-provider-env": "3.40.0", + "@aws-sdk/credential-provider-imds": "3.40.0", + "@aws-sdk/credential-provider-ini": "3.41.0", + "@aws-sdk/credential-provider-process": "3.40.0", + "@aws-sdk/credential-provider-sso": "3.41.0", + "@aws-sdk/credential-provider-web-identity": "3.41.0", + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-credentials": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.40.0.tgz", + "integrity": "sha512-qsaNCDesW2GasDbzpeOA371gxugi05JWxt3EKonLbUfkGKBK7kmmL6EgLIxZuNm2/Ve4RS07PKp8yBGm4xIx9w==", + "requires": { + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-credentials": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.41.0.tgz", + "integrity": "sha512-9s7SWu3RVIQ/MTcBCt35EMzxNQm3avivrbpSOKfJwxR5L+oNKPsV+gSqMlkNZGwOVJyUicIsZGcq/4ON6CjrOg==", + "requires": { + "@aws-sdk/client-sso": "3.41.0", + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-credentials": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.41.0.tgz", + "integrity": "sha512-VqvVoEh9C8xTXl4stKyJC5IKQhS8g1Gi5k6B9HPHLIxFRRfKxkE73DT4pMN6npnus7o0yi0MTFGQFQGYSrFO2g==", + "requires": { + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/eventstream-marshaller": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.40.0.tgz", + "integrity": "sha512-zHGchfkG3B9M8OOKRpByeS5g1/15YQ0+QQHwxQRtm/CPtKBAIAsCZRQaCNBLu9uQMtBBKj5JsDUcjirhGeSvIg==", + "requires": { + "@aws-crypto/crc32": "2.0.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-hex-encoding": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/eventstream-serde-browser": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.40.0.tgz", + "integrity": "sha512-V0AXAfSkhY0hgxDJ0cNA+r42kL8295U7UTCp2Q2fvCaob3wKWh+54KZ2L4IOYTlK3yNzXJ5V6PP1zUuRlsUTew==", + "requires": { + "@aws-sdk/eventstream-marshaller": "3.40.0", + "@aws-sdk/eventstream-serde-universal": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/eventstream-serde-config-resolver": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.40.0.tgz", + "integrity": "sha512-GgGiJBsQ1/SBTpRM/wCdFBCMo1Nybvy46bNVkH1ujCdp8UTLc5PozzNpH+15V2IQbc9sPDYffMab6HSFjDp5vw==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/eventstream-serde-node": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.40.0.tgz", + "integrity": "sha512-CnzX/JZGvhWlg+ooIPVZ78T+5wIm5Ld1BD7jwhlptJa8IjTMvkc8Nh4pAhc7T0ZScy4zZa/oTkqeVYCOVCyd1Q==", + "requires": { + "@aws-sdk/eventstream-marshaller": "3.40.0", + "@aws-sdk/eventstream-serde-universal": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/eventstream-serde-universal": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.40.0.tgz", + "integrity": "sha512-rkHwVMyZJMhp9iBixkuaAGQNer/DPxZ9kxDDtE+LuAMhepTYQ8c4lUW0QQhYbNMWf48QKD1G4FV3JXIj9JfP9A==", + "requires": { + "@aws-sdk/eventstream-marshaller": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/fetch-http-handler": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.40.0.tgz", + "integrity": "sha512-w1HiZromoU+/bbEo89uO81l6UO/M+c2uOMnXntZqe6t3ZHUUUo3AbvhKh0QGVFqRQa+Oi0+95KqWmTHa72/9Iw==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/querystring-builder": "3.40.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-base64-browser": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/hash-blob-browser": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.40.0.tgz", + "integrity": "sha512-l8xyprVVKKH+720VrQ677X6VkvHttDXB4MxkMuxhSvwYBQwsRzP2Wppo7xIAtWGoS+oqlLmD4LCbHdhFRcN5yA==", + "requires": { + "@aws-sdk/chunked-blob-reader": "3.37.0", + "@aws-sdk/chunked-blob-reader-native": "3.37.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/hash-node": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.40.0.tgz", + "integrity": "sha512-yOXXK85DdGDktdnQtXgMdaVKii4wtMjEhJ1mrvx2A9nMFNaPhxvERkVVIUKSWlJRa9ZujOw5jWOx8d2R51/Kjg==", + "requires": { + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-buffer-from": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/hash-stream-node": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.40.0.tgz", + "integrity": "sha512-4yvRwODMGYtj6qrt+fyydV5MwVwPPoyoeqDoXdLo9x75vRY71DT1pMRt8PDOoY/ZwWbIdEt4+V7x0sLt2uy9WA==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/invalid-dependency": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.40.0.tgz", + "integrity": "sha512-axIWtDwCBDDqEgAJipX1FB1ZNpWYXquVwKDMo+7G+ftPBZ4FEq4M1ELhXJL3hhNJ9ZmCQzv+4F6Wnt8dwuzUaQ==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/is-array-buffer": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.37.0.tgz", + "integrity": "sha512-XLjA/a6AuGnCvcJZLsMTy2jxF2upgGhqCCkoIJgLlzzXHSihur13KcmPvW/zcaGnCRj0SvKWXiJHl4vDlW75VQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/md5-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.40.0.tgz", + "integrity": "sha512-P1tzEljMD/MkjSc00TkVBYvfaVv/7S+04YEwE7tpu/jtxWxMHnk3CMKqq/F2iMhY83DRoqoYy+YqnaF4Bzr1uA==", + "requires": { + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-utf8-browser": "3.37.0", + "@aws-sdk/util-utf8-node": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-apply-body-checksum": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.40.0.tgz", + "integrity": "sha512-gNSFlFu/O8cxAM0X64OwiLLN/NPXvK3FsAIJRsfhIW+dX0bEq4lsGPsdU8Tx+9eenaj/Z01uqgWZ6Izar8zVvQ==", + "requires": { + "@aws-sdk/is-array-buffer": "3.37.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-bucket-endpoint": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.41.0.tgz", + "integrity": "sha512-4A0kWHH2qemd4P7CZKS7XB6qtSUP2xMJ7Dn/llxYgvadR0mKEfGPeYPhAss/k7T1JGv+kSTIV30RwUXwdXgE/A==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-arn-parser": "3.37.0", + "@aws-sdk/util-config-provider": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-content-length": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.40.0.tgz", + "integrity": "sha512-sybAJb8v7I/vvL08R3+TI/XDAg9gybQTZ2treC24Ap4+jAOz4QBTHJPMKaUlEeFlMUcq4rj6/u2897ebYH6opw==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-expect-continue": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.40.0.tgz", + "integrity": "sha512-FY6vT0u1ptDZ2bBj1yG/Iyk6HZB7U9fbrpeZNPYzgq8HJxBcTgfLwtB3VLobyhThQm9X2a7R2YZrwtArW8yQfQ==", + "requires": { + "@aws-sdk/middleware-header-default": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-header-default": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-header-default/-/middleware-header-default-3.40.0.tgz", + "integrity": "sha512-eXQ13x/AivPZKoG8/akp9g5xdNHuKftl83GMuk9K6tt4+eAa22TdxiFu4R0UVlKAvo2feqxFrNs5DhhhBeAQWA==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.40.0.tgz", + "integrity": "sha512-/wocR7JFOLM7/+BQM1DgAd6KCFYcdxYu1P7AhI451GlVNuYa5f89zh7p0gt3SRC6monI5lXgpL7RudhDm8fTrA==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-location-constraint": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.40.0.tgz", + "integrity": "sha512-9XaVPYsDQVJbWJH96sNdv4HHY3j1raman+lYxMu4528Awp0OdWUeSsGRYRN+CnRPlkHnfNw4m6SKdWYHxdjshw==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.40.0.tgz", + "integrity": "sha512-19kx0Xg5ymVRKoupmhdmfTBkROcv3DZj508agpyG2YAo0abOObMlIP4Jltg0VD4PhNjGzNh0jFGJnvhjdwv4/A==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-retry": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.40.0.tgz", + "integrity": "sha512-SMUJrukugLL7YJE5X8B2ToukxMWMPwnf7jAFr84ptycCe8bdWv8x8klQ3EtVWpyqochtNlbTi6J/tTQBniUX7A==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/service-error-classification": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0", + "uuid": "^8.3.2" + } + }, + "@aws-sdk/middleware-sdk-s3": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.41.0.tgz", + "integrity": "sha512-B7JOpmIpm1zxERQEMwZCWj3FisTvwfUgpfQglYdqrB6VpIoCM8fk2pmi5KzU/JDeNBlhTouj6mwnhJL/z5jopA==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/signature-v4": "3.40.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-arn-parser": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-sdk-sts": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.40.0.tgz", + "integrity": "sha512-TcrbCvj1PkabFZiNczT3yePZtuEm2fAIw1OVnQyLcF2KW+p62Hv5YkK4MPOfx3LA/0lzjOUO1RNl2x7gzV443Q==", + "requires": { + "@aws-sdk/middleware-signing": "3.40.0", + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/signature-v4": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-serde": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.40.0.tgz", + "integrity": "sha512-uOWfZjlAoBy6xPqp0d4ka83WNNbEVCWn9WwfqBUXThyoTdTooYSpXe5y2YzN0BJa8b+tEZTyWpgamnBpFLp47g==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-signing": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.40.0.tgz", + "integrity": "sha512-RqK5nPbfma0qInMvjtpVkDYY/KkFS6EKlOv3DWTdxbXJ4YuOxgKiuUromhmBUoyjFag0JO7LUWod07H+/DawoA==", + "requires": { + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/signature-v4": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-ssec": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.40.0.tgz", + "integrity": "sha512-ZoRpaZeAIQa1Q+NyEh74ATwOR3nFGfcP6Nu0jFzgqoVijCReMnhtlCRx23ccBu1ZLZNUsNk6MhKjY+ZTfNsjEg==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-stack": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.40.0.tgz", + "integrity": "sha512-hby9HvESUYJxpdALX+6Dn2LPmS5jtMVurGB/+j3MWOvIcDYB4bcSXgVRvXzYnTKwbSupIdbX9zOE2ZAx2SJpUQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.40.0.tgz", + "integrity": "sha512-dzC2fxWnanetFJ1oYgil8df3N36bR1yc/OCOpbdfQNiUk1FrXiCXqH5rHNO8zCvnwJAj8GHFwpFGd9a2Qube2w==", + "requires": { + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/node-config-provider": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.40.0.tgz", + "integrity": "sha512-AmokjgUDECG8osoMfdRsPNweqI+L1pn4bYGk5iTLmzbBi0o4ot0U1FdX8Rf0qJZZwS4t1TXc3s8/PDVknmPxKg==", + "requires": { + "@aws-sdk/property-provider": "3.40.0", + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/node-http-handler": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.40.0.tgz", + "integrity": "sha512-qjda6IbxDhbYr8NHmrMurKkbjgLUkfTMVgagDErDK24Nm3Dn5VaO6J4n6c0Q4OLHlmFaRcUfZSTrOo5DAubqCw==", + "requires": { + "@aws-sdk/abort-controller": "3.40.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/querystring-builder": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/property-provider": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.40.0.tgz", + "integrity": "sha512-Mx4lkShjsYRwW9ujHA1pcnuubrWQ4kF5/DXWNfUiXuSIO/0Lojp1qTLheyBm4vzkJIlx5umyP6NvRAUkEHSN4Q==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/protocol-http": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.40.0.tgz", + "integrity": "sha512-f4ea7/HZkjpvGBrnRIuzc/bhrExWrgDv7eulj4htPukZGHdTqSJD3Jk8lEXWvFuX2vUKQDGhEhCDsqup7YWJQQ==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/querystring-builder": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.40.0.tgz", + "integrity": "sha512-gO24oipnNaxJRBXB7lhLfa96vIMOd8gtMBqJTjelTjS2e1ZP1YY12CNKKTWwafSk8Ge021erZAG/YTOaXGpv+g==", + "requires": { + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-uri-escape": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/querystring-parser": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.40.0.tgz", + "integrity": "sha512-XZIyaKQIiZAM6zelCBcsLHhVDOLafi7XIOd3jy6SymGN8ajj3HqUJ/vdQ5G6ISTk18OrqgqcCOI9oNzv+nrBcA==", + "requires": { + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/s3-request-presigner": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.44.0.tgz", + "integrity": "sha512-PYAgoYu5dMaKSrr7eJlTGUROb5G98iYt0Vq8a7MMa1ZvIArsPB2b8tPS7DzvpMb4Jx8C7ni6kf+0F9YWMmOOpQ==", + "requires": { + "@aws-sdk/middleware-sdk-s3": "3.41.0", + "@aws-sdk/protocol-http": "3.40.0", + "@aws-sdk/signature-v4": "3.40.0", + "@aws-sdk/smithy-client": "3.41.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-create-request": "3.41.0", + "@aws-sdk/util-format-url": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/service-error-classification": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.40.0.tgz", + "integrity": "sha512-c8btKmkvjXczWudXubGdbO3JgmjySBUVC/gCrZDNfwNGsG8RYJJQYYcnmt1gWjelUZsgMDl/2PIzxTlxVF91rA==" + }, + "@aws-sdk/shared-ini-file-loader": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.37.0.tgz", + "integrity": "sha512-+vRBSlfa48R9KL7DpQt3dsu5/+5atjRgoCISblWo3SLpjrx41pKcjKneo7a1u0aP1Xc2oG2TfIyqTWZuOXsmEQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/signature-v4": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.40.0.tgz", + "integrity": "sha512-Q1GNZJRCS3W2qsRtDsX/b6EOSfMXfr6TW46N3LnLTGYZ3KAN2SOSJ1DsW59AuGpEZyRmOhJ9L/Q5U403+bZMXQ==", + "requires": { + "@aws-sdk/is-array-buffer": "3.37.0", + "@aws-sdk/types": "3.40.0", + "@aws-sdk/util-hex-encoding": "3.37.0", + "@aws-sdk/util-uri-escape": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/smithy-client": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.41.0.tgz", + "integrity": "sha512-ldhS0Pf3v6yHCd//kk5DvKcdyeUkKEwxNDRanAp+ekTW68J3XcYgKaPC9sNDhVTDH1zrywTvtEz5zWHEvXjQow==", + "requires": { + "@aws-sdk/middleware-stack": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/types": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.40.0.tgz", + "integrity": "sha512-KpILcfvRaL88TLvo3SY4OuCCg90SvcNLPyjDwUuBqiOyWODjrKShHtAPJzej4CLp92lofh+ul0UnBfV9Jb/5PA==" + }, + "@aws-sdk/url-parser": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.40.0.tgz", + "integrity": "sha512-HwNV+HX7bHgLk5FzTOgdXANsC0SeVz5PMC4Nh+TLz2IoeQnrw4H8dsA4YNonncjern5oC5veKRjQeOoCL5SlSQ==", + "requires": { + "@aws-sdk/querystring-parser": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-arn-parser": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.37.0.tgz", + "integrity": "sha512-njIYn8gzm7Ms17A2oEu0vN/0GJpgq7cNFFtzBrM1cPtrc1jhMRJx5hzS7uX5h6ll8BM92bA3y00evRZFHxQPVQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-base64-browser": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.37.0.tgz", + "integrity": "sha512-o4s/rHVm5k8eC/T7grJQINyYA/mKfDmEWKMA9wk5iBroXlI2rUm7x649TBk5hzoddufk/mffEeNz/1wM7yTmlg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-base64-node": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.37.0.tgz", + "integrity": "sha512-1UPxly1GPrGZtlIWvbNCDIAund4Oyp8cFi9neA43TeNACvrmEQu/nG01pDbOoo0ENoVSVJrNAVBeqKEpqjH2GA==", + "requires": { + "@aws-sdk/util-buffer-from": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-body-length-browser": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.37.0.tgz", + "integrity": "sha512-tClmH1uYelqWT43xxmnOsVFbCQJiIwizp6y4E109G2LIof07inxrO0L8nbwBpjhugVplx6NZr9IaqTFqbdM1gA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-body-length-node": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.37.0.tgz", + "integrity": "sha512-aY3mXdbEajruRi9CHgq/heM89R+Gectj/Xrs1naewmamaN8NJrvjDm3s+cw//lqqSOW903LYHXDgm7wvCzUnFA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-buffer-from": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.37.0.tgz", + "integrity": "sha512-aa3SBwjLwImuJoE4+hxDIWQ9REz3UFb3p7KFPe9qopdXb/yB12RTcbrXVb4whUux4i4mO6KRij0ZNjFZrjrKPg==", + "requires": { + "@aws-sdk/is-array-buffer": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-config-provider": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.40.0.tgz", + "integrity": "sha512-NjZGrA4mqhpr6gkVCAUweurP0Z9d3vFyXJCtulC0BFbpKAnKCf/crSK56NwUaNhAEMCkSuBvjRFzkbfT+HO8bA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-create-request": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-create-request/-/util-create-request-3.41.0.tgz", + "integrity": "sha512-mKTMCDTaQ9HH+ppg1QMcBiSlbvRmw8AOllXTkYS1pdO7gi/Sl21krXfIja2MakuK6cB6D+B8MFP8rfq14Vhhlg==", + "requires": { + "@aws-sdk/middleware-stack": "3.40.0", + "@aws-sdk/smithy-client": "3.41.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-credentials": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-credentials/-/util-credentials-3.37.0.tgz", + "integrity": "sha512-zcLhSZDKgBLhUjSU5HoQpuQiP3v8oE86NmV/tiZVPEaO6YVULEAB2Cfj1hpM/b/JXWzjSHfT06KXT7QUODKS+A==", + "requires": { + "@aws-sdk/shared-ini-file-loader": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-format-url": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.40.0.tgz", + "integrity": "sha512-eOKeYwAvsCQjTSCAW1LVhnjmTyyGK6lZjGTa9wXxxjVE6Fhc66+cCykS8u/u8Vj7BCS+ixiLetXtH1/qd+Bl+g==", + "requires": { + "@aws-sdk/querystring-builder": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-hex-encoding": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.37.0.tgz", + "integrity": "sha512-tn5UpfaeM+rZWqynoNqB8lwtcAXil5YYO3HLGH9himpWAdft/2Z7LK6bsYDpctaAI1WHgMDcL0bw3Id04ZUbhA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-locate-window": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.37.0.tgz", + "integrity": "sha512-NvDCfOhLLVHp27oGUUs8EVirhz91aX5gdxGS7J/sh5PF0cNN8rwaR1vSLR7BxPmJHMO7NH7i9EwiELfLfYcq6g==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-uri-escape": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.37.0.tgz", + "integrity": "sha512-8pKf4YJTELP5lm/CEgYw2atyJBB1RWWqFa0sZx6YJmTlOtLF5G6raUdAi4iDa2hldGt2B6IAdIIyuusT8zeU8Q==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.40.0.tgz", + "integrity": "sha512-C69sTI26bV2EprTv3DTXu9XP7kD9Wu4YVPBzqztOYArd2GDYw3w+jS8SEg3XRbjAKY/mOPZ2Thw4StjpZlWZiA==", + "requires": { + "@aws-sdk/types": "3.40.0", + "bowser": "^2.11.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.40.0.tgz", + "integrity": "sha512-cjIzd0hRZFTTh7iLJD6Bciu++Em1iaM1clyG02xRl0JD5DEtDSR1zO02uu+AeM7GSLGOxIvwOkK2j8ySPAOmBA==", + "requires": { + "@aws-sdk/node-config-provider": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-utf8-browser": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.37.0.tgz", + "integrity": "sha512-tuiOxzfqet1kKGYzlgpMGfhr64AHJnYsFx2jZiH/O6Yq8XQg43ryjQlbJlim/K/XHGNzY0R+nabeJg34q3Ua1g==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-utf8-node": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.37.0.tgz", + "integrity": "sha512-fUAgd7UTCULL36j9/vnXHxVhxvswnq23mYgTCIT8NQ7wHN30q2a89ym1e9DwGeQkJEBOkOcKLn6nsMsN7YQMDQ==", + "requires": { + "@aws-sdk/util-buffer-from": "3.37.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/util-waiter": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.40.0.tgz", + "integrity": "sha512-jdxwNEZdID49ZvyAnxaeNm5w2moIfMLOwj/q6TxKlxYoXMs16FQWkhyfGue0vEASzchS49ewbyt+KBqpT31Ebg==", + "requires": { + "@aws-sdk/abort-controller": "3.40.0", + "@aws-sdk/types": "3.40.0", + "tslib": "^2.3.0" + } + }, + "@aws-sdk/xml-builder": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.37.0.tgz", + "integrity": "sha512-Vf0f4ZQ+IBo/l9wUaTOXLqqQO9b/Ll5yPbg+EhHx8zlHbTHIm89ettkVCGyT/taGagC1X+ZeTK9maX6ymEOBow==", + "requires": { + "tslib": "^2.3.0" + } + }, "@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -1469,6 +2420,11 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, "@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -1498,6 +2454,21 @@ "@types/responselike": "*" } }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/faker": { "version": "5.5.9", "resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.5.9.tgz", @@ -1610,6 +2581,14 @@ "@types/node": "*" } }, + "@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "requires": { + "socket.io": "*" + } + }, "@types/stack-trace": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", @@ -2248,6 +3227,11 @@ } } }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bcrypt": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", @@ -2263,6 +3247,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2664,8 +3653,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", @@ -2729,6 +3717,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cp-file": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-9.1.0.tgz", @@ -3110,6 +4107,31 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz", + "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -3118,6 +4140,11 @@ "ansi-colors": "^4.1.1" } }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3868,6 +4895,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz", "integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==" }, + "fast-xml-parser": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", + "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==" + }, "fastify-warning": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", @@ -7003,6 +8035,34 @@ } } }, + "socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, "sonic-boom": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.2.3.tgz", @@ -7595,6 +8655,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "uuid-random": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", @@ -7724,6 +8789,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index c49df8c..878c3be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "1.0.1", + "version": "1.1.0", "private": true, "scripts": { "build": "node ace build --production", @@ -37,11 +37,13 @@ "@adonisjs/bouncer": "^2.2.5", "@adonisjs/core": "^5.4.2", "@adonisjs/drive": "^2.0.7", + "@adonisjs/drive-s3": "^1.0.5", "@adonisjs/lucid": "^16.3.2", "@adonisjs/mail": "^7.2.4", "@adonisjs/redis": "^7.0.9", "@adonisjs/repl": "^3.1.7", "@adonisjs/view": "^6.1.1", + "@types/socket.io": "^3.0.2", "cloudinary": "^1.26.3", "luxon": "^1.28.0", "nanoid": "^3.1.25", @@ -49,6 +51,7 @@ "phc-bcrypt": "^1.0.7", "proxy-addr": "^2.0.7", "reflect-metadata": "^0.1.13", + "socket.io": "^4.4.1", "source-map-support": "^0.5.19" } } diff --git a/providers/AppProvider.ts b/providers/AppProvider.ts index 96558de..a5d170f 100644 --- a/providers/AppProvider.ts +++ b/providers/AppProvider.ts @@ -7,7 +7,11 @@ export default class AppProvider { public async boot() {} - public async ready() {} + public async ready() { + if (this.app.environment === 'web') { + await import('../start/socket'); + } + } public async shutdown() {} } diff --git a/routes/apis/v1/categories.ts b/routes/apis/v1/categories.ts index 1a31707..7e021fc 100644 --- a/routes/apis/v1/categories.ts +++ b/routes/apis/v1/categories.ts @@ -5,4 +5,5 @@ Route.group(() => { Route.get('/:id', 'Api/v1/CategoriesController.show').middleware('role:admin,teacher,student').as('categories.show'); Route.post('/', 'Api/v1/CategoriesController.create').middleware('role:admin,teacher').as('categories.create'); Route.delete('/:id', 'Api/v1/CategoriesController.delete').middleware('role:admin,teacher').as('categories.delete'); + Route.patch('/:id', 'Api/v1/CategoriesController.update').middleware('role:admin,teacher').as('categories.update'); }).prefix('categories'); diff --git a/routes/apis/v1/courses.ts b/routes/apis/v1/courses.ts index 314d06b..087a483 100644 --- a/routes/apis/v1/courses.ts +++ b/routes/apis/v1/courses.ts @@ -30,6 +30,14 @@ Route.group(() => { .middleware('role:admin,teacher') .as('courses.attach-student'); + Route.post('/:id/attach-student-list', 'Api/v1/CoursesController.attachStudentList') + .middleware('role:admin,teacher') + .as('courses.attach-student-list'); + + Route.patch('/:id/detach-student-list', 'Api/v1/CoursesController.detachStudentList') + .middleware('role:admin,teacher') + .as('courses.detach-student-list'); + Route.delete('/:id/detach-student', 'Api/v1/CoursesController.detachStudent') .middleware('role:admin,teacher') .as('courses.detach-student'); @@ -49,4 +57,8 @@ Route.group(() => { Route.post('/:id/set-status', 'Api/v1/CoursesController.setStatus') .middleware('role:admin,teacher') .as('courses.set-status'); + + Route.get('/:id/lessons-progress', 'Api/v1/CoursesController.getLessonsProgress') + .middleware('role:admin,teacher,student') + .as('courses.get-lessons-progress'); }).prefix('courses'); diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index d868e88..a967e66 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -63,6 +63,11 @@ Route.group(() => { * */ require('./categories'); + + /** + * Stat module + */ + require('./stat'); }).middleware('auth'); }) .prefix('v1') diff --git a/routes/apis/v1/lessons.ts b/routes/apis/v1/lessons.ts index f2fa3f4..7893120 100644 --- a/routes/apis/v1/lessons.ts +++ b/routes/apis/v1/lessons.ts @@ -8,11 +8,14 @@ Route.group(() => { .as('lessons.update-order'); 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.patch('/: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'); - Route.get('/materials/:file', 'Api/v1/LessonsController.getMaterial') + Route.get('/materials/:fileName', 'Api/v1/LessonsController.getMaterial') .middleware('role:admin,teacher,student') .as('lessons.get-material'); + Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress') + .middleware('role:admin,teacher,student') + .as('lessons.get-video-progress'); }).prefix('lessons'); diff --git a/routes/apis/v1/stat.ts b/routes/apis/v1/stat.ts new file mode 100644 index 0000000..109a29e --- /dev/null +++ b/routes/apis/v1/stat.ts @@ -0,0 +1,5 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.group(() => { + Route.get('/', 'Api/v1/StatController.index').middleware('role:admin,teacher,student').as('stat.index'); +}).prefix('stat'); diff --git a/routes/index.ts b/routes/index.ts index d614d40..cfc24ed 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -26,4 +26,9 @@ Route.group(() => { * Api routes */ require('./api'); + + /** + * Other routes + */ + require('./videos'); }); diff --git a/routes/videos.ts b/routes/videos.ts new file mode 100644 index 0000000..1121eb0 --- /dev/null +++ b/routes/videos.ts @@ -0,0 +1,7 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.group(() => { + Route.get('/video/:fileName', 'VideosController.getVideo') + .middleware('role:admin,teacher,student') + .as('videos.get-video'); +}).middleware('auth'); diff --git a/start/socket.ts b/start/socket.ts new file mode 100644 index 0000000..64a2131 --- /dev/null +++ b/start/socket.ts @@ -0,0 +1,6 @@ +import Ws from 'App/Services/WsService'; + +/** + * Start websocket server + */ +Ws.boot(); diff --git a/tsconfig.json b/tsconfig.json index 185f317..007bcc2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,8 @@ "@adonisjs/mail", "@adonisjs/view", "@adonisjs/attachment-lite", - "@adonisjs/bouncer" + "@adonisjs/bouncer", + "@adonisjs/drive-s3" ] } }