mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-12 18:59:11 +03:00
Compare commits
No commits in common. "develop" and "v1.0.0" have entirely different histories.
@ -24,7 +24,7 @@
|
|||||||
"Database": "database",
|
"Database": "database",
|
||||||
"Contracts": "contracts"
|
"Contracts": "contracts"
|
||||||
},
|
},
|
||||||
"preloads": ["./start/routes", "./start/kernel", "./start/event", "./start/bouncer"],
|
"preloads": ["./routes/index", "./start/kernel", "./start/event", "./start/bouncer"],
|
||||||
"providers": [
|
"providers": [
|
||||||
"./providers/AppProvider",
|
"./providers/AppProvider",
|
||||||
"@adonisjs/core",
|
"@adonisjs/core",
|
||||||
@ -34,8 +34,7 @@
|
|||||||
"@adonisjs/mail",
|
"@adonisjs/mail",
|
||||||
"@adonisjs/view",
|
"@adonisjs/view",
|
||||||
"@adonisjs/attachment-lite",
|
"@adonisjs/attachment-lite",
|
||||||
"@adonisjs/bouncer",
|
"@adonisjs/bouncer"
|
||||||
"@adonisjs/drive-s3"
|
|
||||||
],
|
],
|
||||||
"aceProviders": ["@adonisjs/repl"],
|
"aceProviders": ["@adonisjs/repl"],
|
||||||
"metaFiles": [
|
"metaFiles": [
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
# Adonis default .gitignore ignores
|
|
||||||
node_modules
|
|
||||||
build
|
build
|
||||||
coverage
|
node_modules
|
||||||
.vscode
|
|
||||||
.DS_STORE
|
|
||||||
.env
|
|
||||||
tmp
|
|
||||||
uploads
|
|
||||||
|
|
||||||
# Additional .gitignore ignores (any custom file you wish)
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Additional good to have ignores for dockerignore
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*
|
|
||||||
.dockerignore
|
|
||||||
*.md
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
*.log
|
||||||
|
.env
|
||||||
|
Dockerfile
|
||||||
@ -19,8 +19,3 @@ SMTP_PASSWORD=<password>
|
|||||||
MAILGUN_API_KEY=<mailgun-api-key>
|
MAILGUN_API_KEY=<mailgun-api-key>
|
||||||
MAILGUN_DOMAIN=<your-domain>
|
MAILGUN_DOMAIN=<your-domain>
|
||||||
CACHE_VIEWS=false
|
CACHE_VIEWS=false
|
||||||
DRIVE_DISK=local
|
|
||||||
S3_KEY=dummyKey
|
|
||||||
S3_SECRET=dummySecret
|
|
||||||
S3_BUCKET=dummyBucket
|
|
||||||
S3_REGION=dummyRegion
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
"project": "./tsconfig.json",
|
"project": "./tsconfig.json",
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "test/**/*.ts"]
|
"include": ["src/**/*.ts", "src/**/*.js", "test/**/*.ts"]
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint", "import"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["error"]
|
"@typescript-eslint/no-unused-vars": ["error"]
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,4 +6,3 @@ coverage
|
|||||||
.env
|
.env
|
||||||
tmp
|
tmp
|
||||||
uploads
|
uploads
|
||||||
npm-debug.log
|
|
||||||
67
CHANGELOG.md
67
CHANGELOG.md
@ -1,71 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.4.2] - 2022-12-06
|
## Unreleased
|
||||||
|
|
||||||
## Added
|
- API deployment with Docker and Nginx web server.
|
||||||
|
|
||||||
- Uploading video from url source (YouTube, Vimeo, .mp4, etc..)
|
|
||||||
- Added student and teacher to seeders
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
- Removing lesson files from drive when deleting course, category or user.
|
|
||||||
- Fixed lesson display ordering when seeding database
|
|
||||||
- Refactored bouncer actions
|
|
||||||
|
|
||||||
## [1.3.2] - 2022-06-06
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Updated npm dependencies
|
|
||||||
- [Added](https://github.com/sergeyyarkov/educt-server/commit/294a4f584dd39d295372802305004365fb01fdb2) `MAILER` env variable
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
- Fixed schema validator on saving lesson order
|
|
||||||
- Fixed lesson [duration updating](https://github.com/sergeyyarkov/educt-server/commit/ffdebc4f927abe3b63c62c84268f40f9e6663895#diff-86d18314585862595621b4e90f675b79ac74e7a0a47b2797556902eeff75e14a)
|
|
||||||
- Updated cors config
|
|
||||||
- Deleted `api` from path url
|
|
||||||
|
|
||||||
## [1.3.1] - 2022-04-10
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Docker files for building container
|
|
||||||
|
|
||||||
## [1.2.1] - 2022-03-06
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Saving latest notifications about new messages
|
|
||||||
- Saving recent conversation messages based on constant (now 100)
|
|
||||||
- Simple chat implementation between users
|
|
||||||
- Added `about` and `last_login` columns for user
|
|
||||||
- Updating info api route of authorized user
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- Refactored routes import
|
|
||||||
- Fixed updating user info
|
|
||||||
- Cleaning up session store before starting websocket server
|
|
||||||
|
|
||||||
## [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
|
|
||||||
|
|
||||||
## [1.0.0] - 2021-11-25
|
## [1.0.0] - 2021-11-25
|
||||||
|
|
||||||
|
|||||||
25
Dockerfile
25
Dockerfile
@ -1,25 +0,0 @@
|
|||||||
ARG NODE_IMAGE=node:14.17.3-alpine3.11
|
|
||||||
|
|
||||||
FROM ${NODE_IMAGE} AS base
|
|
||||||
RUN apk --no-cache add dumb-init
|
|
||||||
RUN mkdir -p /home/node/app && chown node:node /home/node/app
|
|
||||||
WORKDIR /home/node/app
|
|
||||||
USER node
|
|
||||||
RUN mkdir uploads
|
|
||||||
|
|
||||||
FROM base as builder
|
|
||||||
COPY --chown=node:node package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY --chown=node:node . .
|
|
||||||
RUN node ace build --production
|
|
||||||
|
|
||||||
FROM base as production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV PORT=3333
|
|
||||||
ENV HOST=0.0.0.0
|
|
||||||
WORKDIR /home/node/app
|
|
||||||
COPY --chown=node:node --from=builder /home/node/app/build .
|
|
||||||
RUN npm ci --production
|
|
||||||
|
|
||||||
EXPOSE 3333
|
|
||||||
CMD ["dumb-init", "node", "server.js"]
|
|
||||||
@ -1,8 +1,3 @@
|
|||||||
# educt-server
|
# About API
|
||||||
|
|
||||||
_This service is part of [Educt](https://github.com/sergeyyarkov/educt) which contains API application._
|
...
|
||||||
|
|
||||||
<!-- ## Requirements
|
|
||||||
|
|
||||||
- NodeJS >= 14.17.3
|
|
||||||
- Docker -->
|
|
||||||
|
|||||||
@ -11,66 +11,19 @@
|
|||||||
},
|
},
|
||||||
"list:routes": {
|
"list:routes": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"loadApp": true,
|
"loadApp": true
|
||||||
"stayAlive": true
|
|
||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/core/build/commands/ListRoutes/index",
|
"commandPath": "@adonisjs/core/build/commands/ListRoutes",
|
||||||
"commandName": "list:routes",
|
"commandName": "list:routes",
|
||||||
"description": "List application routes",
|
"description": "List application routes",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
{
|
|
||||||
"name": "verbose",
|
|
||||||
"propertyName": "verbose",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Display more information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "reverse",
|
|
||||||
"propertyName": "reverse",
|
|
||||||
"type": "boolean",
|
|
||||||
"alias": "r",
|
|
||||||
"description": "Reverse routes display"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "methods",
|
|
||||||
"propertyName": "methodsFilter",
|
|
||||||
"type": "array",
|
|
||||||
"alias": "m",
|
|
||||||
"description": "Filter routes by method"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "patterns",
|
|
||||||
"propertyName": "patternsFilter",
|
|
||||||
"type": "array",
|
|
||||||
"alias": "p",
|
|
||||||
"description": "Filter routes by the route pattern"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "names",
|
|
||||||
"propertyName": "namesFilter",
|
|
||||||
"type": "array",
|
|
||||||
"alias": "n",
|
|
||||||
"description": "Filter routes by route name"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "json",
|
"name": "json",
|
||||||
"propertyName": "json",
|
"propertyName": "json",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Output as JSON"
|
"description": "Output as JSON"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "table",
|
|
||||||
"propertyName": "table",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Output as Table"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "max-width",
|
|
||||||
"propertyName": "maxWidth",
|
|
||||||
"type": "number",
|
|
||||||
"description": "Specify maximum rendering width. Ignored for JSON Output"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -102,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
|
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
|
||||||
"commandName": "db:seed",
|
"commandName": "db:seed",
|
||||||
"description": "Execute database seeders",
|
"description": "Execute database seeder files",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -126,19 +79,11 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Define a custom set of seeders files names to run",
|
"description": "Define a custom set of seeders files names to run",
|
||||||
"alias": "f"
|
"alias": "f"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "compact-output",
|
|
||||||
"propertyName": "compactOutput",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "A compact single-line output"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"make:model": {
|
"make:model": {
|
||||||
"settings": {
|
"settings": {},
|
||||||
"loadApp": true
|
|
||||||
},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/MakeModel",
|
"commandPath": "@adonisjs/lucid/build/commands/MakeModel",
|
||||||
"commandName": "make:model",
|
"commandName": "make:model",
|
||||||
"description": "Make a new Lucid model",
|
"description": "Make a new Lucid model",
|
||||||
@ -166,13 +111,6 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"alias": "c",
|
"alias": "c",
|
||||||
"description": "Generate the controller for the model"
|
"description": "Generate the controller for the model"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "factory",
|
|
||||||
"propertyName": "factory",
|
|
||||||
"type": "boolean",
|
|
||||||
"alias": "f",
|
|
||||||
"description": "Generate a factory for the model"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -237,81 +175,13 @@
|
|||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": []
|
"flags": []
|
||||||
},
|
},
|
||||||
"make:factory": {
|
|
||||||
"settings": {},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/MakeFactory",
|
|
||||||
"commandName": "make:factory",
|
|
||||||
"description": "Make a new factory",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"propertyName": "model",
|
|
||||||
"name": "model",
|
|
||||||
"required": true,
|
|
||||||
"description": "The name of the model"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"aliases": [],
|
|
||||||
"flags": [
|
|
||||||
{
|
|
||||||
"name": "model-path",
|
|
||||||
"propertyName": "modelPath",
|
|
||||||
"type": "string",
|
|
||||||
"description": "The path to the model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "exact",
|
|
||||||
"propertyName": "exact",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Create the factory with the exact name as provided",
|
|
||||||
"alias": "e"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"db:wipe": {
|
|
||||||
"settings": {
|
|
||||||
"loadApp": true
|
|
||||||
},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/DbWipe",
|
|
||||||
"commandName": "db:wipe",
|
|
||||||
"description": "Drop all tables, views and types in database",
|
|
||||||
"args": [],
|
|
||||||
"aliases": [],
|
|
||||||
"flags": [
|
|
||||||
{
|
|
||||||
"name": "connection",
|
|
||||||
"propertyName": "connection",
|
|
||||||
"type": "string",
|
|
||||||
"description": "Define a custom database connection",
|
|
||||||
"alias": "c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "drop-views",
|
|
||||||
"propertyName": "dropViews",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Drop all views"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "drop-types",
|
|
||||||
"propertyName": "dropTypes",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Drop all custom types (Postgres only)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "force",
|
|
||||||
"propertyName": "force",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Explicitly force command to run in production"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"migration:run": {
|
"migration:run": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"loadApp": true
|
"loadApp": true
|
||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Run",
|
"commandPath": "@adonisjs/lucid/build/commands/Migration/Run",
|
||||||
"commandName": "migration:run",
|
"commandName": "migration:run",
|
||||||
"description": "Migrate database by running pending migrations",
|
"description": "Run pending migrations",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -332,13 +202,7 @@
|
|||||||
"name": "dry-run",
|
"name": "dry-run",
|
||||||
"propertyName": "dryRun",
|
"propertyName": "dryRun",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Do not run actual queries. Instead view the SQL output"
|
"description": "Print SQL queries, instead of running the migrations"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "compact-output",
|
|
||||||
"propertyName": "compactOutput",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "A compact single-line output"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -348,7 +212,7 @@
|
|||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback",
|
"commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback",
|
||||||
"commandName": "migration:rollback",
|
"commandName": "migration:rollback",
|
||||||
"description": "Rollback migrations to a specific batch number",
|
"description": "Rollback migrations to a given batch number",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -369,19 +233,13 @@
|
|||||||
"name": "dry-run",
|
"name": "dry-run",
|
||||||
"propertyName": "dryRun",
|
"propertyName": "dryRun",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Do not run actual queries. Instead view the SQL output"
|
"description": "Print SQL queries, instead of running the migrations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "batch",
|
"name": "batch",
|
||||||
"propertyName": "batch",
|
"propertyName": "batch",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Define custom batch number for rollback. Use 0 to rollback to initial state"
|
"description": "Define custom batch number for rollback. Use 0 to rollback to initial state"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "compact-output",
|
|
||||||
"propertyName": "compactOutput",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "A compact single-line output"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -391,7 +249,7 @@
|
|||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
|
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
|
||||||
"commandName": "migration:status",
|
"commandName": "migration:status",
|
||||||
"description": "View migrations status",
|
"description": "Check migrations current status.",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -404,117 +262,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"migration:reset": {
|
|
||||||
"settings": {
|
|
||||||
"loadApp": true
|
|
||||||
},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Reset",
|
|
||||||
"commandName": "migration:reset",
|
|
||||||
"description": "Rollback all migrations",
|
|
||||||
"args": [],
|
|
||||||
"aliases": [],
|
|
||||||
"flags": [
|
|
||||||
{
|
|
||||||
"name": "connection",
|
|
||||||
"propertyName": "connection",
|
|
||||||
"type": "string",
|
|
||||||
"description": "Define a custom database connection",
|
|
||||||
"alias": "c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "force",
|
|
||||||
"propertyName": "force",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Explicitly force command to run in production"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dry-run",
|
|
||||||
"propertyName": "dryRun",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Do not run actual queries. Instead view the SQL output"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"migration:refresh": {
|
|
||||||
"settings": {
|
|
||||||
"loadApp": true
|
|
||||||
},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Refresh",
|
|
||||||
"commandName": "migration:refresh",
|
|
||||||
"description": "Rollback and migrate database",
|
|
||||||
"args": [],
|
|
||||||
"aliases": [],
|
|
||||||
"flags": [
|
|
||||||
{
|
|
||||||
"name": "connection",
|
|
||||||
"propertyName": "connection",
|
|
||||||
"type": "string",
|
|
||||||
"description": "Define a custom database connection",
|
|
||||||
"alias": "c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "force",
|
|
||||||
"propertyName": "force",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Explicitly force command to run in production"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dry-run",
|
|
||||||
"propertyName": "dryRun",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Do not run actual queries. Instead view the SQL output"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seed",
|
|
||||||
"propertyName": "seed",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Run seeders"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"migration:fresh": {
|
|
||||||
"settings": {
|
|
||||||
"loadApp": true
|
|
||||||
},
|
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Fresh",
|
|
||||||
"commandName": "migration:fresh",
|
|
||||||
"description": "Drop all tables and re-migrate the database",
|
|
||||||
"args": [],
|
|
||||||
"aliases": [],
|
|
||||||
"flags": [
|
|
||||||
{
|
|
||||||
"name": "connection",
|
|
||||||
"propertyName": "connection",
|
|
||||||
"type": "string",
|
|
||||||
"description": "Define a custom database connection",
|
|
||||||
"alias": "c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "force",
|
|
||||||
"propertyName": "force",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Explicitly force command to run in production"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seed",
|
|
||||||
"propertyName": "seed",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Run seeders"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "drop-views",
|
|
||||||
"propertyName": "dropViews",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Drop all views"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "drop-types",
|
|
||||||
"propertyName": "dropTypes",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Drop all custom types (Postgres only)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"make:mailer": {
|
"make:mailer": {
|
||||||
"settings": {},
|
"settings": {},
|
||||||
"commandPath": "@adonisjs/mail/build/commands/MakeMailer",
|
"commandPath": "@adonisjs/mail/build/commands/MakeMailer",
|
||||||
|
|||||||
@ -7,10 +7,9 @@ import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
|
|||||||
import CategoryService from 'App/Services/CategoryService';
|
import CategoryService from 'App/Services/CategoryService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validators
|
* Validatoes
|
||||||
*/
|
*/
|
||||||
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
||||||
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
|
|
||||||
|
|
||||||
import BaseController from '../../BaseController';
|
import BaseController from '../../BaseController';
|
||||||
|
|
||||||
@ -79,21 +78,6 @@ export default class CategoriesController extends BaseController {
|
|||||||
|
|
||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
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);
|
new Ioc().make(CategoriesController);
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
|
||||||
import { ConversationType } from 'App/Store/MessageStore';
|
|
||||||
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enums
|
|
||||||
*/
|
|
||||||
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import WsService from 'App/Services/WsService';
|
|
||||||
|
|
||||||
import BaseController from '../../BaseController';
|
|
||||||
|
|
||||||
@inject()
|
|
||||||
export default class ChatController extends BaseController {
|
|
||||||
/**
|
|
||||||
* Get list of conversations (users)
|
|
||||||
* GET /chat/conversations
|
|
||||||
*/
|
|
||||||
public async getConversations(ctx: HttpContextContract) {
|
|
||||||
const user = await ctx.auth.authenticate();
|
|
||||||
const data = await WsService.messageStore.getConversations(user.id);
|
|
||||||
const result: IResponse<ConversationType[]> = {
|
|
||||||
success: true,
|
|
||||||
status: HttpStatusEnum.OK,
|
|
||||||
message: 'Fetched conversations.',
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Ioc().make(ChatController);
|
|
||||||
@ -10,8 +10,6 @@ import CourseService from 'App/Services/CourseService';
|
|||||||
* Validators
|
* Validators
|
||||||
*/
|
*/
|
||||||
import AddCourseToUserValidator from 'App/Validators/Course/AddCourseToUserValidator';
|
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 CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
|
||||||
import DelCourseFromUserValidator from 'App/Validators/Course/DelCourseFromUserValidator';
|
import DelCourseFromUserValidator from 'App/Validators/Course/DelCourseFromUserValidator';
|
||||||
import SetCourseStatusValidator from 'App/Validators/Course/SetCourseStatusValidator';
|
import SetCourseStatusValidator from 'App/Validators/Course/SetCourseStatusValidator';
|
||||||
@ -73,7 +71,7 @@ export default class CoursesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Category by course id
|
* Show Category of Course with "id"
|
||||||
* GET /courses/:id/category
|
* GET /courses/:id/category
|
||||||
*/
|
*/
|
||||||
public async showCategory(ctx: HttpContextContract) {
|
public async showCategory(ctx: HttpContextContract) {
|
||||||
@ -87,11 +85,11 @@ export default class CoursesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Lessons by course id
|
* Show Lessons of ourse with "id"
|
||||||
* GET /courses/:id/lessons
|
* GET /courses/:id/lessons
|
||||||
*/
|
*/
|
||||||
public async showLessons(ctx: HttpContextContract) {
|
public async showLessons(ctx: HttpContextContract) {
|
||||||
const result = await this.courseService.fetchCourseLessons(ctx.params.id, ctx);
|
const result = await this.courseService.fetchCourseLessons(ctx.params.id);
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
throw new Exception(result.message, result.status, result.error.code);
|
||||||
@ -173,21 +171,6 @@ export default class CoursesController extends BaseController {
|
|||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
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
|
* Detach student from course
|
||||||
* DELETE /courses/:id/detach-student
|
* DELETE /courses/:id/detach-student
|
||||||
@ -203,21 +186,6 @@ export default class CoursesController extends BaseController {
|
|||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
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
|
* Fetch count of students of course
|
||||||
* GET /courses/:id/students/count
|
* GET /courses/:id/students/count
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import Application from '@ioc:Adonis/Core/Application';
|
||||||
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
|
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||||
import { schema, rules } from '@ioc:Adonis/Core/Validator';
|
import { schema } from '@ioc:Adonis/Core/Validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services
|
* Services
|
||||||
@ -48,7 +49,7 @@ export default class LessonsController extends BaseController {
|
|||||||
* GET /lessons/:id
|
* GET /lessons/:id
|
||||||
*/
|
*/
|
||||||
public async show(ctx: HttpContextContract) {
|
public async show(ctx: HttpContextContract) {
|
||||||
const result = await this.lessonService.fetchLesson(ctx.params.id, ctx);
|
const result = await this.lessonService.fetchLesson(ctx.params.id);
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
throw new Exception(result.message, result.status, result.error.code);
|
||||||
@ -88,7 +89,7 @@ export default class LessonsController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update lesson by id.
|
* Update lesson by id.
|
||||||
* PATCH /lessons/:id
|
* PUT /lessons/:id
|
||||||
*/
|
*/
|
||||||
public async update(ctx: HttpContextContract) {
|
public async update(ctx: HttpContextContract) {
|
||||||
const payload = await ctx.request.validate(UpdateLessonValidator);
|
const payload = await ctx.request.validate(UpdateLessonValidator);
|
||||||
@ -117,18 +118,22 @@ export default class LessonsController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get lesson materials by file name
|
* Get lesson materials by file name
|
||||||
* GET /lessons/materials/:fileName
|
* GET /lessons/materials/:file
|
||||||
*/
|
*/
|
||||||
public async getMaterial(ctx: HttpContextContract) {
|
public async getMaterial(ctx: HttpContextContract) {
|
||||||
const { fileName } = ctx.request.params();
|
const { file } = ctx.request.params();
|
||||||
const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, fileName);
|
const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, file);
|
||||||
|
|
||||||
if (!success && error) {
|
if (!success && error) {
|
||||||
throw new Exception(message, status, error.code);
|
throw new Exception(message, status, error.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send file to user
|
||||||
|
*/
|
||||||
if (data instanceof LessonMaterial) {
|
if (data instanceof LessonMaterial) {
|
||||||
return this.sendFileFromDrive(`materials/${data.name}`, ctx);
|
const path = Application.makePath(data.url);
|
||||||
|
return this.sendFile(ctx, path, data.clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sendResponse(ctx, data, message, status);
|
return this.sendResponse(ctx, data, message, status);
|
||||||
@ -141,7 +146,7 @@ export default class LessonsController extends BaseController {
|
|||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
public async saveOrder(ctx: HttpContextContract) {
|
public async saveOrder(ctx: HttpContextContract) {
|
||||||
const saveOrderSchema = schema.create({
|
const saveOrderSchema = schema.create({
|
||||||
ids: schema.array([rules.minLength(1)]).members(schema.string()),
|
ids: schema.array().members(schema.string()),
|
||||||
});
|
});
|
||||||
const payload = await ctx.request.validate({ schema: saveOrderSchema });
|
const payload = await ctx.request.validate({ schema: saveOrderSchema });
|
||||||
const { data, message, status, success, error } = await this.lessonService.updateOrder(payload.ids);
|
const { data, message, status, success, error } = await this.lessonService.updateOrder(payload.ids);
|
||||||
@ -152,20 +157,6 @@ export default class LessonsController extends BaseController {
|
|||||||
|
|
||||||
return this.sendResponse(ctx, data, message, status);
|
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);
|
new Ioc().make(LessonsController);
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import MeService from 'App/Services/MeService';
|
|||||||
*/
|
*/
|
||||||
import ChangePasswordValidator from 'App/Validators/Password/ChangePasswordValidator';
|
import ChangePasswordValidator from 'App/Validators/Password/ChangePasswordValidator';
|
||||||
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
|
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
|
||||||
import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator';
|
|
||||||
|
|
||||||
import BaseController from '../../BaseController';
|
import BaseController from '../../BaseController';
|
||||||
|
|
||||||
@ -97,7 +96,7 @@ export default class MeController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update contacts of authenticated user
|
* Update contacts of authenticated user
|
||||||
* PATCH /me/contacts
|
* PUT /me/contacts
|
||||||
*/
|
*/
|
||||||
public async updateContacts(ctx: HttpContextContract) {
|
public async updateContacts(ctx: HttpContextContract) {
|
||||||
const payload = await ctx.request.validate(UpdateContactsValidator);
|
const payload = await ctx.request.validate(UpdateContactsValidator);
|
||||||
@ -109,35 +108,6 @@ export default class MeController extends BaseController {
|
|||||||
|
|
||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
return this.sendResponse(ctx, result.data, result.message, result.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update personal info of user
|
|
||||||
* PATCH /me/info
|
|
||||||
*/
|
|
||||||
public async updateInfo(ctx: HttpContextContract) {
|
|
||||||
const payload = await ctx.request.validate(UpdateUserInfoValidator);
|
|
||||||
const result = await this.meService.updateUserInfo(payload, ctx.auth);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get messages of conversation by chat id
|
|
||||||
* GET /me/messages
|
|
||||||
*/
|
|
||||||
public async chatHistory(ctx: HttpContextContract) {
|
|
||||||
const result = await this.meService.fetchChatHistory(ctx.params.chat_id, ctx.auth);
|
|
||||||
|
|
||||||
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(MeController);
|
new Ioc().make(MeController);
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -93,7 +93,7 @@ export default class UsersController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public async delete(ctx: HttpContextContract) {
|
public async delete(ctx: HttpContextContract) {
|
||||||
const result = await this.userService.deleteUser(ctx.params.id, ctx);
|
const result = await this.userService.deleteUser(ctx.params.id);
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
throw new Exception(result.message, result.status, result.error.code);
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||||
import { ResponseContract } from '@ioc:Adonis/Core/Response';
|
import { ResponseContract } from '@ioc:Adonis/Core/Response';
|
||||||
import Drive from '@ioc:Adonis/Core/Drive';
|
|
||||||
import { extname } from 'path';
|
|
||||||
|
|
||||||
export default class BaseController {
|
export default class BaseController {
|
||||||
public sendResponse(ctx: HttpContextContract, data: object, message: string, status = 200, meta?: any): void {
|
public sendResponse(ctx: HttpContextContract, data: object, message: string, status = 200, meta?: any): void {
|
||||||
@ -16,27 +14,7 @@ export default class BaseController {
|
|||||||
return ctx.response.status(response.status).send(response);
|
return ctx.response.status(response.status).send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendStream(ctx: HttpContextContract, ...args: Parameters<ResponseContract['stream']>) {
|
|
||||||
return ctx.response.stream(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendFile(ctx: HttpContextContract, ...args: Parameters<ResponseContract['attachment']>) {
|
public sendFile(ctx: HttpContextContract, ...args: Parameters<ResponseContract['attachment']>) {
|
||||||
return ctx.response.attachment(...args);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
||||||
|
|
||||||
public io: Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
||||||
|
|
||||||
public sessionStore: RedisSessionStore;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
socket: Socket,
|
|
||||||
io: Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>,
|
|
||||||
sessionStore: RedisSessionStore
|
|
||||||
) {
|
|
||||||
this.socket = socket;
|
|
||||||
this.io = io;
|
|
||||||
this.sessionStore = sessionStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handle() {
|
|
||||||
this.onConnected(this.socket);
|
|
||||||
this.socket.on('disconnect', reason => this.onDisconnected(reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendOnline(online: [string, { userId: string; userName: string }][]) {
|
|
||||||
this.io.sockets.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.getOnlineSessions())
|
|
||||||
.then(online => this.sendOnline(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.getOnlineSessions())
|
|
||||||
.then(online => this.socket.broadcast.emit('user:online', online));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On socket connected
|
|
||||||
*/
|
|
||||||
public async onConnected(socket: Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>) {
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +1,16 @@
|
|||||||
enum ColorEnum {
|
enum ColorEnum {
|
||||||
|
Beige = '#F5F5DC',
|
||||||
IndianRed = '#CD5C5C',
|
IndianRed = '#CD5C5C',
|
||||||
FireBrick = '#B22222',
|
FireBrick = '#B22222',
|
||||||
|
Pink = '#FFC0CB',
|
||||||
Tomato = '#FF6347',
|
Tomato = '#FF6347',
|
||||||
Orange = '#FFA500',
|
Orange = '#FFA500',
|
||||||
Gold = '#FFD700',
|
Gold = '#FFD700',
|
||||||
|
Moccasin = '#FFE4B5',
|
||||||
|
Khaki = '#F0E68C',
|
||||||
Purple = '#800080',
|
Purple = '#800080',
|
||||||
Indigo = '#4B0082',
|
Indigo = '#4B0082',
|
||||||
Teal = '#008080',
|
Teal = '#008080',
|
||||||
Cyan = '#0BC5EA',
|
|
||||||
Crimson = '#DC143C',
|
|
||||||
Chocolate = '#D2691E',
|
|
||||||
Salmon = '#FA8072',
|
|
||||||
Violet = '#EE82EE',
|
|
||||||
Blue = '#0000FF',
|
|
||||||
SteelBlue = '#4682B4',
|
|
||||||
RosyBrown = '#BC8F8F',
|
|
||||||
DarkSlateGray = '#2F4F4F',
|
|
||||||
DimGray = '#696969',
|
|
||||||
MediumVioletRed = '#C71585',
|
MediumVioletRed = '#C71585',
|
||||||
MediumOrchid = '#BA55D3',
|
MediumOrchid = '#BA55D3',
|
||||||
MediumPurple = '#9370DB',
|
MediumPurple = '#9370DB',
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import Course from 'App/Models/Course';
|
|
||||||
|
|
||||||
export type FileEntry = {
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class CourseHelper {
|
|
||||||
public static getVideoFileNames(course: Course): Array<FileEntry | null> {
|
|
||||||
const data = course.lessons.map(l => {
|
|
||||||
if (!l.video) return null;
|
|
||||||
return { path: 'videos/', name: l.video.name };
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getMaterialFileNames(course: Course): FileEntry[] {
|
|
||||||
const data = course.lessons.flatMap(l => l.materials.map(m => ({ path: 'materials/', name: m.name })));
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getImageFileName(course: Course): FileEntry | null {
|
|
||||||
if (!course.image) return null;
|
|
||||||
const data = { path: 'images/courses/', name: course.image.name };
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,7 +31,8 @@ export default class AuthMiddleware {
|
|||||||
* driver
|
* driver
|
||||||
*/
|
*/
|
||||||
let guardLastAttempted: string | undefined;
|
let guardLastAttempted: string | undefined;
|
||||||
const authResults: boolean[] = await Promise.all(guards.map(guard => auth.use(guard).check()));
|
const authPromises: Promise<boolean>[] = guards.map(guard => auth.use(guard).check());
|
||||||
|
const authResults: boolean[] = await Promise.all(authPromises);
|
||||||
|
|
||||||
for (let i = 0; i < authResults.length; i += 1) {
|
for (let i = 0; i < authResults.length; i += 1) {
|
||||||
/**
|
/**
|
||||||
@ -53,7 +54,7 @@ export default class AuthMiddleware {
|
|||||||
|
|
||||||
public async handle(ctx: HttpContextContract, next: () => Promise<void>, customGuards: (keyof GuardsList)[]) {
|
public async handle(ctx: HttpContextContract, next: () => Promise<void>, customGuards: (keyof GuardsList)[]) {
|
||||||
/**
|
/**
|
||||||
* Set the authorization header for token
|
* Verify token from cookie and set the authorization header
|
||||||
*/
|
*/
|
||||||
const token = ctx.request.cookie('token');
|
const token = ctx.request.cookie('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Exception } from '@adonisjs/core/build/standalone';
|
import { Exception } from '@adonisjs/core/build/standalone';
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||||
import User from 'App/Models/User';
|
|
||||||
|
|
||||||
export default class Role {
|
export default class Role {
|
||||||
error: {
|
error: {
|
||||||
@ -24,7 +23,7 @@ export default class Role {
|
|||||||
* @param {string[]} roles - Array of roles for route
|
* @param {string[]} roles - Array of roles for route
|
||||||
* @param {string[]} userRole - Array of roles of current user
|
* @param {string[]} userRole - Array of roles of current user
|
||||||
*/
|
*/
|
||||||
protected check(roles: string[], userRoles: string[]): void {
|
protected checkRoles(roles: string[], userRoles: string[]): void {
|
||||||
if (!roles.some(role => userRoles.includes(role))) {
|
if (!roles.some(role => userRoles.includes(role))) {
|
||||||
/**
|
/**
|
||||||
* Failed to verify user rights
|
* Failed to verify user rights
|
||||||
@ -34,20 +33,8 @@ export default class Role {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ auth }: HttpContextContract, next: () => Promise<void>, roles: string[]) {
|
public async handle({ auth }: HttpContextContract, next: () => Promise<void>, roles: string[]) {
|
||||||
const { userId } = auth.use('api').token?.meta;
|
const { userRoles } = auth.use('api').token?.meta;
|
||||||
const user = await User.query().where('id', userId).preload('roles').first();
|
this.checkRoles(roles, userRoles);
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
/**
|
|
||||||
* User not found
|
|
||||||
*/
|
|
||||||
throw new Exception('Cannot identify current user.', this.error.status, this.error.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.check(
|
|
||||||
roles,
|
|
||||||
user.roles.map(role => role.slug)
|
|
||||||
);
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
/* 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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>,
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,7 +14,7 @@ export default class Category extends BaseModel {
|
|||||||
public title: string;
|
public title: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public description: string | null;
|
public description: string;
|
||||||
|
|
||||||
@column({ serializeAs: null })
|
@column({ serializeAs: null })
|
||||||
public color_id: number | null;
|
public color_id: number | null;
|
||||||
@ -24,10 +24,10 @@ export default class Category extends BaseModel {
|
|||||||
})
|
})
|
||||||
public color: BelongsTo<typeof Color>;
|
public color: BelongsTo<typeof Color>;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true, serializeAs: null })
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
|
||||||
public updatedAt: DateTime;
|
public updatedAt: DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,9 +34,6 @@ export default class Course extends BaseModel {
|
|||||||
@column()
|
@column()
|
||||||
public description: string;
|
public description: string;
|
||||||
|
|
||||||
@column()
|
|
||||||
public education_description: string | null;
|
|
||||||
|
|
||||||
@column({ serializeAs: null })
|
@column({ serializeAs: null })
|
||||||
public teacher_id: string;
|
public teacher_id: string;
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,11 @@
|
|||||||
/* eslint-disable import/no-cycle */
|
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import {
|
import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasOne, hasOne } from '@ioc:Adonis/Lucid/Orm';
|
||||||
BaseModel,
|
|
||||||
beforeCreate,
|
|
||||||
BelongsTo,
|
|
||||||
belongsTo,
|
|
||||||
column,
|
|
||||||
hasMany,
|
|
||||||
HasMany,
|
|
||||||
HasOne,
|
|
||||||
hasOne,
|
|
||||||
} from '@ioc:Adonis/Lucid/Orm';
|
|
||||||
import ColorHelper from 'App/Helpers/ColorHelper';
|
import ColorHelper from 'App/Helpers/ColorHelper';
|
||||||
import Course from './Course';
|
import Course from './Course';
|
||||||
import LessonMaterial from './LessonMaterial';
|
// eslint-disable-next-line import/no-cycle
|
||||||
import LessonContent from './LessonContent';
|
import LessonContent from './LessonContent';
|
||||||
import LessonProgress from './LessonProgress';
|
|
||||||
import LessonVideo from './LessonVideo';
|
|
||||||
import Color from './Color';
|
import Color from './Color';
|
||||||
|
|
||||||
export default class Lesson extends BaseModel {
|
export default class Lesson extends BaseModel {
|
||||||
@ -37,9 +24,6 @@ export default class Lesson extends BaseModel {
|
|||||||
@column()
|
@column()
|
||||||
public description: string;
|
public description: string;
|
||||||
|
|
||||||
@column()
|
|
||||||
public linked_video_url: string | null;
|
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public duration: string;
|
public duration: string;
|
||||||
|
|
||||||
@ -61,19 +45,6 @@ export default class Lesson extends BaseModel {
|
|||||||
})
|
})
|
||||||
public content: HasOne<typeof LessonContent>;
|
public content: HasOne<typeof LessonContent>;
|
||||||
|
|
||||||
@hasOne(() => LessonVideo, {
|
|
||||||
foreignKey: 'lesson_id',
|
|
||||||
})
|
|
||||||
public video: HasOne<typeof LessonVideo>;
|
|
||||||
|
|
||||||
@hasMany(() => LessonMaterial, { foreignKey: 'lesson_id' })
|
|
||||||
public materials: HasMany<typeof LessonMaterial>;
|
|
||||||
|
|
||||||
@hasOne(() => LessonProgress, {
|
|
||||||
foreignKey: 'lesson_id',
|
|
||||||
})
|
|
||||||
public progress: HasOne<typeof LessonProgress>;
|
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true })
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|
||||||
@ -97,13 +68,4 @@ export default class Lesson extends BaseModel {
|
|||||||
course.color_id = color.id;
|
course.color_id = color.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize the `$extras` object
|
|
||||||
*/
|
|
||||||
public serializeExtras() {
|
|
||||||
return {
|
|
||||||
materials_count: this.$extras.materials_count,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm';
|
import { BaseModel, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import Lesson from './Lesson';
|
import Lesson from './Lesson';
|
||||||
|
import LessonMaterial from './LessonMaterial';
|
||||||
|
|
||||||
export default class LessonContent extends BaseModel {
|
export default class LessonContent extends BaseModel {
|
||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
@ -11,7 +12,10 @@ export default class LessonContent extends BaseModel {
|
|||||||
public lesson_id: string;
|
public lesson_id: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public body: string;
|
public video_url: string;
|
||||||
|
|
||||||
|
@hasMany(() => LessonMaterial, { foreignKey: 'lesson_content_id' })
|
||||||
|
public materials: HasMany<typeof LessonMaterial>;
|
||||||
|
|
||||||
@belongsTo(() => Lesson, { foreignKey: 'lesson_id' })
|
@belongsTo(() => Lesson, { foreignKey: 'lesson_id' })
|
||||||
public lesson: BelongsTo<typeof Lesson>;
|
public lesson: BelongsTo<typeof Lesson>;
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
/* eslint-disable import/no-cycle */
|
|
||||||
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm';
|
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import Lesson from './Lesson';
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import LessonContent from './LessonContent';
|
||||||
|
|
||||||
export default class LessonMaterial extends BaseModel {
|
export default class LessonMaterial extends BaseModel {
|
||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
public id: number;
|
public id: number;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public name: string;
|
public url: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public size: number;
|
public name: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public clientName: string;
|
public clientName: string;
|
||||||
@ -20,12 +20,12 @@ export default class LessonMaterial extends BaseModel {
|
|||||||
public ext: string;
|
public ext: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public lesson_id: string;
|
public lesson_content_id: number;
|
||||||
|
|
||||||
@belongsTo(() => Lesson, {
|
@belongsTo(() => LessonContent, {
|
||||||
foreignKey: 'lesson_id',
|
foreignKey: 'lesson_content_id',
|
||||||
})
|
})
|
||||||
public lesson: BelongsTo<typeof Lesson>;
|
public content: BelongsTo<typeof LessonContent>;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true })
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
/* 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<typeof Lesson>;
|
|
||||||
|
|
||||||
@belongsTo(() => User, {
|
|
||||||
foreignKey: 'user_id',
|
|
||||||
})
|
|
||||||
public user: BelongsTo<typeof User>;
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
/* 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<typeof Lesson>;
|
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
|
||||||
public createdAt: DateTime;
|
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
|
||||||
public updatedAt: DateTime;
|
|
||||||
}
|
|
||||||
@ -7,8 +7,6 @@ import {
|
|||||||
beforeSave,
|
beforeSave,
|
||||||
column,
|
column,
|
||||||
computed,
|
computed,
|
||||||
HasMany,
|
|
||||||
hasMany,
|
|
||||||
HasOne,
|
HasOne,
|
||||||
hasOne,
|
hasOne,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
@ -19,7 +17,6 @@ import { nanoid } from 'nanoid';
|
|||||||
import Role from 'App/Models/Role';
|
import Role from 'App/Models/Role';
|
||||||
import Contact from 'App/Models/Contact';
|
import Contact from 'App/Models/Contact';
|
||||||
import Course from 'App/Models/Course';
|
import Course from 'App/Models/Course';
|
||||||
import LessonProgress from './LessonProgress';
|
|
||||||
|
|
||||||
export default class User extends BaseModel {
|
export default class User extends BaseModel {
|
||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
@ -65,21 +62,10 @@ export default class User extends BaseModel {
|
|||||||
})
|
})
|
||||||
public likes: ManyToMany<typeof Course>;
|
public likes: ManyToMany<typeof Course>;
|
||||||
|
|
||||||
@hasMany(() => LessonProgress, {
|
@column.dateTime({ autoCreate: true, serializeAs: null })
|
||||||
foreignKey: 'user_id',
|
|
||||||
})
|
|
||||||
public lessons_progress: HasMany<typeof LessonProgress>;
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public about: string | null;
|
|
||||||
|
|
||||||
@column.dateTime()
|
|
||||||
public last_login: DateTime | null;
|
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
|
||||||
public updatedAt: DateTime;
|
public updatedAt: DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models
|
|
||||||
*/
|
|
||||||
import Lesson from 'App/Models/Lesson';
|
|
||||||
import User from 'App/Models/User';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datatypes
|
|
||||||
*/
|
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
|
|
||||||
export default class LessonPolicy extends BasePolicy {
|
|
||||||
public async view(user: User, lesson: Lesson) {
|
|
||||||
/**
|
|
||||||
* Load required data to check permissions
|
|
||||||
*/
|
|
||||||
await user.load(loader => loader.load('roles').load('courses'));
|
|
||||||
await lesson.load('course');
|
|
||||||
|
|
||||||
const isAdminOrTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
|
||||||
|
|
||||||
if (isAdminOrTeacher) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!user.courses.find(course => course.id === lesson.course.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Models
|
|
||||||
*/
|
|
||||||
import User from 'App/Models/User';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datatypes
|
|
||||||
*/
|
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
|
|
||||||
export default class RolePolicy extends BasePolicy {
|
|
||||||
public async manage(user: User, roles: Array<RoleEnum>) {
|
|
||||||
await user.load('roles');
|
|
||||||
|
|
||||||
const isAdmin = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN]);
|
|
||||||
const isTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.TEACHER]);
|
|
||||||
|
|
||||||
if (isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User with role `TEACHER` can manage only user with `STUDENT` role
|
|
||||||
*/
|
|
||||||
if (!(roles.includes(RoleEnum.TEACHER) || roles.includes(RoleEnum.ADMIN)) && isTeacher) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ import Category from 'App/Models/Category';
|
|||||||
* Validators
|
* Validators
|
||||||
*/
|
*/
|
||||||
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
||||||
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
|
|
||||||
|
|
||||||
export default class CategoryRepository {
|
export default class CategoryRepository {
|
||||||
private Category: typeof Category;
|
private Category: typeof Category;
|
||||||
@ -68,22 +67,4 @@ export default class CategoryRepository {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update category by id
|
|
||||||
*
|
|
||||||
* @param id Category id
|
|
||||||
* @param data
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public async update(id: string | number, data: UpdateCategoryValidator['schema']['props']): Promise<Category | null> {
|
|
||||||
const category = await this.Category.query().where('id', id).first();
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
await category.merge(data).save();
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,12 @@ export default class ContactRepository {
|
|||||||
const contacts = await this.Contact.query().where('user_id', user.id).first();
|
const contacts = await this.Contact.query().where('user_id', user.id).first();
|
||||||
|
|
||||||
if (contacts) {
|
if (contacts) {
|
||||||
await contacts.merge(data).save();
|
contacts.phone_number = data.phone_number ?? null;
|
||||||
|
contacts.telegram_id = data.telegram_id?.substring(1) ?? null;
|
||||||
|
contacts.twitter_id = data.twitter_id?.substring(1) ?? null;
|
||||||
|
contacts.vk_id = data.vk_id ?? null;
|
||||||
|
|
||||||
|
await contacts.save();
|
||||||
await user.load('contacts');
|
await user.load('contacts');
|
||||||
|
|
||||||
return contacts;
|
return contacts;
|
||||||
|
|||||||
@ -32,32 +32,6 @@ export default class CourseRepository {
|
|||||||
this.Course = Course;
|
this.Course = Course;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of courses by teacher id
|
|
||||||
*
|
|
||||||
* @param id Teacher id
|
|
||||||
* @returns Array of courses
|
|
||||||
*/
|
|
||||||
public async getByTeacherId(id: string | number): Promise<Course[]> {
|
|
||||||
const data = await this.Course.query()
|
|
||||||
.preload('lessons', q => q.preload('video').preload('materials'))
|
|
||||||
.where('teacher_id', id);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of courses by category id
|
|
||||||
*
|
|
||||||
* @param id Category id
|
|
||||||
* @returns Array of courses
|
|
||||||
*/
|
|
||||||
public async getByCategoryId(id: string | number): Promise<Course[]> {
|
|
||||||
const data = await this.Course.query()
|
|
||||||
.preload('lessons', q => q.preload('video').preload('materials'))
|
|
||||||
.where('category_id', id);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set status on course
|
* Set status on course
|
||||||
*
|
*
|
||||||
@ -75,7 +49,7 @@ export default class CourseRepository {
|
|||||||
* @returns List of courses
|
* @returns List of courses
|
||||||
*/
|
*/
|
||||||
public async getAll(params?: FetchCoursesValidator['schema']['props']): Promise<Course[]> {
|
public async getAll(params?: FetchCoursesValidator['schema']['props']): Promise<Course[]> {
|
||||||
const { status, category_id, limit } = params || {};
|
const { status, category_id } = params || {};
|
||||||
|
|
||||||
const query = this.Course.query();
|
const query = this.Course.query();
|
||||||
|
|
||||||
@ -87,17 +61,12 @@ export default class CourseRepository {
|
|||||||
query.andWhere('status', status);
|
query.andWhere('status', status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
query.limit(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const courses = await query
|
const courses = await query
|
||||||
.preload('color')
|
.preload('color')
|
||||||
.preload('category')
|
.preload('category')
|
||||||
.withCount('students')
|
.withCount('students')
|
||||||
.withCount('likes')
|
.withCount('likes')
|
||||||
.withCount('lessons')
|
.withCount('lessons');
|
||||||
.orderBy('created_at', 'desc');
|
|
||||||
|
|
||||||
return courses;
|
return courses;
|
||||||
}
|
}
|
||||||
@ -112,12 +81,9 @@ export default class CourseRepository {
|
|||||||
const course = await this.Course.query()
|
const course = await this.Course.query()
|
||||||
.preload('teacher')
|
.preload('teacher')
|
||||||
.preload('color')
|
.preload('color')
|
||||||
.preload('category', q => q.preload('color'))
|
.preload('category')
|
||||||
.preload('lessons', q => q.preload('color').orderBy('display_order', 'asc').withCount('materials'))
|
.preload('lessons', q => q.preload('color').orderBy('display_order', 'asc'))
|
||||||
.preload('students', q => q.preload('roles').preload('contacts'))
|
.preload('students')
|
||||||
.withCount('likes')
|
|
||||||
.withCount('lessons')
|
|
||||||
.withCount('students')
|
|
||||||
.where('id', id)
|
.where('id', id)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@ -187,17 +153,8 @@ export default class CourseRepository {
|
|||||||
* @param id Course id
|
* @param id Course id
|
||||||
* @returns List of lessons or null
|
* @returns List of lessons or null
|
||||||
*/
|
*/
|
||||||
public async getLessons(id: string | number, userId: string): Promise<Lesson[] | null> {
|
public async getLessons(id: string | number): Promise<Lesson[] | null> {
|
||||||
const course = await this.Course.query()
|
const course = await this.Course.query().select('id').preload('lessons').where('id', id).first();
|
||||||
.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;
|
return course && course.lessons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +194,6 @@ export default class CourseRepository {
|
|||||||
|
|
||||||
course.title = data.title;
|
course.title = data.title;
|
||||||
course.description = data.description;
|
course.description = data.description;
|
||||||
course.education_description = data.education_description;
|
|
||||||
course.status = data.status;
|
course.status = data.status;
|
||||||
course.teacher_id = data.teacher_id;
|
course.teacher_id = data.teacher_id;
|
||||||
course.category_id = data.category_id;
|
course.category_id = data.category_id;
|
||||||
@ -263,7 +219,6 @@ export default class CourseRepository {
|
|||||||
public async delete(id: string | number): Promise<Course | null> {
|
public async delete(id: string | number): Promise<Course | null> {
|
||||||
const course = await this.Course.query()
|
const course = await this.Course.query()
|
||||||
.preload('category')
|
.preload('category')
|
||||||
.preload('lessons', q => q.preload('video').preload('materials'))
|
|
||||||
.withCount('students')
|
.withCount('students')
|
||||||
.withCount('likes')
|
.withCount('likes')
|
||||||
.withCount('lessons')
|
.withCount('lessons')
|
||||||
@ -300,7 +255,6 @@ export default class CourseRepository {
|
|||||||
course.merge({
|
course.merge({
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
education_description: data.education_description,
|
|
||||||
teacher_id: data.teacher_id,
|
teacher_id: data.teacher_id,
|
||||||
category_id: data.category_id,
|
category_id: data.category_id,
|
||||||
});
|
});
|
||||||
@ -315,11 +269,10 @@ export default class CourseRepository {
|
|||||||
/**
|
/**
|
||||||
* Load other updated data
|
* Load other updated data
|
||||||
*/
|
*/
|
||||||
await course.load('category');
|
course.load('category');
|
||||||
await course.load('teacher');
|
course.load('teacher');
|
||||||
|
|
||||||
await course.save();
|
await course.save();
|
||||||
|
|
||||||
return course;
|
return course;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
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<LessonProgress | null> {
|
|
||||||
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<LessonProgress> {
|
|
||||||
const progress = await this.LessonProgress.create(data);
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,10 @@
|
|||||||
import Drive from '@ioc:Adonis/Core/Drive';
|
|
||||||
import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
|
|
||||||
import { cuid } from '@ioc:Adonis/Core/Helpers';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models
|
* Models
|
||||||
*/
|
*/
|
||||||
|
import Drive from '@ioc:Adonis/Core/Drive';
|
||||||
import Course from 'App/Models/Course';
|
import Course from 'App/Models/Course';
|
||||||
import Lesson from 'App/Models/Lesson';
|
import Lesson from 'App/Models/Lesson';
|
||||||
import LessonMaterial from 'App/Models/LessonMaterial';
|
import LessonMaterial from 'App/Models/LessonMaterial';
|
||||||
import LessonVideo from 'App/Models/LessonVideo';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validators
|
* Validators
|
||||||
@ -21,15 +17,9 @@ export default class LessonRepository {
|
|||||||
|
|
||||||
private LessonMaterial: typeof LessonMaterial;
|
private LessonMaterial: typeof LessonMaterial;
|
||||||
|
|
||||||
private LessonVideo: typeof LessonVideo;
|
|
||||||
|
|
||||||
private Drive: typeof Drive;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.Lesson = Lesson;
|
this.Lesson = Lesson;
|
||||||
this.LessonMaterial = LessonMaterial;
|
this.LessonMaterial = LessonMaterial;
|
||||||
this.LessonVideo = LessonVideo;
|
|
||||||
this.Drive = Drive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,7 +39,7 @@ export default class LessonRepository {
|
|||||||
* @returns Lesson or null
|
* @returns Lesson or null
|
||||||
*/
|
*/
|
||||||
public async getById(id: string | number): Promise<Lesson | null> {
|
public async getById(id: string | number): Promise<Lesson | null> {
|
||||||
const lesson = await this.Lesson.query().where('id', id).preload('color').withCount('materials').first();
|
const lesson = await this.Lesson.query().where('id', id).first();
|
||||||
return lesson;
|
return lesson;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,32 +55,16 @@ export default class LessonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get lesson material by name
|
* Get lesson material
|
||||||
*
|
*
|
||||||
* @param fileName Name of file
|
* @param fileName Name of file
|
||||||
* @returns Lesson material or null
|
* @returns Lesson material file or null
|
||||||
*/
|
*/
|
||||||
public async getMaterialFileByName(fileName: string): Promise<LessonMaterial | null> {
|
public async getMaterialFileByName(fileName: string): Promise<LessonMaterial | null> {
|
||||||
const material = await this.LessonMaterial.query().where('name', fileName).first();
|
const material = await this.LessonMaterial.query().where('name', fileName).first();
|
||||||
return material;
|
return material;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get lesson material by id
|
|
||||||
*
|
|
||||||
* @param id Material id
|
|
||||||
* @returns Lesson material or null
|
|
||||||
*/
|
|
||||||
public async getMaterialFileById(id: number): Promise<LessonMaterial | null> {
|
|
||||||
const material = await this.LessonMaterial.query().where('id', id).first();
|
|
||||||
return material;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getVideoFileByName(fileName: string): Promise<LessonVideo | null> {
|
|
||||||
const video = await this.LessonVideo.query().where('name', fileName).first();
|
|
||||||
return video;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create lesson
|
* Create lesson
|
||||||
*
|
*
|
||||||
@ -105,33 +79,42 @@ export default class LessonRepository {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
duration: data.duration.toFormat('HH:mm:ss'),
|
duration: data.duration.toFormat('HH:mm:ss'),
|
||||||
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
|
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
|
||||||
course_id: course.id,
|
|
||||||
linked_video_url: !data.video ? data.linked_video_url : null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create content
|
* Attach lesson to course
|
||||||
*/
|
*/
|
||||||
await lesson.related('content').create({ body: '' });
|
await lesson.related('course').associate(course);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move video file to disk and save to database
|
* Create content and materials fields in database
|
||||||
*/
|
*/
|
||||||
if (data.video) {
|
const content = await lesson.related('content').create({ video_url: data.video_url });
|
||||||
await this.createVideo(lesson, data.video);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create materials
|
|
||||||
*/
|
|
||||||
if (data.materials) {
|
if (data.materials) {
|
||||||
await this.createMaterials(lesson, data.materials);
|
await Promise.all(
|
||||||
|
data.materials.map(async file => {
|
||||||
|
/**
|
||||||
|
* Move each file to disk and then save in database
|
||||||
|
*/
|
||||||
|
await file.moveToDisk('materials');
|
||||||
|
if (file.state === 'moved') {
|
||||||
|
const url = await Drive.getUrl(`materials/${file.fileName}`);
|
||||||
|
await content.related('materials').create({
|
||||||
|
name: file.fileName,
|
||||||
|
clientName: file.clientName,
|
||||||
|
ext: file.extname,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load data
|
* Load data
|
||||||
*/
|
*/
|
||||||
await lesson.load(loader => loader.load('content').load('materials').load('video').load('progress'));
|
await lesson.load('content', q => q.preload('materials'));
|
||||||
|
|
||||||
return lesson;
|
return lesson;
|
||||||
}
|
}
|
||||||
@ -143,58 +126,19 @@ export default class LessonRepository {
|
|||||||
* @returns Updated lesson
|
* @returns Updated lesson
|
||||||
*/
|
*/
|
||||||
public async update(id: string | number, data: UpdateLessonValidator['schema']['props']): Promise<Lesson | null> {
|
public async update(id: string | number, data: UpdateLessonValidator['schema']['props']): Promise<Lesson | null> {
|
||||||
const lesson = await this.Lesson.query().preload('video').preload('color').where('id', id).first();
|
const lesson = await this.Lesson.query().where('id', id).first();
|
||||||
|
|
||||||
if (lesson) {
|
if (lesson) {
|
||||||
/**
|
await lesson
|
||||||
* Update fields
|
.merge({
|
||||||
*/
|
course_id: data.course_id,
|
||||||
lesson.merge({
|
title: data.title,
|
||||||
title: data.title,
|
description: data.description,
|
||||||
description: data.description,
|
})
|
||||||
course_id: data.course_id,
|
.save();
|
||||||
...(data.duration && { duration: data.duration.toFormat('HH:mm:ss') }),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update linked video url
|
|
||||||
*/
|
|
||||||
if (data.linked_video_url && !data.video) {
|
|
||||||
if (lesson.video) {
|
|
||||||
await this.Drive.delete(`videos/${lesson.video.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await lesson.related('video').query().delete();
|
|
||||||
lesson.merge({ linked_video_url: data.linked_video_url });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update file video
|
|
||||||
*/
|
|
||||||
if (data.video) {
|
|
||||||
if (lesson.video) {
|
|
||||||
await this.Drive.delete(`videos/${lesson.video.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await lesson.related('video').query().delete();
|
|
||||||
await this.createVideo(lesson, data.video);
|
|
||||||
lesson.merge({ linked_video_url: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 lesson;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,89 +149,13 @@ export default class LessonRepository {
|
|||||||
* @returns Deleted lesson or null
|
* @returns Deleted lesson or null
|
||||||
*/
|
*/
|
||||||
public async delete(id: string | number): Promise<Lesson | null> {
|
public async delete(id: string | number): Promise<Lesson | null> {
|
||||||
const lesson = await this.Lesson.query().where('id', id).preload('video').preload('color').first();
|
const lesson = await this.Lesson.query().where('id', id).first();
|
||||||
|
|
||||||
if (lesson) {
|
if (lesson) {
|
||||||
/**
|
|
||||||
* Delete files from disk
|
|
||||||
*/
|
|
||||||
if (lesson.video) {
|
|
||||||
await this.Drive.delete(`videos/${lesson.video.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.deleteMaterials(lesson);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete lesson from database
|
|
||||||
*/
|
|
||||||
await lesson.delete();
|
await lesson.delete();
|
||||||
return lesson;
|
return lesson;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
public async createVideo(lesson: Lesson, video: MultipartFileContract): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import Hash from '@ioc:Adonis/Core/Hash';
|
import Hash from '@ioc:Adonis/Core/Hash';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models
|
* Models
|
||||||
@ -12,7 +11,6 @@ import Role from 'App/Models/Role';
|
|||||||
*/
|
*/
|
||||||
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
||||||
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
||||||
import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator';
|
|
||||||
|
|
||||||
export default class UserRepository {
|
export default class UserRepository {
|
||||||
private User: typeof User;
|
private User: typeof User;
|
||||||
@ -32,11 +30,6 @@ export default class UserRepository {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByIds(ids: Array<string>): Promise<Array<User>> {
|
|
||||||
const data = await this.User.query().preload('contacts').preload('roles').whereIn('id', ids);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user by column
|
* Get user by column
|
||||||
*
|
*
|
||||||
@ -71,7 +64,7 @@ export default class UserRepository {
|
|||||||
*/
|
*/
|
||||||
public async getAll(params?: any) {
|
public async getAll(params?: any) {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
const { search, email, login, first_name, last_name, role, page, limit }: any = params || {};
|
const { search, email, login, first_name, last_name, role, page = 1, limit = 10 }: any = params || {};
|
||||||
|
|
||||||
const query = this.User.query();
|
const query = this.User.query();
|
||||||
|
|
||||||
@ -104,21 +97,8 @@ export default class UserRepository {
|
|||||||
query.whereHas('roles', q => q.where('slug', role));
|
query.whereHas('roles', q => q.where('slug', role));
|
||||||
}
|
}
|
||||||
|
|
||||||
query.preload('contacts').preload('roles').orderBy('created_at', 'desc');
|
const data = await query.preload('contacts').preload('roles').orderBy('created_at', 'desc').paginate(page, limit);
|
||||||
|
return data.toJSON();
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,32 +141,6 @@ export default class UserRepository {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the time of last login column in database for user
|
|
||||||
*
|
|
||||||
* @param id User id
|
|
||||||
*/
|
|
||||||
public async updateLastLogin(id: number | string) {
|
|
||||||
await this.User.query().where('id', id).update({ last_login: DateTime.now() }).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update personal info of user
|
|
||||||
*
|
|
||||||
* @param data New fields
|
|
||||||
*/
|
|
||||||
public async updateInfo(id: string | number, data: UpdateUserInfoValidator['schema']['props']) {
|
|
||||||
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
|
|
||||||
const { about } = data;
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
await user.merge({ about }).save();
|
|
||||||
return { about: user.about };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewrite all user roles
|
* Rewrite all user roles
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||||
import Hash from '@ioc:Adonis/Core/Hash';
|
import Hash from '@ioc:Adonis/Core/Hash';
|
||||||
|
import Logger from '@ioc:Adonis/Core/Logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datatypes
|
* Datatypes
|
||||||
@ -66,29 +67,18 @@ export default class AuthService {
|
|||||||
*/
|
*/
|
||||||
const token = await ctx.auth.use(this.authGuard).generate(user, {
|
const token = await ctx.auth.use(this.authGuard).generate(user, {
|
||||||
expiresIn: '1d',
|
expiresIn: '1d',
|
||||||
userId: user.id,
|
|
||||||
userRoles: user.roles.map(role => role.slug),
|
userRoles: user.roles.map(role => role.slug),
|
||||||
userName: user.fullname,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Set cookie token to response
|
|
||||||
*/
|
|
||||||
ctx.response.cookie('token', token.token);
|
ctx.response.cookie('token', token.token);
|
||||||
|
|
||||||
/**
|
Logger.info(`A token for user with id: "${token.user.id}" was successfully generated.`);
|
||||||
* Save last login date
|
|
||||||
*/
|
|
||||||
await this.userRepository.updateLastLogin(user.id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
message: 'Login success.',
|
message: 'Login success.',
|
||||||
data: {
|
data: token.toJSON(),
|
||||||
userId: user.id,
|
|
||||||
...token.toJSON(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,35 +10,18 @@ import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
|||||||
* Repositories
|
* Repositories
|
||||||
*/
|
*/
|
||||||
import CategoryRepository from 'App/Repositories/CategoryRepository';
|
import CategoryRepository from 'App/Repositories/CategoryRepository';
|
||||||
import CourseRepository from 'App/Repositories/CourseRepository';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validators
|
* Validators
|
||||||
*/
|
*/
|
||||||
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
||||||
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import CourseService from 'App/Services/CourseService';
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class CategoryService {
|
export default class CategoryService {
|
||||||
private categoryRepository: CategoryRepository;
|
private categoryRepository: CategoryRepository;
|
||||||
|
|
||||||
private courseRepository: CourseRepository;
|
constructor(categoryRepository: CategoryRepository) {
|
||||||
|
|
||||||
private courseService: CourseService;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
categoryRepository: CategoryRepository,
|
|
||||||
courseRepository: CourseRepository,
|
|
||||||
courseService: CourseService
|
|
||||||
) {
|
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
this.courseRepository = courseRepository;
|
|
||||||
this.courseService = courseService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,7 +93,7 @@ export default class CategoryService {
|
|||||||
* @returns Deleted category
|
* @returns Deleted category
|
||||||
*/
|
*/
|
||||||
public async deleteCategory(id: string | number): Promise<IResponse> {
|
public async deleteCategory(id: string | number): Promise<IResponse> {
|
||||||
const data = await this.categoryRepository.getById(id);
|
const data = await this.categoryRepository.delete(id);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return {
|
return {
|
||||||
@ -124,10 +107,6 @@ export default class CategoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const courses = await this.courseRepository.getByCategoryId(id);
|
|
||||||
await this.courseService.deleteAllFiles(courses);
|
|
||||||
await this.categoryRepository.delete(id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
@ -135,38 +114,6 @@ export default class CategoryService {
|
|||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update category
|
|
||||||
*
|
|
||||||
* @param id Category id
|
|
||||||
* @returns Updated category
|
|
||||||
*/
|
|
||||||
public async updateCategory(
|
|
||||||
id: string | number,
|
|
||||||
data: UpdateCategoryValidator['schema']['props']
|
|
||||||
): Promise<IResponse> {
|
|
||||||
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);
|
new Ioc().make(CategoryService);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
||||||
import { AuthContract } from '@ioc:Adonis/Addons/Auth';
|
import { AuthContract } from '@ioc:Adonis/Addons/Auth';
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datatypes
|
* Datatypes
|
||||||
@ -24,9 +23,6 @@ import CategoryRepository from 'App/Repositories/CategoryRepository';
|
|||||||
import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
|
import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
|
||||||
import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator';
|
import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator';
|
||||||
import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator';
|
import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator';
|
||||||
import Course from 'App/Models/Course';
|
|
||||||
import Drive from '@ioc:Adonis/Core/Drive';
|
|
||||||
import CourseHelper, { FileEntry } from 'App/Helpers/CourseHelper';
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class CourseService {
|
export default class CourseService {
|
||||||
@ -68,7 +64,7 @@ export default class CourseService {
|
|||||||
* @param id Course id
|
* @param id Course id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchCourse(id: string | number): Promise<IResponse<Course | {}>> {
|
public async fetchCourse(id: string | number): Promise<IResponse> {
|
||||||
const data = await this.courseRepository.getById(id);
|
const data = await this.courseRepository.getById(id);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -155,9 +151,8 @@ export default class CourseService {
|
|||||||
* @param id Course id
|
* @param id Course id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchCourseLessons(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
|
public async fetchCourseLessons(id: string | number): Promise<IResponse> {
|
||||||
const user = await ctx.auth.use('api').authenticate();
|
const data = await this.courseRepository.getLessons(id);
|
||||||
const data = await this.courseRepository.getLessons(id, user.id);
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return {
|
return {
|
||||||
@ -235,9 +230,9 @@ export default class CourseService {
|
|||||||
/**
|
/**
|
||||||
* Find course teacher
|
* Find course teacher
|
||||||
*/
|
*/
|
||||||
const user = await this.userRepository.getById(data.teacher_id);
|
const teacher = await this.userRepository.getById(data.teacher_id);
|
||||||
|
|
||||||
if (!user) {
|
if (!teacher) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
status: HttpStatusEnum.NOT_FOUND,
|
||||||
@ -249,13 +244,13 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHasPermissions = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]);
|
||||||
|
|
||||||
if (!isHasPermissions) {
|
if (!isTeacher) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.BAD_REQUEST,
|
status: HttpStatusEnum.BAD_REQUEST,
|
||||||
message: `User with id "${user.id}" does not have sufficient permissions.`,
|
message: `User with id "${teacher.id}" not a teacher.`,
|
||||||
data: {},
|
data: {},
|
||||||
error: {
|
error: {
|
||||||
code: 'E_BAD_REQUEST',
|
code: 'E_BAD_REQUEST',
|
||||||
@ -312,8 +307,6 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteAllFiles([course]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
@ -330,7 +323,7 @@ export default class CourseService {
|
|||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async updateCourse(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise<IResponse> {
|
public async updateCourse(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise<IResponse> {
|
||||||
const course = await this.courseRepository.getById(id);
|
const course = await this.courseRepository.update(id, data);
|
||||||
|
|
||||||
if (!course) {
|
if (!course) {
|
||||||
return {
|
return {
|
||||||
@ -344,45 +337,11 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.teacher_id) {
|
|
||||||
const teacher = await this.userRepository.getById(data.teacher_id);
|
|
||||||
|
|
||||||
if (!teacher) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
|
||||||
message: 'Teacher not found.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_NOT_FOUND',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await teacher.load('roles');
|
|
||||||
|
|
||||||
const isTeacherOrAdmin = RoleHelper.userContainRoles(teacher.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
|
||||||
|
|
||||||
if (!isTeacherOrAdmin) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.BAD_REQUEST,
|
|
||||||
message: 'Author is not a teacher or admin.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_BAD_REQUEST',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await this.courseRepository.update(id, data);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
message: 'Course updated.',
|
message: 'Course updated.',
|
||||||
data: updated || {},
|
data: course,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,93 +420,6 @@ 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<string>): Promise<IResponse> {
|
|
||||||
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<string>): Promise<IResponse> {
|
|
||||||
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<IResponse> {
|
public async detachUserCourse(id: string | number, studentId: string | number): Promise<IResponse> {
|
||||||
const course = await this.courseRepository.getById(id);
|
const course = await this.courseRepository.getById(id);
|
||||||
|
|
||||||
@ -705,29 +577,6 @@ export default class CourseService {
|
|||||||
data: { count },
|
data: { count },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function will delete all associated course files on disk
|
|
||||||
*
|
|
||||||
* @param courses List of courses
|
|
||||||
*/
|
|
||||||
public async deleteAllFiles(courses: Course[]): Promise<void> {
|
|
||||||
/**
|
|
||||||
* Collect file names
|
|
||||||
*/
|
|
||||||
const images = courses.map(course => CourseHelper.getImageFileName(course)).filter(Boolean) as FileEntry[];
|
|
||||||
const videos = courses.flatMap(course => CourseHelper.getVideoFileNames(course)).filter(Boolean) as FileEntry[];
|
|
||||||
const materials = courses.flatMap(course => CourseHelper.getMaterialFileNames(course));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove files from disk
|
|
||||||
*/
|
|
||||||
const promises = videos
|
|
||||||
.concat(images, materials)
|
|
||||||
.map(file => Drive.delete(`${file.path}${file.name.substring(file.name.lastIndexOf('/') + 1)}`));
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new Ioc().make(CourseService);
|
new Ioc().make(CourseService);
|
||||||
|
|||||||
@ -8,13 +8,11 @@ import Database from '@ioc:Adonis/Lucid/Database';
|
|||||||
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
||||||
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
||||||
import LessonMaterial from 'App/Models/LessonMaterial';
|
import LessonMaterial from 'App/Models/LessonMaterial';
|
||||||
import LessonVideo from 'App/Models/LessonVideo';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repositories
|
* Repositories
|
||||||
*/
|
*/
|
||||||
import CourseRepository from 'App/Repositories/CourseRepository';
|
import CourseRepository from 'App/Repositories/CourseRepository';
|
||||||
import LessonProgressRepository from 'App/Repositories/LessonProgressRepository';
|
|
||||||
import LessonRepository from 'App/Repositories/LessonRepository';
|
import LessonRepository from 'App/Repositories/LessonRepository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,16 +27,9 @@ export default class LessonService {
|
|||||||
|
|
||||||
private lessonRepository: LessonRepository;
|
private lessonRepository: LessonRepository;
|
||||||
|
|
||||||
private lessonProgressRepository: LessonProgressRepository;
|
constructor(courseRepository: CourseRepository, lessonRepository: LessonRepository) {
|
||||||
|
|
||||||
constructor(
|
|
||||||
courseRepository: CourseRepository,
|
|
||||||
lessonRepository: LessonRepository,
|
|
||||||
lessonProgressRepository: LessonProgressRepository
|
|
||||||
) {
|
|
||||||
this.courseRepository = courseRepository;
|
this.courseRepository = courseRepository;
|
||||||
this.lessonRepository = lessonRepository;
|
this.lessonRepository = lessonRepository;
|
||||||
this.lessonProgressRepository = lessonProgressRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +54,7 @@ export default class LessonService {
|
|||||||
* @param id Lesson id
|
* @param id Lesson id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchLesson(id: string | number, ctx: HttpContextContract) {
|
public async fetchLesson(id: string | number): Promise<IResponse> {
|
||||||
const lesson = await this.lessonRepository.getById(id);
|
const lesson = await this.lessonRepository.getById(id);
|
||||||
|
|
||||||
if (!lesson) {
|
if (!lesson) {
|
||||||
@ -78,13 +69,6 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load lesson with materials
|
|
||||||
*/
|
|
||||||
await lesson.load(loader => loader.load('content').load('materials').load('video'));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
@ -96,12 +80,11 @@ export default class LessonService {
|
|||||||
/**
|
/**
|
||||||
* Fetch lesson material by file name
|
* Fetch lesson material by file name
|
||||||
*
|
*
|
||||||
* @param ctx Http context
|
* @param fileName Name of file
|
||||||
* @param name Filename
|
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
public async fetchMaterialFile(ctx: HttpContextContract, fileName: string): Promise<IResponse<LessonMaterial>> {
|
||||||
const material = await this.lessonRepository.getMaterialFileByName(name);
|
const material = await this.lessonRepository.getMaterialFileByName(fileName);
|
||||||
|
|
||||||
if (!material) {
|
if (!material) {
|
||||||
return {
|
return {
|
||||||
@ -118,8 +101,22 @@ export default class LessonService {
|
|||||||
/**
|
/**
|
||||||
* Allow user to view lesson material
|
* Allow user to view lesson material
|
||||||
*/
|
*/
|
||||||
await material.load('lesson');
|
await material.load('content', q => q.preload('lesson'));
|
||||||
await ctx.bouncer.with('LessonPolicy').authorize('view', material.lesson);
|
|
||||||
|
const {
|
||||||
|
content: { lesson },
|
||||||
|
} = material;
|
||||||
|
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
|
message: 'You are not able to view this file.',
|
||||||
|
data: {},
|
||||||
|
error: {
|
||||||
|
code: 'E_FORBIDDEN',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -129,72 +126,6 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchVideoFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonVideo>> {
|
|
||||||
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');
|
|
||||||
await ctx.bouncer.with('LessonPolicy').authorize('view', video.lesson);
|
|
||||||
|
|
||||||
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<IResponse> {
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
|
||||||
|
|
||||||
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
|
* Create lesson
|
||||||
*
|
*
|
||||||
@ -216,9 +147,6 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new lesson
|
|
||||||
*/
|
|
||||||
const lesson = await this.lessonRepository.create(course, data);
|
const lesson = await this.lessonRepository.create(course, data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -310,12 +238,25 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
/**
|
||||||
|
* Allow user to view lesson content
|
||||||
|
*/
|
||||||
|
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
|
message: 'The user is not a student of this course.',
|
||||||
|
data: {},
|
||||||
|
error: {
|
||||||
|
code: 'E_FORBIDDEN',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load lesson with materials
|
* Load lesson content with materials
|
||||||
*/
|
*/
|
||||||
await lesson.load(loader => loader.load('content').load('materials'));
|
await lesson.load('content', q => q.preload('materials'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -5,36 +5,14 @@ import Redis from '@ioc:Adonis/Addons/Redis';
|
|||||||
import Mail from '@ioc:Adonis/Addons/Mail';
|
import Mail from '@ioc:Adonis/Addons/Mail';
|
||||||
import Hash from '@ioc:Adonis/Core/Hash';
|
import Hash from '@ioc:Adonis/Core/Hash';
|
||||||
import Logger from '@ioc:Adonis/Core/Logger';
|
import Logger from '@ioc:Adonis/Core/Logger';
|
||||||
|
|
||||||
/**
|
|
||||||
* Datatypes
|
|
||||||
*/
|
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
||||||
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
||||||
import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repositories
|
|
||||||
*/
|
|
||||||
import ContactRepository from 'App/Repositories/ContactRepository';
|
import ContactRepository from 'App/Repositories/ContactRepository';
|
||||||
import UserRepository from 'App/Repositories/UserRepository';
|
import UserRepository from 'App/Repositories/UserRepository';
|
||||||
|
|
||||||
/**
|
|
||||||
* Validator
|
|
||||||
*/
|
|
||||||
import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator';
|
|
||||||
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
|
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
|
||||||
|
import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum';
|
||||||
/**
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||||
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import Ws from './WsService';
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class MeService {
|
export default class MeService {
|
||||||
@ -59,10 +37,7 @@ export default class MeService {
|
|||||||
const user = await auth.use(this.authGuard).authenticate();
|
const user = await auth.use(this.authGuard).authenticate();
|
||||||
|
|
||||||
await user.load(loader => {
|
await user.load(loader => {
|
||||||
loader
|
loader.load('roles').load('contacts');
|
||||||
.load('roles')
|
|
||||||
.load('contacts')
|
|
||||||
.load('likes', q => q.select('id'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,16 +52,11 @@ export default class MeService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = await Ws.notificationStore.getNotifications(user.id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
message: 'Fetched authorized user data.',
|
message: 'Fetched authorized user data.',
|
||||||
data: {
|
data: user,
|
||||||
...user.toJSON(),
|
|
||||||
notifications,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,9 +244,9 @@ export default class MeService {
|
|||||||
/**
|
/**
|
||||||
* Update user conctact
|
* Update user conctact
|
||||||
*
|
*
|
||||||
* @param data Fields to update
|
* @param data Data to update
|
||||||
* @param auth AuthContract
|
* @param auth AuthContract
|
||||||
* @returns IResponse
|
* @returns Updated user contacts
|
||||||
*/
|
*/
|
||||||
public async updateUserContacts(
|
public async updateUserContacts(
|
||||||
data: UpdateContactsValidator['schema']['props'],
|
data: UpdateContactsValidator['schema']['props'],
|
||||||
@ -306,93 +276,6 @@ export default class MeService {
|
|||||||
data: user.contacts,
|
data: user.contacts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update info about user
|
|
||||||
*
|
|
||||||
* @param data Fields to update
|
|
||||||
* @param auth AuthContract
|
|
||||||
* @returns IResponse
|
|
||||||
*/
|
|
||||||
public async updateUserInfo(
|
|
||||||
data: UpdateUserInfoValidator['schema']['props'],
|
|
||||||
auth: AuthContract
|
|
||||||
): Promise<IResponse> {
|
|
||||||
const user = await auth.use(this.authGuard).authenticate();
|
|
||||||
const { about, ...newContacts } = data;
|
|
||||||
|
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.BAD_REQUEST,
|
|
||||||
message: 'You must specify at least one field.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_BAD_REQUEST',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Personal info updating
|
|
||||||
*/
|
|
||||||
if (about !== undefined) {
|
|
||||||
await this.userRepository.updateInfo(user.id, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contacts updating
|
|
||||||
*/
|
|
||||||
if (data.phone_number !== undefined || data.twitter_id !== undefined || data.telegram_id !== undefined) {
|
|
||||||
const contacts = await this.contactRepository.update(user, newContacts);
|
|
||||||
|
|
||||||
if (!contacts) {
|
|
||||||
await this.contactRepository.create(user, newContacts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
status: HttpStatusEnum.OK,
|
|
||||||
message: 'User contacts updated.',
|
|
||||||
data: { ...data },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch history of chat
|
|
||||||
*
|
|
||||||
* @param chatId Id of user
|
|
||||||
* @param auth AuthContract
|
|
||||||
* @returns IResponse
|
|
||||||
*/
|
|
||||||
public async fetchChatHistory(chatId: string, auth: AuthContract): Promise<IResponse> {
|
|
||||||
const firstUser = await auth.use(this.authGuard).authenticate();
|
|
||||||
const secondUser = await this.userRepository.getById(chatId);
|
|
||||||
|
|
||||||
if (!secondUser) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
|
||||||
message: 'Chat not found.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_NOT_FOUND',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const history = await Ws.messageStore.getHistory(firstUser.id, secondUser.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
status: HttpStatusEnum.OK,
|
|
||||||
message: 'Chat history fetched.',
|
|
||||||
data: {
|
|
||||||
history,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new Ioc().make(MeService);
|
new Ioc().make(MeService);
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
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<IResponse> {
|
|
||||||
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.getOnlineSessions();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
status: HttpStatusEnum.OK,
|
|
||||||
message: 'Fetched stat.',
|
|
||||||
data: {
|
|
||||||
...count.rows[0],
|
|
||||||
online: online.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Ioc().make(StatService);
|
|
||||||
@ -22,48 +22,21 @@ import Role from 'App/Models/Role';
|
|||||||
import RoleRepository from 'App/Repositories/RoleRepository';
|
import RoleRepository from 'App/Repositories/RoleRepository';
|
||||||
import UserRepository from 'App/Repositories/UserRepository';
|
import UserRepository from 'App/Repositories/UserRepository';
|
||||||
|
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import CourseService from './CourseService';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validators
|
* Validators
|
||||||
*/
|
*/
|
||||||
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
||||||
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
||||||
|
|
||||||
/**
|
|
||||||
* Datatypes
|
|
||||||
*/
|
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
import CourseRepository from 'App/Repositories/CourseRepository';
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
private userRepository: UserRepository;
|
private userRepository: UserRepository;
|
||||||
|
|
||||||
private roleRepository: RoleRepository;
|
private roleRepository: RoleRepository;
|
||||||
|
|
||||||
private courseRepository: CourseRepository;
|
constructor(userRepository: UserRepository, roleRepository: RoleRepository) {
|
||||||
|
|
||||||
private courseService: CourseService;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
userRepository: UserRepository,
|
|
||||||
roleRepository: RoleRepository,
|
|
||||||
courseRepository: CourseRepository,
|
|
||||||
courseService: CourseService
|
|
||||||
) {
|
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.roleRepository = roleRepository;
|
this.roleRepository = roleRepository;
|
||||||
this.courseRepository = courseRepository;
|
|
||||||
this.courseService = courseService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,24 +76,16 @@ export default class UserService {
|
|||||||
*/
|
*/
|
||||||
public async fetchUsers(params?: any): Promise<IResponse> {
|
public async fetchUsers(params?: any): Promise<IResponse> {
|
||||||
const users = await this.userRepository.getAll(params);
|
const users = await this.userRepository.getAll(params);
|
||||||
const result: IResponse = {
|
|
||||||
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
message: 'Fetched all users.',
|
message: 'Fetched all users.',
|
||||||
data: {},
|
data: users.data,
|
||||||
|
meta: {
|
||||||
|
pagination: users.meta,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (users instanceof Array) {
|
|
||||||
result.data = users;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.data = users.data;
|
|
||||||
result.meta = {
|
|
||||||
pagination: users.meta,
|
|
||||||
};
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,7 +112,20 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.bouncer.with('RolePolicy').authorize('manage', [role.slug as RoleEnum]);
|
/**
|
||||||
|
* Check user permissions
|
||||||
|
*/
|
||||||
|
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
|
message: 'You dont have permissions to permorm that action',
|
||||||
|
data: {},
|
||||||
|
error: {
|
||||||
|
code: 'E_FORBIDDEN',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new user
|
* Create new user
|
||||||
@ -180,7 +158,7 @@ export default class UserService {
|
|||||||
data: UpdateUserValidator['schema']['props'],
|
data: UpdateUserValidator['schema']['props'],
|
||||||
ctx: HttpContextContract
|
ctx: HttpContextContract
|
||||||
): Promise<IResponse> {
|
): Promise<IResponse> {
|
||||||
const user = await this.userRepository.getById(id);
|
const user = await this.userRepository.update(id, data);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -194,8 +172,6 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user role
|
* Update user role
|
||||||
*/
|
*/
|
||||||
@ -203,18 +179,27 @@ export default class UserService {
|
|||||||
const role = await this.roleRepository.getBySlug(data.role);
|
const role = await this.roleRepository.getBySlug(data.role);
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
await ctx.bouncer.with('RolePolicy').authorize('manage', [data.role]);
|
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
|
message: 'You dont have permissions to permorm that action',
|
||||||
|
data: {},
|
||||||
|
error: {
|
||||||
|
code: 'E_FORBIDDEN',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await this.userRepository.updateRoles(user, [role]);
|
await this.userRepository.updateRoles(user, [role]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await this.userRepository.update(id, data);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
message: 'User updated.',
|
message: 'User updated.',
|
||||||
data: updated || {},
|
data: user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,8 +209,8 @@ export default class UserService {
|
|||||||
* @param id User id
|
* @param id User id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async deleteUser(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
|
public async deleteUser(id: string | number): Promise<IResponse> {
|
||||||
const user = await this.userRepository.getById(id);
|
const user = await this.userRepository.delete(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -239,15 +224,6 @@ export default class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
|
|
||||||
|
|
||||||
if (RoleHelper.userContainRoles(user.roles, [RoleEnum.TEACHER, RoleEnum.ADMIN])) {
|
|
||||||
const courses = await this.courseRepository.getByTeacherId(id);
|
|
||||||
await this.courseService.deleteAllFiles(courses);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userRepository.delete(id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
import { Server } from 'socket.io';
|
|
||||||
import Redis from '@ioc:Adonis/Addons/Redis';
|
|
||||||
import AdonisServer from '@ioc:Adonis/Core/Server';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controllers
|
|
||||||
*/
|
|
||||||
import OnlineController from 'App/Controllers/Ws/OnlineController';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middlewares
|
|
||||||
*/
|
|
||||||
import AuthMiddleware from 'App/Middleware/Ws/Auth';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores
|
|
||||||
*/
|
|
||||||
import RedisSessionStore from 'App/Store/SessionStore';
|
|
||||||
import RedisMessageStore from 'App/Store/MessageStore';
|
|
||||||
import RedisNotificationStore from 'App/Store/NotificationStore';
|
|
||||||
|
|
||||||
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: [string, { userId: string; userName: string }][]) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat
|
|
||||||
*/
|
|
||||||
'chat:message': (data: {
|
|
||||||
content: string;
|
|
||||||
time: string;
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
notificationId?: string | undefined;
|
|
||||||
}) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
|
||||||
/**
|
|
||||||
* User
|
|
||||||
*/
|
|
||||||
'user:logout': () => void;
|
|
||||||
'user:status': () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat
|
|
||||||
*/
|
|
||||||
'chat:message': (data: { content: string; to: string }) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification
|
|
||||||
*/
|
|
||||||
'notification:read': (data: { userId: string; ids: string[] }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InterServerEvents {}
|
|
||||||
|
|
||||||
export interface SocketData {
|
|
||||||
sessionId: string;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WsService {
|
|
||||||
public io: Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
||||||
|
|
||||||
public sessionStore: RedisSessionStore;
|
|
||||||
|
|
||||||
public messageStore: RedisMessageStore;
|
|
||||||
|
|
||||||
public notificationStore: RedisNotificationStore;
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores
|
|
||||||
*/
|
|
||||||
this.sessionStore = new RedisSessionStore(Redis.connection('session'));
|
|
||||||
this.messageStore = new RedisMessageStore(Redis.connection('message'));
|
|
||||||
this.notificationStore = new RedisNotificationStore(Redis.connection('notification'));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up sessions before start
|
|
||||||
*/
|
|
||||||
this.sessionStore.destroyAllSessions().then(() => {
|
|
||||||
/**
|
|
||||||
* Middlewares
|
|
||||||
*/
|
|
||||||
this.setupMiddlewares();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On connection socket
|
|
||||||
*/
|
|
||||||
this.listen();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private listen() {
|
|
||||||
this.io.on('connection', async socket => {
|
|
||||||
const onlineController = new OnlineController(socket, this.io, this.sessionStore);
|
|
||||||
|
|
||||||
onlineController.handle();
|
|
||||||
|
|
||||||
const { sessionId, userId, userName } = socket.data;
|
|
||||||
const isExistSocketData = !!(sessionId && userId && userName);
|
|
||||||
|
|
||||||
if (isExistSocketData) {
|
|
||||||
socket.join(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send session to client
|
|
||||||
*/
|
|
||||||
socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId });
|
|
||||||
|
|
||||||
socket.on('chat:message', async ({ content, to }) => {
|
|
||||||
if (isExistSocketData) {
|
|
||||||
const message = {
|
|
||||||
content,
|
|
||||||
from: userId,
|
|
||||||
to,
|
|
||||||
time: DateTime.now().toISO(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.messageStore.add(userId, to, message);
|
|
||||||
|
|
||||||
if (userId !== to) {
|
|
||||||
const notification = await this.notificationStore.add(to, `You received a new private message`, 'MESSAGE');
|
|
||||||
this.io.to(to).emit('chat:message', { ...message, notificationId: notification.id });
|
|
||||||
} else {
|
|
||||||
this.io.to(to).emit('chat:message', { ...message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('notification:read', data => this.notificationStore.del(data.userId, data.ids));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis';
|
|
||||||
import Logger from '@ioc:Adonis/Core/Logger';
|
|
||||||
import User from 'App/Models/User';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
type MessageDataType = {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
content: string;
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConversationType = {
|
|
||||||
userId: string;
|
|
||||||
fullname: string;
|
|
||||||
lastMessage: MessageDataType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MESSAGE_TTL = 185 * 24 * 60 * 60; // 185 days
|
|
||||||
const MAX_MESSAGES_HISTORY = 100;
|
|
||||||
|
|
||||||
class RedisMessageStore {
|
|
||||||
public redisClient: RedisConnectionContract;
|
|
||||||
|
|
||||||
constructor(redisClient: RedisConnectionContract) {
|
|
||||||
this.redisClient = redisClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add(firstUserId: string, secondUserId: string, data: MessageDataType): Promise<'OK' | 'ERROR'> {
|
|
||||||
try {
|
|
||||||
const msgId = nanoid();
|
|
||||||
const msgKey = `${firstUserId}:${msgId}`;
|
|
||||||
const key = `message:${msgKey}`;
|
|
||||||
|
|
||||||
await this.redisClient
|
|
||||||
.multi()
|
|
||||||
.hmset(key, {
|
|
||||||
from: data.from,
|
|
||||||
to: data.to,
|
|
||||||
content: data.content,
|
|
||||||
time: DateTime.now().toISO(),
|
|
||||||
})
|
|
||||||
.expire(key, MESSAGE_TTL)
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
const convKey = this.createConversationKey(firstUserId, secondUserId);
|
|
||||||
const msgKeys = await this.redisClient.lrange(convKey, 0, -1);
|
|
||||||
|
|
||||||
await this.redisClient.lpush(convKey, msgKey);
|
|
||||||
|
|
||||||
if (msgKeys.length >= MAX_MESSAGES_HISTORY) {
|
|
||||||
const deletedMsgKey = await this.redisClient.rpop(convKey);
|
|
||||||
await this.redisClient.del(`message:${deletedMsgKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'OK';
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
return 'ERROR';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getHistory(firstUserId: string, secondUserId: string): Promise<MessageDataType[]> {
|
|
||||||
const convKey = this.createConversationKey(firstUserId, secondUserId);
|
|
||||||
const msgKeys = await this.redisClient.lrange(convKey, 0, -1);
|
|
||||||
const messages = await Promise.all(msgKeys.map(key => `message:${key}`).map(key => this.getMessage(key)));
|
|
||||||
|
|
||||||
return messages.sort((m1, m2) => new Date(m1.time).valueOf() - new Date(m2.time).valueOf());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getConversations(userId: string): Promise<ConversationType[]> {
|
|
||||||
/**
|
|
||||||
* Getting conversations that had some message history
|
|
||||||
*/
|
|
||||||
const conversations = await this.redisClient.keys(`conversation:*${userId}*`);
|
|
||||||
const userIds = conversations
|
|
||||||
.map(id => id.split(':')[1])
|
|
||||||
.map(id => (userId !== id.split('&')[0] ? id.split('&')[0] : id.split('&')[1]));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch this users
|
|
||||||
*/
|
|
||||||
const users = await User.query().select('id', 'first_name', 'last_name').where('id', 'in', userIds);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch last message of each conversation
|
|
||||||
*/
|
|
||||||
const result = await Promise.all(
|
|
||||||
users.map(async u => {
|
|
||||||
const convKey = this.createConversationKey(userId, u.id);
|
|
||||||
const msgKey = await this.redisClient.lindex(convKey, 0);
|
|
||||||
const lastMessage = await this.getMessage(`message:${msgKey}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: u.id,
|
|
||||||
fullname: `${u.first_name} ${u.last_name}`,
|
|
||||||
lastMessage,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getMessage(key: string): Promise<MessageDataType> {
|
|
||||||
const message = (await this.redisClient.hgetall(key)) as MessageDataType;
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
private createConversationKey(userId1: string, userId2: string): string {
|
|
||||||
const [id1, id2] = [userId1, userId2].sort();
|
|
||||||
return `conversation:${id1}&${id2}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RedisMessageStore;
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis';
|
|
||||||
import Logger from '@ioc:Adonis/Core/Logger';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
const NOTIFICATION_TTL = 185 * 24 * 60 * 60; // 185 days
|
|
||||||
|
|
||||||
type NotificationType = {
|
|
||||||
id: string;
|
|
||||||
type: 'SYSTEM' | 'MESSAGE';
|
|
||||||
content: string;
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RedisNotificationStore {
|
|
||||||
public redisClient: RedisConnectionContract;
|
|
||||||
|
|
||||||
constructor(redisClient: RedisConnectionContract) {
|
|
||||||
this.redisClient = redisClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add(userId: string, content: string, type: NotificationType['type']): Promise<NotificationType> {
|
|
||||||
try {
|
|
||||||
const id = nanoid();
|
|
||||||
const key = `notification:${id}`;
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const notification = {
|
|
||||||
id,
|
|
||||||
content,
|
|
||||||
time: new Date(timestamp).toISOString(),
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.redisClient.zadd(`user:${userId}`, timestamp, key);
|
|
||||||
await this.redisClient.multi().hmset(key, notification).expire(key, NOTIFICATION_TTL).exec();
|
|
||||||
|
|
||||||
return notification;
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getNotifications(userId: string) {
|
|
||||||
const keys = await this.redisClient.zrevrangebyscore(`user:${userId}`, '+inf', '-inf');
|
|
||||||
const notifications = await Promise.all(keys.map(key => this.get(key)));
|
|
||||||
return notifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async get(key: string): Promise<NotificationType> {
|
|
||||||
const notification = (await this.redisClient.hgetall(key)) as NotificationType;
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async del(userId: string, keys: string[]): Promise<number> {
|
|
||||||
if (keys.length === 0) return 0;
|
|
||||||
|
|
||||||
const k = keys.map(v => `notification:${v}`);
|
|
||||||
const count = await this.redisClient.zrem(`user:${userId}`, k);
|
|
||||||
await this.redisClient.del(k);
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RedisNotificationStore;
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
/* 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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async scanSessionKeys(): Promise<Set<string>> {
|
|
||||||
const keys = new Set<string>();
|
|
||||||
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);
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return session data by session id
|
|
||||||
*
|
|
||||||
* @param id Session id
|
|
||||||
* @returns Session data
|
|
||||||
*/
|
|
||||||
public async findSession(id: string): Promise<SessionDataType | undefined> {
|
|
||||||
const values = await this.redisClient.hmget(id, 'userId', 'userName', 'connected');
|
|
||||||
return this.transform(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the all sessions from store
|
|
||||||
*/
|
|
||||||
public async getSessions(): Promise<Array<SessionDataType | undefined>> {
|
|
||||||
const keys = await this.scanSessionKeys();
|
|
||||||
const commands = Array.from(keys).map(key => {
|
|
||||||
const [, ...id] = key.split('-');
|
|
||||||
return ['hmget', id.join('-'), 'userId', 'userName', 'connected'];
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.redisClient
|
|
||||||
.multi(commands)
|
|
||||||
.exec()
|
|
||||||
.then(results => {
|
|
||||||
if (results === null) return [];
|
|
||||||
return results.map(([err, session]: [Error, (string | null)[]]) => (err ? undefined : this.transform(session)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unique sessions with connected flag `true`
|
|
||||||
*/
|
|
||||||
public async getOnlineSessions() {
|
|
||||||
const sessions = await this.getSessions();
|
|
||||||
const online = new Map<string, { userId: string; userName: string }>();
|
|
||||||
|
|
||||||
sessions.forEach(s => {
|
|
||||||
if (s?.connected === true) {
|
|
||||||
online.set(s.userId, s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(online);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set session data to redis store
|
|
||||||
*
|
|
||||||
* @param id Session id
|
|
||||||
* @param data Session data
|
|
||||||
*/
|
|
||||||
public async saveSession(id: string, data: SessionDataType): Promise<void> {
|
|
||||||
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<number> {
|
|
||||||
const session = await this.findSession(id);
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
return this.redisClient.del(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all sessions from redis store
|
|
||||||
*
|
|
||||||
* @returns Count of deleted sessions
|
|
||||||
*/
|
|
||||||
public async destroyAllSessions() {
|
|
||||||
const keys = await this.scanSessionKeys().then(values =>
|
|
||||||
Array.from(values).map(v => {
|
|
||||||
const [, ...key] = v.split('-');
|
|
||||||
return key.join('-');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (keys.length > 0) {
|
|
||||||
const deleted = await this.redisClient.del(keys);
|
|
||||||
return deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RedisSessionStore;
|
|
||||||
@ -6,7 +6,7 @@ export default class CreateCategoryValidator {
|
|||||||
|
|
||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
title: schema.string(),
|
title: schema.string(),
|
||||||
description: schema.string.optional(),
|
description: schema.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {};
|
public messages = {};
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
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 = {};
|
|
||||||
}
|
|
||||||
@ -5,15 +5,15 @@ export default class CreateContactsValidator {
|
|||||||
constructor(protected ctx: HttpContextContract) {}
|
constructor(protected ctx: HttpContextContract) {}
|
||||||
|
|
||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
phone_number: schema.string.optional({}, [
|
||||||
rules.mobile({
|
rules.mobile({
|
||||||
locale: ['ru-RU', 'en-US'],
|
locales: ['ru-RU', 'en-US'],
|
||||||
strict: true,
|
strict: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
vk_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
vk_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
||||||
twitter_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
twitter_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
||||||
telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
telegram_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {};
|
public messages = {};
|
||||||
|
|||||||
@ -5,31 +5,19 @@ export default class UpdateContactsValidator {
|
|||||||
constructor(protected ctx: HttpContextContract) {}
|
constructor(protected ctx: HttpContextContract) {}
|
||||||
|
|
||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
phone_number: schema.string.optional({}, [
|
||||||
rules.mobile({
|
rules.mobile({
|
||||||
locale: ['ru-RU', 'en-US'],
|
locales: ['ru-RU', 'en-US'],
|
||||||
strict: true,
|
strict: true,
|
||||||
}),
|
}),
|
||||||
rules.unique({ table: 'contacts', column: 'phone_number' }),
|
rules.unique({ table: 'contacts', column: 'phone_number' }),
|
||||||
]),
|
]),
|
||||||
vk_id: schema.string.nullableAndOptional({}, [
|
vk_id: schema.string.optional({}, [rules.regex(/^([a-zA-Z0-9_]){1,64}$/)]),
|
||||||
rules.regex(/^([a-zA-Z0-9_]){1,64}$/),
|
twitter_id: schema.string.optional({}, [rules.regex(/(^|[^@\w])@(\w{1,15})\b/)]),
|
||||||
rules.unique({ table: 'contacts', column: 'vk_id' }),
|
telegram_id: schema.string.optional({}, [rules.regex(/(^|[^@\w])@(\w{1,64})\b/)]),
|
||||||
]),
|
|
||||||
twitter_id: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.regex(/(^|[^@\w])@(\w{1,15})\b/),
|
|
||||||
rules.unique({ table: 'contacts', column: 'twitter_id' }),
|
|
||||||
]),
|
|
||||||
telegram_id: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.regex(/(^|[^@\w])@(\w{1,64})\b/),
|
|
||||||
rules.unique({ table: 'contacts', column: 'telegram_id' }),
|
|
||||||
]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {
|
public messages = {
|
||||||
'phone_number.unique': 'This phone number is not available.',
|
'phone_number.unique': 'This phone number is not available',
|
||||||
'vk_id.unique': 'This ID is not available.',
|
|
||||||
'twitter_id.unique': 'Twitter username is not available.',
|
|
||||||
'telegram_id.unique': 'Telegram username is not available.',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
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 = {};
|
|
||||||
}
|
|
||||||
@ -8,10 +8,9 @@ export default class CreateCourseValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
title: schema.string(),
|
title: schema.string(),
|
||||||
description: schema.string(),
|
description: schema.string(),
|
||||||
education_description: schema.string.nullable(),
|
|
||||||
image: schema.file.optional({
|
image: schema.file.optional({
|
||||||
size: '10mb',
|
size: '10mb',
|
||||||
extnames: ['jpg', 'jpeg', 'png', 'webp'],
|
extnames: ['jpg', 'jpeg', 'png'],
|
||||||
}),
|
}),
|
||||||
teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]),
|
teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]),
|
||||||
category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
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 = {};
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ export default class FetchCoursesValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
status: schema.enum.optional(Object.values(CourseStatusEnum)),
|
status: schema.enum.optional(Object.values(CourseStatusEnum)),
|
||||||
category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
||||||
limit: schema.number.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {};
|
public messages = {};
|
||||||
|
|||||||
@ -7,10 +7,9 @@ export default class UpdateCourseValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
title: schema.string.optional(),
|
title: schema.string.optional(),
|
||||||
description: schema.string.optional(),
|
description: schema.string.optional(),
|
||||||
education_description: schema.string.nullable(),
|
|
||||||
image: schema.file.optional({
|
image: schema.file.optional({
|
||||||
size: '10mb',
|
size: '10mb',
|
||||||
extnames: ['jpg', 'jpeg', 'png', 'webp'],
|
extnames: ['jpg', 'jpeg', 'png'],
|
||||||
}),
|
}),
|
||||||
teacher_id: schema.string.optional({}, [rules.exists({ table: 'users', column: 'id' })]),
|
teacher_id: schema.string.optional({}, [rules.exists({ table: 'users', column: 'id' })]),
|
||||||
category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
||||||
|
|||||||
@ -9,14 +9,10 @@ export default class CreateLessonValidator {
|
|||||||
title: schema.string({}, [rules.maxLength(255)]),
|
title: schema.string({}, [rules.maxLength(255)]),
|
||||||
description: schema.string(),
|
description: schema.string(),
|
||||||
duration: schema.date({ format: 'HH:mm:ss' }),
|
duration: schema.date({ format: 'HH:mm:ss' }),
|
||||||
video: schema.file.optional({
|
video_url: schema.string({}, [rules.url()]),
|
||||||
size: '5000mb',
|
|
||||||
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
|
||||||
}),
|
|
||||||
linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]),
|
|
||||||
materials: schema.array.optional().members(
|
materials: schema.array.optional().members(
|
||||||
schema.file({
|
schema.file({
|
||||||
size: '100mb',
|
size: '10mb',
|
||||||
extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'],
|
extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'],
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
@ -5,21 +5,9 @@ export default class UpdateLessonValidator {
|
|||||||
constructor(protected ctx: HttpContextContract) {}
|
constructor(protected ctx: HttpContextContract) {}
|
||||||
|
|
||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
course_id: schema.string.optional({}, [rules.exists({ table: 'courses', column: 'id' })]),
|
course_id: schema.string({}, [rules.exists({ table: 'courses', column: 'id' })]),
|
||||||
title: schema.string.optional({}, [rules.maxLength(255)]),
|
title: schema.string({}, [rules.maxLength(255)]),
|
||||||
description: schema.string.optional(),
|
description: schema.string(),
|
||||||
duration: schema.date.optional({ format: 'HH:mm:ss' }),
|
|
||||||
video: schema.file.optional({
|
|
||||||
size: '5000mb',
|
|
||||||
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
|
||||||
}),
|
|
||||||
linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]),
|
|
||||||
materials: schema.array.nullableAndOptional().members(
|
|
||||||
schema.file({
|
|
||||||
size: '100mb',
|
|
||||||
extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {};
|
public messages = {};
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import { schema, rules } from '@ioc:Adonis/Core/Validator';
|
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
|
||||||
|
|
||||||
export default class UpdateUserInfoValidator {
|
|
||||||
constructor(protected ctx: HttpContextContract) {}
|
|
||||||
|
|
||||||
public schema = schema.create({
|
|
||||||
about: schema.string.nullableAndOptional(),
|
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.mobile({ locale: ['ru-RU', 'en-US'], strict: true }),
|
|
||||||
rules.unique({ table: 'contacts', column: 'phone_number' }),
|
|
||||||
]),
|
|
||||||
vk_id: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.regex(/^([a-zA-Z0-9_]){1,64}$/),
|
|
||||||
rules.unique({ table: 'contacts', column: 'vk_id' }),
|
|
||||||
]),
|
|
||||||
twitter_id: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.regex(/(^|[^@\w])@(\w{1,15})\b/),
|
|
||||||
rules.unique({ table: 'contacts', column: 'twitter_id' }),
|
|
||||||
]),
|
|
||||||
telegram_id: schema.string.nullableAndOptional({}, [
|
|
||||||
rules.regex(/(^|[^@\w])@(\w{1,64})\b/),
|
|
||||||
rules.unique({ table: 'contacts', column: 'telegram_id' }),
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
public messages = {
|
|
||||||
'phone_number.unique': 'This phone number is not available.',
|
|
||||||
'vk_id.unique': 'This ID is not available.',
|
|
||||||
'twitter_id.unique': 'Twitter username is not available.',
|
|
||||||
'telegram_id.unique': 'Telegram username is not available.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ export default class UpdateUserValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
first_name: schema.string.optional(),
|
first_name: schema.string.optional(),
|
||||||
last_name: schema.string.optional(),
|
last_name: schema.string.optional(),
|
||||||
about: schema.string.optional(),
|
|
||||||
login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]),
|
login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]),
|
||||||
email: schema.string.optional({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
|
email: schema.string.optional({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
|
||||||
password: schema.string.optional(),
|
password: schema.string.optional(),
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
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<any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<any> {
|
|
||||||
return validator.validate({
|
|
||||||
schema: this.schemas(type),
|
|
||||||
data,
|
|
||||||
...(this.errorMessages[type] ? { messages: this.errorMessages[type] } : {}),
|
|
||||||
...(this.cacheKey ? { cacheKey: this.cacheKey } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -106,8 +106,8 @@ export const http: ServerConfig = {
|
|||||||
path: '/',
|
path: '/',
|
||||||
maxAge: '1d',
|
maxAge: '1d',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: false,
|
||||||
sameSite: 'none',
|
sameSite: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
11
config/cloudinary.ts
Normal file
11
config/cloudinary.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
|
import { CloudinaryConfig } from '@ioc:Adonis/Addons/Cloudinary';
|
||||||
|
|
||||||
|
const cloudinaryConfig: CloudinaryConfig = {
|
||||||
|
cloudName: Env.get('CLOUDINARY_CLOUD_NAME'),
|
||||||
|
apiKey: Env.get('CLOUDINARY_API_KEY'),
|
||||||
|
apiSecret: Env.get('CLOUDINARY_API_SECRET'),
|
||||||
|
secure: Env.get('CLOUDINARY_SECURE', true),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cloudinaryConfig;
|
||||||
@ -43,7 +43,6 @@ const databaseConfig: DatabaseConfig = {
|
|||||||
database: Env.get('PG_DB_NAME'),
|
database: Env.get('PG_DB_NAME'),
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
disableRollbacksInProduction: true,
|
|
||||||
naturalSort: true,
|
naturalSort: true,
|
||||||
tableName: 'migrations',
|
tableName: 'migrations',
|
||||||
},
|
},
|
||||||
@ -51,7 +50,7 @@ const databaseConfig: DatabaseConfig = {
|
|||||||
paths: ['./database/seeders/MainSeeder'],
|
paths: ['./database/seeders/MainSeeder'],
|
||||||
},
|
},
|
||||||
healthCheck: true,
|
healthCheck: true,
|
||||||
debug: false,
|
debug: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Env from '@ioc:Adonis/Core/Env';
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
import { driveConfig } from '@adonisjs/core/build/config';
|
import { DriveConfig } from '@ioc:Adonis/Core/Drive';
|
||||||
import Application from '@ioc:Adonis/Core/Application';
|
import Application from '@ioc:Adonis/Core/Application';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -18,7 +18,7 @@ import Application from '@ioc:Adonis/Core/Application';
|
|||||||
| defined inside the `contracts` directory.
|
| defined inside the `contracts` directory.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
export default driveConfig({
|
const driveConfig: DriveConfig = {
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default disk
|
| Default disk
|
||||||
@ -93,15 +93,15 @@ export default driveConfig({
|
|||||||
|**************************************************************************
|
|**************************************************************************
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
s3: {
|
// s3: {
|
||||||
driver: 's3',
|
// driver: 's3',
|
||||||
visibility: 'private',
|
// visibility: 'public',
|
||||||
key: Env.get('S3_KEY'),
|
// key: Env.get('S3_KEY'),
|
||||||
secret: Env.get('S3_SECRET'),
|
// secret: Env.get('S3_SECRET'),
|
||||||
region: Env.get('S3_REGION'),
|
// region: Env.get('S3_REGION'),
|
||||||
bucket: Env.get('S3_BUCKET'),
|
// bucket: Env.get('S3_BUCKET'),
|
||||||
endpoint: Env.get('S3_ENDPOINT'),
|
// endpoint: Env.get('S3_ENDPOINT'),
|
||||||
},
|
// },
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -143,4 +143,6 @@ export default driveConfig({
|
|||||||
// usingUniformAcl: false
|
// usingUniformAcl: false
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default driveConfig;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Env from '@ioc:Adonis/Core/Env';
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
import { hashConfig } from '@adonisjs/core/build/config';
|
import { HashConfig } from '@ioc:Adonis/Core/Hash';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -17,7 +17,7 @@ import { hashConfig } from '@adonisjs/core/build/config';
|
|||||||
| defined inside `contracts` directory.
|
| defined inside `contracts` directory.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
export default hashConfig({
|
const hashConfig: HashConfig = {
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default hasher
|
| Default hasher
|
||||||
@ -70,4 +70,6 @@ export default hashConfig({
|
|||||||
rounds: 10,
|
rounds: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default hashConfig;
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Env from '@ioc:Adonis/Core/Env';
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
import { mailConfig } from '@adonisjs/mail/build/config';
|
import { MailConfig } from '@ioc:Adonis/Addons/Mail';
|
||||||
|
|
||||||
export default mailConfig({
|
const mailConfig: MailConfig = {
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default mailer
|
| Default mailer
|
||||||
@ -18,7 +18,7 @@ export default mailConfig({
|
|||||||
| a mailer
|
| a mailer
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
mailer: Env.get('MAILER'),
|
mailer: 'mailgun',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -71,4 +71,6 @@ export default mailConfig({
|
|||||||
domain: Env.get('MAILGUN_DOMAIN'),
|
domain: Env.get('MAILGUN_DOMAIN'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default mailConfig;
|
||||||
|
|||||||
@ -43,30 +43,6 @@ const redisConfig: RedisConfig = {
|
|||||||
keyPrefix: '',
|
keyPrefix: '',
|
||||||
healthCheck: true,
|
healthCheck: true,
|
||||||
},
|
},
|
||||||
session: {
|
|
||||||
host: Env.get('REDIS_HOST'),
|
|
||||||
port: Env.get('REDIS_PORT'),
|
|
||||||
password: Env.get('REDIS_PASSWORD', ''),
|
|
||||||
db: 1,
|
|
||||||
keyPrefix: 'session-',
|
|
||||||
healthCheck: true,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
host: Env.get('REDIS_HOST'),
|
|
||||||
port: Env.get('REDIS_PORT'),
|
|
||||||
password: Env.get('REDIS_PASSWORD', ''),
|
|
||||||
keyPrefix: '',
|
|
||||||
db: 2,
|
|
||||||
healthCheck: true,
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
host: Env.get('REDIS_HOST'),
|
|
||||||
port: Env.get('REDIS_PORT'),
|
|
||||||
password: Env.get('REDIS_PASSWORD', ''),
|
|
||||||
keyPrefix: '',
|
|
||||||
db: 3,
|
|
||||||
healthCheck: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,16 @@
|
|||||||
import { InferDisksFromConfig } from '@adonisjs/core/build/config';
|
|
||||||
import driveConfig from '../config/drive';
|
|
||||||
|
|
||||||
declare module '@ioc:Adonis/Core/Drive' {
|
declare module '@ioc:Adonis/Core/Drive' {
|
||||||
interface DisksList extends InferDisksFromConfig<typeof driveConfig> {}
|
interface DisksList {
|
||||||
|
local: {
|
||||||
|
config: LocalDriverConfig;
|
||||||
|
implementation: LocalDriverContract;
|
||||||
|
};
|
||||||
|
// s3: {
|
||||||
|
// config: S3DriverConfig
|
||||||
|
// implementation: S3DriverContract
|
||||||
|
// }
|
||||||
|
// gcs: {
|
||||||
|
// config: GcsDriverConfig
|
||||||
|
// implementation: GcsDriverContract
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,19 @@
|
|||||||
import { InferListFromConfig } from '@adonisjs/core/build/config';
|
/**
|
||||||
import hashConfig from '../config/hash';
|
* Contract source: https://git.io/Jfefs
|
||||||
|
*
|
||||||
|
* Feel free to let us know via PR, if you find something broken in this contract
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
|
||||||
declare module '@ioc:Adonis/Core/Hash' {
|
declare module '@ioc:Adonis/Core/Hash' {
|
||||||
interface HashersList extends InferListFromConfig<typeof hashConfig> {}
|
interface HashersList {
|
||||||
|
bcrypt: {
|
||||||
|
config: BcryptConfig;
|
||||||
|
implementation: BcryptContract;
|
||||||
|
};
|
||||||
|
argon: {
|
||||||
|
config: ArgonConfig;
|
||||||
|
implementation: ArgonContract;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import { InferMailersFromConfig } from '@adonisjs/mail/build/config';
|
/**
|
||||||
import mailConfig from '../config/mail';
|
* Contract source: https://git.io/JvgAT
|
||||||
|
*
|
||||||
|
* Feel free to let us know via PR, if you find something broken in this contract
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
|
||||||
declare module '@ioc:Adonis/Addons/Mail' {
|
declare module '@ioc:Adonis/Addons/Mail' {
|
||||||
interface MailersList extends InferMailersFromConfig<typeof mailConfig> {}
|
import { MailDrivers } from '@ioc:Adonis/Addons/Mail';
|
||||||
|
|
||||||
|
interface MailersList {
|
||||||
|
smtp: MailDrivers['smtp'];
|
||||||
|
mailgun: MailDrivers['mailgun'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,5 @@
|
|||||||
declare module '@ioc:Adonis/Addons/Redis' {
|
declare module '@ioc:Adonis/Addons/Redis' {
|
||||||
interface RedisConnectionsList {
|
interface RedisConnectionsList {
|
||||||
local: RedisConnectionConfig;
|
local: RedisConnectionConfig;
|
||||||
session: RedisConnectionConfig;
|
|
||||||
message: RedisConnectionConfig;
|
|
||||||
notification: RedisConnectionConfig;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Factory, { FactoryContextContract } from '@ioc:Adonis/Lucid/Factory';
|
import Factory from '@ioc:Adonis/Lucid/Factory';
|
||||||
import { string } from '@ioc:Adonis/Core/Helpers';
|
import { string } from '@ioc:Adonis/Core/Helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,7 +18,7 @@ import Role from 'App/Models/Role';
|
|||||||
import User from 'App/Models/User';
|
import User from 'App/Models/User';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
const makeFakeUser = (faker: FactoryContextContract['faker'], loginPrefix?: string) => {
|
const makeFakeUser = (faker: Faker.FakerStatic, loginPrefix?: string) => {
|
||||||
return {
|
return {
|
||||||
first_name: faker.name.firstName(),
|
first_name: faker.name.firstName(),
|
||||||
last_name: faker.name.lastName(),
|
last_name: faker.name.lastName(),
|
||||||
@ -56,7 +56,7 @@ export const StudentFactory = Factory.define(User, ({ faker }) => makeFakeUser(f
|
|||||||
|
|
||||||
export const LessonContentFactory = Factory.define(LessonContent, ({ faker }) => {
|
export const LessonContentFactory = Factory.define(LessonContent, ({ faker }) => {
|
||||||
return {
|
return {
|
||||||
body: `${faker.lorem.sentences(40)}`,
|
video_url: `https://www.youtube.com/embed/${faker.datatype.string(10)}`,
|
||||||
};
|
};
|
||||||
}).build();
|
}).build();
|
||||||
|
|
||||||
@ -87,7 +87,6 @@ export const CourseFactory = Factory.define(Course, ({ faker }) => {
|
|||||||
return {
|
return {
|
||||||
title: faker.lorem.sentence(8),
|
title: faker.lorem.sentence(8),
|
||||||
description: faker.lorem.sentence(20),
|
description: faker.lorem.sentence(20),
|
||||||
education_description: faker.lorem.sentence(15),
|
|
||||||
status: CourseStatusEnum.PUBLISHED,
|
status: CourseStatusEnum.PUBLISHED,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@ -8,11 +8,9 @@ export default class Users extends BaseSchema {
|
|||||||
table.string('id', 21).primary();
|
table.string('id', 21).primary();
|
||||||
table.string('first_name', 24).notNullable();
|
table.string('first_name', 24).notNullable();
|
||||||
table.string('last_name', 24).notNullable();
|
table.string('last_name', 24).notNullable();
|
||||||
table.text('about');
|
|
||||||
table.string('login', 128).notNullable().unique();
|
table.string('login', 128).notNullable().unique();
|
||||||
table.string('email').notNullable().unique();
|
table.string('email').notNullable().unique();
|
||||||
table.string('password').notNullable();
|
table.string('password').notNullable();
|
||||||
table.timestamp('last_login', { useTz: true });
|
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
table.timestamp('updated_at', { useTz: true });
|
table.timestamp('updated_at', { useTz: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export default class Courses extends BaseSchema {
|
|||||||
table.json('image');
|
table.json('image');
|
||||||
table.string('title').notNullable();
|
table.string('title').notNullable();
|
||||||
table.string('description').notNullable();
|
table.string('description').notNullable();
|
||||||
table.string('education_description', 250);
|
|
||||||
table.string('teacher_id', 21).unsigned().references('users.id').notNullable().onDelete('CASCADE');
|
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.string('category_id', 21).unsigned().references('categories.id').notNullable().onDelete('CASCADE');
|
||||||
table.integer('color_id').unsigned().references('colors.id');
|
table.integer('color_id').unsigned().references('colors.id');
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export default class Lessons extends BaseSchema {
|
|||||||
table.integer('display_order').notNullable();
|
table.integer('display_order').notNullable();
|
||||||
table.integer('color_id').unsigned().references('colors.id');
|
table.integer('color_id').unsigned().references('colors.id');
|
||||||
table.string('description');
|
table.string('description');
|
||||||
table.text('linked_video_url').nullable();
|
|
||||||
table.time('duration').notNullable();
|
table.time('duration').notNullable();
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
table.timestamp('updated_at', { useTz: true });
|
table.timestamp('updated_at', { useTz: true });
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default class LessonContents extends BaseSchema {
|
|||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE');
|
table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE');
|
||||||
table.text('body');
|
table.string('video_url').notNullable();
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
table.timestamp('updated_at', { useTz: true });
|
table.timestamp('updated_at', { useTz: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,11 +6,11 @@ export default class LessonMaterials extends BaseSchema {
|
|||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
|
table.string('url').notNullable();
|
||||||
table.string('name').notNullable();
|
table.string('name').notNullable();
|
||||||
table.integer('size').notNullable();
|
|
||||||
table.string('client_name').notNullable();
|
table.string('client_name').notNullable();
|
||||||
table.string('ext').notNullable();
|
table.string('ext').notNullable();
|
||||||
table.string('lesson_id', 21).unsigned().references('lessons.id').onDelete('CASCADE');
|
table.integer('lesson_content_id').unsigned().references('lesson_contents.id').onDelete('CASCADE');
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
table.timestamp('updated_at', { useTz: true });
|
table.timestamp('updated_at', { useTz: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,13 +7,7 @@ export default class CourseSeeder extends BaseSeeder {
|
|||||||
public async run() {
|
public async run() {
|
||||||
this.CourseFactory = CourseFactory;
|
this.CourseFactory = CourseFactory;
|
||||||
|
|
||||||
const LESSONS_COUNT = 10;
|
await this.CourseFactory.with('lessons', 10, lessonFactory => lessonFactory.with('content'))
|
||||||
|
|
||||||
await this.CourseFactory.with('lessons', LESSONS_COUNT, lessonFactory =>
|
|
||||||
lessonFactory
|
|
||||||
.with('content')
|
|
||||||
.merge(new Array(LESSONS_COUNT).fill(LESSONS_COUNT).map((_, i) => ({ display_order: i + 1 })))
|
|
||||||
)
|
|
||||||
.with('category')
|
.with('category')
|
||||||
.with('teacher')
|
.with('teacher')
|
||||||
.createMany(3);
|
.createMany(3);
|
||||||
|
|||||||
@ -11,48 +11,23 @@ export default class UserSeeder extends BaseSeeder {
|
|||||||
*/
|
*/
|
||||||
const roles = {
|
const roles = {
|
||||||
admin: await Role.findByOrFail('slug', 'admin'),
|
admin: await Role.findByOrFail('slug', 'admin'),
|
||||||
teacher: await Role.findByOrFail('slug', 'teacher'),
|
|
||||||
student: await Role.findByOrFail('slug', 'student'),
|
student: await Role.findByOrFail('slug', 'student'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Administrator
|
* Administrator
|
||||||
*/
|
*/
|
||||||
const administrator = await User.create({
|
const userAdmin = await User.create({
|
||||||
first_name: 'John',
|
first_name: 'John',
|
||||||
last_name: 'Doe',
|
last_name: 'Doe',
|
||||||
login: 'admin',
|
login: 'admin',
|
||||||
password: '123456',
|
password: '123456',
|
||||||
email: 'administrator@example.com',
|
email: 'administrator@example.com',
|
||||||
});
|
});
|
||||||
await administrator.related('roles').attach([roles.admin.id]);
|
await userAdmin.related('roles').attach([roles.admin.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Teacher
|
* Students
|
||||||
*/
|
|
||||||
const teacher = await User.create({
|
|
||||||
first_name: 'Hanna',
|
|
||||||
last_name: 'Liv',
|
|
||||||
login: 'teacher',
|
|
||||||
password: '123456',
|
|
||||||
email: 'teacher@example.com',
|
|
||||||
});
|
|
||||||
await teacher.related('roles').attach([roles.teacher.id]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Student
|
|
||||||
*/
|
|
||||||
const student = await User.create({
|
|
||||||
first_name: 'Ylfa',
|
|
||||||
last_name: 'Erna',
|
|
||||||
login: 'student',
|
|
||||||
password: '123456',
|
|
||||||
email: 'student@example.com',
|
|
||||||
});
|
|
||||||
await student.related('roles').attach([roles.student.id]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Student list
|
|
||||||
*/
|
*/
|
||||||
await StudentFactory.with('contacts').createMany(20);
|
await StudentFactory.with('contacts').createMany(20);
|
||||||
}
|
}
|
||||||
|
|||||||
13
env.ts
13
env.ts
@ -15,30 +15,25 @@
|
|||||||
import Env from '@ioc:Adonis/Core/Env';
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
|
|
||||||
export default Env.rules({
|
export default Env.rules({
|
||||||
|
HOST: Env.schema.string({ format: 'host' }),
|
||||||
PORT: Env.schema.number(),
|
PORT: Env.schema.number(),
|
||||||
APP_KEY: Env.schema.string(),
|
APP_KEY: Env.schema.string(),
|
||||||
APP_NAME: Env.schema.string(),
|
APP_NAME: Env.schema.string(),
|
||||||
NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
|
NODE_ENV: Env.schema.enum(['development', 'production', 'testing'] as const),
|
||||||
DRIVE_DISK: Env.schema.enum(['local', 's3'] as const),
|
DRIVE_DISK: Env.schema.enum(['local'] as const),
|
||||||
PG_HOST: Env.schema.string({ format: 'host' }),
|
PG_HOST: Env.schema.string({ format: 'host' }),
|
||||||
PG_PORT: Env.schema.number(),
|
PG_PORT: Env.schema.number(),
|
||||||
PG_USER: Env.schema.string(),
|
PG_USER: Env.schema.string(),
|
||||||
PG_PASSWORD: Env.schema.string.optional(),
|
PG_PASSWORD: Env.schema.string.optional(),
|
||||||
PG_DB_NAME: Env.schema.string(),
|
PG_DB_NAME: Env.schema.string(),
|
||||||
REDIS_CONNECTION: Env.schema.enum(['local', 'session'] as const),
|
REDIS_CONNECTION: Env.schema.enum(['local'] as const),
|
||||||
REDIS_HOST: Env.schema.string({ format: 'host' }),
|
REDIS_HOST: Env.schema.string({ format: 'host' }),
|
||||||
REDIS_PORT: Env.schema.number(),
|
REDIS_PORT: Env.schema.number(),
|
||||||
REDIS_PASSWORD: Env.schema.string.optional(),
|
REDIS_PASSWORD: Env.schema.string.optional(),
|
||||||
MAILER: Env.schema.enum(['mailgun', 'smtp'] as const),
|
|
||||||
SMTP_HOST: Env.schema.string({ format: 'host' }),
|
SMTP_HOST: Env.schema.string({ format: 'host' }),
|
||||||
SMTP_PORT: Env.schema.number(),
|
SMTP_PORT: Env.schema.number(),
|
||||||
SMTP_USERNAME: Env.schema.string(),
|
SMTP_USERNAME: Env.schema.string(),
|
||||||
SMTP_PASSWORD: Env.schema.string(),
|
SMTP_PASSWORD: Env.schema.string(),
|
||||||
MAILGUN_API_KEY: Env.schema.string(),
|
MAILGUN_API_KEY: Env.schema.string(),
|
||||||
MAILGUN_DOMAIN: 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(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
15833
package-lock.json
generated
15833
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.4.5",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ace build --production",
|
"build": "node ace build --production",
|
||||||
@ -15,44 +15,40 @@
|
|||||||
"db:seed:force": "node ace seed --force"
|
"db:seed:force": "node ace seed --force"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^5.7.0",
|
"@adonisjs/assembler": "^5.3.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||||
"@typescript-eslint/parser": "^5.27.0",
|
"@typescript-eslint/parser": "^4.30.0",
|
||||||
"adonis-preset-ts": "^2.1.0",
|
"adonis-preset-ts": "^2.1.0",
|
||||||
"eslint": "^8.17.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.24.2",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
"pino-pretty": "^8.0.0",
|
"pino-pretty": "^5.1.3",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.3.2",
|
||||||
"typescript": "^4.7.3",
|
"typescript": "^4.4.2",
|
||||||
"youch": "^3.2.0",
|
"youch": "^2.2.2",
|
||||||
"youch-terminal": "^2.1.4"
|
"youch-terminal": "^1.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/application": "^5.2.5",
|
"@adonisjs/attachment-lite": "^1.0.1",
|
||||||
"@adonisjs/attachment-lite": "^1.0.7",
|
"@adonisjs/auth": "^8.0.10",
|
||||||
"@adonisjs/auth": "^8.2.1",
|
"@adonisjs/bouncer": "^2.2.5",
|
||||||
"@adonisjs/bouncer": "^2.3.0",
|
"@adonisjs/core": "^5.4.0",
|
||||||
"@adonisjs/core": "^5.8.3",
|
"@adonisjs/drive": "^2.0.7",
|
||||||
"@adonisjs/drive": "^2.2.0",
|
"@adonisjs/lucid": "^16.2.1",
|
||||||
"@adonisjs/drive-s3": "^1.3.1",
|
"@adonisjs/mail": "^7.2.4",
|
||||||
"@adonisjs/lucid": "^18.0.1",
|
"@adonisjs/redis": "^7.0.9",
|
||||||
"@adonisjs/mail": "^8.1.2",
|
"@adonisjs/repl": "^3.1.7",
|
||||||
"@adonisjs/redis": "^7.3.0",
|
"@adonisjs/view": "^6.1.1",
|
||||||
"@adonisjs/repl": "^3.1.11",
|
|
||||||
"@adonisjs/view": "^6.1.5",
|
|
||||||
"@types/socket.io": "^3.0.2",
|
|
||||||
"cloudinary": "^1.26.3",
|
"cloudinary": "^1.26.3",
|
||||||
"luxon": "^2.4.0",
|
"luxon": "^1.28.0",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.1.25",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.7.1",
|
||||||
"phc-bcrypt": "^1.0.7",
|
"phc-bcrypt": "^1.0.7",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"socket.io": "^4.5.1",
|
"source-map-support": "^0.5.19"
|
||||||
"source-map-support": "^0.5.21"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,7 @@ export default class AppProvider {
|
|||||||
|
|
||||||
public async boot() {}
|
public async boot() {}
|
||||||
|
|
||||||
public async ready() {
|
public async ready() {}
|
||||||
if (this.app.environment === 'web') {
|
|
||||||
await import('../start/socket');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async shutdown() {}
|
public async shutdown() {}
|
||||||
}
|
}
|
||||||
|
|||||||
11
routes/api.ts
Normal file
11
routes/api.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import Route from '@ioc:Adonis/Core/Route';
|
||||||
|
|
||||||
|
Route.group(() => {
|
||||||
|
/**
|
||||||
|
* v1
|
||||||
|
*/
|
||||||
|
require('./apis/v1/index');
|
||||||
|
})
|
||||||
|
.prefix('api')
|
||||||
|
.as('api');
|
||||||
@ -3,4 +3,4 @@ import Route from '@ioc:Adonis/Core/Route';
|
|||||||
Route.group(() => {
|
Route.group(() => {
|
||||||
Route.post('login', 'Api/v1/AuthController.login').as('auth.login');
|
Route.post('login', 'Api/v1/AuthController.login').as('auth.login');
|
||||||
Route.post('logout', 'Api/v1/AuthController.logout').middleware('auth').as('auth.logout');
|
Route.post('logout', 'Api/v1/AuthController.logout').middleware('auth').as('auth.logout');
|
||||||
}).prefix('v1/auth');
|
}).prefix('auth');
|
||||||
@ -5,7 +5,4 @@ Route.group(() => {
|
|||||||
Route.get('/:id', 'Api/v1/CategoriesController.show').middleware('role:admin,teacher,student').as('categories.show');
|
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.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.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');
|
||||||
})
|
|
||||||
.prefix('v1/categories')
|
|
||||||
.middleware('auth');
|
|
||||||
@ -30,14 +30,6 @@ Route.group(() => {
|
|||||||
.middleware('role:admin,teacher')
|
.middleware('role:admin,teacher')
|
||||||
.as('courses.attach-student');
|
.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')
|
Route.delete('/:id/detach-student', 'Api/v1/CoursesController.detachStudent')
|
||||||
.middleware('role:admin,teacher')
|
.middleware('role:admin,teacher')
|
||||||
.as('courses.detach-student');
|
.as('courses.detach-student');
|
||||||
@ -57,10 +49,4 @@ Route.group(() => {
|
|||||||
Route.post('/:id/set-status', 'Api/v1/CoursesController.setStatus')
|
Route.post('/:id/set-status', 'Api/v1/CoursesController.setStatus')
|
||||||
.middleware('role:admin,teacher')
|
.middleware('role:admin,teacher')
|
||||||
.as('courses.set-status');
|
.as('courses.set-status');
|
||||||
|
}).prefix('courses');
|
||||||
Route.get('/:id/lessons-progress', 'Api/v1/CoursesController.getLessonsProgress')
|
|
||||||
.middleware('role:admin,teacher,student')
|
|
||||||
.as('courses.get-lessons-progress');
|
|
||||||
})
|
|
||||||
.prefix('v1/courses')
|
|
||||||
.middleware('auth');
|
|
||||||
@ -8,6 +8,4 @@ Route.group(() => {
|
|||||||
|
|
||||||
return report.healthy ? ctx.response.ok(report) : ctx.response.badRequest(report);
|
return report.healthy ? ctx.response.ok(report) : ctx.response.badRequest(report);
|
||||||
}).as('health');
|
}).as('health');
|
||||||
})
|
}).prefix('health');
|
||||||
.prefix('v1/health')
|
|
||||||
.middleware('auth');
|
|
||||||
@ -5,6 +5,4 @@ Route.group(() => {
|
|||||||
Route.delete('/:id', 'Api/v1/ImagesController.delete').as('images.delete');
|
Route.delete('/:id', 'Api/v1/ImagesController.delete').as('images.delete');
|
||||||
Route.get('/', 'Api/v1/ImagesController.list').as('images.list');
|
Route.get('/', 'Api/v1/ImagesController.list').as('images.list');
|
||||||
Route.get('/:id', 'Api/v1/ImagesController.show').as('images.show');
|
Route.get('/:id', 'Api/v1/ImagesController.show').as('images.show');
|
||||||
})
|
}).prefix('images');
|
||||||
.prefix('v1/images')
|
|
||||||
.middleware('auth');
|
|
||||||
68
routes/apis/v1/index.ts
Normal file
68
routes/apis/v1/index.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import Route from '@ioc:Adonis/Core/Route';
|
||||||
|
|
||||||
|
Route.group(() => {
|
||||||
|
Route.get('/', () => {
|
||||||
|
return 'Backend API.';
|
||||||
|
}).as('index');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Auth module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./auth');
|
||||||
|
|
||||||
|
Route.group(() => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Health check
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./health');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Users module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./users');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Me module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./me');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Courses module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./courses');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Lessons module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./lessons');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Images module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./images');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Categories module
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
require('./categories');
|
||||||
|
}).middleware('auth');
|
||||||
|
})
|
||||||
|
.prefix('v1')
|
||||||
|
.as('v1');
|
||||||
@ -8,16 +8,11 @@ Route.group(() => {
|
|||||||
.as('lessons.update-order');
|
.as('lessons.update-order');
|
||||||
Route.get('/:id', 'Api/v1/LessonsController.show').middleware('role:admin,teacher,student').as('lessons.show');
|
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.delete('/:id', 'Api/v1/LessonsController.delete').middleware('role:admin,teacher').as('lessons.delete');
|
||||||
Route.patch('/:id', 'Api/v1/LessonsController.update').middleware('role:admin,teacher').as('lessons.update');
|
Route.put('/:id', 'Api/v1/LessonsController.update').middleware('role:admin,teacher').as('lessons.update');
|
||||||
Route.get('/:id/content', 'Api/v1/LessonsController.getContent')
|
Route.get('/:id/content', 'Api/v1/LessonsController.getContent')
|
||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('lessons.get-content');
|
.as('lessons.get-content');
|
||||||
Route.get('/materials/:fileName', 'Api/v1/LessonsController.getMaterial')
|
Route.get('/materials/:file', 'Api/v1/LessonsController.getMaterial')
|
||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('lessons.get-material');
|
.as('lessons.get-material');
|
||||||
Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress')
|
}).prefix('lessons');
|
||||||
.middleware('role:admin,teacher,student')
|
|
||||||
.as('lessons.get-video-progress');
|
|
||||||
})
|
|
||||||
.prefix('v1/lessons')
|
|
||||||
.middleware('auth');
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user