mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e90ad2524 | |||
| 135b9faa6b | |||
| 1b13dd6ce1 | |||
| 6fe7b9873b | |||
| 6091987ba4 | |||
| fa8270f88e | |||
| 6b26489f9e | |||
| 933c9aa887 | |||
|
|
7b55d47360 | ||
| 7ec72a53b6 | |||
| a3ce94f1de | |||
| bfd41f391a | |||
| da67d3d4e1 | |||
| ef9f757c35 | |||
| 8f9d6e8cab | |||
| 9134c4d912 | |||
| 7bc2db4451 | |||
| 8508a6c43e | |||
| 5510d9fbaf | |||
| 92065a2ff5 | |||
| b088442d47 | |||
| b9baca7f7f | |||
| 066caa6a9d | |||
| 4c0377d234 | |||
| cc36a77912 | |||
| 0472036b66 | |||
| 7149a0f3eb | |||
| 30fb10b338 | |||
| b6217a4007 | |||
| 06e8acd0b2 | |||
| 4aed828c4e | |||
| 8a8e3cd552 | |||
| 3f5222eb88 | |||
| ff97175fbe | |||
| 303a27089b | |||
| 22ff69a3b4 | |||
| 294a4f584d | |||
| 5171f65429 | |||
| ffdebc4f92 | |||
| 5a01acdb21 | |||
| 59207d8ea4 | |||
| 9c16ddd9a6 | |||
| 3b37fc3b46 | |||
| b1965eeeda | |||
| 793d7d650a | |||
| 416f221066 | |||
| 79e56696fd | |||
| c0aad40853 |
@ -1,6 +1,20 @@
|
|||||||
build
|
# Adonis default .gitignore ignores
|
||||||
node_modules
|
node_modules
|
||||||
.git
|
build
|
||||||
*.log
|
coverage
|
||||||
|
.vscode
|
||||||
|
.DS_STORE
|
||||||
.env
|
.env
|
||||||
Dockerfile
|
tmp
|
||||||
|
uploads
|
||||||
|
|
||||||
|
# Additional .gitignore ignores (any custom file you wish)
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Additional good to have ignores for dockerignore
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
*.md
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|||||||
@ -19,8 +19,8 @@ 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_KEY=dummyKey
|
||||||
S3_SECRET=dummySecret
|
S3_SECRET=dummySecret
|
||||||
S3_BUCKET=dummyBucket
|
S3_BUCKET=dummyBucket
|
||||||
S3_REGION=dummyRegion
|
S3_REGION=dummyRegion
|
||||||
S3_ENDPOINT=dummyEndpoint
|
|
||||||
|
|||||||
@ -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"],
|
"plugins": ["@typescript-eslint", "import"],
|
||||||
"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,3 +6,4 @@ coverage
|
|||||||
.env
|
.env
|
||||||
tmp
|
tmp
|
||||||
uploads
|
uploads
|
||||||
|
npm-debug.log
|
||||||
33
CHANGELOG.md
33
CHANGELOG.md
@ -1,8 +1,37 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased
|
## [1.4.2] - 2022-12-06
|
||||||
|
|
||||||
- API deployment with Docker and Nginx web server.
|
## Added
|
||||||
|
|
||||||
|
- Uploading video from url source (YouTube, Vimeo, .mp4, etc..)
|
||||||
|
- Added student and teacher to seeders
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
- Removing lesson files from drive when deleting course, category or user.
|
||||||
|
- Fixed lesson display ordering when seeding database
|
||||||
|
- Refactored bouncer actions
|
||||||
|
|
||||||
|
## [1.3.2] - 2022-06-06
|
||||||
|
|
||||||
|
## 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
|
## [1.2.1] - 2022-03-06
|
||||||
|
|
||||||
|
|||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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,3 +1,8 @@
|
|||||||
# About API
|
# educt-server
|
||||||
|
|
||||||
...
|
_This service is part of [Educt](https://github.com/sergeyyarkov/educt) which contains API application._
|
||||||
|
|
||||||
|
<!-- ## Requirements
|
||||||
|
|
||||||
|
- NodeJS >= 14.17.3
|
||||||
|
- Docker -->
|
||||||
|
|||||||
@ -11,19 +11,66 @@
|
|||||||
},
|
},
|
||||||
"list:routes": {
|
"list:routes": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"loadApp": true
|
"loadApp": true,
|
||||||
|
"stayAlive": true
|
||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/core/build/commands/ListRoutes",
|
"commandPath": "@adonisjs/core/build/commands/ListRoutes/index",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -55,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
|
"commandPath": "@adonisjs/lucid/build/commands/DbSeed",
|
||||||
"commandName": "db:seed",
|
"commandName": "db:seed",
|
||||||
"description": "Execute database seeder files",
|
"description": "Execute database seeders",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -79,6 +126,12 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -113,6 +166,13 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -177,13 +237,81 @@
|
|||||||
"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": "Run pending migrations",
|
"description": "Migrate database by running pending migrations",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -204,7 +332,13 @@
|
|||||||
"name": "dry-run",
|
"name": "dry-run",
|
||||||
"propertyName": "dryRun",
|
"propertyName": "dryRun",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Print SQL queries, instead of running the migrations"
|
"description": "Do not run actual queries. Instead view the SQL output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "compact-output",
|
||||||
|
"propertyName": "compactOutput",
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "A compact single-line output"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -214,7 +348,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 given batch number",
|
"description": "Rollback migrations to a specific batch number",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -235,13 +369,19 @@
|
|||||||
"name": "dry-run",
|
"name": "dry-run",
|
||||||
"propertyName": "dryRun",
|
"propertyName": "dryRun",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Print SQL queries, instead of running the migrations"
|
"description": "Do not run actual queries. Instead view the SQL output"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -251,7 +391,7 @@
|
|||||||
},
|
},
|
||||||
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
|
"commandPath": "@adonisjs/lucid/build/commands/Migration/Status",
|
||||||
"commandName": "migration:status",
|
"commandName": "migration:status",
|
||||||
"description": "Check migrations current status.",
|
"description": "View migrations status",
|
||||||
"args": [],
|
"args": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": [
|
"flags": [
|
||||||
@ -264,6 +404,117 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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 } from '@ioc:Adonis/Core/Validator';
|
import { schema, rules } from '@ioc:Adonis/Core/Validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services
|
* Services
|
||||||
@ -141,7 +141,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().members(schema.string()),
|
ids: schema.array([rules.minLength(1)]).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);
|
||||||
|
|||||||
@ -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);
|
const result = await this.userService.deleteUser(ctx.params.id, ctx);
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@ -17,9 +17,10 @@ export default class OnlineController {
|
|||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.io = io;
|
this.io = io;
|
||||||
this.sessionStore = sessionStore;
|
this.sessionStore = sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
this.onConnected(socket);
|
public handle() {
|
||||||
|
this.onConnected(this.socket);
|
||||||
this.socket.on('disconnect', reason => this.onDisconnected(reason));
|
this.socket.on('disconnect', reason => this.onDisconnected(reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
app/Helpers/CourseHelper.ts
Normal file
28
app/Helpers/CourseHelper.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,9 @@ 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;
|
||||||
|
|
||||||
|
|||||||
35
app/Policies/LessonPolicy.ts
Normal file
35
app/Policies/LessonPolicy.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Policies/RolePolicy.ts
Normal file
36
app/Policies/RolePolicy.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,32 @@ 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
|
||||||
*
|
*
|
||||||
@ -237,6 +263,7 @@ 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')
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export default class LessonRepository {
|
|||||||
duration: data.duration.toFormat('HH:mm:ss'),
|
duration: data.duration.toFormat('HH:mm:ss'),
|
||||||
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
|
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
|
||||||
course_id: course.id,
|
course_id: course.id,
|
||||||
|
linked_video_url: !data.video ? data.linked_video_url : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +117,9 @@ export default class LessonRepository {
|
|||||||
/**
|
/**
|
||||||
* Move video file to disk and save to database
|
* Move video file to disk and save to database
|
||||||
*/
|
*/
|
||||||
|
if (data.video) {
|
||||||
await this.createVideo(lesson, data.video);
|
await this.createVideo(lesson, data.video);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create materials
|
* Create materials
|
||||||
@ -144,31 +147,38 @@ export default class LessonRepository {
|
|||||||
|
|
||||||
if (lesson) {
|
if (lesson) {
|
||||||
/**
|
/**
|
||||||
* Update files
|
* Update fields
|
||||||
*/
|
*/
|
||||||
lesson.merge({
|
lesson.merge({
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
course_id: data.course_id,
|
course_id: data.course_id,
|
||||||
|
...(data.duration && { duration: data.duration.toFormat('HH:mm:ss') }),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update video
|
* Update linked video url
|
||||||
*/
|
|
||||||
if (data.video) {
|
|
||||||
/**
|
|
||||||
* Delete video from drive and database
|
|
||||||
*/
|
*/
|
||||||
|
if (data.linked_video_url && !data.video) {
|
||||||
if (lesson.video) {
|
if (lesson.video) {
|
||||||
await this.Drive.delete(`videos/${lesson.video.name}`);
|
await this.Drive.delete(`videos/${lesson.video.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await lesson.related('video').query().delete();
|
await lesson.related('video').query().delete();
|
||||||
|
lesson.merge({ linked_video_url: data.linked_video_url });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload new video to drive and save to database
|
* 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);
|
await this.createVideo(lesson, data.video);
|
||||||
|
lesson.merge({ linked_video_url: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,7 +211,10 @@ export default class LessonRepository {
|
|||||||
/**
|
/**
|
||||||
* Delete files from disk
|
* Delete files from disk
|
||||||
*/
|
*/
|
||||||
|
if (lesson.video) {
|
||||||
await this.Drive.delete(`videos/${lesson.video.name}`);
|
await this.Drive.delete(`videos/${lesson.video.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
await this.deleteMaterials(lesson);
|
await this.deleteMaterials(lesson);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export default class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set cookie toke to response
|
* Set cookie token to response
|
||||||
*/
|
*/
|
||||||
ctx.response.cookie('token', token.token);
|
ctx.response.cookie('token', token.token);
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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
|
||||||
@ -17,12 +18,27 @@ import CategoryRepository from 'App/Repositories/CategoryRepository';
|
|||||||
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
import CreateCategoryValidator from 'App/Validators/Category/CreateCategoryValidator';
|
||||||
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
|
import UpdateCategoryValidator from 'App/Validators/Category/UpdateCategoryValidator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services
|
||||||
|
*/
|
||||||
|
import CourseService from 'App/Services/CourseService';
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class CategoryService {
|
export default class CategoryService {
|
||||||
private categoryRepository: CategoryRepository;
|
private categoryRepository: CategoryRepository;
|
||||||
|
|
||||||
constructor(categoryRepository: CategoryRepository) {
|
private courseRepository: CourseRepository;
|
||||||
|
|
||||||
|
private courseService: CourseService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
categoryRepository: CategoryRepository,
|
||||||
|
courseRepository: CourseRepository,
|
||||||
|
courseService: CourseService
|
||||||
|
) {
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
|
this.courseRepository = courseRepository;
|
||||||
|
this.courseService = courseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,7 +110,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.delete(id);
|
const data = await this.categoryRepository.getById(id);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return {
|
return {
|
||||||
@ -108,6 +124,10 @@ 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,
|
||||||
|
|||||||
@ -25,6 +25,8 @@ 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 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 {
|
||||||
@ -233,9 +235,9 @@ export default class CourseService {
|
|||||||
/**
|
/**
|
||||||
* Find course teacher
|
* Find course teacher
|
||||||
*/
|
*/
|
||||||
const teacher = await this.userRepository.getById(data.teacher_id);
|
const user = await this.userRepository.getById(data.teacher_id);
|
||||||
|
|
||||||
if (!teacher) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
status: HttpStatusEnum.NOT_FOUND,
|
||||||
@ -247,13 +249,13 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]);
|
const isHasPermissions = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
|
||||||
|
|
||||||
if (!isTeacher) {
|
if (!isHasPermissions) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.BAD_REQUEST,
|
status: HttpStatusEnum.BAD_REQUEST,
|
||||||
message: `User with id "${teacher.id}" not a teacher.`,
|
message: `User with id "${user.id}" does not have sufficient permissions.`,
|
||||||
data: {},
|
data: {},
|
||||||
error: {
|
error: {
|
||||||
code: 'E_BAD_REQUEST',
|
code: 'E_BAD_REQUEST',
|
||||||
@ -310,6 +312,8 @@ export default class CourseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.deleteAllFiles([course]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
status: HttpStatusEnum.OK,
|
||||||
@ -326,7 +330,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.update(id, data);
|
const course = await this.courseRepository.getById(id);
|
||||||
|
|
||||||
if (!course) {
|
if (!course) {
|
||||||
return {
|
return {
|
||||||
@ -340,11 +344,45 @@ 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: course,
|
data: updated || {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -667,6 +705,29 @@ 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);
|
||||||
|
|||||||
@ -78,20 +78,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
* Allow user to view lesson content
|
|
||||||
*/
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load lesson with materials
|
* Load lesson with materials
|
||||||
@ -109,7 +96,8 @@ export default class LessonService {
|
|||||||
/**
|
/**
|
||||||
* Fetch lesson material by file name
|
* Fetch lesson material by file name
|
||||||
*
|
*
|
||||||
* @param fileName Name of file
|
* @param ctx Http context
|
||||||
|
* @param name Filename
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
||||||
@ -131,18 +119,7 @@ export default class LessonService {
|
|||||||
* Allow user to view lesson material
|
* Allow user to view lesson material
|
||||||
*/
|
*/
|
||||||
await material.load('lesson');
|
await material.load('lesson');
|
||||||
|
await ctx.bouncer.with('LessonPolicy').authorize('view', material.lesson);
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', material.lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You are not able to view this file.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -168,18 +145,7 @@ export default class LessonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await video.load('lesson');
|
await video.load('lesson');
|
||||||
|
await ctx.bouncer.with('LessonPolicy').authorize('view', video.lesson);
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', video.lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'You are not able to view this file.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -211,17 +177,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
|
const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
|
||||||
|
|
||||||
@ -354,20 +310,7 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
|
||||||
* Allow user to view lesson content
|
|
||||||
*/
|
|
||||||
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
|
||||||
message: 'The user is not a student of this course.',
|
|
||||||
data: {},
|
|
||||||
error: {
|
|
||||||
code: 'E_FORBIDDEN',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load lesson with materials
|
* Load lesson with materials
|
||||||
|
|||||||
@ -22,21 +22,48 @@ 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;
|
||||||
|
|
||||||
constructor(userRepository: UserRepository, roleRepository: RoleRepository) {
|
private courseRepository: CourseRepository;
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,20 +147,7 @@ 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
|
||||||
@ -166,7 +180,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.update(id, data);
|
const user = await this.userRepository.getById(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -180,6 +194,8 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -187,27 +203,18 @@ export default class UserService {
|
|||||||
const role = await this.roleRepository.getBySlug(data.role);
|
const role = await this.roleRepository.getBySlug(data.role);
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
await ctx.bouncer.with('RolePolicy').authorize('manage', [data.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: user,
|
data: updated || {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,8 +224,8 @@ export default class UserService {
|
|||||||
* @param id User id
|
* @param id User id
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async deleteUser(id: string | number): Promise<IResponse> {
|
public async deleteUser(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
|
||||||
const user = await this.userRepository.delete(id);
|
const user = await this.userRepository.getById(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@ -232,6 +239,15 @@ 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,
|
||||||
|
|||||||
@ -115,9 +115,10 @@ class WsService {
|
|||||||
|
|
||||||
private listen() {
|
private listen() {
|
||||||
this.io.on('connection', async socket => {
|
this.io.on('connection', async socket => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const onlineController = new OnlineController(socket, this.io, this.sessionStore);
|
const onlineController = new OnlineController(socket, this.io, this.sessionStore);
|
||||||
|
|
||||||
|
onlineController.handle();
|
||||||
|
|
||||||
const { sessionId, userId, userName } = socket.data;
|
const { sessionId, userId, userName } = socket.data;
|
||||||
const isExistSocketData = !!(sessionId && userId && userName);
|
const isExistSocketData = !!(sessionId && userId && userName);
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,6 @@ class RedisSessionStore {
|
|||||||
public async getSessions(): Promise<Array<SessionDataType | undefined>> {
|
public async getSessions(): Promise<Array<SessionDataType | undefined>> {
|
||||||
const keys = await this.scanSessionKeys();
|
const keys = await this.scanSessionKeys();
|
||||||
const commands = Array.from(keys).map(key => {
|
const commands = Array.from(keys).map(key => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [, ...id] = key.split('-');
|
const [, ...id] = key.split('-');
|
||||||
return ['hmget', id.join('-'), 'userId', 'userName', 'connected'];
|
return ['hmget', id.join('-'), 'userId', 'userName', 'connected'];
|
||||||
});
|
});
|
||||||
@ -64,7 +63,10 @@ class RedisSessionStore {
|
|||||||
return this.redisClient
|
return this.redisClient
|
||||||
.multi(commands)
|
.multi(commands)
|
||||||
.exec()
|
.exec()
|
||||||
.then(results => results.map(([err, session]) => (err ? undefined : this.transform(session))));
|
.then(results => {
|
||||||
|
if (results === null) return [];
|
||||||
|
return results.map(([err, session]: [Error, (string | null)[]]) => (err ? undefined : this.transform(session)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default class CreateContactsValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
phone_number: schema.string.nullableAndOptional({}, [
|
||||||
rules.mobile({
|
rules.mobile({
|
||||||
locales: ['ru-RU', 'en-US'],
|
locale: ['ru-RU', 'en-US'],
|
||||||
strict: true,
|
strict: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default class UpdateContactsValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
phone_number: schema.string.nullableAndOptional({}, [
|
||||||
rules.mobile({
|
rules.mobile({
|
||||||
locales: ['ru-RU', 'en-US'],
|
locale: ['ru-RU', 'en-US'],
|
||||||
strict: true,
|
strict: true,
|
||||||
}),
|
}),
|
||||||
rules.unique({ table: 'contacts', column: 'phone_number' }),
|
rules.unique({ table: 'contacts', column: 'phone_number' }),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export default class CreateCourseValidator {
|
|||||||
education_description: schema.string.nullable(),
|
education_description: schema.string.nullable(),
|
||||||
image: schema.file.optional({
|
image: schema.file.optional({
|
||||||
size: '10mb',
|
size: '10mb',
|
||||||
extnames: ['jpg', 'jpeg', 'png'],
|
extnames: ['jpg', 'jpeg', 'png', 'webp'],
|
||||||
}),
|
}),
|
||||||
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' })]),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default class UpdateCourseValidator {
|
|||||||
education_description: schema.string.nullable(),
|
education_description: schema.string.nullable(),
|
||||||
image: schema.file.optional({
|
image: schema.file.optional({
|
||||||
size: '10mb',
|
size: '10mb',
|
||||||
extnames: ['jpg', 'jpeg', 'png'],
|
extnames: ['jpg', 'jpeg', 'png', 'webp'],
|
||||||
}),
|
}),
|
||||||
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,10 +9,11 @@ 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({
|
video: schema.file.optional({
|
||||||
size: '5000mb',
|
size: '5000mb',
|
||||||
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
||||||
}),
|
}),
|
||||||
|
linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]),
|
||||||
materials: schema.array.optional().members(
|
materials: schema.array.optional().members(
|
||||||
schema.file({
|
schema.file({
|
||||||
size: '100mb',
|
size: '100mb',
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default class UpdateLessonValidator {
|
|||||||
size: '5000mb',
|
size: '5000mb',
|
||||||
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
extnames: ['mp4', 'mov', 'avi', 'wmv', 'webm', 'flv'],
|
||||||
}),
|
}),
|
||||||
|
linked_video_url: schema.string.optional([rules.regex(new RegExp('^(http|https|ftp)://'))]),
|
||||||
materials: schema.array.nullableAndOptional().members(
|
materials: schema.array.nullableAndOptional().members(
|
||||||
schema.file({
|
schema.file({
|
||||||
size: '100mb',
|
size: '100mb',
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default class UpdateUserInfoValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
about: schema.string.nullableAndOptional(),
|
about: schema.string.nullableAndOptional(),
|
||||||
phone_number: schema.string.nullableAndOptional({}, [
|
phone_number: schema.string.nullableAndOptional({}, [
|
||||||
rules.mobile({ locales: ['ru-RU', 'en-US'], strict: true }),
|
rules.mobile({ locale: ['ru-RU', 'en-US'], 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.nullableAndOptional({}, [
|
||||||
|
|||||||
@ -106,8 +106,8 @@ export const http: ServerConfig = {
|
|||||||
path: '/',
|
path: '/',
|
||||||
maxAge: '1d',
|
maxAge: '1d',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: true,
|
||||||
sameSite: false,
|
sameSite: 'none',
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -43,6 +43,7 @@ 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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Env from '@ioc:Adonis/Core/Env';
|
import Env from '@ioc:Adonis/Core/Env';
|
||||||
import { DriveConfig } from '@ioc:Adonis/Core/Drive';
|
import { driveConfig } from '@adonisjs/core/build/config';
|
||||||
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.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
const driveConfig: DriveConfig = {
|
export default driveConfig({
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default disk
|
| Default disk
|
||||||
@ -143,6 +143,4 @@ const driveConfig: 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 '@ioc:Adonis/Core/Hash';
|
import { hashConfig } from '@adonisjs/core/build/config';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -17,7 +17,7 @@ import { HashConfig } from '@ioc:Adonis/Core/Hash';
|
|||||||
| defined inside `contracts` directory.
|
| defined inside `contracts` directory.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
const hashConfig: HashConfig = {
|
export default hashConfig({
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default hasher
|
| Default hasher
|
||||||
@ -70,6 +70,4 @@ const hashConfig: 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 '@ioc:Adonis/Addons/Mail';
|
import { mailConfig } from '@adonisjs/mail/build/config';
|
||||||
|
|
||||||
const mailConfig: MailConfig = {
|
export default mailConfig({
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Default mailer
|
| Default mailer
|
||||||
@ -18,7 +18,7 @@ const mailConfig: MailConfig = {
|
|||||||
| a mailer
|
| a mailer
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
mailer: 'mailgun',
|
mailer: Env.get('MAILER'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -71,6 +71,4 @@ const mailConfig: MailConfig = {
|
|||||||
domain: Env.get('MAILGUN_DOMAIN'),
|
domain: Env.get('MAILGUN_DOMAIN'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
export default mailConfig;
|
|
||||||
|
|||||||
@ -1,16 +1,6 @@
|
|||||||
|
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 {
|
interface DisksList extends InferDisksFromConfig<typeof driveConfig> {}
|
||||||
local: {
|
|
||||||
config: LocalDriverConfig;
|
|
||||||
implementation: LocalDriverContract;
|
|
||||||
};
|
|
||||||
s3: {
|
|
||||||
config: S3DriverConfig;
|
|
||||||
implementation: S3DriverContract;
|
|
||||||
};
|
|
||||||
// gcs: {
|
|
||||||
// config: GcsDriverConfig
|
|
||||||
// implementation: GcsDriverContract
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,6 @@
|
|||||||
/**
|
import { InferListFromConfig } from '@adonisjs/core/build/config';
|
||||||
* Contract source: https://git.io/Jfefs
|
import hashConfig from '../config/hash';
|
||||||
*
|
|
||||||
* 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 {
|
interface HashersList extends InferListFromConfig<typeof hashConfig> {}
|
||||||
bcrypt: {
|
|
||||||
config: BcryptConfig;
|
|
||||||
implementation: BcryptContract;
|
|
||||||
};
|
|
||||||
argon: {
|
|
||||||
config: ArgonConfig;
|
|
||||||
implementation: ArgonContract;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,6 @@
|
|||||||
/**
|
import { InferMailersFromConfig } from '@adonisjs/mail/build/config';
|
||||||
* Contract source: https://git.io/JvgAT
|
import mailConfig from '../config/mail';
|
||||||
*
|
|
||||||
* 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' {
|
||||||
import { MailDrivers } from '@ioc:Adonis/Addons/Mail';
|
interface MailersList extends InferMailersFromConfig<typeof mailConfig> {}
|
||||||
|
|
||||||
interface MailersList {
|
|
||||||
smtp: MailDrivers['smtp'];
|
|
||||||
mailgun: MailDrivers['mailgun'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Factory from '@ioc:Adonis/Lucid/Factory';
|
import Factory, { FactoryContextContract } 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: Faker.FakerStatic, loginPrefix?: string) => {
|
const makeFakeUser = (faker: FactoryContextContract['faker'], loginPrefix?: string) => {
|
||||||
return {
|
return {
|
||||||
first_name: faker.name.firstName(),
|
first_name: faker.name.firstName(),
|
||||||
last_name: faker.name.lastName(),
|
last_name: faker.name.lastName(),
|
||||||
|
|||||||
@ -11,6 +11,7 @@ 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,13 @@ export default class CourseSeeder extends BaseSeeder {
|
|||||||
public async run() {
|
public async run() {
|
||||||
this.CourseFactory = CourseFactory;
|
this.CourseFactory = CourseFactory;
|
||||||
|
|
||||||
await this.CourseFactory.with('lessons', 10, lessonFactory => lessonFactory.with('content'))
|
const LESSONS_COUNT = 10;
|
||||||
|
|
||||||
|
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,23 +11,48 @@ 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 userAdmin = await User.create({
|
const administrator = 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 userAdmin.related('roles').attach([roles.admin.id]);
|
await administrator.related('roles').attach([roles.admin.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Students
|
* Teacher
|
||||||
|
*/
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
4
env.ts
4
env.ts
@ -15,11 +15,10 @@
|
|||||||
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', 'testing'] as const),
|
NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
|
||||||
DRIVE_DISK: Env.schema.enum(['local', 's3'] as const),
|
DRIVE_DISK: Env.schema.enum(['local', 's3'] 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(),
|
||||||
@ -30,6 +29,7 @@ export default Env.rules({
|
|||||||
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(),
|
||||||
|
|||||||
16137
package-lock.json
generated
16137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.2.1",
|
"version": "1.4.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ace build --production",
|
"build": "node ace build --production",
|
||||||
@ -15,43 +15,44 @@
|
|||||||
"db:seed:force": "node ace seed --force"
|
"db:seed:force": "node ace seed --force"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^5.3.7",
|
"@adonisjs/assembler": "^5.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^5.27.0",
|
||||||
"adonis-preset-ts": "^2.1.0",
|
"adonis-preset-ts": "^2.1.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.17.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "^12.3.1",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.24.2",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"pino-pretty": "^5.1.3",
|
"pino-pretty": "^8.0.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.6.2",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.7.3",
|
||||||
"youch": "^2.2.2",
|
"youch": "^3.2.0",
|
||||||
"youch-terminal": "^1.1.1"
|
"youch-terminal": "^2.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/attachment-lite": "^1.0.1",
|
"@adonisjs/application": "^5.2.5",
|
||||||
"@adonisjs/auth": "^8.0.10",
|
"@adonisjs/attachment-lite": "^1.0.7",
|
||||||
"@adonisjs/bouncer": "^2.2.5",
|
"@adonisjs/auth": "^8.2.1",
|
||||||
"@adonisjs/core": "^5.4.2",
|
"@adonisjs/bouncer": "^2.3.0",
|
||||||
"@adonisjs/drive": "^2.0.7",
|
"@adonisjs/core": "^5.8.3",
|
||||||
"@adonisjs/drive-s3": "^1.0.5",
|
"@adonisjs/drive": "^2.2.0",
|
||||||
"@adonisjs/lucid": "^16.3.2",
|
"@adonisjs/drive-s3": "^1.3.1",
|
||||||
"@adonisjs/mail": "^7.2.4",
|
"@adonisjs/lucid": "^18.0.1",
|
||||||
"@adonisjs/redis": "^7.1.0",
|
"@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",
|
"@types/socket.io": "^3.0.2",
|
||||||
"cloudinary": "^1.26.3",
|
"cloudinary": "^1.26.3",
|
||||||
"luxon": "^1.28.0",
|
"luxon": "^2.4.0",
|
||||||
"nanoid": "^3.1.25",
|
"nanoid": "^3.3.4",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.3",
|
||||||
"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.4.1",
|
"socket.io": "^4.5.1",
|
||||||
"source-map-support": "^0.5.19"
|
"source-map-support": "^0.5.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,5 +21,3 @@ const server = new Ignitor(__dirname).httpServer();
|
|||||||
server.start().catch(error => {
|
server.start().catch(error => {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', () => server.kill(10));
|
|
||||||
|
|||||||
@ -6,11 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
import Lesson from 'App/Models/Lesson';
|
|
||||||
import Role from 'App/Models/Role';
|
|
||||||
import User from 'App/Models/User';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -34,22 +29,7 @@ import User from 'App/Models/User';
|
|||||||
| NOTE: Always export the "actions" const from this file
|
| NOTE: Always export the "actions" const from this file
|
||||||
|****************************************************************
|
|****************************************************************
|
||||||
*/
|
*/
|
||||||
export const { actions } = Bouncer.define('manageUserRole', async (user: User, role: Role) => {
|
export const { actions } = Bouncer;
|
||||||
await user.load('roles');
|
|
||||||
return !(
|
|
||||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
|
||||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
|
||||||
);
|
|
||||||
}).define('viewLessonContent', async (user: User, lesson: Lesson) => {
|
|
||||||
await user.load(loader => loader.load('roles').load('courses'));
|
|
||||||
await lesson.load('course');
|
|
||||||
|
|
||||||
if (!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER])) {
|
|
||||||
return !!user.courses.find(course => course.id === lesson.course.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -74,4 +54,7 @@ export const { actions } = Bouncer.define('manageUserRole', async (user: User, r
|
|||||||
| NOTE: Always export the "policies" const from this file
|
| NOTE: Always export the "policies" const from this file
|
||||||
|****************************************************************
|
|****************************************************************
|
||||||
*/
|
*/
|
||||||
export const { policies } = Bouncer.registerPolicies({});
|
export const { policies } = Bouncer.registerPolicies({
|
||||||
|
LessonPolicy: () => import('App/Policies/LessonPolicy'),
|
||||||
|
RolePolicy: () => import('App/Policies/RolePolicy'),
|
||||||
|
});
|
||||||
|
|||||||
@ -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('api/v1/auth');
|
}).prefix('v1/auth');
|
||||||
|
|||||||
@ -7,5 +7,5 @@ Route.group(() => {
|
|||||||
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');
|
Route.patch('/:id', 'Api/v1/CategoriesController.update').middleware('role:admin,teacher').as('categories.update');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/categories')
|
.prefix('v1/categories')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -5,5 +5,5 @@ Route.group(() => {
|
|||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('chat.get-conversations');
|
.as('chat.get-conversations');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/chat')
|
.prefix('v1/chat')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -62,5 +62,5 @@ Route.group(() => {
|
|||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('courses.get-lessons-progress');
|
.as('courses.get-lessons-progress');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/courses')
|
.prefix('v1/courses')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -9,5 +9,5 @@ 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('api/v1/health')
|
.prefix('v1/health')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -6,5 +6,5 @@ Route.group(() => {
|
|||||||
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('api/v1/images')
|
.prefix('v1/images')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -19,5 +19,5 @@ Route.group(() => {
|
|||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('lessons.get-video-progress');
|
.as('lessons.get-video-progress');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/lessons')
|
.prefix('v1/lessons')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -19,5 +19,5 @@ Route.group(() => {
|
|||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
.as('me.update-contacts');
|
.as('me.update-contacts');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/me')
|
.prefix('v1/me')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -3,5 +3,5 @@ import Route from '@ioc:Adonis/Core/Route';
|
|||||||
Route.group(() => {
|
Route.group(() => {
|
||||||
Route.get('/', 'Api/v1/StatController.index').middleware('role:admin,teacher,student').as('stat.index');
|
Route.get('/', 'Api/v1/StatController.index').middleware('role:admin,teacher,student').as('stat.index');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/stat')
|
.prefix('v1/stat')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -7,11 +7,11 @@ Route.group(() => {
|
|||||||
Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update');
|
Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update');
|
||||||
Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete');
|
Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete');
|
||||||
Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles')
|
Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles')
|
||||||
.middleware('role:admin,teacher')
|
.middleware('role:admin')
|
||||||
.as('users.attach-role');
|
.as('users.attach-role');
|
||||||
Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles')
|
Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles')
|
||||||
.middleware('role:admin')
|
.middleware('role:admin')
|
||||||
.as('users.detach-role');
|
.as('users.detach-role');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/users')
|
.prefix('v1/users')
|
||||||
.middleware('auth');
|
.middleware('auth');
|
||||||
|
|||||||
@ -1,30 +1,17 @@
|
|||||||
{
|
{
|
||||||
"extends": "./node_modules/adonis-preset-ts/tsconfig",
|
"extends": "./node_modules/adonis-preset-ts/tsconfig",
|
||||||
"include": [
|
"include": ["**/*"],
|
||||||
"**/*"
|
"exclude": ["node_modules", "build"],
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"build"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"App/*": [
|
"App/*": ["./app/*"],
|
||||||
"./app/*"
|
"Config/*": ["./config/*"],
|
||||||
],
|
"Contracts/*": ["./contracts/*"],
|
||||||
"Config/*": [
|
"Database/*": ["./database/*"]
|
||||||
"./config/*"
|
|
||||||
],
|
|
||||||
"Contracts/*": [
|
|
||||||
"./contracts/*"
|
|
||||||
],
|
|
||||||
"Database/*": [
|
|
||||||
"./database/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"types": [
|
"types": [
|
||||||
"@adonisjs/core",
|
"@adonisjs/core",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user