├── resources └── views │ ├── errors │ ├── not-found.edge │ ├── server-error.edge │ └── unauthorized.edge │ ├── emails │ └── verify.edge │ ├── test.edge │ ├── password │ ├── forgot.edge │ └── reset.edge │ ├── welcome.edge │ ├── users │ └── manage.edge │ └── layouts │ └── app.edge ├── .env.test ├── database ├── factories │ └── index.ts └── migrations │ ├── 1668806613838_tokens.ts │ ├── 1667756114567_roles.ts │ └── 1667756129692_users.ts ├── .gitignore ├── app ├── Enums │ └── Roles.ts ├── Middleware │ ├── VerifiedEmail.ts │ ├── Role.ts │ ├── SilentAuth.ts │ └── Auth.ts ├── Models │ ├── Role.ts │ ├── User.ts │ └── Token.ts ├── Exceptions │ └── Handler.ts ├── Mailers │ └── VerifyEmail.ts └── Controllers │ └── Http │ ├── VerifyEmailController.ts │ ├── AuthController.ts │ ├── UsersController.ts │ └── PasswordResetController.ts ├── public └── favicon.ico ├── .editorconfig ├── tests ├── functional │ └── hello_world.spec.ts └── bootstrap.ts ├── .env.example ├── contracts ├── tests.ts ├── hash.ts ├── drive.ts ├── mail.ts ├── env.ts ├── events.ts └── auth.ts ├── start ├── globals.ts ├── kernel.ts └── routes.ts ├── providers └── AppProvider.ts ├── ace ├── server.ts ├── commands └── index.ts ├── tsconfig.json ├── package.json ├── test.ts ├── env.ts ├── .adonisrc.json ├── config ├── database.ts ├── mail.ts ├── hash.ts ├── static.ts ├── auth.ts ├── session.ts ├── cors.ts ├── drive.ts ├── bodyparser.ts ├── app.ts └── shield.ts └── ace-manifest.json /resources/views/errors/not-found.edge: -------------------------------------------------------------------------------- 1 |

It's a 404

2 | -------------------------------------------------------------------------------- /resources/views/errors/server-error.edge: -------------------------------------------------------------------------------- 1 |

It's a 500

2 | -------------------------------------------------------------------------------- /resources/views/errors/unauthorized.edge: -------------------------------------------------------------------------------- 1 |

It's a 403

2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | ASSETS_DRIVER=fake 3 | SESSION_DRIVER=memory 4 | -------------------------------------------------------------------------------- /database/factories/index.ts: -------------------------------------------------------------------------------- 1 | // import Factory from '@ioc:Adonis/Lucid/Factory' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | .DS_STORE 6 | .env 7 | tmp 8 | -------------------------------------------------------------------------------- /app/Enums/Roles.ts: -------------------------------------------------------------------------------- 1 | enum Roles { 2 | USER = 1, 3 | ADMIN = 2 4 | } 5 | 6 | export default Roles -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adocasts/user-role-authentication-example/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.json] 11 | insert_final_newline = ignore 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /tests/functional/hello_world.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | 3 | test('display welcome page', async ({ client }) => { 4 | const response = await client.get('/') 5 | 6 | response.assertStatus(200) 7 | response.assertTextIncludes('

It Works!

') 8 | }) 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3333 2 | HOST=0.0.0.0 3 | NODE_ENV=development 4 | APP_KEY=rBrogZ-FSjeO9d0w1cSFJ1WZRwU2DxhC 5 | DRIVE_DISK=local 6 | SESSION_DRIVER=cookie 7 | CACHE_VIEWS=false 8 | DB_CONNECTION=pg 9 | PG_HOST=localhost 10 | PG_PORT=5432 11 | PG_USER=lucid 12 | PG_PASSWORD= 13 | PG_DB_NAME=lucid 14 | SMTP_PORT=587 15 | SMTP_HOST= 16 | SMTP_USERNAME= 17 | SMTP_PASSWORD= 18 | -------------------------------------------------------------------------------- /resources/views/emails/verify.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 |
6 | {{ inspect(flashMessages.all()) }} 7 | 8 |

Please Verify Your Email

9 |

10 | We've send you an email with a link to verify your email, please click that link to continue. 11 |

12 |
13 | 14 | @endsection -------------------------------------------------------------------------------- /contracts/tests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://bit.ly/3DP1ypf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import '@japa/runner' 9 | 10 | declare module '@japa/runner' { 11 | interface TestContext { 12 | // Extend context 13 | } 14 | 15 | interface Test { 16 | // Extend test 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /start/globals.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Preloaded File 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Any code written inside this file will be executed during the application 7 | | boot. 8 | | 9 | */ 10 | import View from '@ioc:Adonis/Core/View' 11 | import Roles from 'App/Enums/Roles' 12 | 13 | View.global('Roles', Roles) -------------------------------------------------------------------------------- /contracts/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/Jfefs 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import { InferListFromConfig } from '@adonisjs/core/build/config' 9 | import hashConfig from '../config/hash' 10 | 11 | declare module '@ioc:Adonis/Core/Hash' { 12 | interface HashersList extends InferListFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JBt3I 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import { InferDisksFromConfig } from '@adonisjs/core/build/config' 9 | import driveConfig from '../config/drive' 10 | 11 | declare module '@ioc:Adonis/Core/Drive' { 12 | interface DisksList extends InferDisksFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JvgAT 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import { InferMailersFromConfig } from '@adonisjs/mail/build/config' 9 | import mailConfig from '../config/mail' 10 | 11 | declare module '@ioc:Adonis/Addons/Mail' { 12 | interface MailersList extends InferMailersFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /resources/views/test.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 |
6 | {{ inspect(flashMessages.all()) }} 7 | 8 | @if (auth.user) 9 | @if (nonVerifiedEmail) 10 |

You're Email Is NOT Verified

11 | @else 12 |

You're Email Is Verified

13 | @endif 14 | @else 15 |

You're Not Authenticated

16 | @endif 17 |
18 | 19 | @endsection -------------------------------------------------------------------------------- /app/Middleware/VerifiedEmail.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | 3 | export default class VerifiedEmail { 4 | public async handle({ auth, view }: HttpContextContract, next: () => Promise) { 5 | if (auth.user && !auth.user.isEmailVerified) { 6 | view.share({ nonVerifiedEmail: true }) 7 | } 8 | 9 | // code for middleware goes here. ABOVE THE NEXT CALL 10 | await next() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/Models/Role.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' 3 | 4 | export default class Role extends BaseModel { 5 | @column({ isPrimary: true }) 6 | public id: number 7 | 8 | @column() 9 | public name: string 10 | 11 | @column.dateTime({ autoCreate: true }) 12 | public createdAt: DateTime 13 | 14 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 15 | public updatedAt: DateTime 16 | } 17 | -------------------------------------------------------------------------------- /providers/AppProvider.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class AppProvider { 4 | constructor (protected app: ApplicationContract) { 5 | } 6 | 7 | public register () { 8 | // Register your own bindings 9 | } 10 | 11 | public async boot () { 12 | // IoC container is ready 13 | } 14 | 15 | public async ready () { 16 | // App is ready 17 | } 18 | 19 | public async shutdown () { 20 | // Cleanup, since app is going down 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Ace Commands 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is the entry point for running ace commands. 7 | | 8 | */ 9 | 10 | require('reflect-metadata') 11 | require('source-map-support').install({ handleUncaughtExceptions: false }) 12 | 13 | const { Ignitor } = require('@adonisjs/core/build/standalone') 14 | new Ignitor(__dirname) 15 | .ace() 16 | .handle(process.argv.slice(2)) 17 | -------------------------------------------------------------------------------- /resources/views/password/forgot.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 |
6 | {{ inspect(flashMessages.all()) }} 7 | 8 |

Reset Your Password

9 |

10 | Enter you email below and we'll send you a password reset link 11 |

12 | 13 |
14 | 15 | 18 |
19 |
20 | 21 | @endsection -------------------------------------------------------------------------------- /app/Middleware/Role.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Roles from 'App/Enums/Roles' 3 | 4 | export default class Role { 5 | // .middleware(['auth', 'role:admin']) 6 | public async handle({ response, auth }: HttpContextContract, next: () => Promise, guards: string[]) { 7 | const roleIds = guards.map(guard => Roles[guard.toUpperCase()]) 8 | 9 | if (!roleIds.includes(auth.user?.roleId)) { 10 | return response.unauthorized({ error: `This is restricted to ${guards.join(', ')} users` }) 11 | } 12 | 13 | // code for middleware goes here. ABOVE THE NEXT CALL 14 | await next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Middleware/SilentAuth.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | 3 | /** 4 | * Silent auth middleware can be used as a global middleware to silent check 5 | * if the user is logged-in or not. 6 | * 7 | * The request continues as usual, even when the user is not logged-in. 8 | */ 9 | export default class SilentAuthMiddleware { 10 | /** 11 | * Handle request 12 | */ 13 | public async handle({ auth }: HttpContextContract, next: () => Promise) { 14 | /** 15 | * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be 16 | * set to the instance of the currently logged in user. 17 | */ 18 | await auth.check() 19 | await next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | AdonisJs Server 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file is meant to bootstrap the AdonisJs application 7 | | and start the HTTP server to accept incoming connections. You must avoid 8 | | making this file dirty and instead make use of `lifecycle hooks` provided 9 | | by AdonisJs service providers for custom code. 10 | | 11 | */ 12 | 13 | import 'reflect-metadata' 14 | import sourceMapSupport from 'source-map-support' 15 | import { Ignitor } from '@adonisjs/core/build/standalone' 16 | 17 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 18 | 19 | new Ignitor(__dirname) 20 | .httpServer() 21 | .start() 22 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | import { listDirectoryFiles } from '@adonisjs/core/build/standalone' 2 | import Application from '@ioc:Adonis/Core/Application' 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Exporting an array of commands 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Instead of manually exporting each file from this directory, we use the 10 | | helper `listDirectoryFiles` to recursively collect and export an array 11 | | of filenames. 12 | | 13 | | Couple of things to note: 14 | | 15 | | 1. The file path must be relative from the project root and not this directory. 16 | | 2. We must ignore this file to avoid getting into an infinite loop 17 | | 18 | */ 19 | export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) 20 | -------------------------------------------------------------------------------- /resources/views/password/reset.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 |
6 | {{ inspect(flashMessages.all()) }} 7 | 8 |

Reset Your Password

9 | 10 | @if (isValid) 11 |

12 | Enter your new password below 13 |

14 | 15 |
16 | 17 | 18 | 21 |
22 | @else 23 |

24 | Your token is invalid or expired. Please try again 25 |

26 | @endif 27 |
28 | 29 | @endsection -------------------------------------------------------------------------------- /contracts/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JTm6U 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Env' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Getting types for validated environment variables 12 | |-------------------------------------------------------------------------- 13 | | 14 | | The `default` export from the "../env.ts" file exports types for the 15 | | validated environment variables. Here we merge them with the `EnvTypes` 16 | | interface so that you can enjoy intellisense when using the "Env" 17 | | module. 18 | | 19 | */ 20 | 21 | type CustomTypes = typeof import('../env').default 22 | interface EnvTypes extends CustomTypes { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/1668806613838_tokens.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'tokens' 5 | 6 | public async up () { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') 10 | table.string('type').notNullable() 11 | table.string('token', 64).notNullable() 12 | 13 | /** 14 | * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL 15 | */ 16 | table.timestamp('expires_at', { useTz: true }) 17 | table.timestamp('created_at', { useTz: true }) 18 | table.timestamp('updated_at', { useTz: true }) 19 | }) 20 | } 21 | 22 | public async down () { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "adonis-preset-ts/tsconfig.json", 3 | "include": [ 4 | "**/*" 5 | ], 6 | "exclude": [ 7 | "node_modules", 8 | "build" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "build", 12 | "rootDir": "./", 13 | "sourceMap": true, 14 | "paths": { 15 | "App/*": [ 16 | "./app/*" 17 | ], 18 | "Config/*": [ 19 | "./config/*" 20 | ], 21 | "Contracts/*": [ 22 | "./contracts/*" 23 | ], 24 | "Database/*": [ 25 | "./database/*" 26 | ] 27 | }, 28 | "types": [ 29 | "@adonisjs/core", 30 | "@adonisjs/repl", 31 | "@adonisjs/session", 32 | "@adonisjs/view", 33 | "@adonisjs/shield", 34 | "@japa/preset-adonis/build/adonis-typings", 35 | "@adonisjs/lucid", 36 | "@adonisjs/auth", 37 | "@adonisjs/mail" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JfefG 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Event' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Define typed events 12 | |-------------------------------------------------------------------------- 13 | | 14 | | You can define types for events inside the following interface and 15 | | AdonisJS will make sure that all listeners and emit calls adheres 16 | | to the defined types. 17 | | 18 | | For example: 19 | | 20 | | interface EventsList { 21 | | 'new:user': UserModel 22 | | } 23 | | 24 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 25 | | an instance of the the UserModel only. 26 | | 27 | */ 28 | interface EventsList { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/1667756114567_roles.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | import Roles from 'App/Enums/Roles' 3 | 4 | export default class extends BaseSchema { 5 | protected tableName = 'roles' 6 | 7 | public async up () { 8 | this.schema.createTable(this.tableName, (table) => { 9 | table.increments('id') 10 | table.string('name', 50).notNullable() 11 | 12 | /** 13 | * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL 14 | */ 15 | table.timestamp('created_at', { useTz: true }) 16 | table.timestamp('updated_at', { useTz: true }) 17 | }) 18 | 19 | this.defer(async (db) => { 20 | await db.table(this.tableName).multiInsert([{ 21 | id: Roles.USER, 22 | name: 'User' 23 | }, { 24 | id: Roles.ADMIN, 25 | name: 'Admin' 26 | }]) 27 | }) 28 | } 29 | 30 | public async down () { 31 | this.schema.dropTable(this.tableName) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Http Exception Handler 4 | |-------------------------------------------------------------------------- 5 | | 6 | | AdonisJs will forward all exceptions occurred during an HTTP request to 7 | | the following class. You can learn more about exception handling by 8 | | reading docs. 9 | | 10 | | The exception handler extends a base `HttpExceptionHandler` which is not 11 | | mandatory, however it can do lot of heavy lifting to handle the errors 12 | | properly. 13 | | 14 | */ 15 | 16 | import Logger from '@ioc:Adonis/Core/Logger' 17 | import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler' 18 | 19 | export default class ExceptionHandler extends HttpExceptionHandler { 20 | protected statusPages = { 21 | '403': 'errors/unauthorized', 22 | '404': 'errors/not-found', 23 | '500..599': 'errors/server-error', 24 | } 25 | 26 | constructor () { 27 | super(Logger) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/1667756129692_users.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | import Roles from 'App/Enums/Roles' 3 | 4 | export default class extends BaseSchema { 5 | protected tableName = 'users' 6 | 7 | public async up() { 8 | this.schema.createTable(this.tableName, (table) => { 9 | table.increments('id').primary() 10 | table.integer('role_id').unsigned().references('id').inTable('roles').defaultTo(Roles.USER) 11 | table.string('email', 255).notNullable().unique() 12 | table.string('password', 180).notNullable() 13 | table.string('remember_me_token').nullable() 14 | table.boolean('is_email_verified').notNullable().defaultTo(false) 15 | 16 | /** 17 | * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL 18 | */ 19 | table.timestamp('created_at', { useTz: true }).notNullable() 20 | table.timestamp('updated_at', { useTz: true }).notNullable() 21 | }) 22 | } 23 | 24 | public async down() { 25 | this.schema.dropTable(this.tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonis-user-role-authentication", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node ace serve --watch", 7 | "build": "node ace build --production", 8 | "start": "node server.js" 9 | }, 10 | "devDependencies": { 11 | "@adonisjs/assembler": "^5.9.3", 12 | "@japa/preset-adonis": "^1.2.0", 13 | "@japa/runner": "^2.2.2", 14 | "adonis-preset-ts": "^2.1.0", 15 | "pino-pretty": "^9.1.1", 16 | "typescript": "~4.6", 17 | "youch": "^3.2.2", 18 | "youch-terminal": "^2.1.5" 19 | }, 20 | "dependencies": { 21 | "@adonisjs/auth": "^8.2.3", 22 | "@adonisjs/core": "^5.8.8", 23 | "@adonisjs/lucid": "^18.2.0", 24 | "@adonisjs/mail": "^8.1.2", 25 | "@adonisjs/repl": "^3.1.11", 26 | "@adonisjs/session": "^6.4.0", 27 | "@adonisjs/shield": "^7.1.0", 28 | "@adonisjs/view": "^6.2.0", 29 | "luxon": "^3.1.0", 30 | "pg": "^8.8.0", 31 | "phc-argon2": "^1.1.3", 32 | "proxy-addr": "^2.0.7", 33 | "reflect-metadata": "^0.1.13", 34 | "source-map-support": "^0.5.21" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/welcome.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 | @if (!auth.user) 6 |
7 |

Register Form

8 | 9 | 10 | 13 |
14 | 15 |
16 |

Login Form

17 | 18 | 19 | 22 | 27 |
28 | @else 29 |
30 | @if (auth.user.isAdmin) 31 |
Admin User {{ auth.user.email }}
32 | 37 | @else 38 | Regular User {{ auth.user.email }} 39 | @endif 40 | Logout 41 |
42 | @endif 43 | 44 | @endsection -------------------------------------------------------------------------------- /app/Mailers/VerifyEmail.ts: -------------------------------------------------------------------------------- 1 | import { BaseMailer, MessageContract } from '@ioc:Adonis/Addons/Mail' 2 | import User from 'App/Models/User' 3 | import Env from '@ioc:Adonis/Core/Env' 4 | import Route from '@ioc:Adonis/Core/Route' 5 | 6 | export default class VerifyEmail extends BaseMailer { 7 | constructor(private user: User, private token: string) { 8 | super() 9 | } 10 | /** 11 | * WANT TO USE A DIFFERENT MAILER? 12 | * 13 | * Uncomment the following line of code to use a different 14 | * mailer and chain the ".options" method to pass custom 15 | * options to the send method 16 | */ 17 | // public mailer = this.mail.use() 18 | 19 | /** 20 | * The prepare method is invoked automatically when you run 21 | * "VerifyEmail.send". 22 | * 23 | * Use this method to prepare the email message. The method can 24 | * also be async. 25 | */ 26 | public prepare(message: MessageContract) { 27 | const domain = Env.get('DOMAIN') 28 | const path = Route.makeUrl('verify.email.verify', [this.token]) 29 | const url = domain + path 30 | message 31 | .subject('Please Verify Your Email') 32 | .from('noreply@adocasts.com') 33 | .to(this.user.email) 34 | .html(` 35 | Please click the following link to verify your email 36 | Verify email 37 | `) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Controllers/Http/VerifyEmailController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Token from 'App/Models/Token' 3 | 4 | export default class VerifyEmailController { 5 | public async index({ view, auth }: HttpContextContract) { 6 | await auth.user?.sendVerifyEmail() 7 | return view.render('emails/verify') 8 | } 9 | 10 | public async verify({ response, session, params, auth }: HttpContextContract) { 11 | const user = await Token.getTokenUser(params.token, 'VERIFY_EMAIL') 12 | const isMatch = user?.id === auth.user?.id 13 | 14 | // if token is valid and bound to a user, but user is not authenticated 15 | if (user && !auth.user) { 16 | // return to login page & verify email after successful login 17 | session.put('isVerifyingEmail', true) 18 | return response.redirect().toPath('/') 19 | } 20 | 21 | // if token is invalid, not bound to a user, or does not match the auth user 22 | if (!user || !isMatch) { 23 | // handle invalid token 24 | session.flash('token', 'Your token is invalid or expired') 25 | return response.redirect().toRoute('verify.email') 26 | } 27 | 28 | user.isEmailVerified = true 29 | await user.save() 30 | await Token.expireTokens(user, 'verifyEmailTokens') 31 | 32 | return response.redirect().toPath('/') 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Tests 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file boots the AdonisJS application and configures 7 | | the Japa tests runner. 8 | | 9 | | For the most part you will never edit this file. The configuration 10 | | for the tests can be controlled via ".adonisrc.json" and 11 | | "tests/bootstrap.ts" files. 12 | | 13 | */ 14 | 15 | process.env.NODE_ENV = 'test' 16 | 17 | import 'reflect-metadata' 18 | import sourceMapSupport from 'source-map-support' 19 | import { Ignitor } from '@adonisjs/core/build/standalone' 20 | import { configure, processCliArgs, run, RunnerHooksHandler } from '@japa/runner' 21 | 22 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 23 | 24 | const kernel = new Ignitor(__dirname).kernel('test') 25 | 26 | kernel 27 | .boot() 28 | .then(() => import('./tests/bootstrap')) 29 | .then(({ runnerHooks, ...config }) => { 30 | const app: RunnerHooksHandler[] = [() => kernel.start()] 31 | 32 | configure({ 33 | ...kernel.application.rcFile.tests, 34 | ...processCliArgs(process.argv.slice(2)), 35 | ...config, 36 | ...{ 37 | importer: (filePath) => import(filePath), 38 | setup: app.concat(runnerHooks.setup), 39 | teardown: runnerHooks.teardown, 40 | }, 41 | cwd: kernel.application.appRoot 42 | }) 43 | 44 | run() 45 | }) 46 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Validating Environment Variables 4 | |-------------------------------------------------------------------------- 5 | | 6 | | In this file we define the rules for validating environment variables. 7 | | By performing validation we ensure that your application is running in 8 | | a stable environment with correct configuration values. 9 | | 10 | | This file is read automatically by the framework during the boot lifecycle 11 | | and hence do not rename or move this file to a different location. 12 | | 13 | */ 14 | 15 | import Env from '@ioc:Adonis/Core/Env' 16 | 17 | export default Env.rules({ 18 | HOST: Env.schema.string({ format: 'host' }), 19 | PORT: Env.schema.number(), 20 | DOMAIN: Env.schema.string(), 21 | APP_KEY: Env.schema.string(), 22 | APP_NAME: Env.schema.string(), 23 | CACHE_VIEWS: Env.schema.boolean(), 24 | SESSION_DRIVER: Env.schema.string(), 25 | DRIVE_DISK: Env.schema.enum(['local'] as const), 26 | NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), 27 | 28 | DB_CONNECTION: Env.schema.string(), 29 | PG_HOST: Env.schema.string({ format: 'host' }), 30 | PG_PORT: Env.schema.number(), 31 | PG_USER: Env.schema.string(), 32 | PG_PASSWORD: Env.schema.string.optional(), 33 | PG_DB_NAME: Env.schema.string(), 34 | 35 | SMTP_HOST: Env.schema.string({ format: 'host' }), 36 | SMTP_PORT: Env.schema.number(), 37 | SMTP_USERNAME: Env.schema.string(), 38 | SMTP_PASSWORD: Env.schema.string(), 39 | }) 40 | -------------------------------------------------------------------------------- /.adonisrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": true, 3 | "commands": [ 4 | "./commands", 5 | "@adonisjs/core/build/commands/index.js", 6 | "@adonisjs/repl/build/commands", 7 | "@adonisjs/lucid/build/commands", 8 | "@adonisjs/mail/build/commands" 9 | ], 10 | "exceptionHandlerNamespace": "App/Exceptions/Handler", 11 | "aliases": { 12 | "App": "app", 13 | "Config": "config", 14 | "Database": "database", 15 | "Contracts": "contracts" 16 | }, 17 | "preloads": [ 18 | "./start/routes", 19 | "./start/kernel", 20 | { 21 | "file": "./start/globals", 22 | "environment": [ 23 | "web" 24 | ] 25 | } 26 | ], 27 | "providers": [ 28 | "./providers/AppProvider", 29 | "@adonisjs/core", 30 | "@adonisjs/session", 31 | "@adonisjs/view", 32 | "@adonisjs/shield", 33 | "@adonisjs/lucid", 34 | "@adonisjs/auth", 35 | "@adonisjs/mail" 36 | ], 37 | "metaFiles": [ 38 | { 39 | "pattern": "public/**", 40 | "reloadServer": false 41 | }, 42 | { 43 | "pattern": "resources/views/**/*.edge", 44 | "reloadServer": false 45 | } 46 | ], 47 | "aceProviders": [ 48 | "@adonisjs/repl" 49 | ], 50 | "tests": { 51 | "suites": [ 52 | { 53 | "name": "functional", 54 | "files": [ 55 | "tests/functional/**/*.spec(.ts|.js)" 56 | ], 57 | "timeout": 60000 58 | } 59 | ] 60 | }, 61 | "testProviders": [ 62 | "@japa/preset-adonis/TestsProvider" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /app/Controllers/Http/AuthController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 3 | import User from 'App/Models/User' 4 | 5 | export default class AuthController { 6 | public async register({ request, response, auth }: HttpContextContract) { 7 | const userSchema = schema.create({ 8 | email: schema.string([rules.email(), rules.trim()]), 9 | password: schema.string([rules.minLength(8)]) 10 | }) 11 | 12 | const data = await request.validate({ schema: userSchema }) 13 | const user = await User.create(data) 14 | 15 | await auth.login(user) 16 | await user.sendVerifyEmail() 17 | 18 | return response.redirect().toPath('/') 19 | } 20 | 21 | public async login({ request, response, session, auth }: HttpContextContract) { 22 | const { email, password } = request.only(['email', 'password']) 23 | 24 | try { 25 | await auth.attempt(email, password) 26 | } catch (_error) { 27 | session.flash('errors', 'Email or password is incorrect') 28 | return response.redirect().back() 29 | } 30 | 31 | if (auth.user && session.has('isVerifyingEmail')) { 32 | // verify their email 33 | auth.user.isEmailVerified = true 34 | await auth.user.save() 35 | } 36 | 37 | return response.redirect().toPath('/') 38 | } 39 | 40 | public async logout({ response, auth }: HttpContextContract) { 41 | await auth.logout() 42 | return response.redirect().toPath('/') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Controllers/Http/UsersController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Role from 'App/Models/Role' 3 | import User from 'App/Models/User' 4 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 5 | import Roles from 'App/Enums/Roles' 6 | 7 | export default class UsersController { 8 | public async manage({ view }: HttpContextContract) { 9 | const users = await User.query() 10 | .orderBy('email') 11 | 12 | const roles = await Role.query() 13 | .orderBy('name') 14 | 15 | return view.render('users/manage', { users, roles }) 16 | } 17 | 18 | public async role({ request, response, params, auth }: HttpContextContract) { 19 | const roleSchema = schema.create({ 20 | roleId: schema.number([rules.exists({ table: 'roles', column: 'id' })]) 21 | }) 22 | 23 | const data = await request.validate({ schema: roleSchema }) 24 | const user = await User.findOrFail(params.id) 25 | const isAuthUser = user.id === auth.user?.id 26 | 27 | await user.merge(data).save() 28 | 29 | return isAuthUser && user.roleId !== Roles.ADMIN 30 | ? response.redirect().toPath('/') 31 | : response.redirect().back() 32 | } 33 | 34 | public async destroy({ response, params, auth }: HttpContextContract) { 35 | const user = await User.findOrFail(params.id) 36 | const isAuthUser = user.id === auth.user?.id 37 | 38 | await user.delete() 39 | 40 | return isAuthUser 41 | ? response.redirect().toPath('/') 42 | : response.redirect().back() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/database.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JesV9 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { DatabaseConfig } from '@ioc:Adonis/Lucid/Database' 10 | 11 | const databaseConfig: DatabaseConfig = { 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Connection 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The primary connection for making database queries across the application 18 | | You can use any key from the `connections` object defined in this same 19 | | file. 20 | | 21 | */ 22 | connection: Env.get('DB_CONNECTION'), 23 | 24 | connections: { 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | PostgreSQL config 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Configuration for PostgreSQL database. Make sure to install the driver 31 | | from npm when using this connection 32 | | 33 | | npm i pg 34 | | 35 | */ 36 | pg: { 37 | client: 'pg', 38 | connection: { 39 | host: Env.get('PG_HOST'), 40 | port: Env.get('PG_PORT'), 41 | user: Env.get('PG_USER'), 42 | password: Env.get('PG_PASSWORD', ''), 43 | database: Env.get('PG_DB_NAME'), 44 | }, 45 | migrations: { 46 | naturalSort: true, 47 | }, 48 | healthCheck: false, 49 | debug: false, 50 | }, 51 | 52 | } 53 | } 54 | 55 | export default databaseConfig 56 | -------------------------------------------------------------------------------- /resources/views/users/manage.edge: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 | 5 |
6 |

User Management

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @each (user in users) 19 | 20 | 26 | 37 | 38 | 45 | 46 | @endeach 47 | 48 |
EmailRoleJoined
21 | {{ user.email }} 22 | @if (user.id === auth.user?.id) 23 | (you) 24 | @endif 25 | 27 |
28 | 35 |
36 |
{{ user.createdAt.toLocaleString() }} 39 |
40 | 43 |
44 |
49 |
50 | 51 | @endsection -------------------------------------------------------------------------------- /config/mail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JvgAf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { mailConfig } from '@adonisjs/mail/build/config' 10 | 11 | export default mailConfig({ 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Default mailer 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The following mailer will be used to send emails, when you don't specify 18 | | a mailer 19 | | 20 | */ 21 | mailer: 'smtp', 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Mailers 26 | |-------------------------------------------------------------------------- 27 | | 28 | | You can define or more mailers to send emails from your application. A 29 | | single `driver` can be used to define multiple mailers with different 30 | | config. 31 | | 32 | | For example: Postmark driver can be used to have different mailers for 33 | | sending transactional and promotional emails 34 | | 35 | */ 36 | mailers: { 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Smtp 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Uses SMTP protocol for sending email 43 | | 44 | */ 45 | smtp: { 46 | driver: 'smtp', 47 | host: Env.get('SMTP_HOST'), 48 | port: Env.get('SMTP_PORT'), 49 | auth: { 50 | user: Env.get('SMTP_USERNAME'), 51 | pass: Env.get('SMTP_PASSWORD'), 52 | type: 'login', 53 | } 54 | }, 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /start/kernel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Application middleware 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is used to define middleware for HTTP requests. You can register 7 | | middleware as a `closure` or an IoC container binding. The bindings are 8 | | preferred, since they keep this file clean. 9 | | 10 | */ 11 | 12 | import Server from '@ioc:Adonis/Core/Server' 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Global middleware 17 | |-------------------------------------------------------------------------- 18 | | 19 | | An array of global middleware, that will be executed in the order they 20 | | are defined for every HTTP requests. 21 | | 22 | */ 23 | Server.middleware.register([ 24 | () => import('@ioc:Adonis/Core/BodyParser'), 25 | () => import('App/Middleware/SilentAuth') 26 | ]) 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Named middleware 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Named middleware are defined as key-value pair. The value is the namespace 34 | | or middleware function and key is the alias. Later you can use these 35 | | alias on individual routes. For example: 36 | | 37 | | { auth: () => import('App/Middleware/Auth') } 38 | | 39 | | and then use it as follows 40 | | 41 | | Route.get('dashboard', 'UserController.dashboard').middleware('auth') 42 | | 43 | */ 44 | Server.middleware.registerNamed({ 45 | auth: () => import('App/Middleware/Auth'), 46 | role: () => import('App/Middleware/Role'), 47 | verifyEmail: () => import('App/Middleware/VerifiedEmail') 48 | }) 49 | -------------------------------------------------------------------------------- /app/Models/User.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import Hash from '@ioc:Adonis/Core/Hash' 3 | import { column, beforeSave, BaseModel, belongsTo, BelongsTo, computed, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 4 | import Role from './Role' 5 | import Roles from 'App/Enums/Roles' 6 | import Token from './Token' 7 | import VerifyEmail from 'App/Mailers/VerifyEmail' 8 | 9 | export default class User extends BaseModel { 10 | @column({ isPrimary: true }) 11 | public id: number 12 | 13 | @column() 14 | public roleId: number 15 | 16 | @column() 17 | public email: string 18 | 19 | @column({ serializeAs: null }) 20 | public password: string 21 | 22 | @column() 23 | public rememberMeToken: string | null 24 | 25 | @column() 26 | public isEmailVerified: boolean = false 27 | 28 | @column.dateTime({ autoCreate: true }) 29 | public createdAt: DateTime 30 | 31 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 32 | public updatedAt: DateTime 33 | 34 | @computed() 35 | public get isAdmin() { 36 | return this.roleId === Roles.ADMIN 37 | } 38 | 39 | @belongsTo(() => Role) 40 | public role: BelongsTo 41 | 42 | @hasMany(() => Token) 43 | public tokens: HasMany 44 | 45 | @hasMany(() => Token, { 46 | onQuery: query => query.where('type', 'PASSWORD_RESET') 47 | }) 48 | public passwordResetTokens: HasMany 49 | 50 | @hasMany(() => Token, { 51 | onQuery: query => query.where('type', 'VERIFY_EMAIL') 52 | }) 53 | public verifyEmailTokens: HasMany 54 | 55 | @beforeSave() 56 | public static async hashPassword (user: User) { 57 | if (user.$dirty.password) { 58 | user.password = await Hash.make(user.password) 59 | } 60 | } 61 | 62 | public async sendVerifyEmail() { 63 | const token = await Token.generateVerifyEmailToken(this) 64 | await new VerifyEmail(this, token).sendLater() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /start/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Routes 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is dedicated for defining HTTP routes. A single file is enough 7 | | for majority of projects, however you can define routes in different 8 | | files and just make sure to import them inside this file. For example 9 | | 10 | | Define routes in following two files 11 | | ├── start/routes/cart.ts 12 | | ├── start/routes/customer.ts 13 | | 14 | | and then import them inside `start/routes.ts` as follows 15 | | 16 | | import './routes/cart' 17 | | import './routes/customer'' 18 | | 19 | */ 20 | 21 | import Route from '@ioc:Adonis/Core/Route' 22 | 23 | Route.get('/', async ({ view }) => { 24 | return view.render('welcome') 25 | }) 26 | 27 | Route.get('/test', async ({ view }) => { 28 | return view.render('test') 29 | }).middleware(['verifyEmail']) 30 | 31 | Route.post('/auth/register', 'AuthController.register').as('auth.register') 32 | Route.post('/auth/login', 'AuthController.login').as('auth.login') 33 | Route.get('/auth/logout', 'AuthController.logout').as('auth.logout') 34 | 35 | Route.get('/verify/email', 'VerifyEmailController.index').as('verify.email').middleware(['auth']) 36 | Route.get('/verify/email/:token', 'VerifyEmailController.verify').as('verify.email.verify') 37 | 38 | Route.get('/password/forgot', 'PasswordResetController.forgot').as('password.forgot') 39 | Route.post('/password/send', 'PasswordResetController.send').as('password.send') 40 | Route.get('/password/reset/:token', 'PasswordResetController.reset').as('password.reset') 41 | Route.post('/password/store', 'PasswordResetController.store').as('password.store') 42 | 43 | Route.group(() => { 44 | 45 | Route.get('/manage', 'UsersController.manage').as('manage') 46 | Route.patch('/:id/role', 'UsersController.role').as('role') 47 | Route.delete('/:id', 'UsersController.destroy').as('destroy') 48 | 49 | }).prefix('users').as('users').middleware(['auth', 'role:admin']) -------------------------------------------------------------------------------- /config/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefW 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { hashConfig } from '@adonisjs/core/build/config' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Hash Config 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The `HashConfig` relies on the `HashList` interface which is 17 | | defined inside `contracts` directory. 18 | | 19 | */ 20 | export default hashConfig({ 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Default hasher 24 | |-------------------------------------------------------------------------- 25 | | 26 | | By default we make use of the argon hasher to hash values. However, feel 27 | | free to change the default value 28 | | 29 | */ 30 | default: Env.get('HASH_DRIVER', 'argon'), 31 | 32 | list: { 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Argon 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Argon mapping uses the `argon2` driver to hash values. 39 | | 40 | | Make sure you install the underlying dependency for this driver to work. 41 | | https://www.npmjs.com/package/phc-argon2. 42 | | 43 | | npm install phc-argon2 44 | | 45 | */ 46 | argon: { 47 | driver: 'argon2', 48 | variant: 'id', 49 | iterations: 3, 50 | memory: 4096, 51 | parallelism: 1, 52 | saltSize: 16, 53 | }, 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Bcrypt 58 | |-------------------------------------------------------------------------- 59 | | 60 | | Bcrypt mapping uses the `bcrypt` driver to hash values. 61 | | 62 | | Make sure you install the underlying dependency for this driver to work. 63 | | https://www.npmjs.com/package/phc-bcrypt. 64 | | 65 | | npm install phc-bcrypt 66 | | 67 | */ 68 | bcrypt: { 69 | driver: 'bcrypt', 70 | rounds: 10, 71 | }, 72 | }, 73 | }) 74 | -------------------------------------------------------------------------------- /resources/views/layouts/app.edge: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AdonisJS - A fully featured web framework for Node.js 7 | 8 | 89 | 90 | 91 | @if (nonVerifiedEmail) 92 |

93 | Please verify your email by clicking here 94 |

95 | @endif 96 | 97 |
98 | @!section('content') 99 |
100 | 101 | 102 | -------------------------------------------------------------------------------- /tests/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File source: https://bit.ly/3ukaHTz 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { Config } from '@japa/runner' 9 | import TestUtils from '@ioc:Adonis/Core/TestUtils' 10 | import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Japa Plugins 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Japa plugins allows you to add additional features to Japa. By default 18 | | we register the assertion plugin. 19 | | 20 | | Feel free to remove existing plugins or add more. 21 | | 22 | */ 23 | export const plugins: Config['plugins'] = [assert(), runFailedTests(), apiClient()] 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Japa Reporters 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Japa reporters displays/saves the progress of tests as they are executed. 31 | | By default, we register the spec reporter to show a detailed report 32 | | of tests on the terminal. 33 | | 34 | */ 35 | export const reporters: Config['reporters'] = [specReporter()] 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Runner hooks 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Runner hooks are executed after booting the AdonisJS app and 43 | | before the test files are imported. 44 | | 45 | | You can perform actions like starting the HTTP server or running migrations 46 | | within the runner hooks 47 | | 48 | */ 49 | export const runnerHooks: Required> = { 50 | setup: [() => TestUtils.ace().loadCommands()], 51 | teardown: [], 52 | } 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Configure individual suites 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The configureSuite method gets called for every test suite registered 60 | | within ".adonisrc.json" file. 61 | | 62 | | You can use this method to configure suites. For example: Only start 63 | | the HTTP server when it is a functional suite. 64 | */ 65 | export const configureSuite: Config['configureSuite'] = (suite) => { 66 | if (suite.name === 'functional') { 67 | suite.setup(() => TestUtils.httpServer().start()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Controllers/Http/PasswordResetController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Mail from '@ioc:Adonis/Addons/Mail' 3 | import Route from '@ioc:Adonis/Core/Route' 4 | import Env from '@ioc:Adonis/Core/Env' 5 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 6 | import User from 'App/Models/User' 7 | import Token from 'App/Models/Token' 8 | 9 | export default class PasswordResetController { 10 | public async forgot({ view }: HttpContextContract) { 11 | return view.render('password.forgot') 12 | } 13 | 14 | public async send({ request, response, session }: HttpContextContract) { 15 | const emailSchema = schema.create({ 16 | email: schema.string([rules.email()]) 17 | }) 18 | 19 | const { email } = await request.validate({ schema: emailSchema }) 20 | const user = await User.findBy('email', email) 21 | const token = await Token.generatePasswordResetToken(user) 22 | const resetLink = Route.makeUrl('password.reset', [token]) 23 | 24 | if (user) { 25 | await Mail.sendLater(message => { 26 | message 27 | .from('noreply@adocasts.com') 28 | .to(user.email) 29 | .subject('Reset Your Password') 30 | .html(`Reset your password by clicking here`) 31 | }) 32 | } 33 | 34 | session.flash('success', 'If an account matches the provided email, you will recieve a password reset link shortly') 35 | return response.redirect().back() 36 | } 37 | 38 | public async reset({ view, params }: HttpContextContract) { 39 | const token = params.token 40 | const isValid = await Token.verify(token, 'PASSWORD_RESET') 41 | 42 | return view.render('password/reset', { isValid, token }) 43 | } 44 | 45 | public async store({ request, response, session, auth }: HttpContextContract) { 46 | const passwordSchema = schema.create({ 47 | token: schema.string(), 48 | password: schema.string([rules.minLength(8)]) 49 | }) 50 | 51 | const { token, password } = await request.validate({ schema: passwordSchema }) 52 | const user = await Token.getTokenUser(token, 'PASSWORD_RESET') 53 | 54 | if (!user) { 55 | session.flash('error', 'Token expired or associated user could not be found') 56 | return response.redirect().back() 57 | } 58 | 59 | await user.merge({ password }).save() 60 | await auth.login(user) 61 | await Token.expireTokens(user, 'passwordResetTokens') 62 | 63 | return response.redirect().toPath('/') 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JOdz5 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this 5 | * file. 6 | */ 7 | 8 | import User from 'App/Models/User' 9 | 10 | declare module '@ioc:Adonis/Addons/Auth' { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Providers 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The providers are used to fetch users. The Auth module comes pre-bundled 17 | | with two providers that are `Lucid` and `Database`. Both uses database 18 | | to fetch user details. 19 | | 20 | | You can also create and register your own custom providers. 21 | | 22 | */ 23 | interface ProvidersList { 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | User Provider 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The following provider uses Lucid models as a driver for fetching user 30 | | details from the database for authentication. 31 | | 32 | | You can create multiple providers using the same underlying driver with 33 | | different Lucid models. 34 | | 35 | */ 36 | user: { 37 | implementation: LucidProviderContract 38 | config: LucidProviderConfig 39 | } 40 | } 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Guards 45 | |-------------------------------------------------------------------------- 46 | | 47 | | The guards are used for authenticating users using different drivers. 48 | | The auth module comes with 3 different guards. 49 | | 50 | | - SessionGuardContract 51 | | - BasicAuthGuardContract 52 | | - OATGuardContract ( Opaque access token ) 53 | | 54 | | Every guard needs a provider for looking up users from the database. 55 | | 56 | */ 57 | interface GuardsList { 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Web Guard 61 | |-------------------------------------------------------------------------- 62 | | 63 | | The web guard uses sessions for maintaining user login state. It uses 64 | | the `user` provider for fetching user details. 65 | | 66 | */ 67 | web: { 68 | implementation: SessionGuardContract<'user', 'web'> 69 | config: SessionGuardConfig<'user'> 70 | client: SessionClientContract<'user'> 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Middleware/Auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationException } from '@adonisjs/auth/build/standalone' 2 | import type { GuardsList } from '@ioc:Adonis/Addons/Auth' 3 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 4 | 5 | /** 6 | * Auth middleware is meant to restrict un-authenticated access to a given route 7 | * or a group of routes. 8 | * 9 | * You must register this middleware inside `start/kernel.ts` file under the list 10 | * of named middleware. 11 | */ 12 | export default class AuthMiddleware { 13 | /** 14 | * The URL to redirect to when request is Unauthorized 15 | */ 16 | protected redirectTo = '/login' 17 | 18 | /** 19 | * Authenticates the current HTTP request against a custom set of defined 20 | * guards. 21 | * 22 | * The authentication loop stops as soon as the user is authenticated using any 23 | * of the mentioned guards and that guard will be used by the rest of the code 24 | * during the current request. 25 | */ 26 | protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) { 27 | /** 28 | * Hold reference to the guard last attempted within the for loop. We pass 29 | * the reference of the guard to the "AuthenticationException", so that 30 | * it can decide the correct response behavior based upon the guard 31 | * driver 32 | */ 33 | let guardLastAttempted: string | undefined 34 | 35 | for (let guard of guards) { 36 | guardLastAttempted = guard 37 | 38 | if (await auth.use(guard).check()) { 39 | /** 40 | * Instruct auth to use the given guard as the default guard for 41 | * the rest of the request, since the user authenticated 42 | * succeeded here 43 | */ 44 | auth.defaultGuard = guard 45 | return true 46 | } 47 | } 48 | 49 | /** 50 | * Unable to authenticate using any guard 51 | */ 52 | throw new AuthenticationException( 53 | 'Unauthorized access', 54 | 'E_UNAUTHORIZED_ACCESS', 55 | guardLastAttempted, 56 | this.redirectTo, 57 | ) 58 | } 59 | 60 | /** 61 | * Handle request 62 | */ 63 | public async handle ( 64 | { auth }: HttpContextContract, 65 | next: () => Promise, 66 | customGuards: (keyof GuardsList)[] 67 | ) { 68 | /** 69 | * Uses the user defined guards or the default guard mentioned in 70 | * the config file 71 | */ 72 | const guards = customGuards.length ? customGuards : [auth.name] 73 | await this.authenticate(auth, guards) 74 | await next() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Models/Token.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 3 | import { string } from '@ioc:Adonis/Core/Helpers' 4 | import User from './User' 5 | 6 | type TokenType = 'PASSWORD_RESET' | 'VERIFY_EMAIL' 7 | 8 | export default class Token extends BaseModel { 9 | @column({ isPrimary: true }) 10 | public id: number 11 | 12 | @column() 13 | public userId: number | null 14 | 15 | @column() 16 | public type: string 17 | 18 | @column() 19 | public token: string 20 | 21 | @column.dateTime() 22 | public expiresAt: DateTime | null 23 | 24 | @column.dateTime({ autoCreate: true }) 25 | public createdAt: DateTime 26 | 27 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 28 | public updatedAt: DateTime 29 | 30 | @belongsTo(() => User) 31 | public user: BelongsTo 32 | 33 | public static async generateVerifyEmailToken(user: User) { 34 | const token = string.generateRandom(64) 35 | 36 | await Token.expireTokens(user, 'verifyEmailTokens') 37 | const record = await user.related('tokens').create({ 38 | type: 'VERIFY_EMAIL', 39 | expiresAt: DateTime.now().plus({ hours: 24 }), 40 | token 41 | }) 42 | 43 | return record.token 44 | } 45 | 46 | public static async generatePasswordResetToken(user: User | null) { 47 | const token = string.generateRandom(64) 48 | 49 | if (!user) return token 50 | 51 | await Token.expireTokens(user, 'passwordResetTokens') 52 | const record = await user.related('tokens').create({ 53 | type: 'PASSWORD_RESET', 54 | expiresAt: DateTime.now().plus({ hour: 1 }), 55 | token 56 | }) 57 | 58 | return record.token 59 | } 60 | 61 | public static async expireTokens(user: User, relationName: 'passwordResetTokens' | 'verifyEmailTokens') { 62 | await user.related(relationName).query().update({ 63 | expiresAt: DateTime.now() 64 | }) 65 | } 66 | 67 | public static async getTokenUser(token: string, type: TokenType) { 68 | const record = await Token.query() 69 | .preload('user') 70 | .where('token', token) 71 | .where('type', type) 72 | .where('expiresAt', '>', DateTime.now().toSQL()) 73 | .orderBy('createdAt', 'desc') 74 | .first() 75 | 76 | return record?.user 77 | } 78 | 79 | public static async verify(token: string, type: TokenType) { 80 | const record = await Token.query() 81 | .where('expiresAt', '>', DateTime.now().toSQL()) 82 | .where('token', token) 83 | .where('type', type) 84 | .first() 85 | 86 | return !!record 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /config/static.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jfefl 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import { AssetsConfig } from '@ioc:Adonis/Core/Static' 9 | 10 | const staticConfig: AssetsConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Enabled 14 | |-------------------------------------------------------------------------- 15 | | 16 | | A boolean to enable or disable serving static files. The static files 17 | | are served from the `public` directory inside the application root. 18 | | However, you can override the default path inside `.adonisrc.json` 19 | | file. 20 | | 21 | | 22 | */ 23 | enabled: true, 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Handling Dot Files 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Decide how you want the static assets server to handle the `dotfiles`. 31 | | By default, we ignore them as if they don't exists. However, you 32 | | can choose between one of the following options. 33 | | 34 | | - ignore: Behave as if the file doesn't exists. Results in 404. 35 | | - deny: Deny access to the file. Results in 403. 36 | | - allow: Serve the file contents 37 | | 38 | */ 39 | dotFiles: 'ignore', 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Generating Etag 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Handle whether or not to generate etags for the files. Etag allows browser 47 | | to utilize the cache when file hasn't been changed. 48 | | 49 | */ 50 | etag: true, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Set Last Modified 55 | |-------------------------------------------------------------------------- 56 | | 57 | | Whether or not to set the `Last-Modified` header in the response. Uses 58 | | the file system's last modified value. 59 | | 60 | */ 61 | lastModified: true, 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Max age 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Set the value for the max-age directive. Set a higher value in production 69 | | if you fingerprint your assets. 70 | | 71 | | Learn more: https://docs.adonisjs.com/guides/deployment#serving-static-assets 72 | | 73 | */ 74 | maxAge: 0, 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Immutable 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Set the immutable directive. Set it to `true` if the assets are generated 82 | | with a fingerprint. In others words the file name changes when the file 83 | | contents change. 84 | | 85 | */ 86 | immutable: false, 87 | } 88 | 89 | export default staticConfig 90 | -------------------------------------------------------------------------------- /config/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JY0mp 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { AuthConfig } from '@ioc:Adonis/Addons/Auth' 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Authentication Mapping 13 | |-------------------------------------------------------------------------- 14 | | 15 | | List of available authentication mapping. You must first define them 16 | | inside the `contracts/auth.ts` file before mentioning them here. 17 | | 18 | */ 19 | const authConfig: AuthConfig = { 20 | guard: 'web', 21 | guards: { 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Web Guard 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Web guard uses classic old school sessions for authenticating users. 28 | | If you are building a standard web application, it is recommended to 29 | | use web guard with session driver 30 | | 31 | */ 32 | web: { 33 | driver: 'session', 34 | 35 | provider: { 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Driver 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Name of the driver 42 | | 43 | */ 44 | driver: 'lucid', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Identifier key 49 | |-------------------------------------------------------------------------- 50 | | 51 | | The identifier key is the unique key on the model. In most cases specifying 52 | | the primary key is the right choice. 53 | | 54 | */ 55 | identifierKey: 'id', 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Uids 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Uids are used to search a user against one of the mentioned columns. During 63 | | login, the auth module will search the user mentioned value against one 64 | | of the mentioned columns to find their user record. 65 | | 66 | */ 67 | uids: ['email'], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Model 72 | |-------------------------------------------------------------------------- 73 | | 74 | | The model to use for fetching or finding users. The model is imported 75 | | lazily since the config files are read way earlier in the lifecycle 76 | | of booting the app and the models may not be in a usable state at 77 | | that time. 78 | | 79 | */ 80 | model: () => import('App/Models/User'), 81 | }, 82 | }, 83 | }, 84 | } 85 | 86 | export default authConfig 87 | -------------------------------------------------------------------------------- /config/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JeYHp 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import Application from '@ioc:Adonis/Core/Application' 10 | import { sessionConfig } from '@adonisjs/session/build/config' 11 | 12 | export default sessionConfig({ 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Enable/Disable sessions 16 | |-------------------------------------------------------------------------- 17 | | 18 | | Setting the following property to "false" will disable the session for the 19 | | entire application 20 | | 21 | */ 22 | enabled: true, 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Driver 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The session driver to use. You can choose between one of the following 30 | | drivers. 31 | | 32 | | - cookie (Uses signed cookies to store session values) 33 | | - file (Uses filesystem to store session values) 34 | | - redis (Uses redis. Make sure to install "@adonisjs/redis" as well) 35 | | 36 | | Note: Switching drivers will make existing sessions invalid. 37 | | 38 | */ 39 | driver: Env.get('SESSION_DRIVER'), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Cookie name 44 | |-------------------------------------------------------------------------- 45 | | 46 | | The name of the cookie that will hold the session id. 47 | | 48 | */ 49 | cookieName: 'adonis-session', 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Clear session when browser closes 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Whether or not you want to destroy the session when browser closes. Setting 57 | | this value to `true` will ignore the `age`. 58 | | 59 | */ 60 | clearWithBrowser: false, 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Session age 65 | |-------------------------------------------------------------------------- 66 | | 67 | | The duration for which session stays active after no activity. A new HTTP 68 | | request to the server is considered as activity. 69 | | 70 | | The value can be a number in milliseconds or a string that must be valid 71 | | as per https://npmjs.org/package/ms package. 72 | | 73 | | Example: `2 days`, `2.5 hrs`, `1y`, `5s` and so on. 74 | | 75 | */ 76 | age: '2h', 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Cookie values 81 | |-------------------------------------------------------------------------- 82 | | 83 | | The cookie settings are used to setup the session id cookie and also the 84 | | driver will use the same values. 85 | | 86 | */ 87 | cookie: { 88 | path: '/', 89 | httpOnly: true, 90 | sameSite: false, 91 | }, 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Configuration for the file driver 96 | |-------------------------------------------------------------------------- 97 | | 98 | | The file driver needs absolute path to the directory in which sessions 99 | | must be stored. 100 | | 101 | */ 102 | file: { 103 | location: Application.tmpPath('sessions'), 104 | }, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Redis driver 109 | |-------------------------------------------------------------------------- 110 | | 111 | | The redis connection you want session driver to use. The same connection 112 | | must be defined inside `config/redis.ts` file as well. 113 | | 114 | */ 115 | redisConnection: 'local', 116 | }) 117 | -------------------------------------------------------------------------------- /config/cors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefC 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import { CorsConfig } from '@ioc:Adonis/Core/Cors' 9 | 10 | const corsConfig: CorsConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Enabled 14 | |-------------------------------------------------------------------------- 15 | | 16 | | A boolean to enable or disable CORS integration from your AdonisJs 17 | | application. 18 | | 19 | | Setting the value to `true` will enable the CORS for all HTTP request. However, 20 | | you can define a function to enable/disable it on per request basis as well. 21 | | 22 | */ 23 | enabled: false, 24 | 25 | // You can also use a function that return true or false. 26 | // enabled: (request) => request.url().startsWith('/api') 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Origin 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. 34 | | The value can be one of the following: 35 | | 36 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 37 | | 38 | | Boolean (true) - Allow current request origin. 39 | | Boolean (false) - Disallow all. 40 | | String - Comma separated list of allowed origins. 41 | | Array - An array of allowed origins. 42 | | String (*) - A wildcard (*) to allow all request origins. 43 | | Function - Receives the current origin string and should return 44 | | one of the above values. 45 | | 46 | */ 47 | origin: true, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Methods 52 | |-------------------------------------------------------------------------- 53 | | 54 | | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` 55 | | is checked against the following list. 56 | | 57 | | Following is the list of default methods. Feel free to add more. 58 | */ 59 | methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Headers 64 | |-------------------------------------------------------------------------- 65 | | 66 | | List of headers to be allowed for `Access-Control-Allow-Headers` header. 67 | | The value can be one of the following: 68 | | 69 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers 70 | | 71 | | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. 72 | | Boolean(false) - Disallow all headers. 73 | | String - Comma separated list of allowed headers. 74 | | Array - An array of allowed headers. 75 | | Function - Receives the current header and should return one of the above values. 76 | | 77 | */ 78 | headers: true, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Expose Headers 83 | |-------------------------------------------------------------------------- 84 | | 85 | | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. 86 | | header. By default following 6 simple response headers are exposed. 87 | | 88 | | Cache-Control 89 | | Content-Language 90 | | Content-Type 91 | | Expires 92 | | Last-Modified 93 | | Pragma 94 | | 95 | | In order to add more headers, simply define them inside the following array. 96 | | 97 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 98 | | 99 | */ 100 | exposeHeaders: [ 101 | 'cache-control', 102 | 'content-language', 103 | 'content-type', 104 | 'expires', 105 | 'last-modified', 106 | 'pragma', 107 | ], 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Credentials 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, 115 | | then header will be set, otherwise not. 116 | | 117 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 118 | | 119 | */ 120 | credentials: true, 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | MaxAge 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Define `Access-Control-Max-Age` header in seconds. 128 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 129 | | 130 | */ 131 | maxAge: 90, 132 | } 133 | 134 | export default corsConfig 135 | -------------------------------------------------------------------------------- /config/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JBt3o 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { driveConfig } from '@adonisjs/core/build/config' 10 | import Application from '@ioc:Adonis/Core/Application' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Drive Config 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The `DriveConfig` relies on the `DisksList` interface which is 18 | | defined inside the `contracts` directory. 19 | | 20 | */ 21 | export default driveConfig({ 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Default disk 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The default disk to use for managing file uploads. The value is driven by 28 | | the `DRIVE_DISK` environment variable. 29 | | 30 | */ 31 | disk: Env.get('DRIVE_DISK'), 32 | 33 | disks: { 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Local 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Uses the local file system to manage files. Make sure to turn off serving 40 | | files when not using this disk. 41 | | 42 | */ 43 | local: { 44 | driver: 'local', 45 | visibility: 'public', 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Storage root - Local driver only 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Define an absolute path to the storage directory from where to read the 53 | | files. 54 | | 55 | */ 56 | root: Application.tmpPath('uploads'), 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Serve files - Local driver only 61 | |-------------------------------------------------------------------------- 62 | | 63 | | When this is set to true, AdonisJS will configure a files server to serve 64 | | files from the disk root. This is done to mimic the behavior of cloud 65 | | storage services that has inbuilt capabilities to serve files. 66 | | 67 | */ 68 | serveFiles: true, 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Base path - Local driver only 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Base path is always required when "serveFiles = true". Also make sure 76 | | the `basePath` is unique across all the disks using "local" driver and 77 | | you are not registering routes with this prefix. 78 | | 79 | */ 80 | basePath: '/uploads', 81 | }, 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | S3 Driver 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Uses the S3 cloud storage to manage files. Make sure to install the s3 89 | | drive separately when using it. 90 | | 91 | |************************************************************************** 92 | | npm i @adonisjs/drive-s3 93 | |************************************************************************** 94 | | 95 | */ 96 | // s3: { 97 | // driver: 's3', 98 | // visibility: 'public', 99 | // key: Env.get('S3_KEY'), 100 | // secret: Env.get('S3_SECRET'), 101 | // region: Env.get('S3_REGION'), 102 | // bucket: Env.get('S3_BUCKET'), 103 | // endpoint: Env.get('S3_ENDPOINT'), 104 | // 105 | // // For minio to work 106 | // // forcePathStyle: true, 107 | // }, 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | GCS Driver 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Uses the Google cloud storage to manage files. Make sure to install the GCS 115 | | drive separately when using it. 116 | | 117 | |************************************************************************** 118 | | npm i @adonisjs/drive-gcs 119 | |************************************************************************** 120 | | 121 | */ 122 | // gcs: { 123 | // driver: 'gcs', 124 | // visibility: 'public', 125 | // keyFilename: Env.get('GCS_KEY_FILENAME'), 126 | // bucket: Env.get('GCS_BUCKET'), 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Uniform ACL - Google cloud storage only 131 | |-------------------------------------------------------------------------- 132 | | 133 | | When using the Uniform ACL on the bucket, the "visibility" option is 134 | | ignored. Since, the files ACL is managed by the google bucket policies 135 | | directly. 136 | | 137 | |************************************************************************** 138 | | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access 139 | |************************************************************************** 140 | | 141 | | The following option just informs drive whether your bucket is using uniform 142 | | ACL or not. The actual setting needs to be toggled within the Google cloud 143 | | console. 144 | | 145 | */ 146 | // usingUniformAcl: false, 147 | // }, 148 | }, 149 | }) 150 | -------------------------------------------------------------------------------- /config/bodyparser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jfefn 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser' 9 | 10 | const bodyParserConfig: BodyParserConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | White listed methods 14 | |-------------------------------------------------------------------------- 15 | | 16 | | HTTP methods for which body parsing must be performed. It is a good practice 17 | | to avoid body parsing for `GET` requests. 18 | | 19 | */ 20 | whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | JSON parser settings 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The settings for the JSON parser. The types defines the request content 28 | | types which gets processed by the JSON parser. 29 | | 30 | */ 31 | json: { 32 | encoding: 'utf-8', 33 | limit: '1mb', 34 | strict: true, 35 | types: [ 36 | 'application/json', 37 | 'application/json-patch+json', 38 | 'application/vnd.api+json', 39 | 'application/csp-report', 40 | ], 41 | }, 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Form parser settings 46 | |-------------------------------------------------------------------------- 47 | | 48 | | The settings for the `application/x-www-form-urlencoded` parser. The types 49 | | defines the request content types which gets processed by the form parser. 50 | | 51 | */ 52 | form: { 53 | encoding: 'utf-8', 54 | limit: '1mb', 55 | queryString: {}, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Convert empty strings to null 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Convert empty form fields to null. HTML forms results in field string 63 | | value when the field is left blank. This option normalizes all the blank 64 | | field values to "null" 65 | | 66 | */ 67 | convertEmptyStringsToNull: true, 68 | 69 | types: [ 70 | 'application/x-www-form-urlencoded', 71 | ], 72 | }, 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Raw body parser settings 77 | |-------------------------------------------------------------------------- 78 | | 79 | | Raw body just reads the request body stream as a plain text, which you 80 | | can process by hand. This must be used when request body type is not 81 | | supported by the body parser. 82 | | 83 | */ 84 | raw: { 85 | encoding: 'utf-8', 86 | limit: '1mb', 87 | queryString: {}, 88 | types: [ 89 | 'text/*', 90 | ], 91 | }, 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Multipart parser settings 96 | |-------------------------------------------------------------------------- 97 | | 98 | | The settings for the `multipart/form-data` parser. The types defines the 99 | | request content types which gets processed by the form parser. 100 | | 101 | */ 102 | multipart: { 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Auto process 106 | |-------------------------------------------------------------------------- 107 | | 108 | | The auto process option will process uploaded files and writes them to 109 | | the `tmp` folder. You can turn it off and then manually use the stream 110 | | to pipe stream to a different destination. 111 | | 112 | | It is recommended to keep `autoProcess=true`. Unless you are processing bigger 113 | | file sizes. 114 | | 115 | */ 116 | autoProcess: true, 117 | 118 | /* 119 | |-------------------------------------------------------------------------- 120 | | Files to be processed manually 121 | |-------------------------------------------------------------------------- 122 | | 123 | | You can turn off `autoProcess` for certain routes by defining 124 | | routes inside the following array. 125 | | 126 | | NOTE: Make sure the route pattern starts with a leading slash. 127 | | 128 | | Correct 129 | | ```js 130 | | /projects/:id/file 131 | | ``` 132 | | 133 | | Incorrect 134 | | ```js 135 | | projects/:id/file 136 | | ``` 137 | */ 138 | processManually: [], 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Temporary file name 143 | |-------------------------------------------------------------------------- 144 | | 145 | | When auto processing is on. We will use this method to compute the temporary 146 | | file name. AdonisJs will compute a unique `tmpPath` for you automatically, 147 | | However, you can also define your own custom method. 148 | | 149 | */ 150 | // tmpFileName () { 151 | // }, 152 | 153 | /* 154 | |-------------------------------------------------------------------------- 155 | | Encoding 156 | |-------------------------------------------------------------------------- 157 | | 158 | | Request body encoding 159 | | 160 | */ 161 | encoding: 'utf-8', 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | Convert empty strings to null 166 | |-------------------------------------------------------------------------- 167 | | 168 | | Convert empty form fields to null. HTML forms results in field string 169 | | value when the field is left blank. This option normalizes all the blank 170 | | field values to "null" 171 | | 172 | */ 173 | convertEmptyStringsToNull: true, 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Max Fields 178 | |-------------------------------------------------------------------------- 179 | | 180 | | The maximum number of fields allowed in the request body. The field includes 181 | | text inputs and files both. 182 | | 183 | */ 184 | maxFields: 1000, 185 | 186 | /* 187 | |-------------------------------------------------------------------------- 188 | | Request body limit 189 | |-------------------------------------------------------------------------- 190 | | 191 | | The total limit to the multipart body. This includes all request files 192 | | and fields data. 193 | | 194 | */ 195 | limit: '20mb', 196 | 197 | /* 198 | |-------------------------------------------------------------------------- 199 | | Types 200 | |-------------------------------------------------------------------------- 201 | | 202 | | The types that will be considered and parsed as multipart body. 203 | | 204 | */ 205 | types: [ 206 | 'multipart/form-data', 207 | ], 208 | }, 209 | } 210 | 211 | export default bodyParserConfig 212 | -------------------------------------------------------------------------------- /config/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefZ 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import proxyAddr from 'proxy-addr' 9 | import Env from '@ioc:Adonis/Core/Env' 10 | import { ServerConfig } from '@ioc:Adonis/Core/Server' 11 | import { LoggerConfig } from '@ioc:Adonis/Core/Logger' 12 | import { ProfilerConfig } from '@ioc:Adonis/Core/Profiler' 13 | import { ValidatorConfig } from '@ioc:Adonis/Core/Validator' 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Application secret key 18 | |-------------------------------------------------------------------------- 19 | | 20 | | The secret to encrypt and sign different values in your application. 21 | | Make sure to keep the `APP_KEY` as an environment variable and secure. 22 | | 23 | | Note: Changing the application key for an existing app will make all 24 | | the cookies invalid and also the existing encrypted data will not 25 | | be decrypted. 26 | | 27 | */ 28 | export const appKey: string = Env.get('APP_KEY') 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Http server configuration 33 | |-------------------------------------------------------------------------- 34 | | 35 | | The configuration for the HTTP(s) server. Make sure to go through all 36 | | the config properties to make keep server secure. 37 | | 38 | */ 39 | export const http: ServerConfig = { 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Allow method spoofing 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Method spoofing enables defining custom HTTP methods using a query string 46 | | `_method`. This is usually required when you are making traditional 47 | | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and 48 | | so on. 49 | | 50 | */ 51 | allowMethodSpoofing: true, 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Subdomain offset 56 | |-------------------------------------------------------------------------- 57 | */ 58 | subdomainOffset: 2, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Request Ids 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Setting this value to `true` will generate a unique request id for each 66 | | HTTP request and set it as `x-request-id` header. 67 | | 68 | */ 69 | generateRequestId: false, 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Trusting proxy servers 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` 77 | | headers. 78 | | 79 | */ 80 | trustProxy: proxyAddr.compile('loopback'), 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Generating Etag 85 | |-------------------------------------------------------------------------- 86 | | 87 | | Whether or not to generate an etag for every response. 88 | | 89 | */ 90 | etag: false, 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | JSONP Callback 95 | |-------------------------------------------------------------------------- 96 | */ 97 | jsonpCallbackName: 'callback', 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Cookie settings 102 | |-------------------------------------------------------------------------- 103 | */ 104 | cookie: { 105 | domain: '', 106 | path: '/', 107 | maxAge: '2h', 108 | httpOnly: true, 109 | secure: false, 110 | sameSite: false, 111 | }, 112 | } 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Logger 117 | |-------------------------------------------------------------------------- 118 | */ 119 | export const logger: LoggerConfig = { 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | Application name 123 | |-------------------------------------------------------------------------- 124 | | 125 | | The name of the application you want to add to the log. It is recommended 126 | | to always have app name in every log line. 127 | | 128 | | The `APP_NAME` environment variable is automatically set by AdonisJS by 129 | | reading the `name` property from the `package.json` file. 130 | | 131 | */ 132 | name: Env.get('APP_NAME'), 133 | 134 | /* 135 | |-------------------------------------------------------------------------- 136 | | Toggle logger 137 | |-------------------------------------------------------------------------- 138 | | 139 | | Enable or disable logger application wide 140 | | 141 | */ 142 | enabled: true, 143 | 144 | /* 145 | |-------------------------------------------------------------------------- 146 | | Logging level 147 | |-------------------------------------------------------------------------- 148 | | 149 | | The level from which you want the logger to flush logs. It is recommended 150 | | to make use of the environment variable, so that you can define log levels 151 | | at deployment level and not code level. 152 | | 153 | */ 154 | level: Env.get('LOG_LEVEL', 'info'), 155 | 156 | /* 157 | |-------------------------------------------------------------------------- 158 | | Pretty print 159 | |-------------------------------------------------------------------------- 160 | | 161 | | It is highly advised NOT to use `prettyPrint` in production, since it 162 | | can have huge impact on performance. 163 | | 164 | */ 165 | prettyPrint: Env.get('NODE_ENV') === 'development', 166 | } 167 | 168 | /* 169 | |-------------------------------------------------------------------------- 170 | | Profiler 171 | |-------------------------------------------------------------------------- 172 | */ 173 | export const profiler: ProfilerConfig = { 174 | /* 175 | |-------------------------------------------------------------------------- 176 | | Toggle profiler 177 | |-------------------------------------------------------------------------- 178 | | 179 | | Enable or disable profiler 180 | | 181 | */ 182 | enabled: true, 183 | 184 | /* 185 | |-------------------------------------------------------------------------- 186 | | Blacklist actions/row labels 187 | |-------------------------------------------------------------------------- 188 | | 189 | | Define an array of actions or row labels that you want to disable from 190 | | getting profiled. 191 | | 192 | */ 193 | blacklist: [], 194 | 195 | /* 196 | |-------------------------------------------------------------------------- 197 | | Whitelist actions/row labels 198 | |-------------------------------------------------------------------------- 199 | | 200 | | Define an array of actions or row labels that you want to whitelist for 201 | | the profiler. When whitelist is defined, then `blacklist` is ignored. 202 | | 203 | */ 204 | whitelist: [], 205 | } 206 | 207 | /* 208 | |-------------------------------------------------------------------------- 209 | | Validator 210 | |-------------------------------------------------------------------------- 211 | | 212 | | Configure the global configuration for the validator. Here's the reference 213 | | to the default config https://git.io/JT0WE 214 | | 215 | */ 216 | export const validator: ValidatorConfig = { 217 | } 218 | -------------------------------------------------------------------------------- /config/shield.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jvwvt 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { ShieldConfig } from '@ioc:Adonis/Addons/Shield' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Content Security Policy 14 | |-------------------------------------------------------------------------- 15 | | 16 | | Content security policy filters out the origins not allowed to execute 17 | | and load resources like scripts, styles and fonts. There are wide 18 | | variety of options to choose from. 19 | */ 20 | export const csp: ShieldConfig['csp'] = { 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Enable/disable CSP 24 | |-------------------------------------------------------------------------- 25 | | 26 | | The CSP rules are disabled by default for seamless onboarding. 27 | | 28 | */ 29 | enabled: false, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Directives 34 | |-------------------------------------------------------------------------- 35 | | 36 | | All directives are defined in camelCase and here is the list of 37 | | available directives and their possible values. 38 | | 39 | | https://content-security-policy.com 40 | | 41 | | @example 42 | | directives: { 43 | | defaultSrc: ["'self'", '@nonce', 'cdnjs.cloudflare.com'] 44 | | } 45 | | 46 | */ 47 | directives: { 48 | }, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Report only 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Setting `reportOnly=true` will not block the scripts from running and 56 | | instead report them to a URL. 57 | | 58 | */ 59 | reportOnly: false, 60 | } 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | CSRF Protection 65 | |-------------------------------------------------------------------------- 66 | | 67 | | CSRF Protection adds another layer of security by making sure, actionable 68 | | routes does have a valid token to execute an action. 69 | | 70 | */ 71 | export const csrf: ShieldConfig['csrf'] = { 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Enable/Disable CSRF 75 | |-------------------------------------------------------------------------- 76 | */ 77 | enabled: true, 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Routes to Ignore 82 | |-------------------------------------------------------------------------- 83 | | 84 | | Define an array of route patterns that you want to ignore from CSRF 85 | | validation. Make sure the route patterns are started with a leading 86 | | slash. Example: 87 | | 88 | | `/foo/bar` 89 | | 90 | | Also you can define a function that is evaluated on every HTTP Request. 91 | | ``` 92 | | exceptRoutes: ({ request }) => request.url().includes('/api') 93 | | ``` 94 | | 95 | */ 96 | exceptRoutes: [], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Enable Sharing Token Via Cookie 101 | |-------------------------------------------------------------------------- 102 | | 103 | | When the following flag is enabled, AdonisJS will drop `XSRF-TOKEN` 104 | | cookie that frontend frameworks can read and return back as a 105 | | `X-XSRF-TOKEN` header. 106 | | 107 | | The cookie has `httpOnly` flag set to false, so it is little insecure and 108 | | can be turned off when you are not using a frontend framework making 109 | | AJAX requests. 110 | | 111 | */ 112 | enableXsrfCookie: true, 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Methods to Validate 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Define an array of HTTP methods to be validated for a valid CSRF token. 120 | | 121 | */ 122 | methods: ['POST', 'PUT', 'PATCH', 'DELETE'], 123 | } 124 | 125 | /* 126 | |-------------------------------------------------------------------------- 127 | | DNS Prefetching 128 | |-------------------------------------------------------------------------- 129 | | 130 | | DNS prefetching allows browsers to proactively perform domain name 131 | | resolution in background. 132 | | 133 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 134 | | 135 | */ 136 | export const dnsPrefetch: ShieldConfig['dnsPrefetch'] = { 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | Enable/disable this feature 140 | |-------------------------------------------------------------------------- 141 | */ 142 | enabled: true, 143 | 144 | /* 145 | |-------------------------------------------------------------------------- 146 | | Allow or Dis-Allow Explicitly 147 | |-------------------------------------------------------------------------- 148 | | 149 | | The `enabled` boolean does not set `X-DNS-Prefetch-Control` header. However 150 | | the `allow` boolean controls the value of `X-DNS-Prefetch-Control` header. 151 | | 152 | | - When `allow = true`, then `X-DNS-Prefetch-Control = 'on'` 153 | | - When `allow = false`, then `X-DNS-Prefetch-Control = 'off'` 154 | | 155 | */ 156 | allow: true, 157 | } 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Iframe Options 162 | |-------------------------------------------------------------------------- 163 | | 164 | | xFrame defines whether or not your website can be embedded inside an 165 | | iframe. Choose from one of the following options. 166 | | 167 | | - DENY 168 | | - SAMEORIGIN 169 | | - ALLOW-FROM http://example.com 170 | | 171 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 172 | */ 173 | export const xFrame: ShieldConfig['xFrame'] = { 174 | enabled: true, 175 | action: 'DENY', 176 | } 177 | 178 | /* 179 | |-------------------------------------------------------------------------- 180 | | Http Strict Transport Security 181 | |-------------------------------------------------------------------------- 182 | | 183 | | A security to ensure that a browser always makes a connection over 184 | | HTTPS. 185 | | 186 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 187 | | 188 | */ 189 | export const hsts: ShieldConfig['hsts'] = { 190 | enabled: true, 191 | /* 192 | |-------------------------------------------------------------------------- 193 | | Max Age 194 | |-------------------------------------------------------------------------- 195 | | 196 | | Control, how long the browser should remember that a site is only to be 197 | | accessed using HTTPS. 198 | | 199 | */ 200 | maxAge: '180 days', 201 | 202 | /* 203 | |-------------------------------------------------------------------------- 204 | | Include Subdomains 205 | |-------------------------------------------------------------------------- 206 | | 207 | | Apply rules on the subdomains as well. 208 | | 209 | */ 210 | includeSubDomains: true, 211 | 212 | /* 213 | |-------------------------------------------------------------------------- 214 | | Preloading 215 | |-------------------------------------------------------------------------- 216 | | 217 | | Google maintains a service to register your domain and it will preload 218 | | the HSTS policy. Learn more https://hstspreload.org/ 219 | | 220 | */ 221 | preload: false, 222 | } 223 | 224 | /* 225 | |-------------------------------------------------------------------------- 226 | | No Sniff 227 | |-------------------------------------------------------------------------- 228 | | 229 | | Browsers have a habit of sniffing content-type of a response. Which means 230 | | files with .txt extension containing Javascript code will be executed as 231 | | Javascript. You can disable this behavior by setting nosniff to false. 232 | | 233 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 234 | | 235 | */ 236 | export const contentTypeSniffing: ShieldConfig['contentTypeSniffing'] = { 237 | enabled: true, 238 | } 239 | -------------------------------------------------------------------------------- /ace-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "dump:rcfile": { 4 | "settings": {}, 5 | "commandPath": "@adonisjs/core/build/commands/DumpRc", 6 | "commandName": "dump:rcfile", 7 | "description": "Dump contents of .adonisrc.json file along with defaults", 8 | "args": [], 9 | "aliases": [], 10 | "flags": [] 11 | }, 12 | "list:routes": { 13 | "settings": { 14 | "loadApp": true, 15 | "stayAlive": true 16 | }, 17 | "commandPath": "@adonisjs/core/build/commands/ListRoutes/index", 18 | "commandName": "list:routes", 19 | "description": "List application routes", 20 | "args": [], 21 | "aliases": [], 22 | "flags": [ 23 | { 24 | "name": "verbose", 25 | "propertyName": "verbose", 26 | "type": "boolean", 27 | "description": "Display more information" 28 | }, 29 | { 30 | "name": "reverse", 31 | "propertyName": "reverse", 32 | "type": "boolean", 33 | "alias": "r", 34 | "description": "Reverse routes display" 35 | }, 36 | { 37 | "name": "methods", 38 | "propertyName": "methodsFilter", 39 | "type": "array", 40 | "alias": "m", 41 | "description": "Filter routes by method" 42 | }, 43 | { 44 | "name": "patterns", 45 | "propertyName": "patternsFilter", 46 | "type": "array", 47 | "alias": "p", 48 | "description": "Filter routes by the route pattern" 49 | }, 50 | { 51 | "name": "names", 52 | "propertyName": "namesFilter", 53 | "type": "array", 54 | "alias": "n", 55 | "description": "Filter routes by route name" 56 | }, 57 | { 58 | "name": "json", 59 | "propertyName": "json", 60 | "type": "boolean", 61 | "description": "Output as JSON" 62 | }, 63 | { 64 | "name": "table", 65 | "propertyName": "table", 66 | "type": "boolean", 67 | "description": "Output as Table" 68 | }, 69 | { 70 | "name": "max-width", 71 | "propertyName": "maxWidth", 72 | "type": "number", 73 | "description": "Specify maximum rendering width. Ignored for JSON Output" 74 | } 75 | ] 76 | }, 77 | "generate:key": { 78 | "settings": {}, 79 | "commandPath": "@adonisjs/core/build/commands/GenerateKey", 80 | "commandName": "generate:key", 81 | "description": "Generate a new APP_KEY secret", 82 | "args": [], 83 | "aliases": [], 84 | "flags": [] 85 | }, 86 | "repl": { 87 | "settings": { 88 | "loadApp": true, 89 | "environment": "repl", 90 | "stayAlive": true 91 | }, 92 | "commandPath": "@adonisjs/repl/build/commands/AdonisRepl", 93 | "commandName": "repl", 94 | "description": "Start a new REPL session", 95 | "args": [], 96 | "aliases": [], 97 | "flags": [] 98 | }, 99 | "db:seed": { 100 | "settings": { 101 | "loadApp": true 102 | }, 103 | "commandPath": "@adonisjs/lucid/build/commands/DbSeed", 104 | "commandName": "db:seed", 105 | "description": "Execute database seeders", 106 | "args": [], 107 | "aliases": [], 108 | "flags": [ 109 | { 110 | "name": "connection", 111 | "propertyName": "connection", 112 | "type": "string", 113 | "description": "Define a custom database connection for the seeders", 114 | "alias": "c" 115 | }, 116 | { 117 | "name": "interactive", 118 | "propertyName": "interactive", 119 | "type": "boolean", 120 | "description": "Run seeders in interactive mode", 121 | "alias": "i" 122 | }, 123 | { 124 | "name": "files", 125 | "propertyName": "files", 126 | "type": "array", 127 | "description": "Define a custom set of seeders files names to run", 128 | "alias": "f" 129 | }, 130 | { 131 | "name": "compact-output", 132 | "propertyName": "compactOutput", 133 | "type": "boolean", 134 | "description": "A compact single-line output" 135 | } 136 | ] 137 | }, 138 | "make:model": { 139 | "settings": { 140 | "loadApp": true 141 | }, 142 | "commandPath": "@adonisjs/lucid/build/commands/MakeModel", 143 | "commandName": "make:model", 144 | "description": "Make a new Lucid model", 145 | "args": [ 146 | { 147 | "type": "string", 148 | "propertyName": "name", 149 | "name": "name", 150 | "required": true, 151 | "description": "Name of the model class" 152 | } 153 | ], 154 | "aliases": [], 155 | "flags": [ 156 | { 157 | "name": "migration", 158 | "propertyName": "migration", 159 | "type": "boolean", 160 | "alias": "m", 161 | "description": "Generate the migration for the model" 162 | }, 163 | { 164 | "name": "controller", 165 | "propertyName": "controller", 166 | "type": "boolean", 167 | "alias": "c", 168 | "description": "Generate the controller for the model" 169 | }, 170 | { 171 | "name": "factory", 172 | "propertyName": "factory", 173 | "type": "boolean", 174 | "alias": "f", 175 | "description": "Generate a factory for the model" 176 | } 177 | ] 178 | }, 179 | "make:migration": { 180 | "settings": { 181 | "loadApp": true 182 | }, 183 | "commandPath": "@adonisjs/lucid/build/commands/MakeMigration", 184 | "commandName": "make:migration", 185 | "description": "Make a new migration file", 186 | "args": [ 187 | { 188 | "type": "string", 189 | "propertyName": "name", 190 | "name": "name", 191 | "required": true, 192 | "description": "Name of the migration file" 193 | } 194 | ], 195 | "aliases": [], 196 | "flags": [ 197 | { 198 | "name": "connection", 199 | "propertyName": "connection", 200 | "type": "string", 201 | "description": "The connection flag is used to lookup the directory for the migration file" 202 | }, 203 | { 204 | "name": "folder", 205 | "propertyName": "folder", 206 | "type": "string", 207 | "description": "Pre-select a migration directory" 208 | }, 209 | { 210 | "name": "create", 211 | "propertyName": "create", 212 | "type": "string", 213 | "description": "Define the table name for creating a new table" 214 | }, 215 | { 216 | "name": "table", 217 | "propertyName": "table", 218 | "type": "string", 219 | "description": "Define the table name for altering an existing table" 220 | } 221 | ] 222 | }, 223 | "make:seeder": { 224 | "settings": {}, 225 | "commandPath": "@adonisjs/lucid/build/commands/MakeSeeder", 226 | "commandName": "make:seeder", 227 | "description": "Make a new Seeder file", 228 | "args": [ 229 | { 230 | "type": "string", 231 | "propertyName": "name", 232 | "name": "name", 233 | "required": true, 234 | "description": "Name of the seeder class" 235 | } 236 | ], 237 | "aliases": [], 238 | "flags": [] 239 | }, 240 | "make:factory": { 241 | "settings": {}, 242 | "commandPath": "@adonisjs/lucid/build/commands/MakeFactory", 243 | "commandName": "make:factory", 244 | "description": "Make a new factory", 245 | "args": [ 246 | { 247 | "type": "string", 248 | "propertyName": "model", 249 | "name": "model", 250 | "required": true, 251 | "description": "The name of the model" 252 | } 253 | ], 254 | "aliases": [], 255 | "flags": [ 256 | { 257 | "name": "model-path", 258 | "propertyName": "modelPath", 259 | "type": "string", 260 | "description": "The path to the model" 261 | }, 262 | { 263 | "name": "exact", 264 | "propertyName": "exact", 265 | "type": "boolean", 266 | "description": "Create the factory with the exact name as provided", 267 | "alias": "e" 268 | } 269 | ] 270 | }, 271 | "db:wipe": { 272 | "settings": { 273 | "loadApp": true 274 | }, 275 | "commandPath": "@adonisjs/lucid/build/commands/DbWipe", 276 | "commandName": "db:wipe", 277 | "description": "Drop all tables, views and types in database", 278 | "args": [], 279 | "aliases": [], 280 | "flags": [ 281 | { 282 | "name": "connection", 283 | "propertyName": "connection", 284 | "type": "string", 285 | "description": "Define a custom database connection", 286 | "alias": "c" 287 | }, 288 | { 289 | "name": "drop-views", 290 | "propertyName": "dropViews", 291 | "type": "boolean", 292 | "description": "Drop all views" 293 | }, 294 | { 295 | "name": "drop-types", 296 | "propertyName": "dropTypes", 297 | "type": "boolean", 298 | "description": "Drop all custom types (Postgres only)" 299 | }, 300 | { 301 | "name": "force", 302 | "propertyName": "force", 303 | "type": "boolean", 304 | "description": "Explicitly force command to run in production" 305 | } 306 | ] 307 | }, 308 | "migration:run": { 309 | "settings": { 310 | "loadApp": true 311 | }, 312 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Run", 313 | "commandName": "migration:run", 314 | "description": "Migrate database by running pending migrations", 315 | "args": [], 316 | "aliases": [], 317 | "flags": [ 318 | { 319 | "name": "connection", 320 | "propertyName": "connection", 321 | "type": "string", 322 | "description": "Define a custom database connection", 323 | "alias": "c" 324 | }, 325 | { 326 | "name": "force", 327 | "propertyName": "force", 328 | "type": "boolean", 329 | "description": "Explicitly force to run migrations in production" 330 | }, 331 | { 332 | "name": "dry-run", 333 | "propertyName": "dryRun", 334 | "type": "boolean", 335 | "description": "Do not run actual queries. Instead view the SQL output" 336 | }, 337 | { 338 | "name": "compact-output", 339 | "propertyName": "compactOutput", 340 | "type": "boolean", 341 | "description": "A compact single-line output" 342 | } 343 | ] 344 | }, 345 | "migration:rollback": { 346 | "settings": { 347 | "loadApp": true 348 | }, 349 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback", 350 | "commandName": "migration:rollback", 351 | "description": "Rollback migrations to a specific batch number", 352 | "args": [], 353 | "aliases": [], 354 | "flags": [ 355 | { 356 | "name": "connection", 357 | "propertyName": "connection", 358 | "type": "string", 359 | "description": "Define a custom database connection", 360 | "alias": "c" 361 | }, 362 | { 363 | "name": "force", 364 | "propertyName": "force", 365 | "type": "boolean", 366 | "description": "Explictly force to run migrations in production" 367 | }, 368 | { 369 | "name": "dry-run", 370 | "propertyName": "dryRun", 371 | "type": "boolean", 372 | "description": "Do not run actual queries. Instead view the SQL output" 373 | }, 374 | { 375 | "name": "batch", 376 | "propertyName": "batch", 377 | "type": "number", 378 | "description": "Define custom batch number for rollback. Use 0 to rollback to initial state" 379 | }, 380 | { 381 | "name": "compact-output", 382 | "propertyName": "compactOutput", 383 | "type": "boolean", 384 | "description": "A compact single-line output" 385 | } 386 | ] 387 | }, 388 | "migration:status": { 389 | "settings": { 390 | "loadApp": true 391 | }, 392 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Status", 393 | "commandName": "migration:status", 394 | "description": "View migrations status", 395 | "args": [], 396 | "aliases": [], 397 | "flags": [ 398 | { 399 | "name": "connection", 400 | "propertyName": "connection", 401 | "type": "string", 402 | "description": "Define a custom database connection", 403 | "alias": "c" 404 | } 405 | ] 406 | }, 407 | "migration:reset": { 408 | "settings": { 409 | "loadApp": true 410 | }, 411 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Reset", 412 | "commandName": "migration:reset", 413 | "description": "Rollback all migrations", 414 | "args": [], 415 | "aliases": [], 416 | "flags": [ 417 | { 418 | "name": "connection", 419 | "propertyName": "connection", 420 | "type": "string", 421 | "description": "Define a custom database connection", 422 | "alias": "c" 423 | }, 424 | { 425 | "name": "force", 426 | "propertyName": "force", 427 | "type": "boolean", 428 | "description": "Explicitly force command to run in production" 429 | }, 430 | { 431 | "name": "dry-run", 432 | "propertyName": "dryRun", 433 | "type": "boolean", 434 | "description": "Do not run actual queries. Instead view the SQL output" 435 | } 436 | ] 437 | }, 438 | "migration:refresh": { 439 | "settings": { 440 | "loadApp": true 441 | }, 442 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Refresh", 443 | "commandName": "migration:refresh", 444 | "description": "Rollback and migrate database", 445 | "args": [], 446 | "aliases": [], 447 | "flags": [ 448 | { 449 | "name": "connection", 450 | "propertyName": "connection", 451 | "type": "string", 452 | "description": "Define a custom database connection", 453 | "alias": "c" 454 | }, 455 | { 456 | "name": "force", 457 | "propertyName": "force", 458 | "type": "boolean", 459 | "description": "Explicitly force command to run in production" 460 | }, 461 | { 462 | "name": "dry-run", 463 | "propertyName": "dryRun", 464 | "type": "boolean", 465 | "description": "Do not run actual queries. Instead view the SQL output" 466 | }, 467 | { 468 | "name": "seed", 469 | "propertyName": "seed", 470 | "type": "boolean", 471 | "description": "Run seeders" 472 | } 473 | ] 474 | }, 475 | "migration:fresh": { 476 | "settings": { 477 | "loadApp": true 478 | }, 479 | "commandPath": "@adonisjs/lucid/build/commands/Migration/Fresh", 480 | "commandName": "migration:fresh", 481 | "description": "Drop all tables and re-migrate the database", 482 | "args": [], 483 | "aliases": [], 484 | "flags": [ 485 | { 486 | "name": "connection", 487 | "propertyName": "connection", 488 | "type": "string", 489 | "description": "Define a custom database connection", 490 | "alias": "c" 491 | }, 492 | { 493 | "name": "force", 494 | "propertyName": "force", 495 | "type": "boolean", 496 | "description": "Explicitly force command to run in production" 497 | }, 498 | { 499 | "name": "seed", 500 | "propertyName": "seed", 501 | "type": "boolean", 502 | "description": "Run seeders" 503 | }, 504 | { 505 | "name": "drop-views", 506 | "propertyName": "dropViews", 507 | "type": "boolean", 508 | "description": "Drop all views" 509 | }, 510 | { 511 | "name": "drop-types", 512 | "propertyName": "dropTypes", 513 | "type": "boolean", 514 | "description": "Drop all custom types (Postgres only)" 515 | } 516 | ] 517 | }, 518 | "make:mailer": { 519 | "settings": {}, 520 | "commandPath": "@adonisjs/mail/build/commands/MakeMailer", 521 | "commandName": "make:mailer", 522 | "description": "Make a new mailer class", 523 | "args": [ 524 | { 525 | "type": "string", 526 | "propertyName": "name", 527 | "name": "name", 528 | "required": true, 529 | "description": "Name of the mailer class" 530 | } 531 | ], 532 | "aliases": [], 533 | "flags": [] 534 | } 535 | }, 536 | "aliases": {} 537 | } 538 | --------------------------------------------------------------------------------