From 4edeb880a62e5b0bb709de8a7933dfe776432c4f Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Tue, 31 Dec 2024 10:16:45 +0700 Subject: [PATCH] create: new folder --- .adonisrc.json | 45 +++ .editorconfig | 14 + .env.example | 21 + .gitignore | 8 + README.md | 35 ++ ace | 16 + ace-manifest.json | 332 +++++++++++++++ app/Base/Repositories/BaseRepository.ts | 381 ++++++++++++++++++ app/Base/Services/BaseService.ts | 127 ++++++ app/Base/Services/ParseParamService.ts | 215 ++++++++++ app/Base/Services/ParseUrlService.ts | 72 ++++ app/Controllers/Http/Auth/AuthController.ts | 83 ++++ .../Http/User/AccountController.ts | 96 +++++ app/Controllers/Http/User/RoleController.ts | 90 +++++ app/Exceptions/DefaultException.ts | 16 + app/Exceptions/Handler.ts | 45 +++ app/Middleware/Auth.ts | 76 ++++ app/Middleware/SilentAuth.ts | 21 + app/Models/User/Account.ts | 62 +++ app/Models/User/Role.ts | 23 ++ app/Repositories/User/AccountRepository.ts | 17 + app/Repositories/User/RoleRepository.ts | 9 + app/Services/Auth/AuthService.ts | 113 ++++++ app/Services/User/AccountService.ts | 41 ++ app/Services/User/RoleService.ts | 9 + app/Validators/User/CreateAccountValidator.ts | 37 ++ app/Validators/User/CreateRoleValidator.ts | 18 + app/Validators/User/UpdateAccountValidator.ts | 37 ++ app/Validators/User/UpdateRoleValidator.ts | 18 + commands/MakeModule.ts | 270 +++++++++++++ commands/index.ts | 19 + config/ally.ts | 34 ++ config/app.ts | 217 ++++++++++ config/auth.ts | 109 +++++ config/bodyparser.ts | 211 ++++++++++ config/cors.ts | 134 ++++++ config/database.ts | 55 +++ config/hash.ts | 75 ++++ config/mail.ts | 59 +++ config/session.ts | 118 ++++++ config/shield.ts | 238 +++++++++++ config/static.ts | 64 +++ contracts/ally.ts | 15 + contracts/auth.ts | 72 ++++ contracts/env.ts | 24 ++ contracts/events.ts | 30 ++ contracts/hash.ts | 19 + contracts/mail.ts | 14 + contracts/request.ts | 5 + contracts/response.ts | 6 + database/factories/index.ts | 1 + .../support_function/alpha_requirement.sql | 31 ++ .../support_function/grant_access.sql | 7 + .../user-schema/ddl_table_user_schema.sql | 114 ++++++ env.ts | 31 ++ package.json | 40 ++ providers/AppProvider.ts | 94 +++++ public/favicon.ico | Bin 0 -> 15406 bytes resources/views/emails/restore_password.edge | 3 + resources/views/errors/not-found.edge | 1 + resources/views/errors/server-error.edge | 1 + resources/views/errors/unauthorized.edge | 1 + resources/views/index.edge | 110 +++++ resources/views/welcome.edge | 110 +++++ server.ts | 21 + start/kernel.ts | 44 ++ start/routes.ts | 59 +++ start/routes/auth/auth.ts | 8 + start/routes/user/roles.ts | 6 + start/routes/user/users.ts | 6 + tes.txt | 1 - tsconfig.json | 40 ++ 72 files changed, 4593 insertions(+), 1 deletion(-) create mode 100644 .adonisrc.json create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ace create mode 100644 ace-manifest.json create mode 100644 app/Base/Repositories/BaseRepository.ts create mode 100644 app/Base/Services/BaseService.ts create mode 100644 app/Base/Services/ParseParamService.ts create mode 100644 app/Base/Services/ParseUrlService.ts create mode 100644 app/Controllers/Http/Auth/AuthController.ts create mode 100644 app/Controllers/Http/User/AccountController.ts create mode 100644 app/Controllers/Http/User/RoleController.ts create mode 100644 app/Exceptions/DefaultException.ts create mode 100644 app/Exceptions/Handler.ts create mode 100644 app/Middleware/Auth.ts create mode 100644 app/Middleware/SilentAuth.ts create mode 100644 app/Models/User/Account.ts create mode 100644 app/Models/User/Role.ts create mode 100644 app/Repositories/User/AccountRepository.ts create mode 100644 app/Repositories/User/RoleRepository.ts create mode 100644 app/Services/Auth/AuthService.ts create mode 100644 app/Services/User/AccountService.ts create mode 100644 app/Services/User/RoleService.ts create mode 100644 app/Validators/User/CreateAccountValidator.ts create mode 100644 app/Validators/User/CreateRoleValidator.ts create mode 100644 app/Validators/User/UpdateAccountValidator.ts create mode 100644 app/Validators/User/UpdateRoleValidator.ts create mode 100644 commands/MakeModule.ts create mode 100644 commands/index.ts create mode 100644 config/ally.ts create mode 100644 config/app.ts create mode 100644 config/auth.ts create mode 100644 config/bodyparser.ts create mode 100644 config/cors.ts create mode 100644 config/database.ts create mode 100644 config/hash.ts create mode 100644 config/mail.ts create mode 100644 config/session.ts create mode 100644 config/shield.ts create mode 100644 config/static.ts create mode 100644 contracts/ally.ts create mode 100644 contracts/auth.ts create mode 100644 contracts/env.ts create mode 100644 contracts/events.ts create mode 100644 contracts/hash.ts create mode 100644 contracts/mail.ts create mode 100644 contracts/request.ts create mode 100644 contracts/response.ts create mode 100644 database/factories/index.ts create mode 100644 database/sql-only/support_function/alpha_requirement.sql create mode 100644 database/sql-only/support_function/grant_access.sql create mode 100644 database/sql-only/user-schema/ddl_table_user_schema.sql create mode 100644 env.ts create mode 100644 package.json create mode 100644 providers/AppProvider.ts create mode 100644 public/favicon.ico create mode 100644 resources/views/emails/restore_password.edge create mode 100644 resources/views/errors/not-found.edge create mode 100644 resources/views/errors/server-error.edge create mode 100644 resources/views/errors/unauthorized.edge create mode 100644 resources/views/index.edge create mode 100644 resources/views/welcome.edge create mode 100644 server.ts create mode 100644 start/kernel.ts create mode 100644 start/routes.ts create mode 100644 start/routes/auth/auth.ts create mode 100644 start/routes/user/roles.ts create mode 100644 start/routes/user/users.ts delete mode 100644 tes.txt create mode 100644 tsconfig.json diff --git a/.adonisrc.json b/.adonisrc.json new file mode 100644 index 0000000..f258666 --- /dev/null +++ b/.adonisrc.json @@ -0,0 +1,45 @@ +{ + "typescript": true, + "commands": [ + "./commands", + "@adonisjs/core/commands", + "@adonisjs/repl/build/commands", + "@adonisjs/lucid/build/commands", + "@adonisjs/mail/build/commands" + ], + "exceptionHandlerNamespace": "App/Exceptions/Handler", + "aliases": { + "App": "app", + "Config": "config", + "Database": "database", + "Contracts": "contracts" + }, + "preloads": [ + "./start/routes", + "./start/kernel" + ], + "providers": [ + "./providers/AppProvider", + "@adonisjs/core", + "@adonisjs/session", + "@adonisjs/view", + "@adonisjs/shield", + "@adonisjs/lucid", + "@adonisjs/auth", + "@adonisjs/mail", + "@adonisjs/ally" + ], + "metaFiles": [ + { + "pattern": "public/**", + "reloadServer": false + }, + { + "pattern": "resources/views/**/*.edge", + "reloadServer": false + } + ], + "aceProviders": [ + "@adonisjs/repl" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9c61e39 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +insert_final_newline = ignore + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..43c3946 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +PORT=3333 +HOST=0.0.0.0 +NODE_ENV=development +APP_KEY=ltoWFqOgfC6B8eVs6Kj2YLxyyXCvwgcu +SESSION_DRIVER=cookie +CACHE_VIEWS=false + +DB_CONNECTION=pg +PG_HOST=ec2-54-179-80-119.ap-southeast-1.compute.amazonaws.com +PG_PORT=5432 +PG_USER=roadreportpgdb +PG_PASSWORD=roadreportpgdb1qaz +PG_DB_NAME=pg_roadreport_dev + +SMTP_HOST=localhost +SMTP_PORT=587 +SMTP_USERNAME= +SMTP_PASSWORD= + +GOOGLE_CLIENT_ID=clientId +GOOGLE_CLIENT_SECRET=clientSecret diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25f8b2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +build +coverage +.vscode +.DS_STORE +.env +tmp +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2014bd3 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# AdonisJS v5 Boilerplate + +Using repository and service pattern + +## Setup + +1. Clone this repository `git clone git@gitlab.com:profile-image/boilerplate/adonisjs-v5-boilerplate.git` +2. Copy file `.env.example` to `.env` +3. Create database +4. Run `boilerplate.sql` in folder `database/sql` +5. Change database name and connection in `.env` +6. Run command `npm install` to install dependencies +7. Run server with `npm run dev` + +## Creating Module + +Create model, repository, service, controller, validators and route using below command: + +```bash +node ace make:module --endpoint --soft-delete --uuid +``` + +Example: I will make module for user table with soft delete + +```bash +node ace make:module User User --endpoint users --soft-delete +``` + +Notes: + +1. Namespace is required and using CamelCase. +2. ModelName is required and using CamelCase. +3. EndpointName is required and using lowercase. If endpoint have more than one word, separate them with `-`. +4. --soft-delete is optional. Use only when your table has `deleted_at` column. +5. --uuid is optional. Use only when your primaryKey using uuid. diff --git a/ace b/ace new file mode 100644 index 0000000..c175031 --- /dev/null +++ b/ace @@ -0,0 +1,16 @@ +/* +|-------------------------------------------------------------------------- +| Ace Commands +|-------------------------------------------------------------------------- +| +| This file is the entry point for running ace commands. +| +*/ + +require('reflect-metadata') +require('source-map-support').install({ handleUncaughtExceptions: false }) + +const { Ignitor } = require('@adonisjs/core/build/standalone') +new Ignitor(__dirname) + .ace() + .handle(process.argv.slice(2)) diff --git a/ace-manifest.json b/ace-manifest.json new file mode 100644 index 0000000..bde7f2d --- /dev/null +++ b/ace-manifest.json @@ -0,0 +1,332 @@ +{ + "commands": { + "make:module": { + "settings": { + "loadApp": false, + "stayAlive": false + }, + "commandPath": "./commands/MakeModule", + "commandName": "make:module", + "description": "Make a new module", + "args": [ + { + "type": "string", + "propertyName": "domain", + "name": "domain", + "required": true, + "description": "Domain name" + }, + { + "type": "string", + "propertyName": "model", + "name": "model", + "required": true, + "description": "Model name" + } + ], + "aliases": [], + "flags": [ + { + "name": "soft-delete", + "propertyName": "softDelete", + "type": "boolean", + "description": "Enable soft delete" + }, + { + "name": "enable-uuid", + "propertyName": "enableUUID", + "type": "boolean", + "alias": "uuid", + "description": "Enable soft delete" + }, + { + "name": "endpoint", + "propertyName": "endpoint", + "type": "string", + "alias": "e", + "description": "Set endpoint name" + } + ] + }, + "dump:rcfile": { + "settings": {}, + "commandPath": "@adonisjs/core/commands/DumpRc", + "commandName": "dump:rcfile", + "description": "Dump contents of .adonisrc.json file along with defaults", + "args": [], + "aliases": [], + "flags": [] + }, + "list:routes": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/core/commands/ListRoutes", + "commandName": "list:routes", + "description": "List application routes", + "args": [], + "aliases": [], + "flags": [ + { + "name": "json", + "propertyName": "json", + "type": "boolean", + "description": "Output as JSON" + } + ] + }, + "generate:key": { + "settings": {}, + "commandPath": "@adonisjs/core/commands/GenerateKey", + "commandName": "generate:key", + "description": "Generate a new APP_KEY secret", + "args": [], + "aliases": [], + "flags": [] + }, + "repl": { + "settings": { + "loadApp": true, + "environment": "repl", + "stayAlive": true + }, + "commandPath": "@adonisjs/repl/build/commands/AdonisRepl", + "commandName": "repl", + "description": "Start a new REPL session", + "args": [], + "aliases": [], + "flags": [] + }, + "db:seed": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/lucid/build/commands/DbSeed", + "commandName": "db:seed", + "description": "Execute database seeder files", + "args": [], + "aliases": [], + "flags": [ + { + "name": "connection", + "propertyName": "connection", + "type": "string", + "description": "Define a custom database connection for the seeders", + "alias": "c" + }, + { + "name": "interactive", + "propertyName": "interactive", + "type": "boolean", + "description": "Run seeders in interactive mode", + "alias": "i" + }, + { + "name": "files", + "propertyName": "files", + "type": "array", + "description": "Define a custom set of seeders files names to run", + "alias": "f" + } + ] + }, + "make:model": { + "settings": {}, + "commandPath": "@adonisjs/lucid/build/commands/MakeModel", + "commandName": "make:model", + "description": "Make a new Lucid model", + "args": [ + { + "type": "string", + "propertyName": "name", + "name": "name", + "required": true, + "description": "Name of the model class" + } + ], + "aliases": [], + "flags": [ + { + "name": "migration", + "propertyName": "migration", + "type": "boolean", + "alias": "m", + "description": "Generate the migration for the model" + }, + { + "name": "controller", + "propertyName": "controller", + "type": "boolean", + "alias": "c", + "description": "Generate the controller for the model" + } + ] + }, + "make:migration": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/lucid/build/commands/MakeMigration", + "commandName": "make:migration", + "description": "Make a new migration file", + "args": [ + { + "type": "string", + "propertyName": "name", + "name": "name", + "required": true, + "description": "Name of the migration file" + } + ], + "aliases": [], + "flags": [ + { + "name": "connection", + "propertyName": "connection", + "type": "string", + "description": "The connection flag is used to lookup the directory for the migration file" + }, + { + "name": "folder", + "propertyName": "folder", + "type": "string", + "description": "Pre-select a migration directory" + }, + { + "name": "create", + "propertyName": "create", + "type": "string", + "description": "Define the table name for creating a new table" + }, + { + "name": "table", + "propertyName": "table", + "type": "string", + "description": "Define the table name for altering an existing table" + } + ] + }, + "make:seeder": { + "settings": {}, + "commandPath": "@adonisjs/lucid/build/commands/MakeSeeder", + "commandName": "make:seeder", + "description": "Make a new Seeder file", + "args": [ + { + "type": "string", + "propertyName": "name", + "name": "name", + "required": true, + "description": "Name of the seeder class" + } + ], + "aliases": [], + "flags": [] + }, + "migration:run": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/lucid/build/commands/Migration/Run", + "commandName": "migration:run", + "description": "Run pending 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 to run migrations in production" + }, + { + "name": "dry-run", + "propertyName": "dryRun", + "type": "boolean", + "description": "Print SQL queries, instead of running the migrations" + } + ] + }, + "migration:rollback": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback", + "commandName": "migration:rollback", + "description": "Rollback migrations to a given batch number", + "args": [], + "aliases": [], + "flags": [ + { + "name": "connection", + "propertyName": "connection", + "type": "string", + "description": "Define a custom database connection", + "alias": "c" + }, + { + "name": "force", + "propertyName": "force", + "type": "boolean", + "description": "Explictly force to run migrations in production" + }, + { + "name": "dry-run", + "propertyName": "dryRun", + "type": "boolean", + "description": "Print SQL queries, instead of running the migrations" + }, + { + "name": "batch", + "propertyName": "batch", + "type": "number", + "description": "Define custom batch number for rollback. Use 0 to rollback to initial state" + } + ] + }, + "migration:status": { + "settings": { + "loadApp": true + }, + "commandPath": "@adonisjs/lucid/build/commands/Migration/Status", + "commandName": "migration:status", + "description": "Check migrations current status.", + "args": [], + "aliases": [], + "flags": [ + { + "name": "connection", + "propertyName": "connection", + "type": "string", + "description": "Define a custom database connection", + "alias": "c" + } + ] + }, + "make:mailer": { + "settings": {}, + "commandPath": "@adonisjs/mail/build/commands/MakeMailer", + "commandName": "make:mailer", + "description": "Make a new mailer class", + "args": [ + { + "type": "string", + "propertyName": "name", + "name": "name", + "required": true, + "description": "Name of the mailer class" + } + ], + "aliases": [], + "flags": [] + } + }, + "aliases": {} +} diff --git a/app/Base/Repositories/BaseRepository.ts b/app/Base/Repositories/BaseRepository.ts new file mode 100644 index 0000000..8022731 --- /dev/null +++ b/app/Base/Repositories/BaseRepository.ts @@ -0,0 +1,381 @@ +import { DateTime } from "luxon" + +export default class BaseRepository { + protected model: any + protected mainModel: any + protected isSoftDelete: boolean + protected RELATIONS: string[] + protected RELATION_OPTIONS: any + + constructor(model: any) { + this.model = model + this.mainModel = model + this.isSoftDelete = model.softDelete + } + + async getAll(pagination: any, sort: any, whereClauses: any, fields: any, search: any) { + try { + this.model = this.mainModel + this.model = this.model.query() + this.model = this.parseSelectedFields(this.model, fields) + this.model = this.parseWhere(this.model, whereClauses) + this.model = this.parseSearch(this.model, whereClauses, search) + this.model = this.parseRelation(this.model) + this.model = this.parseSort(this.model, sort) + if (pagination.page && pagination.limit) { + if (this.isSoftDelete) { + return await this.model.whereNull('deleted_at').paginate(pagination.page, pagination.limit) + } + return await this.model.paginate(pagination.page, pagination.limit) + } else { + if (this.isSoftDelete) { + return await this.model.whereNull('deleted_at') + } + return await this.model + } + } catch (error) { + throw error + } + } + + async get(data: any = {}) { + try { + this.model = this.mainModel + this.model = this.model.query() + if (this.isSoftDelete) { + this.model = this.model.whereNull('deleted_at') + } + if (data.sort) { + this.model = this.parseSort(this.model, data.sort) + } + return await this.model + } catch (error) { + throw error + } + } + + async store(data: any) { + try { + this.model = this.mainModel + return await this.model.create(data) + } catch (error) { + throw error + } + } + + async multiInsert(data: any[]) { + try { + this.model = this.mainModel + return await this.model.createMany(data) + } catch (error) { + throw error + } + } + + async show(id: any, fields: any) { + try { + this.model = this.mainModel + this.model = this.model.query().where(this.model.primaryKey, id) + this.model = this.parseSelectedFields(this.model, fields) + this.model = this.parseRelation(this.model) + if (this.isSoftDelete) { + this.model = this.model.whereNull('deleted_at') + } + return await this.model.first() + } catch (error) { + throw error + } + } + + async update(id: any, data: any) { + try { + this.model = this.mainModel + if (! await this.model.find(id)) { + return null + } + data.updated_at = DateTime.now() + if (Object.keys(data).length) { + await this.model.query().where(this.model.primaryKey, id).update(data) + } + return await this.model.find(id) + } catch (error) { + throw error + } + } + + async delete(id: any) { + try { + this.model = this.mainModel + const result = await this.model.find(id) + if (this.isSoftDelete) { + await this.model.query().where(this.model.primaryKey, id).update({ deleted_at: DateTime.local() }) + } else { + await this.model.query().where(this.model.primaryKey, id).delete() + } + return result + } catch (error) { + throw error + } + } + + async deleteAll() { + try { + this.model = this.mainModel + if (this.isSoftDelete) { + return await this.model.query().whereNull('deleted_at').update({ deleted_at: DateTime.local() }) + } else { + return await this.model.query().delete() + } + } catch (error) { + throw error + } + } + + async first() { + try { + this.model = this.mainModel + this.model = this.model.query() + if (this.isSoftDelete) { + this.model = this.model.whereNull('deleted_at') + } + return await this.model.first() + } catch (error) { + throw error + } + } + + async find(id: any) { + try { + this.model = this.mainModel + this.model = this.model.query().where(this.model.primaryKey, id) + if (this.isSoftDelete) { + this.model = this.model.whereNull('deleted_at') + } + return await this.model.first() + } catch (error) { + throw error + } + } + + setRelation(relation: string[]) { + this.RELATIONS = relation + } + + setRelationOptions(relationOptions: any) { + this.RELATION_OPTIONS = relationOptions + } + + parseSelectedFields(model: any, fields: any) { + if (fields) { + model = model.select(fields) + } + return model + } + + parseWhere(model: any, whereClauses: any) { + if (whereClauses.data) { + if (whereClauses.operation == 'and') { + whereClauses.data.forEach((whereClause: any) => { + if (whereClause.operator == 'between') { + model = model.whereBetween(whereClause.attribute, whereClause.value) + } else { + if (whereClause.value == 'null') { + model = model.whereNull(whereClause.attribute) + } else { + if (whereClause.attribute.includes('.')) { + const attr = whereClause.attribute.split('.') + model = model.whereHas(attr[0], (builder: any) => { + builder.where(attr[1], whereClause.operator, whereClause.value) + }) + } else { + model = model.where(whereClause.attribute, whereClause.operator, whereClause.value) + } + } + } + }); + } else { + whereClauses.data.forEach((whereClause: any, index: number) => { + if (whereClause.operator == 'between') { + model = model.whereBetween(whereClause.attribute, whereClause.value) + } else { + if (index == 0) { + if (whereClause.value == 'null') { + model = model.whereNull(whereClause.attribute) + } else { + if (whereClause.attribute.includes('.')) { + const attr = whereClause.attribute.split('.') + model = model.whereHas(attr[0], (builder: any) => { + builder.where(attr[1], whereClause.operator, whereClause.value) + }) + } else { + model = model.where(whereClause.attribute, whereClause.operator, whereClause.value) + } + } + } else { + if (whereClause.value == 'null') { + model = model.orWhereNull(whereClause.attribute) + } else { + if (whereClause.attribute.includes('.')) { + const attr = whereClause.attribute.split('.') + model = model.orWhereHas(attr[0], (builder: any) => { + builder.where(attr[1], whereClause.operator, whereClause.value) + }) + } else { + model = model.orWhere(whereClause.attribute, whereClause.operator, whereClause.value) + } + } + } + } + }); + } + } + return model + } + + parseRelation(model: any) { + if (this.RELATIONS) { + this.RELATIONS.forEach((relation) => { + if (relation.split('.').length > 1) { + const firstRelation = relation.substr(0, relation.indexOf('.')) + model = model.preload(firstRelation, (query) => { + if (this.RELATION_OPTIONS) { + let relationOption = this.RELATION_OPTIONS.find((item: any) => { return item.relation == firstRelation }) + this.parseRelationOption(query, relationOption) + } + this.parseNestedRelation(query, relation.substr(relation.indexOf('.') + 1), firstRelation) + }) + } else { + model = model.preload(relation, (query) => { + if (this.RELATION_OPTIONS) { + let relationOption = this.RELATION_OPTIONS.find((item: any) => { return item.relation == relation }) + this.parseRelationOption(query, relationOption) + } + }) + } + }) + } + return model + } + + parseNestedRelation(query: any, relation: string, firstRelation: string) { + let relations = this.RELATIONS.filter(d => { return typeof d == 'string' }) + relations = relations.filter(d => { return d.includes(firstRelation + '.') }) + if (relations.length > 1) { + relations.map(data => { + this.parseNestedRelation(query, data.substr(data.indexOf('.') + 1), relation.substr(0, data.indexOf('.'))) + }) + } else { + if (relation.indexOf('.') > 0) { + let subRelation = relation.substr(0, relation.indexOf('.')) + query.preload(subRelation, (subQuery) => { + if (this.RELATION_OPTIONS) { + let relationOption = this.RELATION_OPTIONS.find((item: any) => { return item.relation == subRelation }) + this.parseRelationOption(subQuery, relationOption) + } + this.parseNestedRelation(subQuery, relation.substr(relation.indexOf('.') + 1), subRelation) + }) + } else { + query.preload(relation, (subQuery) => { + if (this.RELATION_OPTIONS) { + let relationOption = this.RELATION_OPTIONS.find((item: any) => { return item.relation == relation }) + this.parseRelationOption(subQuery, relationOption) + } + }) + } + } + } + + parseRelationOption(query: any, relationOption: any) { + if (relationOption) { + if (relationOption.fields) { + query = query.select(relationOption.fields) + } + if (relationOption.sort) { + query = query.orderBy(relationOption.sort, relationOption.order) + } + if (relationOption.filter) { + query = this.parseWhere(query, relationOption.filter) + } + if (relationOption.limit) { + query = query.limit(relationOption.limit) + } + if (relationOption.search) { + query = this.parseSearch(query, relationOption.filter, relationOption.search) + } + } + return query + } + + parseSort(model: any, sort: any[]) { + if (sort) { + sort.forEach((sort: any) => { + model = model.orderBy(sort.attribute, sort.order) + }); + } + return model + } + + parseSearch(model: any, whereClauses: any, search: any) { + if (search) { + const data = search.data + const attributes = search.attributes + const operator = search.operator + if (attributes) { + model = model.where((query) => { + if (whereClauses.data.length > 0) { + attributes.forEach((attribute: string) => { + if (attribute.includes('.')) { + const attr = attribute.split('.') + const field = attr[attr.length - 1] + const relations = attr.slice(0, attr.length - 1) + query.whereHas(relations[0], (query: any) => { + this.parseNestedSearch(query, relations.slice(1), field, data, operator) + }) + } else { + query.orWhere(attribute, operator, data) + } + }); + } else { + attributes.forEach((attribute: any, index: number) => { + if (index == 0) { + if (attribute.includes('.')) { + const attr = attribute.split('.') + const field = attr[attr.length - 1] + const relations = attr.slice(0, attr.length - 1) + query.whereHas(relations[0], (query: any) => { + this.parseNestedSearch(query, relations.slice(1), field, data, operator) + }) + } else { + query.where(attribute, operator, data) + } + } else { + if (attribute.includes('.')) { + const attr = attribute.split('.') + const field = attr[attr.length - 1] + const relations = attr.slice(0, attr.length - 1) + query.orWhereHas(relations[0], (query: any) => { + this.parseNestedSearch(query, relations.slice(1), field, data, operator) + }) + } else { + query.orWhere(attribute, operator, data) + } + } + }); + } + }) + } + } + return model + } + + parseNestedSearch(model: any, relations: any, field: any, value: any, operator: any = 'ilike') { + if (relations.length > 0) { + model = model.whereHas(relations[0], (query: any) => { + this.parseNestedSearch(query, relations.slice(1), field, value) + }) + } else { + model = model.where(field, operator, value) + } + return model + } +} diff --git a/app/Base/Services/BaseService.ts b/app/Base/Services/BaseService.ts new file mode 100644 index 0000000..5a55d1d --- /dev/null +++ b/app/Base/Services/BaseService.ts @@ -0,0 +1,127 @@ +export default class BaseService { + repository: any + + constructor(repository: any) { + this.repository = repository + } + + async getAll(options: any) { + try { + this.repository.setRelation(options.relation) + this.repository.setRelationOptions(options.relationOptions) + const results = await this.repository.getAll(options.pagination, options.sort, options.filter, options.fields, options.search) + return results + } catch (error) { + throw error + } + } + + async store(data: any) { + try { + return await this.repository.store(data) + } catch (error) { + throw error + } + } + + async show(id: any, options: any = {}) { + try { + this.repository.setRelation(options.relation) + this.repository.setRelationOptions(options.relationOptions) + return await this.repository.show(id, options.fields) + } catch (error) { + throw error + } + } + + async find(id: any) { + try { + return await this.repository.find(id) + } catch (error) { + throw error + } + } + + async first() { + try { + return await this.repository.first() + } catch (error) { + throw error + } + } + + async update(id: any, data: any) { + try { + return await this.repository.update(id, data) + } catch (error) { + throw error + } + } + + async delete(id: any) { + try { + return await this.repository.delete(id) + } catch (error) { + throw error + } + } + + async deleteAll() { + try { + return await this.repository.deleteAll() + } catch (error) { + throw error + } + } + + async getDeletedAll(options: any) { + try { + this.repository.setRelation(options.relation) + const results = await this.repository.getDeletedAll(options.pagination, options.sort, options.filter, options.fields, options.search) + return results + } catch (error) { + throw error + } + } + + async showDeleted(id: any, options: any = {}) { + try { + this.repository.setRelation(options.relation) + return await this.repository.showDeleted(id, options.fields) + } catch (error) { + throw error + } + } + + async restore(id: any) { + try { + return await this.repository.restore(id) + } catch (error) { + throw error + } + } + + async restoreAll() { + try { + return await this.repository.restoreAll() + } catch (error) { + throw error + } + } + + async permanentDelete(id: any) { + try { + return await this.repository.permanentDelete(id) + } catch (error) { + throw error + } + } + + async permanentDeleteAll() { + try { + return await this.repository.permanentDeleteAll() + } catch (error) { + throw error + } + } +} diff --git a/app/Base/Services/ParseParamService.ts b/app/Base/Services/ParseParamService.ts new file mode 100644 index 0000000..c6d8083 --- /dev/null +++ b/app/Base/Services/ParseParamService.ts @@ -0,0 +1,215 @@ +export default class ParseParamService { + LIKE = 'like'; + EQUAL = 'eq'; + NOT_EQUAL = 'ne'; + GREATER_THAN = 'gt'; + GREATER_THAN_EQUAL = 'gte'; + LESS_THAN = 'lt'; + LESS_THAN_EQUAL = 'lte'; + BETWEEN = 'between'; + + OPERATOR_LIKE = 'ILIKE'; + OPERATOR_EQUAL = '='; + OPERATOR_NOT_EQUAL = '!='; + OPERATOR_GREATER_THAN = '>'; + OPERATOR_GREATER_THAN_EQUAL = '>='; + OPERATOR_LESS_THAN = '<'; + OPERATOR_LESS_THAN_EQUAL = '<='; + + OPERATION_DEFAULT = "and"; + + parse(data: { sort: any; fields: any; embed: any; search: any; page: any; limit: any; }) { + const paginateParams = this.parsePaginateParams(data) + const sortParams = this.parseSortParams(data.sort) + const filterParams = this.parseFilterParams(data) + const projectionParams = this.parseProjectionParams(data.fields) + const relationParams = this.parseRelationParams(data.embed) + const relationOptionParams = this.parseRelationOptionParams(data) + const searchParams = this.parseSearch(data.search) + + const results = { + pagination: paginateParams, + sort: sortParams, + filter: filterParams, + fields: projectionParams, + relation: relationParams, + search: searchParams, + relationOptions: relationOptionParams + } + + return results + } + + parsePaginateParams(data: { page: any; limit: any; }) { + return { + page: data.page ?? null, + limit: data.limit ?? null + } + } + + parseSortParams(data: string) { + if (data) { + const sorts = data.split(',') + const parsedSort: any[] = [] + + sorts.forEach((sort: string) => { + if (sort.includes('-')) { + parsedSort.push({ + order: 'desc', + attribute: sort.split('-')[1] + }) + } else { + parsedSort.push({ + order: 'asc', + attribute: sort + }) + } + }); + + return parsedSort + } + } + + parseFilterParams(data: { [x: string]: { [x: string]: any; }; operation?: any; }) { + const filters: any[] = [] + + Object.keys(data).forEach(key => { + if (key != 'page' && key != 'limit' && key != 'sort' && key != 'fields' && key != 'embed' && key != 'operation' && key != 'search' && !key.includes('.')) { + Object.keys(data[key]).forEach(k => { + if (k == 'between') { + const values = data[key][k].split(',') + if (values.length != 2) { + return + } else { + filters.push({ + attribute: key, + operator: this.BETWEEN, + value: values + }) + } + } else { + if (data[key][k].includes(',')) { + const values = data[key][k].split(',') + values.forEach((val: any) => { + filters.push(this.parseFilter(key, k, val)) + }); + } else { + filters.push(this.parseFilter(key, k, data[key][k])) + } + } + }) + } + }) + + return { + operation: data.operation || 'and', + data: filters + } + } + + parseProjectionParams(data: string) { + if (data) { + return data.split(',') + } + } + + parseRelationParams(data: string) { + if (data) { + return data.split(',') + } + } + + parseRelationOptionParams(data: { [x: string]: any; embed: any; }) { + const relationOptions: any[] = [] + if (data.embed) { + data.embed.replace(/\./g, ',').split(',').forEach((relation: string) => { + const option: any = {} + option.relation = relation + + if (data[`${relation}.fields`]) { + option.fields = data[`${relation}.fields`].split(',') + } + + if (data[`${relation}.sort`]) { + option.sort = data[`${relation}.sort`].replace('-', '') + option.order = data[`${relation}.sort`].includes('-') ? 'desc' : 'asc' + } + + const filterParams = Object.keys(data).filter((key: string) => key.split('.')[0] == relation && key != `${relation}.operation` && key != `${relation}.limit` && key != `${relation}.search` && key != `${relation}.fields` && key != `${relation}.sort`).map(key => { + const filter: any = {} + const operator: any = Object.keys(data[key])[0] + filter.attribute = key.split('.')[1] + filter.operator = this.parseOperator(operator) + filter.value = data[key][operator] + return filter + }) + + if (filterParams.length > 0) { + option.filter = { + operation: data[`${relation}.operation`] || 'and', + data: filterParams + } + } + + if (data[`${relation}.limit`]) { + option.limit = data[`${relation}.limit`] + } + + if (data[`${relation}.search`]) { + option.search = this.parseSearch(data[`${relation}.search`]) + } + + relationOptions.push(option) + }) + } + return relationOptions + } + + parseSearch(data: { [x: string]: any; }) { + if (data) { + const search: any = { + data: null, + attributes: [], + operator: this.OPERATOR_LIKE + } + + Object.keys(data).forEach(key => { + search.data = `%${data[key]}%` + search.attributes = key.split(',') + }) + + return search + } + } + + parseFilter(attribute: string, operator: string, value: string) { + if (operator == this.LIKE) { + value = `%${value}%` + } + + return { + attribute: attribute, + operator: this.parseOperator(operator), + value: value + } + } + + parseOperator(operator: any) { + switch (operator) { + case this.LIKE: + return this.OPERATOR_LIKE + case this.EQUAL: + return this.OPERATOR_EQUAL + case this.NOT_EQUAL: + return this.OPERATOR_NOT_EQUAL + case this.GREATER_THAN: + return this.OPERATOR_GREATER_THAN + case this.GREATER_THAN_EQUAL: + return this.OPERATOR_GREATER_THAN_EQUAL + case this.LESS_THAN: + return this.OPERATOR_LESS_THAN + case this.LESS_THAN_EQUAL: + return this.OPERATOR_LESS_THAN_EQUAL + } + } +} diff --git a/app/Base/Services/ParseUrlService.ts b/app/Base/Services/ParseUrlService.ts new file mode 100644 index 0000000..3af166d --- /dev/null +++ b/app/Base/Services/ParseUrlService.ts @@ -0,0 +1,72 @@ +export default class ParseUrlService { + parseUrl(request: any, currentPage: any, lastPage: any) { + const url: any = { + nextUrl: null, + prevUrl: null + } + + if (request) { + let urlToParsed: any = request.completeUrl(true) + url.nextUrl = this.parseNextUrl(urlToParsed, currentPage, lastPage) + url.prevUrl = this.parsePreviousUrl(urlToParsed, currentPage, lastPage) + } + + return url + } + + parseNextUrl(url: string, currentPage: number, lastPage: number) { + let nextUrl: any = null + + if (currentPage < lastPage && currentPage >= 1) { + if (url.includes('page=')) { + const splitedUrl = url.split('page=') + if (splitedUrl[1].includes('&')) { + const lengthToRemove = splitedUrl[1].split('&')[0].length + nextUrl = splitedUrl[0] + 'page=' + (currentPage + 1) + splitedUrl[1].substring(lengthToRemove) + } else { + nextUrl = splitedUrl[0] + 'page=' + (currentPage + 1) + } + } + } else if (currentPage < 1) { + if (url.includes('page=')) { + const splitedUrl = url.split('page=') + if (splitedUrl[1].includes('&')) { + const lengthToRemove = splitedUrl[1].split('&')[0].length + nextUrl = splitedUrl[0] + 'page=' + 1 + splitedUrl[1].substring(lengthToRemove) + } else { + nextUrl = splitedUrl[0] + 'page=' + 1 + } + } + } + + return nextUrl + } + + parsePreviousUrl(url: string, currentPage: number, lastPage: number) { + let previousUrl: any = null + + if (currentPage <= lastPage && currentPage > 1) { + if (url.includes('page=')) { + const splitedUrl = url.split('page=') + if (splitedUrl[1].includes('&')) { + const lengthToRemove = splitedUrl[1].split('&')[0].length + previousUrl = splitedUrl[0] + 'page=' + (currentPage - 1) + splitedUrl[1].substring(lengthToRemove) + } else { + previousUrl = splitedUrl[0] + 'page=' + (currentPage - 1) + } + } + } else if (currentPage > lastPage && lastPage != 0) { + if (url.includes('page=')) { + const splitedUrl = url.split('page=') + if (splitedUrl[1].includes('&')) { + const lengthToRemove = splitedUrl[1].split('&')[0].length + previousUrl = splitedUrl[0] + 'page=' + lastPage + splitedUrl[1].substring(lengthToRemove) + } else { + previousUrl = splitedUrl[0] + 'page=' + lastPage + } + } + } + + return previousUrl + } +} diff --git a/app/Controllers/Http/Auth/AuthController.ts b/app/Controllers/Http/Auth/AuthController.ts new file mode 100644 index 0000000..15ec13f --- /dev/null +++ b/app/Controllers/Http/Auth/AuthController.ts @@ -0,0 +1,83 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import AuthService from 'App/Services/Auth/AuthService' +import AccountService from 'App/Services/User/AccountService' +import Base64 from 'base-64' + +export default class AuthController { + service = new AuthService() + accountService = new AccountService() + + public async login ({ auth, request, response }: HttpContextContract) { + try { + const credentials = this.getBasicAuth(request.header('authorization')) + const rememberMe = request.body().remember_me ?? false + const token = await this.service.login(credentials, auth, rememberMe) + return response.api(token, 'OK', 200, request) + } catch (error) { + return response.error(error.message) + } + } + + public async logout ({ auth, response }: HttpContextContract) { + try { + await auth.use('api').revoke() + return response.api(null, 'Logout successful!') + } catch (error) { + return response.error(error.message) + } + } + + public async oauthRedirect ({ ally, response }) { + try { + return ally.use('google').redirect() + } catch (error) { + return response.error(error.message) + } + } + + public async oauthCallback ({ ally, auth, response }) { + try { + const google = await ally.use('google').user() + let user = await this.accountService.findByEmail(google.email) + if (!user) { + return response.error('akun tidak terdaftar') + }else{ + if (!user.google_id) { + await this.accountService.update(user.id, { + google_id: google.id, + status:true + }) + } + await user.load('role') + const authResult = await this.service.generateToken(auth, user, true) + const encodeToken = Base64.encode(JSON.stringify(authResult)) + return response.redirect().toPath(`http://localhost:4200/google-redirect?token=${encodeToken}`) + } + } catch (error) { + return response.error(error.message) + } + } + public async forgotPassword ({ request, response }) { + try { + const data = await request.all() + await this.service.forgotPassword(data.credential, request) + return response.api(null, 'Forgot password link has been sent to your email') + } catch (error) { + return response.error(error.message) + } + } + + public async restorePassword ({ request, response }) { + try { + const data = await request.all() + await this.service.restorePassword(data) + return response.api(null, 'Password restored!') + } catch (error) { + return response.error(error.message) + } + } + getBasicAuth(authHeader: any) { + const data = Base64.decode(authHeader.split(' ')[1]).split(':') + return { email: data[0], password: data[1] } + } +} diff --git a/app/Controllers/Http/User/AccountController.ts b/app/Controllers/Http/User/AccountController.ts new file mode 100644 index 0000000..932fd35 --- /dev/null +++ b/app/Controllers/Http/User/AccountController.ts @@ -0,0 +1,96 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import AccountService from 'App/Services/User/AccountService' +import CreateAccountValidator from 'App/Validators/User/CreateAccountValidator' +import UpdateAccountValidator from 'App/Validators/User/UpdateAccountValidator' +import { ValidationException } from '@ioc:Adonis/Core/Validator' + +export default class AccountController { + service = new AccountService() + FETCHED_ATTRIBUTE = [ + 'urole_id', + 'username', + 'pwd', + 'email', + 'google_id', + 'fullname', + 'avatar', + 'is_ban', + ] + + public async index ({ request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.getAll(options) + return response.api(result, 'OK', 200, request) + } catch (error) { + return response.error(error.message) + } + } + + public async store ({ request, response }: HttpContextContract) { + try { + await request.validate(CreateAccountValidator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.store(data) + return response.api(result, 'Account created!', 201) + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async show ({ params, request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.show(params.id, options) + if (!result) { + return response.api(null, `Account with id: ${params.id} not found`) + } + return response.api(result) + } catch (error) { + return response.error(error.message) + } + } + + public async update ({ params, request, response }: HttpContextContract) { + try { + await request.validate(UpdateAccountValidator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.update(params.id, data) + if (!result) { + return response.api(null, `Account with id: ${params.id} not found`) + } + return response.api(result, 'Account updated!') + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async destroy ({ params, response }: HttpContextContract) { + try { + const result = await this.service.delete(params.id) + if (!result) { + return response.api(null, `Account with id: ${params.id} not found`) + } + return response.api(null, 'Account deleted!') + } catch (error) { + return response.error(error.message) + } + } + + public async destroyAll ({ response }: HttpContextContract) { + try { + await this.service.deleteAll() + return response.api(null, 'All Account deleted!') + } catch (error) { + return response.error(error.message) + } + } +} diff --git a/app/Controllers/Http/User/RoleController.ts b/app/Controllers/Http/User/RoleController.ts new file mode 100644 index 0000000..22361da --- /dev/null +++ b/app/Controllers/Http/User/RoleController.ts @@ -0,0 +1,90 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import RoleService from 'App/Services/User/RoleService' +import CreateRoleValidator from 'App/Validators/User/CreateRoleValidator' +import UpdateRoleValidator from 'App/Validators/User/UpdateRoleValidator' +import { ValidationException } from '@ioc:Adonis/Core/Validator' + +export default class RoleController { + service = new RoleService() + FETCHED_ATTRIBUTE = [ + 'code', + 'name', + ] + + public async index ({ request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.getAll(options) + return response.api(result, 'OK', 200, request) + } catch (error) { + return response.error(error.message) + } + } + + public async store ({ request, response }: HttpContextContract) { + try { + await request.validate(CreateRoleValidator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.store(data) + return response.api(result, 'Role created!', 201) + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async show ({ params, request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.show(params.id, options) + if (!result) { + return response.api(null, `Role with id: ${params.id} not found`) + } + return response.api(result) + } catch (error) { + return response.error(error.message) + } + } + + public async update ({ params, request, response }: HttpContextContract) { + try { + await request.validate(UpdateRoleValidator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.update(params.id, data) + if (!result) { + return response.api(null, `Role with id: ${params.id} not found`) + } + return response.api(result, 'Role updated!') + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async destroy ({ params, response }: HttpContextContract) { + try { + const result = await this.service.delete(params.id) + if (!result) { + return response.api(null, `Role with id: ${params.id} not found`) + } + return response.api(null, 'Role deleted!') + } catch (error) { + return response.error(error.message) + } + } + + public async destroyAll ({ response }: HttpContextContract) { + try { + await this.service.deleteAll() + return response.api(null, 'All Role deleted!') + } catch (error) { + return response.error(error.message) + } + } +} diff --git a/app/Exceptions/DefaultException.ts b/app/Exceptions/DefaultException.ts new file mode 100644 index 0000000..98607a4 --- /dev/null +++ b/app/Exceptions/DefaultException.ts @@ -0,0 +1,16 @@ +import { Exception } from '@adonisjs/core/build/standalone' + +/* +|-------------------------------------------------------------------------- +| Exception +|-------------------------------------------------------------------------- +| +| The Exception class imported from `@adonisjs/core` allows defining +| a status code and error code for every exception. +| +| @example +| new DefaultException('message', 500, 'E_RUNTIME_EXCEPTION') +| +*/ +export default class DefaultException extends Exception { +} diff --git a/app/Exceptions/Handler.ts b/app/Exceptions/Handler.ts new file mode 100644 index 0000000..24389d6 --- /dev/null +++ b/app/Exceptions/Handler.ts @@ -0,0 +1,45 @@ +/* +|-------------------------------------------------------------------------- +| Http Exception Handler +|-------------------------------------------------------------------------- +| +| AdonisJs will forward all exceptions occurred during an HTTP request to +| the following class. You can learn more about exception handling by +| reading docs. +| +| The exception handler extends a base `HttpExceptionHandler` which is not +| mandatory, however it can do lot of heavy lifting to handle the errors +| properly. +| +*/ + +import Logger from '@ioc:Adonis/Core/Logger' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler' + +export default class ExceptionHandler extends HttpExceptionHandler { + protected statusPages = { + '403': 'errors/unauthorized', + '404': 'errors/not-found', + '500..599': 'errors/server-error', + } + + constructor () { + super(Logger) + } + + public async handle(error: any, ctx: HttpContextContract) { + /** + * Self handle the validation exception + */ + if (error.code === 'E_ROUTE_NOT_FOUND') { + return ctx.response.error(`Route [${ctx.request.method()}] ${ctx.request.completeUrl()} not found!`, null, 404) + } + + /** + * Forward rest of the exceptions to the parent class + */ + return super.handle(error, ctx) + } + +} diff --git a/app/Middleware/Auth.ts b/app/Middleware/Auth.ts new file mode 100644 index 0000000..581b241 --- /dev/null +++ b/app/Middleware/Auth.ts @@ -0,0 +1,76 @@ +import { GuardsList } from '@ioc:Adonis/Addons/Auth' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import { AuthenticationException } from '@adonisjs/auth/build/standalone' + +/** + * Auth middleware is meant to restrict un-authenticated access to a given route + * or a group of routes. + * + * You must register this middleware inside `start/kernel.ts` file under the list + * of named middleware. + */ +export default class AuthMiddleware { + /** + * The URL to redirect to when request is Unauthorized + */ + protected redirectTo = '/login' + + /** + * Authenticates the current HTTP request against a custom set of defined + * guards. + * + * The authentication loop stops as soon as the user is authenticated using any + * of the mentioned guards and that guard will be used by the rest of the code + * during the current request. + */ + protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) { + /** + * Hold reference to the guard last attempted within the for loop. We pass + * the reference of the guard to the "AuthenticationException", so that + * it can decide the correct response behavior based upon the guard + * driver + */ + let guardLastAttempted: string | undefined + + for (let guard of guards) { + guardLastAttempted = guard + + if (await auth.use(guard).check()) { + /** + * Instruct auth to use the given guard as the default guard for + * the rest of the request, since the user authenticated + * succeeded here + */ + auth.defaultGuard = guard + return true + } + } + + /** + * Unable to authenticate using any guard + */ + throw new AuthenticationException( + 'Unauthorized access', + 'E_UNAUTHORIZED_ACCESS', + guardLastAttempted, + this.redirectTo, + ) + } + + /** + * Handle request + */ + public async handle ( + { auth }: HttpContextContract, + next: () => Promise, + customGuards: (keyof GuardsList)[] + ) { + /** + * Uses the user defined guards or the default guard mentioned in + * the config file + */ + const guards = customGuards.length ? customGuards : [auth.name] + await this.authenticate(auth, guards) + await next() + } +} diff --git a/app/Middleware/SilentAuth.ts b/app/Middleware/SilentAuth.ts new file mode 100644 index 0000000..8288620 --- /dev/null +++ b/app/Middleware/SilentAuth.ts @@ -0,0 +1,21 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +/** + * Silent auth middleware can be used as a global middleware to silent check + * if the user is logged-in or not. + * + * The request continues as usual, even when the user is not logged-in. + */ +export default class SilentAuthMiddleware { + /** + * Handle request + */ + public async handle({ auth }: HttpContextContract, next: () => Promise) { + /** + * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be + * set to the instance of the currently logged in user. + */ + await auth.check() + await next() + } +} diff --git a/app/Models/User/Account.ts b/app/Models/User/Account.ts new file mode 100644 index 0000000..0955d10 --- /dev/null +++ b/app/Models/User/Account.ts @@ -0,0 +1,62 @@ +import { DateTime } from 'luxon' +import { BaseModel, beforeFetch, beforeFind, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' +import Role from './Role' + +export default class Account extends BaseModel { + public static softDelete = true + + @column({ isPrimary: true }) + public id: string + + @column() + public urole_id: string + + @column() + public username: string + + @column({ serializeAs: null }) + public pwd: string + + @column() + public email: string + + @column() + public google_id: string + + @column() + public fullname: string + + @column() + public avatar: string + + @column() + public is_ban: boolean + + @column.dateTime({ autoCreate: true }) + public created_at: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updated_at: DateTime + + @column.dateTime() + public deleted_at: DateTime + + static get table() { + return "user.account" + } + + @beforeFind() + public static findWithoutSoftDeletes(query) { + query.whereNull("deleted_at") + } + + @beforeFetch() + public static fetchWithoutSoftDeletes(query) { + query.whereNull("deleted_at") + } + + @belongsTo(() => Role, { + foreignKey: 'urole_id' + }) + public role: BelongsTo +} diff --git a/app/Models/User/Role.ts b/app/Models/User/Role.ts new file mode 100644 index 0000000..baccc4d --- /dev/null +++ b/app/Models/User/Role.ts @@ -0,0 +1,23 @@ +import { DateTime } from 'luxon' +import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' + +export default class Role extends BaseModel { + @column({ isPrimary: true }) + public id: string + + @column() + public code: string + + @column() + public name: string + + @column.dateTime({ autoCreate: true }) + public created_at: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updated_at: DateTime + + static get table() { + return "user.role" + } +} diff --git a/app/Repositories/User/AccountRepository.ts b/app/Repositories/User/AccountRepository.ts new file mode 100644 index 0000000..9324296 --- /dev/null +++ b/app/Repositories/User/AccountRepository.ts @@ -0,0 +1,17 @@ +import BaseRepository from "App/Base/Repositories/BaseRepository"; +import Account from "App/Models/User/Account"; + +export default class AccountRepository extends BaseRepository { + constructor() { + super(Account) + } + + async findByEmail(email: string) { + try { + return await this.model.query().where('email', email).first() + } catch (error) { + throw error + } + } +} + \ No newline at end of file diff --git a/app/Repositories/User/RoleRepository.ts b/app/Repositories/User/RoleRepository.ts new file mode 100644 index 0000000..2222295 --- /dev/null +++ b/app/Repositories/User/RoleRepository.ts @@ -0,0 +1,9 @@ +import BaseRepository from "App/Base/Repositories/BaseRepository"; +import Role from "App/Models/User/Role"; + +export default class RoleRepository extends BaseRepository { + constructor() { + super(Role) + } +} + \ No newline at end of file diff --git a/app/Services/Auth/AuthService.ts b/app/Services/Auth/AuthService.ts new file mode 100644 index 0000000..62c3e06 --- /dev/null +++ b/app/Services/Auth/AuthService.ts @@ -0,0 +1,113 @@ +import AccountRepository from "App/Repositories/User/AccountRepository" +import Hash from "@ioc:Adonis/Core/Hash" +import * as jwt from 'jsonwebtoken' +import DefaultException from "App/Exceptions/DefaultException" +import moment from 'moment' +import Base64 from 'base-64' +import Env from '@ioc:Adonis/Core/Env' +import Mail from "@ioc:Adonis/Addons/Mail" + +export default class AuthService { + accountRepository = new AccountRepository() + + async login (credentials: any, auth: any, rememberMe: boolean) { + try { + const user = await this.accountRepository.findByEmail(credentials.email) + if (!user || !(await Hash.verify(user.pwd, credentials.password))) { + throw new DefaultException('Invalid email or password!') + } + if (user.is_ban) { + throw new DefaultException('User banned!') + } + await user.load("role"); + return await this.generateToken(auth, user, rememberMe) + } catch (error) { + throw error + } + } + + async forgotPassword (credential, request) { + try { + const user = await this.accountRepository.findByEmail(credential) + if (!user) { + throw new DefaultException('User not found') + } + + const token = await this.generateRestorePasswordToken(user) + const restoreUrlWithToken = request.header('origin') + 'reset-password?token=' + token + + await this.sendEmailRestorePassword(user.email, restoreUrlWithToken) + } catch (error) { + throw error + } + } + + async restorePassword (request) { + try { + const { expiresIn, credential } = await this.decryptToken(request.token) + if (!this.checkExpirationToken(expiresIn)) { + throw new DefaultException('Token expired') + } + + const user = await this.accountRepository.findByEmail(credential) + await this.accountRepository.update(user.id, { + pwd: await Hash.make(request.pwd) + }) + } catch (error) { + throw error + } + } + + async generateToken(auth: any, user: any, rememberMe: boolean) { + try { + const expiresIn = rememberMe ? '7days' : '4hours' + const token = await auth.use('api').generate(user, {expiresIn}) + const jwtToken = jwt.sign({user}, 'PeSgVkYp3s6v9y$B&E)H@McQfTjWnZq4', {expiresIn: rememberMe ? '7d' : '4h'}) + return { token: token.token, jwtToken } + } catch (error) { + throw error + } + } + + async generateRestorePasswordToken (user) { + try { + const expiresIn = moment().add(30, 'm') + const token = Base64.encode(expiresIn + ';' + user.email) + return token + } catch (error) { + throw error + } + } + + async decryptToken (token) { + try { + const encrypt = Base64.decode(token).split(';') + const result = { + expiresIn: encrypt[0], + credential: encrypt[1] + } + return result + } catch (error) { + throw error + } + } + + async sendEmailRestorePassword (email, token) { + try { + await Mail.send((message) => { + message + .from(Env.get('SMTP_USERNAME'), 'Me') + .to(email) + .subject('Restore Password!') + .htmlView('emails/restore_password', { token }) + }) + } catch (error) { + throw error + } + } + + checkExpirationToken (expiresIn) { + return expiresIn > moment() + } + +} diff --git a/app/Services/User/AccountService.ts b/app/Services/User/AccountService.ts new file mode 100644 index 0000000..11f4014 --- /dev/null +++ b/app/Services/User/AccountService.ts @@ -0,0 +1,41 @@ +import BaseService from "App/Base/Services/BaseService" +import AccountRepository from "App/Repositories/User/AccountRepository" +import Hash from '@ioc:Adonis/Core/Hash' + +export default class AccountService extends BaseService { + constructor() { + super(new AccountRepository()) + } + + async store(data: any) { + try { + if (data.pwd) { + data.pwd = await Hash.make(data.pwd) + } + return await this.repository.store(data) + } catch (error) { + throw error + } + } + + async update(id: any, data: any) { + try { + if (data.pwd) { + data.pwd = await Hash.make(data.pwd) + } + return await this.repository.update(id, data) + } catch (error) { + throw error + } + } + + async findByEmail (email: string) { + try { + const akun = await this.repository.findByEmail(email) + return akun + } catch (error) { + throw error + } + } +} + \ No newline at end of file diff --git a/app/Services/User/RoleService.ts b/app/Services/User/RoleService.ts new file mode 100644 index 0000000..2caf5fa --- /dev/null +++ b/app/Services/User/RoleService.ts @@ -0,0 +1,9 @@ +import BaseService from "App/Base/Services/BaseService" +import RoleRepository from "App/Repositories/User/RoleRepository" + +export default class RoleService extends BaseService { + constructor() { + super(new RoleRepository()) + } +} + \ No newline at end of file diff --git a/app/Validators/User/CreateAccountValidator.ts b/app/Validators/User/CreateAccountValidator.ts new file mode 100644 index 0000000..8a5f73b --- /dev/null +++ b/app/Validators/User/CreateAccountValidator.ts @@ -0,0 +1,37 @@ +import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import Account from 'App/Models/User/Account' +import Role from 'App/Models/User/Role' + +export default class CreateAccountValidator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + urole_id: schema.string({}, [ + rules.exists({table: Role.table, column: Role.primaryKey}) + ]), + username: schema.string({}, [ + rules.maxLength(100), + rules.unique({table: Account.table, column: 'username', where: {deleted_at: null}}) + ]), + pwd: schema.string({}, [ + rules.minLength(6) + ]), + email: schema.string({}, [ + rules.maxLength(255), + rules.email(), + rules.unique({column: 'email', table: Account.table, where: {deleted_at: null}}) + ]), + google_id: schema.string.optional({}, [ + rules.maxLength(255) + ]), + fullname: schema.string({}, [ + rules.maxLength(100) + ]), + avatar: schema.string.optional(), + is_ban: schema.boolean.optional(), + }) +} diff --git a/app/Validators/User/CreateRoleValidator.ts b/app/Validators/User/CreateRoleValidator.ts new file mode 100644 index 0000000..481a528 --- /dev/null +++ b/app/Validators/User/CreateRoleValidator.ts @@ -0,0 +1,18 @@ +import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class CreateRoleValidator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + code: schema.string({}, [ + rules.maxLength(4) + ]), + name: schema.string({}, [ + rules.maxLength(50) + ]), + }) +} diff --git a/app/Validators/User/UpdateAccountValidator.ts b/app/Validators/User/UpdateAccountValidator.ts new file mode 100644 index 0000000..2a2b4a3 --- /dev/null +++ b/app/Validators/User/UpdateAccountValidator.ts @@ -0,0 +1,37 @@ +import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import Role from 'App/Models/User/Role' +import Account from 'App/Models/User/Account' + +export default class UpdateAccountValidator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + urole_id: schema.string.optional({}, [ + rules.exists({table: Role.table, column: Role.primaryKey}) + ]), + username: schema.string.optional({}, [ + rules.maxLength(100), + rules.unique({table: Account.table, column: 'username', where: {deleted_at: null}, whereNot: {id: this.ctx.params.id}}) + ]), + pwd: schema.string.optional({}, [ + rules.minLength(6) + ]), + email: schema.string.optional({}, [ + rules.maxLength(255), + rules.email(), + rules.unique({column: 'email', table: Account.table, where: {deleted_at: null}, whereNot: {id: this.ctx.params.id}}) + ]), + google_id: schema.string.optional({}, [ + rules.maxLength(255) + ]), + fullname: schema.string.optional({}, [ + rules.maxLength(100) + ]), + avatar: schema.string.optional(), + is_ban: schema.boolean.optional(), + }) +} diff --git a/app/Validators/User/UpdateRoleValidator.ts b/app/Validators/User/UpdateRoleValidator.ts new file mode 100644 index 0000000..e860e50 --- /dev/null +++ b/app/Validators/User/UpdateRoleValidator.ts @@ -0,0 +1,18 @@ +import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class UpdateRoleValidator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + code: schema.string.optional({}, [ + rules.maxLength(4) + ]), + name: schema.string.optional({}, [ + rules.maxLength(50) + ]), + }) +} diff --git a/commands/MakeModule.ts b/commands/MakeModule.ts new file mode 100644 index 0000000..c19619b --- /dev/null +++ b/commands/MakeModule.ts @@ -0,0 +1,270 @@ +import { BaseCommand, args, flags } from '@adonisjs/core/build/standalone' +import fs from 'fs' + +export default class MakeModule extends BaseCommand { + public static commandName = 'make:module' + public static description = 'Make a new module' + + @args.string({ description: "Domain name" }) + public domain: string + + @args.string({ description: "Model name" }) + public model: string + + @flags.boolean({ description: "Enable soft delete" }) + public softDelete: boolean + + @flags.boolean({ alias: 'uuid', description: "Enable soft delete" }) + public enableUUID: boolean + + @flags.string({ alias: 'e', description: "Set endpoint name"}) + public endpoint: string + + public static settings = { + loadApp: false, + stayAlive: false, + } + + public async run() { + if (!fs.existsSync(`app/Models/${this.domain}`)) { + if (!fs.existsSync('app/Models')) { + fs.mkdirSync(`app/Models`) + } + fs.mkdirSync(`app/Models/${this.domain}`) + } + if (!fs.existsSync(`app/Repositories/${this.domain}`)) { + if (!fs.existsSync('app/Repositories')) { + fs.mkdirSync(`app/Repositories`) + } + fs.mkdirSync(`app/Repositories/${this.domain}`) + } + if (!fs.existsSync(`app/Services/${this.domain}`)) { + if (!fs.existsSync('app/Services')) { + fs.mkdirSync(`app/Services`) + } + fs.mkdirSync(`app/Services/${this.domain}`) + } + if (!fs.existsSync(`app/Controllers/Http/${this.domain}`)) { + if (!fs.existsSync('app/Controllers')) { + fs.mkdirSync(`app/Controllers`) + fs.mkdirSync(`app/Controllers/Http`) + } + fs.mkdirSync(`app/Controllers/Http/${this.domain}`) + } + if (!fs.existsSync(`app/Validators/${this.domain}`)) { + if (!fs.existsSync('app/Validators')) { + fs.mkdirSync(`app/Validators`) + } + fs.mkdirSync(`app/Validators/${this.domain}`) + } + if (!fs.existsSync(`start/routes/${this.domain.toLowerCase()}`)) { + if (!fs.existsSync('start/routes')) { + fs.mkdirSync(`start/routes`) + } + fs.mkdirSync(`start/routes/${this.domain.toLowerCase()}`) + } + + fs.writeFileSync(`app/Models/${this.domain}/${this.model}.ts`, this.generateModel()) + this.logger.success(`${this.model} Model Created!`) + + fs.writeFileSync(`app/Repositories/${this.domain}/${this.model}Repository.ts`, this.generateRepository()) + this.logger.success(`${this.model} Repository Created!`) + + fs.writeFileSync(`app/Services/${this.domain}/${this.model}Service.ts`, this.generateService()) + this.logger.success(`${this.model} Service Created!`) + + fs.writeFileSync(`app/Controllers/Http/${this.domain}/${this.model}Controller.ts`, this.generateController()) + this.logger.success(`${this.model} Controller Created!`) + + fs.writeFileSync(`app/Validators/${this.domain}/Create${this.model}Validator.ts`, this.generateCreateValidator()) + this.logger.success(`${this.model} Create Validator Created!`) + + fs.writeFileSync(`app/Validators/${this.domain}/Update${this.model}Validator.ts`, this.generateUpdateValidator()) + this.logger.success(`${this.model} Update Validator Created!`) + + fs.writeFileSync(`start/routes/${this.domain.toLowerCase()}/${this.endpoint}.ts`, this.generateRoute()) + this.logger.success(`${this.model} Route Created!`) + } + + generateModel() { + return `import { DateTime } from 'luxon' +import { BaseModel${this.enableUUID ? ', beforeCreate' : ''}${this.softDelete ? ', beforeFetch, beforeFind' : ''}, column } from '@ioc:Adonis/Lucid/Orm'${this.enableUUID ? "\nimport { v4 as uuidv4, v5 as uuidv5 } from 'uuid'" : ''} + +export default class ${this.model} extends BaseModel { + ${this.softDelete ? 'public static softDelete = true\n\n ' : ''}@column({ isPrimary: true }) + public id: string + + @column.dateTime({ autoCreate: true }) + public created_at: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updated_at: DateTime + + ${this.softDelete ? '@column.dateTime()\n public deleted_at: DateTime\n\n ' : ''}static get table() { + return "" // table name + }${this.softDelete ? '\n\n @beforeFind()\n public static findWithoutSoftDeletes(query) {\n query.whereNull("deleted_at")\n }\n\n @beforeFetch()\n public static fetchWithoutSoftDeletes(query) {\n query.whereNull("deleted_at")\n }' : ''}${this.enableUUID ? "\n\n @beforeCreate()\n public static setUUID(data: " + this.model + ") {\n const namespace = uuidv4()\n data.id = uuidv5('" + this.model + "', namespace)\n }" : ''} +} +` + } + + generateRepository() { + return `import BaseRepository from "App/Base/Repositories/BaseRepository"; +import ${this.model} from "App/Models/${this.domain}/${this.model}"; + +export default class ${this.model}Repository extends BaseRepository { + constructor() { + super(${this.model}) + } +} + ` + } + + generateService() { + return `import BaseService from "App/Base/Services/BaseService" +import ${this.model}Repository from "App/Repositories/${this.domain}/${this.model}Repository" + +export default class ${this.model}Service extends BaseService { + constructor() { + super(new ${this.model}Repository()) + } +} + ` + } + + generateController() { + return `import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import ${this.model}Service from 'App/Services/${this.domain}/${this.model}Service' +import Create${this.model}Validator from 'App/Validators/${this.domain}/Create${this.model}Validator' +import Update${this.model}Validator from 'App/Validators/${this.domain}/Update${this.model}Validator' +import { ValidationException } from '@ioc:Adonis/Core/Validator' + +export default class ${this.model}Controller { + service = new ${this.model}Service() + FETCHED_ATTRIBUTE = [ + // attribute + ] + + public async index ({ request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.getAll(options) + return response.api(result, 'OK', 200, request) + } catch (error) { + return response.error(error.message) + } + } + + public async store ({ request, response }: HttpContextContract) { + try { + await request.validate(Create${this.model}Validator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.store(data) + return response.api(result, '${this.model} created!', 201) + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async show ({ params, request, response }: HttpContextContract) { + try { + const options = request.parseParams(request.all()) + const result = await this.service.show(params.id, options) + if (!result) { + return response.api(null, ${'`'+ this.model + ' with id: ${params.id} not found`'}) + } + return response.api(result) + } catch (error) { + return response.error(error.message) + } + } + + public async update ({ params, request, response }: HttpContextContract) { + try { + await request.validate(Update${this.model}Validator) + const data = request.only(this.FETCHED_ATTRIBUTE) + const result = await this.service.update(params.id, data) + if (!result) { + return response.api(null, ${'`'+ this.model + ' with id: ${params.id} not found`'}) + } + return response.api(result, '${this.model} updated!') + } catch (error) { + if (error instanceof ValidationException) { + const errorValidation: any = error + return response.error(errorValidation.message, errorValidation.messages.errors, 422) + } + return response.error(error.message) + } + } + + public async destroy ({ params, response }: HttpContextContract) { + try { + const result = await this.service.delete(params.id) + if (!result) { + return response.api(null, ${'`'+ this.model + ' with id: ${params.id} not found`'}) + } + return response.api(null, '${this.model} deleted!') + } catch (error) { + return response.error(error.message) + } + } + + public async destroyAll ({ response }: HttpContextContract) { + try { + await this.service.deleteAll() + return response.api(null, 'All ${this.model} deleted!') + } catch (error) { + return response.error(error.message) + } + } +} +` + } + + generateCreateValidator() { + return `import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class Create${this.model}Validator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + // your validation rules + }) +} +` + } + + generateUpdateValidator() { + return `import { schema, validator, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class Update${this.model}Validator { + constructor (protected ctx: HttpContextContract) { + } + + public reporter = validator.reporters.api + + public schema = schema.create({ + // your validation rules + }) +} +` + } + + generateRoute() { + return `import Route from '@ioc:Adonis/Core/Route' + +Route.group(function () { + Route.delete('/', '${this.domain}/${this.model}Controller.destroyAll').as('${this.endpoint}.destroyAll') +}).prefix('${this.endpoint}') +Route.resource('${this.endpoint}', '${this.domain}/${this.model}Controller').apiOnly() +` + } +} diff --git a/commands/index.ts b/commands/index.ts new file mode 100644 index 0000000..0d92924 --- /dev/null +++ b/commands/index.ts @@ -0,0 +1,19 @@ +import { listDirectoryFiles } from '@adonisjs/core/build/standalone' +import Application from '@ioc:Adonis/Core/Application' + +/* +|-------------------------------------------------------------------------- +| Exporting an array of commands +|-------------------------------------------------------------------------- +| +| Instead of manually exporting each file from this directory, we use the +| helper `listDirectoryFiles` to recursively collect and export an array +| of filenames. +| +| Couple of things to note: +| +| 1. The file path must be relative from the project root and not this directory. +| 2. We must ignore this file to avoid getting into an infinite loop +| +*/ +export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) diff --git a/config/ally.ts b/config/ally.ts new file mode 100644 index 0000000..d85d4c8 --- /dev/null +++ b/config/ally.ts @@ -0,0 +1,34 @@ +/** + * Config source: https://git.io/JOdi5 + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import { AllyConfig } from '@ioc:Adonis/Addons/Ally' + +/* +|-------------------------------------------------------------------------- +| Ally Config +|-------------------------------------------------------------------------- +| +| The `AllyConfig` relies on the `SocialProviders` interface which is +| defined inside `contracts/ally.ts` file. +| +*/ +const allyConfig: AllyConfig = { + /* + |-------------------------------------------------------------------------- + | Google driver + |-------------------------------------------------------------------------- + */ + google: { + driver: 'google', + clientId: Env.get('GOOGLE_CLIENT_ID'), + clientSecret: Env.get('GOOGLE_CLIENT_SECRET'), + callbackUrl: 'http://localhost:3333/auth/google/callback', + }, +} + +export default allyConfig diff --git a/config/app.ts b/config/app.ts new file mode 100644 index 0000000..adc372a --- /dev/null +++ b/config/app.ts @@ -0,0 +1,217 @@ +/** + * Config source: https://git.io/JfefZ + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import proxyAddr from 'proxy-addr' +import Env from '@ioc:Adonis/Core/Env' +import { ServerConfig } from '@ioc:Adonis/Core/Server' +import { LoggerConfig } from '@ioc:Adonis/Core/Logger' +import { ProfilerConfig } from '@ioc:Adonis/Core/Profiler' +import { ValidatorConfig } from '@ioc:Adonis/Core/Validator' + +/* +|-------------------------------------------------------------------------- +| Application secret key +|-------------------------------------------------------------------------- +| +| The secret to encrypt and sign different values in your application. +| Make sure to keep the `APP_KEY` as an environment variable and secure. +| +| Note: Changing the application key for an existing app will make all +| the cookies invalid and also the existing encrypted data will not +| be decrypted. +| +*/ +export const appKey: string = Env.get('APP_KEY') + +/* +|-------------------------------------------------------------------------- +| Http server configuration +|-------------------------------------------------------------------------- +| +| The configuration for the HTTP(s) server. Make sure to go through all +| the config properties to make keep server secure. +| +*/ +export const http: ServerConfig = { + /* + |-------------------------------------------------------------------------- + | Allow method spoofing + |-------------------------------------------------------------------------- + | + | Method spoofing enables defining custom HTTP methods using a query string + | `_method`. This is usually required when you are making traditional + | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and + | so on. + | + */ + allowMethodSpoofing: false, + + /* + |-------------------------------------------------------------------------- + | Subdomain offset + |-------------------------------------------------------------------------- + */ + subdomainOffset: 2, + + /* + |-------------------------------------------------------------------------- + | Request Ids + |-------------------------------------------------------------------------- + | + | Setting this value to `true` will generate a unique request id for each + | HTTP request and set it as `x-request-id` header. + | + */ + generateRequestId: false, + + /* + |-------------------------------------------------------------------------- + | Trusting proxy servers + |-------------------------------------------------------------------------- + | + | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` + | headers. + | + */ + trustProxy: proxyAddr.compile('loopback'), + + /* + |-------------------------------------------------------------------------- + | Generating Etag + |-------------------------------------------------------------------------- + | + | Whether or not to generate an etag for every response. + | + */ + etag: false, + + /* + |-------------------------------------------------------------------------- + | JSONP Callback + |-------------------------------------------------------------------------- + */ + jsonpCallbackName: 'callback', + + /* + |-------------------------------------------------------------------------- + | Cookie settings + |-------------------------------------------------------------------------- + */ + cookie: { + domain: '', + path: '/', + maxAge: '2h', + httpOnly: true, + secure: false, + sameSite: false, + }, +} + +/* +|-------------------------------------------------------------------------- +| Logger +|-------------------------------------------------------------------------- +*/ +export const logger: LoggerConfig = { + /* + |-------------------------------------------------------------------------- + | Application name + |-------------------------------------------------------------------------- + | + | The name of the application you want to add to the log. It is recommended + | to always have app name in every log line. + | + | The `APP_NAME` environment variable is automatically set by AdonisJS by + | reading the `name` property from the `package.json` file. + | + */ + name: Env.get('APP_NAME'), + + /* + |-------------------------------------------------------------------------- + | Toggle logger + |-------------------------------------------------------------------------- + | + | Enable or disable logger application wide + | + */ + enabled: true, + + /* + |-------------------------------------------------------------------------- + | Logging level + |-------------------------------------------------------------------------- + | + | The level from which you want the logger to flush logs. It is recommended + | to make use of the environment variable, so that you can define log levels + | at deployment level and not code level. + | + */ + level: Env.get('LOG_LEVEL', 'info'), + + /* + |-------------------------------------------------------------------------- + | Pretty print + |-------------------------------------------------------------------------- + | + | It is highly advised NOT to use `prettyPrint` in production, since it + | can have huge impact on performance. + | + */ + prettyPrint: Env.get('NODE_ENV') === 'development', +} + +/* +|-------------------------------------------------------------------------- +| Profiler +|-------------------------------------------------------------------------- +*/ +export const profiler: ProfilerConfig = { + /* + |-------------------------------------------------------------------------- + | Toggle profiler + |-------------------------------------------------------------------------- + | + | Enable or disable profiler + | + */ + enabled: true, + + /* + |-------------------------------------------------------------------------- + | Blacklist actions/row labels + |-------------------------------------------------------------------------- + | + | Define an array of actions or row labels that you want to disable from + | getting profiled. + | + */ + blacklist: [], + + /* + |-------------------------------------------------------------------------- + | Whitelist actions/row labels + |-------------------------------------------------------------------------- + | + | Define an array of actions or row labels that you want to whitelist for + | the profiler. When whitelist is defined, then `blacklist` is ignored. + | + */ + whitelist: [], +} + +/* +|-------------------------------------------------------------------------- +| Validator +|-------------------------------------------------------------------------- +| +| Configure the global configuration for the validator. Here's the reference +| to the default config https://git.io/JT0WE +| +*/ +export const validator: ValidatorConfig = { +} diff --git a/config/auth.ts b/config/auth.ts new file mode 100644 index 0000000..f2b95fb --- /dev/null +++ b/config/auth.ts @@ -0,0 +1,109 @@ +/** + * Config source: https://git.io/JY0mp + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import { AuthConfig } from '@ioc:Adonis/Addons/Auth' + +/* +|-------------------------------------------------------------------------- +| Authentication Mapping +|-------------------------------------------------------------------------- +| +| List of available authentication mapping. You must first define them +| inside the `contracts/auth.ts` file before mentioning them here. +| +*/ +const authConfig: AuthConfig = { + guard: 'api', + guards: { + /* + |-------------------------------------------------------------------------- + | OAT Guard + |-------------------------------------------------------------------------- + | + | OAT (Opaque access tokens) guard uses database backed tokens to authenticate + | HTTP request. This guard DOES NOT rely on sessions or cookies and uses + | Authorization header value for authentication. + | + | Use this guard to authenticate mobile apps or web clients that cannot rely + | on cookies/sessions. + | + */ + api: { + driver: 'oat', + + /* + |-------------------------------------------------------------------------- + | Tokens provider + |-------------------------------------------------------------------------- + | + | Uses SQL database for managing tokens. Use the "database" driver, when + | tokens are the secondary mode of authentication. + | For example: The Github personal tokens + | + | The foreignKey column is used to make the relationship between the user + | and the token. You are free to use any column name here. + | + */ + tokenProvider: { + type: 'api', + driver: 'database', + table: 'user.api_tokens', + foreignKey: 'user_id', + }, + + provider: { + /* + |-------------------------------------------------------------------------- + | Driver + |-------------------------------------------------------------------------- + | + | Name of the driver + | + */ + driver: 'lucid', + + /* + |-------------------------------------------------------------------------- + | Identifier key + |-------------------------------------------------------------------------- + | + | The identifier key is the unique key on the model. In most cases specifying + | the primary key is the right choice. + | + */ + identifierKey: 'id', + + /* + |-------------------------------------------------------------------------- + | Uids + |-------------------------------------------------------------------------- + | + | Uids are used to search a user against one of the mentioned columns. During + | login, the auth module will search the user mentioned value against one + | of the mentioned columns to find their user record. + | + */ + uids: ['email'], + + /* + |-------------------------------------------------------------------------- + | Model + |-------------------------------------------------------------------------- + | + | The model to use for fetching or finding users. The model is imported + | lazily since the config files are read way earlier in the lifecycle + | of booting the app and the models may not be in a usable state at + | that time. + | + */ + model: () => import('App/Models/User/Account'), + }, + }, + }, +} + +export default authConfig diff --git a/config/bodyparser.ts b/config/bodyparser.ts new file mode 100644 index 0000000..738800c --- /dev/null +++ b/config/bodyparser.ts @@ -0,0 +1,211 @@ +/** + * Config source: https://git.io/Jfefn + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser' + +const bodyParserConfig: BodyParserConfig = { + /* + |-------------------------------------------------------------------------- + | White listed methods + |-------------------------------------------------------------------------- + | + | HTTP methods for which body parsing must be performed. It is a good practice + | to avoid body parsing for `GET` requests. + | + */ + whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], + + /* + |-------------------------------------------------------------------------- + | JSON parser settings + |-------------------------------------------------------------------------- + | + | The settings for the JSON parser. The types defines the request content + | types which gets processed by the JSON parser. + | + */ + json: { + encoding: 'utf-8', + limit: '1mb', + strict: true, + types: [ + 'application/json', + 'application/json-patch+json', + 'application/vnd.api+json', + 'application/csp-report', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Form parser settings + |-------------------------------------------------------------------------- + | + | The settings for the `application/x-www-form-urlencoded` parser. The types + | defines the request content types which gets processed by the form parser. + | + */ + form: { + encoding: 'utf-8', + limit: '1mb', + queryString: {}, + + /* + |-------------------------------------------------------------------------- + | Convert empty strings to null + |-------------------------------------------------------------------------- + | + | Convert empty form fields to null. HTML forms results in field string + | value when the field is left blank. This option normalizes all the blank + | field values to "null" + | + */ + convertEmptyStringsToNull: true, + + types: [ + 'application/x-www-form-urlencoded', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Raw body parser settings + |-------------------------------------------------------------------------- + | + | Raw body just reads the request body stream as a plain text, which you + | can process by hand. This must be used when request body type is not + | supported by the body parser. + | + */ + raw: { + encoding: 'utf-8', + limit: '1mb', + queryString: {}, + types: [ + 'text/*', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Multipart parser settings + |-------------------------------------------------------------------------- + | + | The settings for the `multipart/form-data` parser. The types defines the + | request content types which gets processed by the form parser. + | + */ + multipart: { + /* + |-------------------------------------------------------------------------- + | Auto process + |-------------------------------------------------------------------------- + | + | The auto process option will process uploaded files and writes them to + | the `tmp` folder. You can turn it off and then manually use the stream + | to pipe stream to a different destination. + | + | It is recommended to keep `autoProcess=true`. Unless you are processing bigger + | file sizes. + | + */ + autoProcess: true, + + /* + |-------------------------------------------------------------------------- + | Files to be processed manually + |-------------------------------------------------------------------------- + | + | You can turn off `autoProcess` for certain routes by defining + | routes inside the following array. + | + | NOTE: Make sure the route pattern starts with a leading slash. + | + | Correct + | ```js + | /projects/:id/file + | ``` + | + | Incorrect + | ```js + | projects/:id/file + | ``` + */ + processManually: [], + + /* + |-------------------------------------------------------------------------- + | Temporary file name + |-------------------------------------------------------------------------- + | + | When auto processing is on. We will use this method to compute the temporary + | file name. AdonisJs will compute a unique `tmpPath` for you automatically, + | However, you can also define your own custom method. + | + */ + // tmpFileName () { + // }, + + /* + |-------------------------------------------------------------------------- + | Encoding + |-------------------------------------------------------------------------- + | + | Request body encoding + | + */ + encoding: 'utf-8', + + /* + |-------------------------------------------------------------------------- + | Convert empty strings to null + |-------------------------------------------------------------------------- + | + | Convert empty form fields to null. HTML forms results in field string + | value when the field is left blank. This option normalizes all the blank + | field values to "null" + | + */ + convertEmptyStringsToNull: true, + + /* + |-------------------------------------------------------------------------- + | Max Fields + |-------------------------------------------------------------------------- + | + | The maximum number of fields allowed in the request body. The field includes + | text inputs and files both. + | + */ + maxFields: 1000, + + /* + |-------------------------------------------------------------------------- + | Request body limit + |-------------------------------------------------------------------------- + | + | The total limit to the multipart body. This includes all request files + | and fields data. + | + */ + limit: '20mb', + + /* + |-------------------------------------------------------------------------- + | Types + |-------------------------------------------------------------------------- + | + | The types that will be considered and parsed as multipart body. + | + */ + types: [ + 'multipart/form-data', + ], + }, +} + +export default bodyParserConfig diff --git a/config/cors.ts b/config/cors.ts new file mode 100644 index 0000000..fc2c4f9 --- /dev/null +++ b/config/cors.ts @@ -0,0 +1,134 @@ +/** + * Config source: https://git.io/JfefC + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import { CorsConfig } from '@ioc:Adonis/Core/Cors' + +const corsConfig: CorsConfig = { + /* + |-------------------------------------------------------------------------- + | Enabled + |-------------------------------------------------------------------------- + | + | A boolean to enable or disable CORS integration from your AdonisJs + | application. + | + | Setting the value to `true` will enable the CORS for all HTTP request. However, + | you can define a function to enable/disable it on per request basis as well. + | + */ + enabled: true, + + // You can also use a function that return true or false. + // enabled: (request) => request.url().startsWith('/api') + + /* + |-------------------------------------------------------------------------- + | Origin + |-------------------------------------------------------------------------- + | + | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. + | The value can be one of the following: + | + | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + | + | Boolean (true) - Allow current request origin. + | Boolean (false) - Disallow all. + | String - Comma separated list of allowed origins. + | Array - An array of allowed origins. + | String (*) - A wildcard (*) to allow all request origins. + | Function - Receives the current origin string and should return + | one of the above values. + | + */ + origin: '*', + + /* + |-------------------------------------------------------------------------- + | Methods + |-------------------------------------------------------------------------- + | + | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` + | is checked against the following list. + | + | Following is the list of default methods. Feel free to add more. + */ + methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], + + /* + |-------------------------------------------------------------------------- + | Headers + |-------------------------------------------------------------------------- + | + | List of headers to be allowed for `Access-Control-Allow-Headers` header. + | The value can be one of the following: + | + | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers + | + | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. + | Boolean(false) - Disallow all headers. + | String - Comma separated list of allowed headers. + | Array - An array of allowed headers. + | Function - Receives the current header and should return one of the above values. + | + */ + headers: true, + + /* + |-------------------------------------------------------------------------- + | Expose Headers + |-------------------------------------------------------------------------- + | + | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. + | header. By default following 6 simple response headers are exposed. + | + | Cache-Control + | Content-Language + | Content-Type + | Expires + | Last-Modified + | Pragma + | + | In order to add more headers, simply define them inside the following array. + | + | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + | + */ + exposeHeaders: [ + 'cache-control', + 'content-language', + 'content-type', + 'expires', + 'last-modified', + 'pragma', + ], + + /* + |-------------------------------------------------------------------------- + | Credentials + |-------------------------------------------------------------------------- + | + | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, + | then header will be set, otherwise not. + | + | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + | + */ + credentials: true, + + /* + |-------------------------------------------------------------------------- + | MaxAge + |-------------------------------------------------------------------------- + | + | Define `Access-Control-Max-Age` header in seconds. + | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + | + */ + maxAge: 90, +} + +export default corsConfig diff --git a/config/database.ts b/config/database.ts new file mode 100644 index 0000000..96b1ff7 --- /dev/null +++ b/config/database.ts @@ -0,0 +1,55 @@ +/** + * Config source: https://git.io/JesV9 + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import { DatabaseConfig } from '@ioc:Adonis/Lucid/Database' + +const databaseConfig: DatabaseConfig = { + /* + |-------------------------------------------------------------------------- + | Connection + |-------------------------------------------------------------------------- + | + | The primary connection for making database queries across the application + | You can use any key from the `connections` object defined in this same + | file. + | + */ + connection: Env.get('DB_CONNECTION'), + + connections: { + /* + |-------------------------------------------------------------------------- + | PostgreSQL config + |-------------------------------------------------------------------------- + | + | Configuration for PostgreSQL database. Make sure to install the driver + | from npm when using this connection + | + | npm i pg + | + */ + pg: { + client: 'pg', + connection: { + host: Env.get('PG_HOST'), + port: Env.get('PG_PORT'), + user: Env.get('PG_USER'), + password: Env.get('PG_PASSWORD', ''), + database: Env.get('PG_DB_NAME'), + }, + migrations: { + naturalSort: true, + }, + healthCheck: false, + debug: false, + }, + + } +} + +export default databaseConfig diff --git a/config/hash.ts b/config/hash.ts new file mode 100644 index 0000000..cd731e6 --- /dev/null +++ b/config/hash.ts @@ -0,0 +1,75 @@ +/** + * Config source: https://git.io/JfefW + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import { HashConfig } from '@ioc:Adonis/Core/Hash' + +/* +|-------------------------------------------------------------------------- +| Hash Config +|-------------------------------------------------------------------------- +| +| The `HashConfig` relies on the `HashList` interface which is +| defined inside `contracts` directory. +| +*/ +const hashConfig: HashConfig = { + /* + |-------------------------------------------------------------------------- + | Default hasher + |-------------------------------------------------------------------------- + | + | By default we make use of the bcrypt hasher to hash values. However, feel + | free to change the default value + | + */ + default: Env.get('HASH_DRIVER', 'argon'), + + list: { + /* + |-------------------------------------------------------------------------- + | Argon + |-------------------------------------------------------------------------- + | + | Argon mapping uses the `argon2` driver to hash values. + | + | Make sure you install the underlying dependency for this driver to work. + | https://www.npmjs.com/package/phc-argon2. + | + | npm install phc-argon2 + | + */ + argon: { + driver: 'argon2', + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }, + + /* + |-------------------------------------------------------------------------- + | Bcrypt + |-------------------------------------------------------------------------- + | + | Bcrypt mapping uses the `bcrypt` driver to hash values. + | + | Make sure you install the underlying dependency for this driver to work. + | https://www.npmjs.com/package/phc-bcrypt. + | + | npm install phc-bcrypt + | + */ + bcrypt: { + driver: 'bcrypt', + rounds: 10, + }, + }, +} + +export default hashConfig diff --git a/config/mail.ts b/config/mail.ts new file mode 100644 index 0000000..dd4fed5 --- /dev/null +++ b/config/mail.ts @@ -0,0 +1,59 @@ +/** + * Config source: https://git.io/JvgAf + * + * Feel free to let us know via PR, if you find something broken in this contract + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import { MailConfig } from '@ioc:Adonis/Addons/Mail' + +const mailConfig: MailConfig = { + /* + |-------------------------------------------------------------------------- + | Default mailer + |-------------------------------------------------------------------------- + | + | The following mailer will be used to send emails, when you don't specify + | a mailer + | + */ + mailer: 'smtp', + + /* + |-------------------------------------------------------------------------- + | Mailers + |-------------------------------------------------------------------------- + | + | You can define or more mailers to send emails from your application. A + | single `driver` can be used to define multiple mailers with different + | config. + | + | For example: Postmark driver can be used to have different mailers for + | sending transactional and promotional emails + | + */ + mailers: { + /* + |-------------------------------------------------------------------------- + | Smtp + |-------------------------------------------------------------------------- + | + | Uses SMTP protocol for sending email + | + */ + smtp: { + driver: 'smtp', + host: Env.get('SMTP_HOST'), + port: Env.get('SMTP_PORT'), + auth: { + user: Env.get('SMTP_USERNAME'), + pass: Env.get('SMTP_PASSWORD'), + type: 'login', + } + }, + + }, +} + +export default mailConfig diff --git a/config/session.ts b/config/session.ts new file mode 100644 index 0000000..083f7c2 --- /dev/null +++ b/config/session.ts @@ -0,0 +1,118 @@ +/** + * Config source: https://git.io/JeYHp + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import Application from '@ioc:Adonis/Core/Application' +import { SessionConfig } from '@ioc:Adonis/Addons/Session' + +const sessionConfig: SessionConfig = { + /* + |-------------------------------------------------------------------------- + | Enable/Disable sessions + |-------------------------------------------------------------------------- + | + | Setting the following property to "false" will disable the session for the + | entire application + | + */ + enabled: true, + + /* + |-------------------------------------------------------------------------- + | Driver + |-------------------------------------------------------------------------- + | + | The session driver to use. You can choose between one of the following + | drivers. + | + | - cookie (Uses signed cookies to store session values) + | - file (Uses filesystem to store session values) + | - redis (Uses redis. Make sure to install "@adonisjs/redis" as well) + | + | Note: Switching drivers will make existing sessions invalid. + | + */ + driver: Env.get('SESSION_DRIVER'), + + /* + |-------------------------------------------------------------------------- + | Cookie name + |-------------------------------------------------------------------------- + | + | The name of the cookie that will hold the session id. + | + */ + cookieName: 'adonis-session', + + /* + |-------------------------------------------------------------------------- + | Clear session when browser closes + |-------------------------------------------------------------------------- + | + | Whether or not you want to destroy the session when browser closes. Setting + | this value to `true` will ignore the `age`. + | + */ + clearWithBrowser: false, + + /* + |-------------------------------------------------------------------------- + | Session age + |-------------------------------------------------------------------------- + | + | The duration for which session stays active after no activity. A new HTTP + | request to the server is considered as activity. + | + | The value can be a number in milliseconds or a string that must be valid + | as per https://npmjs.org/package/ms package. + | + | Example: `2 days`, `2.5 hrs`, `1y`, `5s` and so on. + | + */ + age: '2h', + + /* + |-------------------------------------------------------------------------- + | Cookie values + |-------------------------------------------------------------------------- + | + | The cookie settings are used to setup the session id cookie and also the + | driver will use the same values. + | + */ + cookie: { + path: '/', + httpOnly: true, + sameSite: false, + }, + + /* + |-------------------------------------------------------------------------- + | Configuration for the file driver + |-------------------------------------------------------------------------- + | + | The file driver needs absolute path to the directory in which sessions + | must be stored. + | + */ + file: { + location: Application.tmpPath('sessions'), + }, + + /* + |-------------------------------------------------------------------------- + | Redis driver + |-------------------------------------------------------------------------- + | + | The redis connection you want session driver to use. The same connection + | must be defined inside `config/redis.ts` file as well. + | + */ + redisConnection: 'local', +} + +export default sessionConfig diff --git a/config/shield.ts b/config/shield.ts new file mode 100644 index 0000000..4074832 --- /dev/null +++ b/config/shield.ts @@ -0,0 +1,238 @@ +/** + * Config source: https://git.io/Jvwvt + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import Env from '@ioc:Adonis/Core/Env' +import { ShieldConfig } from '@ioc:Adonis/Addons/Shield' + +/* +|-------------------------------------------------------------------------- +| Content Security Policy +|-------------------------------------------------------------------------- +| +| Content security policy filters out the origins not allowed to execute +| and load resources like scripts, styles and fonts. There are wide +| variety of options to choose from. +*/ +export const csp: ShieldConfig['csp'] = { + /* + |-------------------------------------------------------------------------- + | Enable/disable CSP + |-------------------------------------------------------------------------- + | + | The CSP rules are disabled by default for seamless onboarding. + | + */ + enabled: false, + + /* + |-------------------------------------------------------------------------- + | Directives + |-------------------------------------------------------------------------- + | + | All directives are defined in camelCase and here is the list of + | available directives and their possible values. + | + | https://content-security-policy.com + | + | @example + | directives: { + | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] + | } + | + */ + directives: { + }, + + /* + |-------------------------------------------------------------------------- + | Report only + |-------------------------------------------------------------------------- + | + | Setting `reportOnly=true` will not block the scripts from running and + | instead report them to a URL. + | + */ + reportOnly: false, +} + +/* +|-------------------------------------------------------------------------- +| CSRF Protection +|-------------------------------------------------------------------------- +| +| CSRF Protection adds another layer of security by making sure, actionable +| routes does have a valid token to execute an action. +| +*/ +export const csrf: ShieldConfig['csrf'] = { + /* + |-------------------------------------------------------------------------- + | Enable/Disable CSRF + |-------------------------------------------------------------------------- + */ + enabled: Env.get('NODE_ENV') !== 'testing', + + /* + |-------------------------------------------------------------------------- + | Routes to Ignore + |-------------------------------------------------------------------------- + | + | Define an array of route patterns that you want to ignore from CSRF + | validation. Make sure the route patterns are started with a leading + | slash. Example: + | + | `/foo/bar` + | + | Also you can define a function that is evaluated on every HTTP Request. + | ``` + | exceptRoutes: ({ request }) => request.url().includes('/api') + | ``` + | + */ + exceptRoutes: [], + + /* + |-------------------------------------------------------------------------- + | Enable Sharing Token Via Cookie + |-------------------------------------------------------------------------- + | + | When the following flag is enabled, AdonisJS will drop `XSRF-TOKEN` + | cookie that frontend frameworks can read and return back as a + | `X-XSRF-TOKEN` header. + | + | The cookie has `httpOnly` flag set to false, so it is little insecure and + | can be turned off when you are not using a frontend framework making + | AJAX requests. + | + */ + enableXsrfCookie: true, + + /* + |-------------------------------------------------------------------------- + | Methods to Validate + |-------------------------------------------------------------------------- + | + | Define an array of HTTP methods to be validated for a valid CSRF token. + | + */ + methods: ['POST', 'PUT', 'PATCH', 'DELETE'], +} + +/* +|-------------------------------------------------------------------------- +| DNS Prefetching +|-------------------------------------------------------------------------- +| +| DNS prefetching allows browsers to proactively perform domain name +| resolution in background. +| +| Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control +| +*/ +export const dnsPrefetch: ShieldConfig['dnsPrefetch'] = { + /* + |-------------------------------------------------------------------------- + | Enable/disable this feature + |-------------------------------------------------------------------------- + */ + enabled: true, + + /* + |-------------------------------------------------------------------------- + | Allow or Dis-Allow Explicitly + |-------------------------------------------------------------------------- + | + | The `enabled` boolean does not set `X-DNS-Prefetch-Control` header. However + | the `allow` boolean controls the value of `X-DNS-Prefetch-Control` header. + | + | - When `allow = true`, then `X-DNS-Prefetch-Control = 'on'` + | - When `allow = false`, then `X-DNS-Prefetch-Control = 'off'` + | + */ + allow: true, +} + +/* +|-------------------------------------------------------------------------- +| Iframe Options +|-------------------------------------------------------------------------- +| +| xFrame defines whether or not your website can be embedded inside an +| iframe. Choose from one of the following options. +| +| - DENY +| - SAMEORIGIN +| - ALLOW-FROM http://example.com +| +| Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options +*/ +export const xFrame: ShieldConfig['xFrame'] = { + enabled: true, + action: 'DENY', +} + +/* +|-------------------------------------------------------------------------- +| Http Strict Transport Security +|-------------------------------------------------------------------------- +| +| A security to ensure that a browser always makes a connection over +| HTTPS. +| +| Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security +| +*/ +export const hsts: ShieldConfig['hsts'] = { + enabled: true, + /* + |-------------------------------------------------------------------------- + | Max Age + |-------------------------------------------------------------------------- + | + | Control, how long the browser should remember that a site is only to be + | accessed using HTTPS. + | + */ + maxAge: '180 days', + + /* + |-------------------------------------------------------------------------- + | Include Subdomains + |-------------------------------------------------------------------------- + | + | Apply rules on the subdomains as well. + | + */ + includeSubDomains: true, + + /* + |-------------------------------------------------------------------------- + | Preloading + |-------------------------------------------------------------------------- + | + | Google maintains a service to register your domain and it will preload + | the HSTS policy. Learn more https://hstspreload.org/ + | + */ + preload: false, +} + +/* +|-------------------------------------------------------------------------- +| No Sniff +|-------------------------------------------------------------------------- +| +| Browsers have a habit of sniffing content-type of a response. Which means +| files with .txt extension containing Javascript code will be executed as +| Javascript. You can disable this behavior by setting nosniff to false. +| +| Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options +| +*/ +export const contentTypeSniffing: ShieldConfig['contentTypeSniffing'] = { + enabled: true, +} diff --git a/config/static.ts b/config/static.ts new file mode 100644 index 0000000..400c7cc --- /dev/null +++ b/config/static.ts @@ -0,0 +1,64 @@ +/** + * Config source: https://git.io/Jfefl + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import { AssetsConfig } from '@ioc:Adonis/Core/Static' + +const staticConfig: AssetsConfig = { + /* + |-------------------------------------------------------------------------- + | Enabled + |-------------------------------------------------------------------------- + | + | A boolean to enable or disable serving static files. The static files + | are served from the `public` directory inside the application root. + | However, you can override the default path inside `.adonisrc.json` + | file. + | + | + */ + enabled: true, + + /* + |-------------------------------------------------------------------------- + | Handling Dot Files + |-------------------------------------------------------------------------- + | + | Decide how you want the static assets server to handle the `dotfiles`. + | By default, we ignore them as if they don't exists. However, you + | can choose between one of the following options. + | + | - ignore: Behave as if the file doesn't exists. Results in 404. + | - deny: Deny access to the file. Results in 403. + | - allow: Serve the file contents + | + */ + dotFiles: 'ignore', + + /* + |-------------------------------------------------------------------------- + | Generating Etag + |-------------------------------------------------------------------------- + | + | Handle whether or not to generate etags for the files. Etag allows browser + | to utilize the cache when file hasn't been changed. + | + */ + etag: true, + + /* + |-------------------------------------------------------------------------- + | Set Last Modified + |-------------------------------------------------------------------------- + | + | Whether or not to set the `Last-Modified` header in the response. Uses + | the file system's last modified value. + | + */ + lastModified: true, +} + +export default staticConfig diff --git a/contracts/ally.ts b/contracts/ally.ts new file mode 100644 index 0000000..56450a2 --- /dev/null +++ b/contracts/ally.ts @@ -0,0 +1,15 @@ +/** + * Contract source: https://git.io/JOdiQ + * + * Feel free to let us know via PR, if you find something broken in this contract + * file. + */ + +declare module '@ioc:Adonis/Addons/Ally' { + interface SocialProviders { + google: { + config: GoogleDriverConfig + implementation: GoogleDriverContract + } + } +} diff --git a/contracts/auth.ts b/contracts/auth.ts new file mode 100644 index 0000000..8685afb --- /dev/null +++ b/contracts/auth.ts @@ -0,0 +1,72 @@ +/** + * Contract source: https://git.io/JOdz5 + * + * Feel free to let us know via PR, if you find something broken in this + * file. + */ + +import Account from 'App/Models/Account' + +declare module '@ioc:Adonis/Addons/Auth' { + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | The providers are used to fetch users. The Auth module comes pre-bundled + | with two providers that are `Lucid` and `Database`. Both uses database + | to fetch user details. + | + | You can also create and register your own custom providers. + | + */ + interface ProvidersList { + /* + |-------------------------------------------------------------------------- + | User Provider + |-------------------------------------------------------------------------- + | + | The following provider uses Lucid models as a driver for fetching user + | details from the database for authentication. + | + | You can create multiple providers using the same underlying driver with + | different Lucid models. + | + */ + user: { + implementation: LucidProviderContract + config: LucidProviderConfig + } + } + + /* + |-------------------------------------------------------------------------- + | Guards + |-------------------------------------------------------------------------- + | + | The guards are used for authenticating users using different drivers. + | The auth module comes with 3 different guards. + | + | - SessionGuardContract + | - BasicAuthGuardContract + | - OATGuardContract ( Opaque access token ) + | + | Every guard needs a provider for looking up users from the database. + | + */ + interface GuardsList { + /* + |-------------------------------------------------------------------------- + | OAT Guard + |-------------------------------------------------------------------------- + | + | OAT, stands for (Opaque access tokens) guard uses database backed tokens + | to authenticate requests. + | + */ + api: { + implementation: OATGuardContract<'user', 'api'> + config: OATGuardConfig<'user'> + } + } +} diff --git a/contracts/env.ts b/contracts/env.ts new file mode 100644 index 0000000..3dd4be1 --- /dev/null +++ b/contracts/env.ts @@ -0,0 +1,24 @@ +/** + * Contract source: https://git.io/JTm6U + * + * Feel free to let us know via PR, if you find something broken in this contract + * file. + */ + +declare module '@ioc:Adonis/Core/Env' { + /* + |-------------------------------------------------------------------------- + | Getting types for validated environment variables + |-------------------------------------------------------------------------- + | + | The `default` export from the "../env.ts" file exports types for the + | validated environment variables. Here we merge them with the `EnvTypes` + | interface so that you can enjoy intellisense when using the "Env" + | module. + | + */ + + type CustomTypes = typeof import("../env").default; + interface EnvTypes extends CustomTypes { + } +} diff --git a/contracts/events.ts b/contracts/events.ts new file mode 100644 index 0000000..665d2e9 --- /dev/null +++ b/contracts/events.ts @@ -0,0 +1,30 @@ +/** + * Contract source: https://git.io/JfefG + * + * Feel free to let us know via PR, if you find something broken in this contract + * file. + */ + +declare module '@ioc:Adonis/Core/Event' { + /* + |-------------------------------------------------------------------------- + | Define typed events + |-------------------------------------------------------------------------- + | + | You can define types for events inside the following interface and + | AdonisJS will make sure that all listeners and emit calls adheres + | to the defined types. + | + | For example: + | + | interface EventsList { + | 'new:user': UserModel + | } + | + | Now calling `Event.emit('new:user')` will statically ensure that passed value is + | an instance of the the UserModel only. + | + */ + interface EventsList { + } +} diff --git a/contracts/hash.ts b/contracts/hash.ts new file mode 100644 index 0000000..7cf4f6a --- /dev/null +++ b/contracts/hash.ts @@ -0,0 +1,19 @@ +/** + * 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' { + interface HashersList { + bcrypt: { + config: BcryptConfig, + implementation: BcryptContract, + }, + argon: { + config: ArgonConfig, + implementation: ArgonContract, + }, + } +} diff --git a/contracts/mail.ts b/contracts/mail.ts new file mode 100644 index 0000000..cb3413e --- /dev/null +++ b/contracts/mail.ts @@ -0,0 +1,14 @@ +/** + * 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' { + import { MailDrivers } from '@ioc:Adonis/Addons/Mail' + + interface MailersList { + smtp: MailDrivers['smtp'], + } +} diff --git a/contracts/request.ts b/contracts/request.ts new file mode 100644 index 0000000..1e1fa2f --- /dev/null +++ b/contracts/request.ts @@ -0,0 +1,5 @@ +declare module '@ioc:Adonis/Core/Request' { + interface RequestContract { + parseParams(request: any): any + } +} \ No newline at end of file diff --git a/contracts/response.ts b/contracts/response.ts new file mode 100644 index 0000000..c1bd744 --- /dev/null +++ b/contracts/response.ts @@ -0,0 +1,6 @@ +declare module '@ioc:Adonis/Core/Response' { + interface ResponseContract { + api(data?: any | null, message?: string | null, code?: number | null, request?: any | null): any, + error(message?: string | null, errors?: any | null, code?: number | null): any + } +} \ No newline at end of file diff --git a/database/factories/index.ts b/database/factories/index.ts new file mode 100644 index 0000000..de08b6b --- /dev/null +++ b/database/factories/index.ts @@ -0,0 +1 @@ +// import Factory from '@ioc:Adonis/Lucid/Factory' diff --git a/database/sql-only/support_function/alpha_requirement.sql b/database/sql-only/support_function/alpha_requirement.sql new file mode 100644 index 0000000..d06f3c2 --- /dev/null +++ b/database/sql-only/support_function/alpha_requirement.sql @@ -0,0 +1,31 @@ +-- CREATE DIRECTORY FOR TABLESPACE + # sudo mkdir /data/postgres/tablespaces/ts_pg_roadreport_dev + +-- GRANT PERMISSION ON TS FOR postgres + # sudo chown postgres:postgres /data/postgres/tablespaces/ts_pg_roadreport_dev +-- CREATE TABLESPACE FOR DATA + +CREATE TABLESPACE "ts_pg_roadreport_dev" +OWNER postgres +LOCATION '/data/postgres/tablespaces/ts_pg_roadreport_dev'; + + +-- CREATE DATABASE + +CREATE DATABASE "pg_roadreport_dev" + WITH + OWNER = postgres + ENCODING = 'UTF8' + TABLESPACE = "ts_pg_roadreport_dev" + CONNECTION LIMIT = -1; + +-- ENABLE UUID SUPPORT +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- CREATE USER +CREATE ROLE "roadreportpgdb" LOGIN PASSWORD 'roadreportpgdb1qaz'; + +-- ADD USER ACCESS IN PG HBA_CONF + +-- Refresh PG_CONF +SELECT pg_reload_conf(); diff --git a/database/sql-only/support_function/grant_access.sql b/database/sql-only/support_function/grant_access.sql new file mode 100644 index 0000000..e16e58c --- /dev/null +++ b/database/sql-only/support_function/grant_access.sql @@ -0,0 +1,7 @@ +-- === Grant Script on database--- + +-- === GRANT PUBLIC SCHEMA ==== -- +GRANT USAGE ON SCHEMA "public" TO roadreportpgdb; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO roadreportpgdb; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO roadreportpgdb; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO roadreportpgdb; \ No newline at end of file diff --git a/database/sql-only/user-schema/ddl_table_user_schema.sql b/database/sql-only/user-schema/ddl_table_user_schema.sql new file mode 100644 index 0000000..75e4794 --- /dev/null +++ b/database/sql-only/user-schema/ddl_table_user_schema.sql @@ -0,0 +1,114 @@ +-- =============START USER SCHEMA================== + +DROP SCHEMA IF EXISTS "user"; +CREATE SCHEMA "user" + + +-- ---------------------------- +-- Table structure for role +-- ---------------------------- + +DROP TABLE IF EXISTS "user"."role" CASCADE; + +-- Ver 0.1.0 ( Design ) +-- Ver 0.1.0 ( Current & Implemented ) +CREATE TABLE "user"."role"( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "code" VARCHAR(5) NOT NULL , + "name" VARCHAR(100) NOT NULL , + "desc" TEXT NULL , + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , + "updated_at" TIMESTAMP NULL , + PRIMARY KEY ("id") +); + +-- INDEX TABLE role +CREATE INDEX "pkey_urole" ON "user"."role" ("id"); + + +-- ---------------------------- +-- Table structure for account +-- ---------------------------- + +DROP TABLE IF EXISTS "user"."account" CASCADE; + +-- Ver 0.1.0 ( Design ) +-- Ver 0.1.0 ( Current & Implemented ) +CREATE TABLE "user"."account"( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "urole_id" UUID NOT NULL , + "username" varchar(255) NOT NULL UNIQUE, + "pwd" TEXT NOT NULL , + "fullname" varchar(255) NOT NULL , + "shortname" varchar(255) NOT NULL , + "email" varchar(255) NOT NULL UNIQUE , + "avatar" varchar(255) NOT NULL , + "note" varchar(255) NULL , + "status" BOOLEAN NOT NULL DEFAULT FALSE , + "is_ban" BOOLEAN NOT NULL DEFAULT FALSE , + "last_active" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , + "updated_at" TIMESTAMP NULL , + "deleted_at" TIMESTAMP NULL , + PRIMARY KEY ("id"), + FOREIGN KEY ("urole_id") REFERENCES "user"."role"("id") ON UPDATE CASCADE ON DELETE CASCADE +); + +-- INDEX TABLE account +CREATE INDEX "pkey_uaccount" ON "user"."account" ("id"); +CREATE INDEX "fkey_uaccount_urole" ON "user"."account" ("urole_id"); + + + +-- ---------------------------- +-- Table structure for api_tokens +-- ---------------------------- + +DROP TABLE IF EXISTS "user"."api_tokens" CASCADE; + +-- Ver 0.1.0 ( Design ) +-- Ver 0.1.0 ( Current & Implemented ) +CREATE TABLE "user"."api_tokens"( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "user_id" UUID NOT NULL , + "name" VARCHAR(255) NOT NULL , + "type" VARCHAR(255) NOT NULL , + "token" VARCHAR(255) NOT NULL , + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMP , + PRIMARY KEY ("id"), + FOREIGN KEY ("user_id") REFERENCES "user"."account"("id") +); + +-- INDEX TABLE account +CREATE INDEX "pkey_uapi_tokens" ON "user"."api_tokens" ("id"); +CREATE INDEX "fkey_uapi_tokens_uaccount" ON "user"."api_tokens" ("user_id"); + + + +-- ---------------------------- +-- Table structure for event +-- ---------------------------- + +DROP TABLE IF EXISTS "user"."event" CASCADE; +DROP SEQUENCE IF EXISTS event_id_seq; + +-- Ver 1 +-- ( Current & Implemented ) +CREATE TABLE "user"."event"( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "uaccount_id" UUID NOT NULL , + "project_id" UUID NULL , + "fullname" VARCHAR NOT NULL , + "title" VARCHAR NOT NULL , + "body" TEXT NULL , + "start" DATE NOT NULL , + "end" DATE NOT NULL , + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP NULL, + "deleted_at" TIMESTAMP NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("uaccount_id") REFERENCES "user"."account"("id") +); + +-- =============END USER SCHEMA================== diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..abf2cc4 --- /dev/null +++ b/env.ts @@ -0,0 +1,31 @@ +/* +|-------------------------------------------------------------------------- +| Validating Environment Variables +|-------------------------------------------------------------------------- +| +| In this file we define the rules for validating environment variables. +| By performing validation we ensure that your application is running in +| a stable environment with correct configuration values. +| +| This file is read automatically by the framework during the boot lifecycle +| and hence do not rename or move this file to a different location. +| +*/ + +import Env from '@ioc:Adonis/Core/Env' + +export default Env.rules({ + HOST: Env.schema.string({ format: 'host' }), + PORT: Env.schema.number(), + APP_KEY: Env.schema.string(), + APP_NAME: Env.schema.string(), + CACHE_VIEWS: Env.schema.boolean(), + SESSION_DRIVER: Env.schema.string(), + NODE_ENV: Env.schema.enum(['development', 'production', 'testing'] as const), + SMTP_HOST: Env.schema.string({ format: 'host' }), + SMTP_PORT: Env.schema.number(), + SMTP_USERNAME: Env.schema.string(), + SMTP_PASSWORD: Env.schema.string(), + GOOGLE_CLIENT_ID: Env.schema.string(), + GOOGLE_CLIENT_SECRET: Env.schema.string(), +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..b772cdb --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "roadreport-backend", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "node ace build --production", + "start": "node server.js", + "dev": "node ace serve --watch" + }, + "devDependencies": { + "@adonisjs/assembler": "^5.3.5", + "@types/uuid": "^8.3.1", + "adonis-preset-ts": "^2.1.0", + "pino-pretty": "^5.1.2", + "typescript": "^4.2.4", + "youch": "^2.2.2", + "youch-terminal": "^1.1.1" + }, + "dependencies": { + "@adonisjs/ally": "^4.1.1", + "@adonisjs/auth": "^8.2.1", + "@adonisjs/core": "^5.1.11", + "@adonisjs/lucid": "^18.0.0", + "@adonisjs/mail": "^7.2.4", + "@adonisjs/repl": "^3.1.5", + "@adonisjs/session": "^6.1.1", + "@adonisjs/shield": "^7.0.5", + "@adonisjs/view": "^6.0.8", + "base-64": "^1.0.0", + "jsonwebtoken": "^8.5.1", + "luxon": "^2.0.2", + "moment": "^2.29.1", + "pg": "^8.7.1", + "phc-argon2": "^1.1.2", + "proxy-addr": "^2.0.7", + "reflect-metadata": "^0.1.13", + "source-map-support": "^0.5.19", + "uuid": "^8.3.2" + } +} diff --git a/providers/AppProvider.ts b/providers/AppProvider.ts new file mode 100644 index 0000000..25a3a1d --- /dev/null +++ b/providers/AppProvider.ts @@ -0,0 +1,94 @@ +import { ApplicationContract } from '@ioc:Adonis/Core/Application' +import ParseParamService from 'App/Base/Services/ParseParamService' +import ParseUrlService from 'App/Base/Services/ParseUrlService' + +export default class AppProvider { + public static needsApplication = true + constructor (protected app: ApplicationContract) { + } + + public register () { + // Register your own bindings + } + + public async boot () { + // IoC container is ready + this.extendRequest() + this.extendResponse() + } + + public async ready () { + // App is ready + } + + public async shutdown () { + // Cleanup, since app is going down + } + + extendResponse() { + const Response = this.app.container.use('Adonis/Core/Response') + + Response.macro('api', function (data, message = 'OK', code: any = 200, request = null) { + const parseUrlService = new ParseUrlService() + + if (data) { + if (data.rows) { + const url = parseUrlService.parseUrl(request, data.currentPage ?? 1, data.lastPage ?? 1) + + this.status(code).json({ + data: data.rows, + page: data.currentPage ?? 1, + total: data.total ? parseInt(data.total) : data.rows.length, + perPage: data.perPage ?? data.rows.length, + lastPage: data.lastPage ?? 1, + nextPage: url.nextUrl, + previousPage: url.prevUrl, + statusCode: code, + message: message + }) + } else if (data.length || Array.isArray(data)) { + this.status(code).json({ + data: data, + page: 1, + total: data.length, + perPage: data.length, + lastPage: 1, + nextPage: null, + previousPage: null, + statusCode: code, + message: message + }) + } else { + this.status(code).json({ + data: data, + statusCode: code, + message: message + }) + } + } else { + this.status(code).json({ + data: null, + statusCode: code, + message: message + }) + } + }) + + Response.macro('error', function (message, errors = null, code: any = 400) { + this.status(code).json({ + statusCode: code, + message: message, + errors: errors + }) + }) + } + + extendRequest() { + const Request = this.app.container.use('Adonis/Core/Request') + + Request.macro('parseParams', function (request) { + const parseParamService = new ParseParamService() + return parseParamService.parse(request) + }) + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e16f52333f9411b545ac6cec668907d02c90a13c GIT binary patch literal 15406 zcmeGiX;TzObW+I=Fdq^{QPvw#a1|9n5mW@cF!>NsKt#%epnyTd3zc|O1yn!^wGtH- zDWzmZF{P$75tPf)lo%Bwig}DGen)q|?tcB= z_DZrC)KJBWXiW2*&&Oq%sKI zzwQbjYdL-L6x7w$>Xr1=G`&8jrKCVjP0fGh-aUwqi-ULX-hmLFKYtGDA%Zf>UU@7%cqD*K-N zH-^V*{C1bL(NH2 zR*8ySY_)xB{=9Iy4@>=d^zb1J=+n1N{*j9!ptPjKWWjU9*=aBo7Zpoo4@;w?+vJV9 zfHFRQ^r%g`*;!dI(R)099+^V8T3!x4x^|PqA7zM*S!#+L{f6h@qzMz@!2bOv+oGKk zFm;+_-pJp+n;rah^(s%dlGqKi0|WJnr@IGl3-FCafATa(&En10N4~ZwNT4j)) z579%k4TW(3{(b1v%fVoKvHYfViteVYKD=o7MZ#FIw3l}o>I(93wiSZ+@N2dV; zVP{4L_)he(Du0yYV%bGrh70G>NO|JgWpZ1y_Ve+9w{PF_{0a*S z`2SCzJ_Q%DA6t{3AzFvBvw5DvKR>QdY1haUDe;g2$GNK6#6 zhtS{V*Ou0yXeV>^UD%7OQ1CXZ4p_X9--5#JalyOsZK+yq32RQZ`Q*L)`PrZrGo> z&#dzEu3gZ-cOO9C4*IVw0rbb9pNh$3PK|z(lHw9s>{GUF1IK>-;kVPLW#JpUOH$)WRQ>3AE$$1|zo#tj{Qtwf&0wd*ADw70W^tOEyh@P!XGB>%^2tf!>0 zFeIOpcVTlvX&ghy7yC4N#7L8TaUa9@rTZv1xJTp^1LoYMk~5>d{nfY@HAixF*yCQA zk)Cc;e#D81ih{kn_dstF+t`wnq-PsdRaMa#kCZXAw2imtIKUX15HR1$-`CGzAMhN< zxD(oQbAp0l({Q) zy)iZ>hCkEFN=t1fJCn`R!vk)U_=*tF&WK(TrL&gDxQzzPFJOT4l(2dYwaI=zdlu>+ z*TXl{1N8dL__SAzXIgyRawZmEmy)q$@iN?VPvqzG-|!Evm0de_&~@rZVu@(uSRgwu{2GrNr3q^#74<5ke zNxpFEOUF&A$nm7 z@D30f9BlJ-Dy3hj58Fw8ura{7$v=La-p8cS>xedJ8&ny{Q^7txdGdtjkl=dl2-&3a z3=Ij-VT1!Lw#V1JM@*%n8AK zHCxMJ2|v`6Jd!VjKywMnoxu`%9f6B?0nE!=9Pz!)?J{ficM7qk<@(L6C4)le(}@`< ZbFIv^q63N!bVeN@=|Ku8BOrSO{sjn2P4NH# literal 0 HcmV?d00001 diff --git a/resources/views/emails/restore_password.edge b/resources/views/emails/restore_password.edge new file mode 100644 index 0000000..c729b11 --- /dev/null +++ b/resources/views/emails/restore_password.edge @@ -0,0 +1,3 @@ +

+ token: {{token}} +

\ No newline at end of file diff --git a/resources/views/errors/not-found.edge b/resources/views/errors/not-found.edge new file mode 100644 index 0000000..6e17964 --- /dev/null +++ b/resources/views/errors/not-found.edge @@ -0,0 +1 @@ +

It's a 404

diff --git a/resources/views/errors/server-error.edge b/resources/views/errors/server-error.edge new file mode 100644 index 0000000..5515755 --- /dev/null +++ b/resources/views/errors/server-error.edge @@ -0,0 +1 @@ +

It's a 500

diff --git a/resources/views/errors/unauthorized.edge b/resources/views/errors/unauthorized.edge new file mode 100644 index 0000000..535a61b --- /dev/null +++ b/resources/views/errors/unauthorized.edge @@ -0,0 +1 @@ +

It's a 403

diff --git a/resources/views/index.edge b/resources/views/index.edge new file mode 100644 index 0000000..e476393 --- /dev/null +++ b/resources/views/index.edge @@ -0,0 +1,110 @@ + + + + + + AdonisJS - A fully featured web framework for Node.js + + + + + +
+
+

It Works!

+

+ Congratulations, you have just created your first AdonisJS app. +

+ +
    +
  • + The route for this page is defined inside start/routes.ts file +
  • + +
  • + You can update this page by editing resources/views/welcome.edge file +
  • + +
  • + If you run into problems, you can reach us on Discord or the Forum. +
  • +
+
+
+ + diff --git a/resources/views/welcome.edge b/resources/views/welcome.edge new file mode 100644 index 0000000..e476393 --- /dev/null +++ b/resources/views/welcome.edge @@ -0,0 +1,110 @@ + + + + + + AdonisJS - A fully featured web framework for Node.js + + + + + +
+
+

It Works!

+

+ Congratulations, you have just created your first AdonisJS app. +

+ +
    +
  • + The route for this page is defined inside start/routes.ts file +
  • + +
  • + You can update this page by editing resources/views/welcome.edge file +
  • + +
  • + If you run into problems, you can reach us on Discord or the Forum. +
  • +
+
+
+ + diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..ef04a46 --- /dev/null +++ b/server.ts @@ -0,0 +1,21 @@ +/* +|-------------------------------------------------------------------------- +| AdonisJs Server +|-------------------------------------------------------------------------- +| +| The contents in this file is meant to bootstrap the AdonisJs application +| and start the HTTP server to accept incoming connections. You must avoid +| making this file dirty and instead make use of `lifecycle hooks` provided +| by AdonisJs service providers for custom code. +| +*/ + +import 'reflect-metadata' +import sourceMapSupport from 'source-map-support' +import { Ignitor } from '@adonisjs/core/build/standalone' + +sourceMapSupport.install({ handleUncaughtExceptions: false }) + +new Ignitor(__dirname) + .httpServer() + .start() diff --git a/start/kernel.ts b/start/kernel.ts new file mode 100644 index 0000000..57cfaea --- /dev/null +++ b/start/kernel.ts @@ -0,0 +1,44 @@ +/* +|-------------------------------------------------------------------------- +| Application middleware +|-------------------------------------------------------------------------- +| +| This file is used to define middleware for HTTP requests. You can register +| middleware as a `closure` or an IoC container binding. The bindings are +| preferred, since they keep this file clean. +| +*/ + +import Server from '@ioc:Adonis/Core/Server' + +/* +|-------------------------------------------------------------------------- +| Global middleware +|-------------------------------------------------------------------------- +| +| An array of global middleware, that will be executed in the order they +| are defined for every HTTP requests. +| +*/ +Server.middleware.register([ + () => import('@ioc:Adonis/Core/BodyParser'), +]) + +/* +|-------------------------------------------------------------------------- +| Named middleware +|-------------------------------------------------------------------------- +| +| Named middleware are defined as key-value pair. The value is the namespace +| or middleware function and key is the alias. Later you can use these +| alias on individual routes. For example: +| +| { auth: () => import('App/Middleware/Auth') } +| +| and then use it as follows +| +| Route.get('dashboard', 'UserController.dashboard').middleware('auth') +| +*/ +Server.middleware.registerNamed({ +}) diff --git a/start/routes.ts b/start/routes.ts new file mode 100644 index 0000000..1f78146 --- /dev/null +++ b/start/routes.ts @@ -0,0 +1,59 @@ +/* +|-------------------------------------------------------------------------- +| Routes +|-------------------------------------------------------------------------- +| +| This file is dedicated for defining HTTP routes. A single file is enough +| for majority of projects, however you can define routes in different +| files and just make sure to import them inside this file. For example +| +| Define routes in following two files +| ├── start/routes/cart.ts +| ├── start/routes/customer.ts +| +| and then import them inside `start/routes.ts` as follows +| +| import './routes/cart' +| import './routes/customer'' +| +*/ + +import Route from '@ioc:Adonis/Core/Route' +import fs from 'fs'; + +Route.group(function () { + if (fs.existsSync(`${__dirname}/routes`)) { + const folders = fs.readdirSync(`${__dirname}/routes`) + folders.map((folder) => { + if (folder != 'auth') { + const files = fs.readdirSync(`${__dirname}/routes/${folder}`) + files.map((file) => { + if (!file.includes('.map')) { + require(`${__dirname}/routes/${folder}/${file}`) + } + }) + } + }) + } +}).prefix('api') + +Route.group(function () { + if (fs.existsSync(`${__dirname}/routes/auth`)) { + const files = fs.readdirSync(`${__dirname}/routes/auth`) + files.map((file) => { + if (!file.includes('.map')) { + require(`${__dirname}/routes/auth/${file}`) + } + }) + } +}).prefix('auth') + +Route.get('/', async ({ view }) => { + return view.render('welcome') +}) + +Route.get('/api', async ({ response }) => { + return response.api(null, 'It works!') +}) + +Route.on('*').render('index') diff --git a/start/routes/auth/auth.ts b/start/routes/auth/auth.ts new file mode 100644 index 0000000..ae36fa5 --- /dev/null +++ b/start/routes/auth/auth.ts @@ -0,0 +1,8 @@ +import Route from '@ioc:Adonis/Core/Route' + +Route.post('/login', 'Auth/AuthController.login').as('auth.login') +Route.post('/logout', 'Auth/AuthController.logout').as('auth.logout') +Route.get('/google/redirect', 'Auth/AuthController.oauthRedirect').as('auth.redirect') +Route.get('/google/callback', 'Auth/AuthController.oauthCallback').as('auth.callback') +Route.post('/forgot-password', 'Auth/AuthController.forgotPassword').as('auth.forgot-password') +Route.post('/reset-password', 'Auth/AuthController.restorePassword').as('auth.reset-password') \ No newline at end of file diff --git a/start/routes/user/roles.ts b/start/routes/user/roles.ts new file mode 100644 index 0000000..0953ab6 --- /dev/null +++ b/start/routes/user/roles.ts @@ -0,0 +1,6 @@ +import Route from '@ioc:Adonis/Core/Route' + +Route.group(function () { + Route.delete('/', 'User/RoleController.destroyAll').as('roles.destroyAll') +}).prefix('roles') +Route.resource('roles', 'User/RoleController').apiOnly() diff --git a/start/routes/user/users.ts b/start/routes/user/users.ts new file mode 100644 index 0000000..646fff8 --- /dev/null +++ b/start/routes/user/users.ts @@ -0,0 +1,6 @@ +import Route from '@ioc:Adonis/Core/Route' + +Route.group(function () { + Route.delete('/', 'User/AccountController.destroyAll').as('users.destroyAll') +}).prefix('users') +Route.resource('users', 'User/AccountController').apiOnly() diff --git a/tes.txt b/tes.txt deleted file mode 100644 index d179357..0000000 --- a/tes.txt +++ /dev/null @@ -1 +0,0 @@ -tes \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dac3212 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "./node_modules/adonis-preset-ts/tsconfig", + "include": [ + "**/*" + ], + "exclude": [ + "node_modules", + "build" + ], + "compilerOptions": { + "outDir": "build", + "rootDir": "./", + "sourceMap": true, + "paths": { + "App/*": [ + "./app/*" + ], + "Config/*": [ + "./config/*" + ], + "Contracts/*": [ + "./contracts/*" + ], + "Database/*": [ + "./database/*" + ] + }, + "types": [ + "@adonisjs/core", + "@adonisjs/repl", + "@adonisjs/session", + "@adonisjs/view", + "@adonisjs/shield", + "@adonisjs/lucid", + "@adonisjs/auth", + "@adonisjs/mail", + "@adonisjs/ally" + ] + } +}