├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── socialcard.png ├── .gitignore ├── .husky └── commit-msg ├── .prettierignore ├── LICENSE.md ├── README.md ├── instructions.md ├── instructions.ts ├── package-lock.json ├── package.json ├── templates ├── contracts │ ├── events.txt │ └── response.txt ├── controllers │ ├── AuthController.txt │ ├── ConfirmPasswordController.txt │ ├── EmailVerificationController.txt │ ├── PasswordResetController.txt │ ├── PasswordResetRequestController.txt │ └── RegisterController.txt ├── middlewares │ ├── Auth.txt │ ├── ConfirmPassword.txt │ └── Guest.txt ├── migrations │ ├── password_reset_tokens.txt │ └── users.txt ├── models │ ├── PasswordResetToken.txt │ └── User.txt ├── postcss.config.txt ├── providers │ └── AppProvider.txt ├── resources │ ├── css │ │ └── app.txt │ ├── js │ │ └── app.txt │ └── views │ │ ├── auth │ │ ├── confirm-password.txt │ │ ├── forgot-password.txt │ │ ├── login.txt │ │ ├── register.txt │ │ ├── resend-verification.txt │ │ └── reset-password.txt │ │ ├── dashboard.txt │ │ ├── home.txt │ │ └── layouts │ │ └── app.txt ├── start │ └── routes.txt ├── tailwind.config.txt ├── validators │ ├── EmailValidator.txt │ ├── LoginValidator.txt │ ├── PasswordResetValidator.txt │ └── RegisterValidator.txt └── webpack.config.txt └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. And following this guidelines will make your pull request easier to merge 4 | 5 | ## Prerequisites 6 | 7 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor to make sure it uses correct settings. 8 | - Fork the repository and clone your fork. 9 | - Install dependencies: `npm install`. 10 | 11 | ## Coding style 12 | 13 | We make use of [standard](https://standardjs.com/) to lint our code. Standard does not need a config file and comes with set of non-configurable rules. 14 | 15 | ## Development work-flow 16 | 17 | Always make sure to lint and test your code before pushing it to the GitHub. 18 | 19 | ```bash 20 | npm test 21 | ``` 22 | 23 | Just lint the code 24 | 25 | ```bash 26 | npm run lint 27 | ``` 28 | 29 | **Make sure you add sufficient tests for the change**. 30 | 31 | ## Other notes 32 | 33 | - Do not change version number inside the `package.json` file. 34 | - Do not update `CHANGELOG.md` file. 35 | 36 | ## Need help? 37 | 38 | Feel free to ask. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 6 | 7 | - Ensure the issue isn't already reported. 8 | - Ensure you are reporting the bug in the correct repo. 9 | 10 | *Delete the above section and the instructions in the sections below before submitting* 11 | 12 | ## Description 13 | 14 | If this is a feature request, explain why it should be added. Specific use-cases are best. 15 | 16 | For bug reports, please provide as much *relevant* info as possible. 17 | 18 | ## Package version 19 | 20 | 21 | ## Error Message & Stack Trace 22 | 23 | ## Relevant Information 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/mezielabs/flair/blob/master/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/socialcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mezielabs/flair/f829f10beb4fb37ea410751576557b4d94f80623/.github/socialcard.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | test/__app 4 | .DS_STORE 5 | .nyc_output 6 | .idea 7 | .vscode/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | *.log 11 | build 12 | dist 13 | shrinkwrap.yaml 14 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Chimezie Enyinnaya, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Social card of Flair

2 | 3 | # Flair 4 | > AdonisJS authentication scaffolding 5 | 6 | [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url] 7 | 8 | Flair provides a way to quickly add authentication to your AdonisJS application using TailwindCSS and Alpine.js. 9 | 10 | ## Prerequisites 11 | While doing its thing, Flair is going to create and overwrite some files. Hence, Flair is meant to be used on a fresh AdonisJS application. In addition to that, your application should already have the following packages installed and configred: 12 | 13 | * Encore (can be easily done at the point of creating the application) 14 | * Lucid 15 | * Auth 16 | * Shield 17 | 18 | ## Setup 19 | 20 | First, install Flair with your preffered package manager: 21 | 22 | ```bash 23 | npm install @mezielabs/flair 24 | 25 | # or 26 | yarn add @mezielabs/flair 27 | 28 | # or 29 | pnpm add @mezielabs/flair 30 | ``` 31 | 32 | Next, configure the package using the `configure` command: 33 | 34 | ```bash 35 | node ace configure @mezielabs/flair 36 | ``` 37 | 38 | This will scaffold and create the necessary files. 39 | 40 | Finally, make sure to add the following middlewares inside the `start/kernel.ts` file: 41 | 42 | ```ts 43 | Server.middleware.registerNamed({ 44 | '...', 45 | guest: () => import('App/Middleware/Guest'), 46 | confirmPassword: () => import('App/Middleware/ConfirmPassword'), 47 | }) 48 | ``` 49 | 50 | ## Learn AdonisJS 51 | 52 | Want to learn how to build projects like this with AdonisJS? Check out [Adonis Mastery](https://adonismastery.com), where you get to learn AdonisJS through practical screencasts. 53 | 54 | [npm-image]: https://img.shields.io/npm/v/flair.svg?style=for-the-badge&logo=npm 55 | [npm-url]: https://npmjs.org/package/flair "npm" 56 | 57 | [license-image]: https://img.shields.io/npm/l/flair?color=blueviolet&style=for-the-badge 58 | [license-url]: LICENSE.md "license" 59 | 60 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 61 | [typescript-url]: "typescript" 62 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | Congratulations! You have configured `@mezielabs/flair` package successfully. Make sure to add the following middlewares inside the `start/kernel.ts` file: 2 | 3 | ```ts 4 | Server.middleware.registerNamed({ 5 | '...', 6 | guest: () => import('App/Middleware/Guest'), 7 | confirmPassword: () => import('App/Middleware/ConfirmPassword'), 8 | }) 9 | ``` 10 | -------------------------------------------------------------------------------- /instructions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @mezielabs/flair 3 | * 4 | * (c) Chimezie Enyinnaya 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'path' 11 | import * as sinkStatic from '@adonisjs/sink' 12 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 13 | 14 | /** 15 | * Returns absolute path to the stub relative from the templates 16 | * directory 17 | */ 18 | function getStub(...relativePaths: string[]) { 19 | return join(__dirname, 'templates', ...relativePaths) 20 | } 21 | 22 | /** 23 | * Create the model files 24 | */ 25 | function makeModels(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 26 | const modelsDirectory = app.resolveNamespaceDirectory('models') || 'app/Models' 27 | 28 | /** 29 | * User model 30 | */ 31 | const userModelPath = join(modelsDirectory, 'User.ts') 32 | const userModelTemplate = new sink.files.MustacheFile( 33 | projectRoot, 34 | userModelPath, 35 | getStub('models/User.txt') 36 | ) 37 | 38 | userModelTemplate.overwrite = true 39 | 40 | userModelTemplate.commit() 41 | sink.logger.action('create').succeeded(userModelPath) 42 | 43 | /** 44 | * PasswordResetToken model 45 | */ 46 | const passwordResetTokenModelPath = join(modelsDirectory, 'PasswordResetToken.ts') 47 | const passwordResetTokenModelTemplate = new sink.files.MustacheFile( 48 | projectRoot, 49 | passwordResetTokenModelPath, 50 | getStub('models/PasswordResetToken.txt') 51 | ) 52 | 53 | if (passwordResetTokenModelTemplate.exists()) { 54 | sink.logger.action('create').skipped(`${passwordResetTokenModelPath} file already exists`) 55 | } else { 56 | passwordResetTokenModelTemplate.commit() 57 | sink.logger.action('create').succeeded(passwordResetTokenModelPath) 58 | } 59 | } 60 | 61 | /** 62 | * Create the migration files 63 | */ 64 | function makeMigrations(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 65 | const migrationsDirectory = app.directoriesMap.get('migrations') || 'database' 66 | 67 | /** 68 | * users migration 69 | */ 70 | const usersMigrationPath = join(migrationsDirectory, `${Date.now()}_users.ts`) 71 | 72 | const usersMigrationTemplate = new sink.files.MustacheFile( 73 | projectRoot, 74 | usersMigrationPath, 75 | getStub('migrations/users.txt') 76 | ) 77 | 78 | if (usersMigrationTemplate.exists()) { 79 | sink.logger.action('create').skipped(`${usersMigrationPath} file already exists`) 80 | } else { 81 | usersMigrationTemplate.commit() 82 | sink.logger.action('create').succeeded(usersMigrationPath) 83 | } 84 | 85 | /** 86 | * password_reset_tokens migration 87 | */ 88 | const passwordResetTokensMigrationPath = join( 89 | migrationsDirectory, 90 | `${Date.now()}_password_reset_tokens.ts` 91 | ) 92 | 93 | const passwordResetTokensMigrationTemplate = new sink.files.MustacheFile( 94 | projectRoot, 95 | passwordResetTokensMigrationPath, 96 | getStub('migrations/password_reset_tokens.txt') 97 | ) 98 | 99 | if (passwordResetTokensMigrationTemplate.exists()) { 100 | sink.logger.action('create').skipped(`${passwordResetTokensMigrationPath} file already exists`) 101 | } else { 102 | passwordResetTokensMigrationTemplate.commit() 103 | sink.logger.action('create').succeeded(passwordResetTokensMigrationPath) 104 | } 105 | } 106 | 107 | /** 108 | * Create the middlewares 109 | */ 110 | function makeMiddlewares(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 111 | const middlewareDirectory = app.resolveNamespaceDirectory('middleware') || 'app/Middleware' 112 | 113 | /** 114 | * Auth middleware 115 | */ 116 | const authMiddlewarePath = join(middlewareDirectory, 'Auth.ts') 117 | const authMiddlewareTemplate = new sink.files.MustacheFile( 118 | projectRoot, 119 | authMiddlewarePath, 120 | getStub('middlewares/Auth.txt') 121 | ) 122 | 123 | authMiddlewareTemplate.overwrite = true 124 | 125 | authMiddlewareTemplate.commit() 126 | sink.logger.action('create').succeeded(authMiddlewarePath) 127 | 128 | /** 129 | * Guest middleware 130 | */ 131 | const guestMiddlewarePath = join(middlewareDirectory, 'Guest.ts') 132 | const guestMiddlewareTemplate = new sink.files.MustacheFile( 133 | projectRoot, 134 | guestMiddlewarePath, 135 | getStub('middlewares/Guest.txt') 136 | ) 137 | 138 | if (guestMiddlewareTemplate.exists()) { 139 | sink.logger.action('create').skipped(`${guestMiddlewarePath} file already exists`) 140 | } else { 141 | guestMiddlewareTemplate.commit() 142 | sink.logger.action('create').succeeded(guestMiddlewarePath) 143 | } 144 | 145 | /** 146 | * ConfirmPassword middleware 147 | */ 148 | const confirmPasswordMiddlewarePath = join(middlewareDirectory, 'ConfirmPassword.ts') 149 | const confirmPasswordMiddlewareTemplate = new sink.files.MustacheFile( 150 | projectRoot, 151 | confirmPasswordMiddlewarePath, 152 | getStub('middlewares/ConfirmPassword.txt') 153 | ) 154 | 155 | if (confirmPasswordMiddlewareTemplate.exists()) { 156 | sink.logger.action('create').skipped(`${confirmPasswordMiddlewarePath} file already exists`) 157 | } else { 158 | confirmPasswordMiddlewareTemplate.commit() 159 | sink.logger.action('create').succeeded(confirmPasswordMiddlewarePath) 160 | } 161 | } 162 | 163 | /** 164 | * Create the controllers 165 | */ 166 | function makeControllers(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 167 | const controllerDirectory = 168 | app.resolveNamespaceDirectory('httpControllers') || 'app/Controllers/Http' 169 | 170 | /** 171 | * AuthController 172 | */ 173 | const authControllerPath = join(controllerDirectory, 'Auth/AuthController.ts') 174 | const authControllerTemplate = new sink.files.MustacheFile( 175 | projectRoot, 176 | authControllerPath, 177 | getStub('controllers/AuthController.txt') 178 | ) 179 | 180 | if (authControllerTemplate.exists()) { 181 | sink.logger.action('create').skipped(`${authControllerPath} file already exists`) 182 | } else { 183 | authControllerTemplate.commit() 184 | sink.logger.action('create').succeeded(authControllerPath) 185 | } 186 | 187 | /** 188 | * RegisterController 189 | */ 190 | const registerControllerPath = join(controllerDirectory, 'Auth/RegisterController.ts') 191 | const registerControllerTemplate = new sink.files.MustacheFile( 192 | projectRoot, 193 | registerControllerPath, 194 | getStub('controllers/RegisterController.txt') 195 | ) 196 | 197 | if (registerControllerTemplate.exists()) { 198 | sink.logger.action('create').skipped(`${registerControllerPath} file already exists`) 199 | } else { 200 | registerControllerTemplate.commit() 201 | sink.logger.action('create').succeeded(registerControllerPath) 202 | } 203 | 204 | /** 205 | * ConfirmPasswordController 206 | */ 207 | const confirmPasswordControllerPath = join( 208 | controllerDirectory, 209 | 'Auth/ConfirmPasswordController.ts' 210 | ) 211 | const confirmPasswordControllerTemplate = new sink.files.MustacheFile( 212 | projectRoot, 213 | confirmPasswordControllerPath, 214 | getStub('controllers/ConfirmPasswordController.txt') 215 | ) 216 | 217 | if (confirmPasswordControllerTemplate.exists()) { 218 | sink.logger.action('create').skipped(`${confirmPasswordControllerPath} file already exists`) 219 | } else { 220 | confirmPasswordControllerTemplate.commit() 221 | sink.logger.action('create').succeeded(confirmPasswordControllerPath) 222 | } 223 | 224 | /** 225 | * EmailVerificationController 226 | */ 227 | const emailVerificationControllerPath = join( 228 | controllerDirectory, 229 | 'Auth/EmailVerificationController.ts' 230 | ) 231 | const emailVerificationControllerTemplate = new sink.files.MustacheFile( 232 | projectRoot, 233 | emailVerificationControllerPath, 234 | getStub('controllers/EmailVerificationController.txt') 235 | ) 236 | 237 | if (emailVerificationControllerTemplate.exists()) { 238 | sink.logger.action('create').skipped(`${emailVerificationControllerPath} file already exists`) 239 | } else { 240 | emailVerificationControllerTemplate.commit() 241 | sink.logger.action('create').succeeded(emailVerificationControllerPath) 242 | } 243 | 244 | /** 245 | * PasswordResetController 246 | */ 247 | const passwordResetControllerPath = join(controllerDirectory, 'Auth/PasswordResetController.ts') 248 | const passwordResetControllerTemplate = new sink.files.MustacheFile( 249 | projectRoot, 250 | passwordResetControllerPath, 251 | getStub('controllers/PasswordResetController.txt') 252 | ) 253 | 254 | if (passwordResetControllerTemplate.exists()) { 255 | sink.logger.action('create').skipped(`${passwordResetControllerPath} file already exists`) 256 | } else { 257 | passwordResetControllerTemplate.commit() 258 | sink.logger.action('create').succeeded(passwordResetControllerPath) 259 | } 260 | 261 | /** 262 | * PasswordResetRequestController 263 | */ 264 | const passwordResetRequestControllerPath = join( 265 | controllerDirectory, 266 | 'Auth/PasswordResetRequestController.ts' 267 | ) 268 | const passwordResetRequestControllerTemplate = new sink.files.MustacheFile( 269 | projectRoot, 270 | passwordResetRequestControllerPath, 271 | getStub('controllers/PasswordResetRequestController.txt') 272 | ) 273 | 274 | if (passwordResetRequestControllerTemplate.exists()) { 275 | sink.logger 276 | .action('create') 277 | .skipped(`${passwordResetRequestControllerPath} file already exists`) 278 | } else { 279 | passwordResetRequestControllerTemplate.commit() 280 | sink.logger.action('create').succeeded(passwordResetRequestControllerPath) 281 | } 282 | } 283 | 284 | /** 285 | * Create validators 286 | */ 287 | function makeValidators(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 288 | const validatorsDirectory = app.resolveNamespaceDirectory('validators') || 'app/Validators' 289 | 290 | /** 291 | * EmailValidator 292 | */ 293 | const emailValidatorPath = join(validatorsDirectory, 'EmailValidator.ts') 294 | const emailValidatorTemplate = new sink.files.MustacheFile( 295 | projectRoot, 296 | emailValidatorPath, 297 | getStub('validators/EmailValidator.txt') 298 | ) 299 | 300 | if (emailValidatorTemplate.exists()) { 301 | sink.logger.action('create').skipped(`${emailValidatorPath} file already exists`) 302 | } else { 303 | emailValidatorTemplate.commit() 304 | sink.logger.action('create').succeeded(emailValidatorPath) 305 | } 306 | 307 | /** 308 | * LoginValidator 309 | */ 310 | const loginValidatorPath = join(validatorsDirectory, 'LoginValidator.ts') 311 | const loginValidatorTemplate = new sink.files.MustacheFile( 312 | projectRoot, 313 | loginValidatorPath, 314 | getStub('validators/LoginValidator.txt') 315 | ) 316 | 317 | if (loginValidatorTemplate.exists()) { 318 | sink.logger.action('create').skipped(`${loginValidatorPath} file already exists`) 319 | } else { 320 | loginValidatorTemplate.commit() 321 | sink.logger.action('create').succeeded(loginValidatorPath) 322 | } 323 | 324 | /** 325 | * PasswordResetValidator 326 | */ 327 | const passwordResetValidatorPath = join(validatorsDirectory, 'PasswordResetValidator.ts') 328 | const passwordResetValidatorTemplate = new sink.files.MustacheFile( 329 | projectRoot, 330 | passwordResetValidatorPath, 331 | getStub('validators/PasswordResetValidator.txt') 332 | ) 333 | 334 | if (passwordResetValidatorTemplate.exists()) { 335 | sink.logger.action('create').skipped(`${passwordResetValidatorPath} file already exists`) 336 | } else { 337 | passwordResetValidatorTemplate.commit() 338 | sink.logger.action('create').succeeded(passwordResetValidatorPath) 339 | } 340 | 341 | /** 342 | * RegisterValidator 343 | */ 344 | const registerValidatorPath = join(validatorsDirectory, 'RegisterValidator.ts') 345 | const registerValidatorTemplate = new sink.files.MustacheFile( 346 | projectRoot, 347 | registerValidatorPath, 348 | getStub('validators/RegisterValidator.txt') 349 | ) 350 | 351 | if (registerValidatorTemplate.exists()) { 352 | sink.logger.action('create').skipped(`${registerValidatorPath} file already exists`) 353 | } else { 354 | registerValidatorTemplate.commit() 355 | sink.logger.action('create').succeeded(registerValidatorPath) 356 | } 357 | } 358 | 359 | /** 360 | * Create AppProvider 361 | */ 362 | function makeProvider(projectRoot: string, sink: typeof sinkStatic) { 363 | const appProviderPath = join('providers', 'AppProvider.ts') 364 | const template = new sink.files.MustacheFile( 365 | projectRoot, 366 | appProviderPath, 367 | getStub('providers/AppProvider.txt') 368 | ) 369 | 370 | template.overwrite = true 371 | 372 | template.commit() 373 | sink.logger.action('create').succeeded(appProviderPath) 374 | } 375 | 376 | /** 377 | * Create EventsContract 378 | */ 379 | function makeEventsContract(projectRoot: string, sink: typeof sinkStatic) { 380 | const appProviderPath = join('contracts', 'events.ts') 381 | const template = new sink.files.MustacheFile( 382 | projectRoot, 383 | appProviderPath, 384 | getStub('contracts/events.txt') 385 | ) 386 | 387 | template.overwrite = true 388 | 389 | template.commit() 390 | sink.logger.action('create').succeeded(appProviderPath) 391 | } 392 | 393 | /** 394 | * Create EventsContract 395 | */ 396 | function makeRoutes(projectRoot: string, sink: typeof sinkStatic) { 397 | const appProviderPath = join('start', 'routes.ts') 398 | const template = new sink.files.MustacheFile( 399 | projectRoot, 400 | appProviderPath, 401 | getStub('start/routes.txt') 402 | ) 403 | 404 | template.overwrite = true 405 | 406 | template.commit() 407 | sink.logger.action('create').succeeded(appProviderPath) 408 | } 409 | 410 | /** 411 | * Create assets 412 | */ 413 | function makeAssets(projectRoot: string, sink: typeof sinkStatic) { 414 | const resourcesDirectory = 'resources' 415 | 416 | /** 417 | * app.css 418 | */ 419 | const appCssPath = join(resourcesDirectory, 'css/app.css') 420 | const appCssTemplate = new sink.files.MustacheFile( 421 | projectRoot, 422 | appCssPath, 423 | getStub('resources/css/app.txt') 424 | ) 425 | 426 | appCssTemplate.overwrite = true 427 | 428 | appCssTemplate.commit() 429 | sink.logger.action('create').succeeded(appCssPath) 430 | 431 | /** 432 | * app.js 433 | */ 434 | const appJsPath = join(resourcesDirectory, 'js/app.js') 435 | const appJsTemplate = new sink.files.MustacheFile( 436 | projectRoot, 437 | appJsPath, 438 | getStub('resources/js/app.txt') 439 | ) 440 | 441 | appJsTemplate.overwrite = true 442 | 443 | appJsTemplate.commit() 444 | sink.logger.action('create').succeeded(appJsPath) 445 | } 446 | 447 | /** 448 | * Create EventsContract 449 | */ 450 | function makeTailwindFiles(projectRoot: string, sink: typeof sinkStatic) { 451 | /** 452 | * PostCSS config 453 | */ 454 | const postCssConfigPath = 'postcss.config.js' 455 | const postCssConfigTemplate = new sink.files.MustacheFile( 456 | projectRoot, 457 | postCssConfigPath, 458 | getStub('postcss.config.txt') 459 | ) 460 | 461 | if (postCssConfigTemplate.exists()) { 462 | sink.logger.action('create').skipped(`${postCssConfigPath} file already exists`) 463 | } else { 464 | postCssConfigTemplate.commit() 465 | sink.logger.action('create').succeeded(postCssConfigPath) 466 | } 467 | 468 | /** 469 | * Tailwind config 470 | */ 471 | const tailwindConfigPath = 'tailwind.config.js' 472 | const tailwindConfigTemplate = new sink.files.MustacheFile( 473 | projectRoot, 474 | tailwindConfigPath, 475 | getStub('tailwind.config.txt') 476 | ) 477 | 478 | if (tailwindConfigTemplate.exists()) { 479 | sink.logger.action('create').skipped(`${tailwindConfigPath} file already exists`) 480 | } else { 481 | tailwindConfigTemplate.commit() 482 | sink.logger.action('create').succeeded(tailwindConfigPath) 483 | } 484 | 485 | /** 486 | * Webpack config 487 | */ 488 | const webpackConfigPath = 'webpack.config.js' 489 | const webpackConfigTemplate = new sink.files.MustacheFile( 490 | projectRoot, 491 | webpackConfigPath, 492 | getStub('webpack.config.txt') 493 | ) 494 | 495 | webpackConfigTemplate.overwrite = true 496 | 497 | webpackConfigTemplate.commit() 498 | sink.logger.action('create').succeeded(webpackConfigPath) 499 | } 500 | 501 | /** 502 | * Instructions to be executed when setting up the package. 503 | */ 504 | export default async function instructions( 505 | projectRoot: string, 506 | app: ApplicationContract, 507 | sink: typeof sinkStatic 508 | ) { 509 | makeModels(projectRoot, app, sink) 510 | 511 | makeMigrations(projectRoot, app, sink) 512 | 513 | makeMiddlewares(projectRoot, app, sink) 514 | 515 | makeControllers(projectRoot, app, sink) 516 | 517 | makeValidators(projectRoot, app, sink) 518 | 519 | makeProvider(projectRoot, sink) 520 | 521 | makeEventsContract(projectRoot, sink) 522 | 523 | makeRoutes(projectRoot, sink) 524 | 525 | makeAssets(projectRoot, sink) 526 | 527 | makeTailwindFiles(projectRoot, sink) 528 | 529 | /** 530 | * Install required dependencies 531 | */ 532 | const pkg = new sink.files.PackageJsonFile(projectRoot) 533 | 534 | pkg.install('tailwindcss', undefined, true) 535 | pkg.install('@tailwindcss/forms', undefined, true) 536 | pkg.install('autoprefixer', undefined, true) 537 | pkg.install('postcss', undefined, true) 538 | pkg.install('postcss-loader', '^6.0.0', true) 539 | pkg.install('alpinejs', undefined, true) 540 | 541 | const logLines = [`Installing: ${sink.logger.colors.gray(pkg.getInstalls(true).list.join(', '))}`] 542 | 543 | const spinner = sink.logger.await(logLines.join(' ')) 544 | 545 | try { 546 | await pkg.commitAsync() 547 | spinner.update('Packages installed') 548 | } catch (error) { 549 | spinner.update('Unable to install packages') 550 | sink.logger.fatal(error) 551 | } 552 | 553 | spinner.stop() 554 | } 555 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mezielabs/flair", 3 | "version": "1.0.6", 4 | "description": "AdonisJS authentication scaffolding", 5 | "scripts": { 6 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 7 | "pretest": "npm run lint", 8 | "clean": "del-cli build", 9 | "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", 10 | "compile": "npm run lint && npm run clean && tsc && npm run copyfiles", 11 | "build": "npm run compile", 12 | "prepublishOnly": "npm run build", 13 | "lint": "eslint . --ext=.ts", 14 | "format": "prettier --write .", 15 | "commit": "git-cz", 16 | "release": "np --no-tests --message=\"chore(release): %s\"", 17 | "version": "npm run build", 18 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json mezielabs/flair" 19 | }, 20 | "keywords": [ 21 | "adonisjs", 22 | "adonis", 23 | "authentication" 24 | ], 25 | "author": "Chimezie Enyinnaya", 26 | "license": "MIT", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/mezielabs/flair.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mezielabs/flair/issues" 33 | }, 34 | "homepage": "https://github.com/mezielabs/flair#readme", 35 | "devDependencies": { 36 | "@adonisjs/core": "^5.8.3", 37 | "@adonisjs/mrm-preset": "^5.0.3", 38 | "@adonisjs/require-ts": "^2.0.12", 39 | "@adonisjs/sink": "^5.3.2", 40 | "@types/node": "^17.0.41", 41 | "commitizen": "^4.2.4", 42 | "copyfiles": "^2.4.1", 43 | "cz-conventional-changelog": "^3.3.0", 44 | "del-cli": "^4.0.1", 45 | "eslint-config-prettier": "^8.5.0", 46 | "eslint-plugin-adonis": "^2.1.0", 47 | "eslint-plugin-prettier": "^4.0.0", 48 | "github-label-sync": "^2.2.0", 49 | "husky": "^8.0.1", 50 | "mrm": "^4.0.0", 51 | "np": "^7.6.1", 52 | "prettier": "^2.6.2", 53 | "typescript": "^4.7.3" 54 | }, 55 | "mrmConfig": { 56 | "core": false, 57 | "license": "MIT", 58 | "services": [], 59 | "minNodeVersion": "14.15.4", 60 | "probotApps": [] 61 | }, 62 | "files": [ 63 | "build/templates", 64 | "build/instructions.js", 65 | "build/instructions.md" 66 | ], 67 | "eslintConfig": { 68 | "extends": [ 69 | "plugin:adonis/typescriptPackage", 70 | "prettier" 71 | ], 72 | "plugins": [ 73 | "prettier" 74 | ], 75 | "rules": { 76 | "prettier/prettier": [ 77 | "error", 78 | { 79 | "endOfLine": "auto" 80 | } 81 | ] 82 | } 83 | }, 84 | "eslintIgnore": [ 85 | "build" 86 | ], 87 | "prettier": { 88 | "trailingComma": "es5", 89 | "semi": false, 90 | "singleQuote": true, 91 | "useTabs": false, 92 | "quoteProps": "consistent", 93 | "bracketSpacing": true, 94 | "arrowParens": "always", 95 | "printWidth": 100 96 | }, 97 | "config": { 98 | "commitizen": { 99 | "path": "cz-conventional-changelog" 100 | } 101 | }, 102 | "np": { 103 | "contents": ".", 104 | "anyBranch": false 105 | }, 106 | "publishConfig": { 107 | "access": "public" 108 | }, 109 | "adonisjs": { 110 | "instructions": "./build/instructions.js", 111 | "instructionsMd": "./build/instructions.md", 112 | "templates": { 113 | "basePath": "./build/templates", 114 | "contracts": [ 115 | { 116 | "src": "contracts/response.txt", 117 | "dest": "response" 118 | } 119 | ], 120 | "views": [ 121 | { 122 | "src": "resources/views/home.txt", 123 | "dest": "home.edge" 124 | }, 125 | { 126 | "src": "resources/views/dashboard.txt", 127 | "dest": "dashboard.edge" 128 | }, 129 | { 130 | "src": "resources/views/layouts/app.txt", 131 | "dest": "layouts/app.edge" 132 | }, 133 | { 134 | "src": "resources/views/auth/register.txt", 135 | "dest": "auth/register.edge" 136 | }, 137 | { 138 | "src": "resources/views/auth/login.txt", 139 | "dest": "auth/login.edge" 140 | }, 141 | { 142 | "src": "resources/views/auth/forgot-password.txt", 143 | "dest": "auth/forgot-password.edge" 144 | }, 145 | { 146 | "src": "resources/views/auth/confirm-password.txt", 147 | "dest": "auth/confirm-password.edge" 148 | }, 149 | { 150 | "src": "resources/views/auth/reset-password.txt", 151 | "dest": "auth/reset-password.edge" 152 | }, 153 | { 154 | "src": "resources/views/auth/resend-verification.txt", 155 | "dest": "auth/resend-verification.edge" 156 | } 157 | ] 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /templates/contracts/events.txt: -------------------------------------------------------------------------------- 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 | import User from 'App/Models/User' 9 | 10 | declare module '@ioc:Adonis/Core/Event' { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Define typed events 14 | |-------------------------------------------------------------------------- 15 | | 16 | | You can define types for events inside the following interface and 17 | | AdonisJS will make sure that all listeners and emit calls adheres 18 | | to the defined types. 19 | | 20 | | For example: 21 | | 22 | | interface EventsList { 23 | | 'new:user': UserModel 24 | | } 25 | | 26 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 27 | | an instance of the the UserModel only. 28 | | 29 | */ 30 | interface EventsList { 31 | userRegistered: User 32 | passwordResetRequested: { 33 | user: User 34 | token: string 35 | } 36 | passwordReset: User 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/contracts/response.txt: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Response' { 2 | interface ResponseContract { 3 | intended(defaultUri?: string): this 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /templates/controllers/AuthController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import LoginValidator from 'App/Validators/LoginValidator' 3 | 4 | export default class AuthController { 5 | public create({ view }: HttpContextContract) { 6 | return view.render('auth/login') 7 | } 8 | 9 | public async store({ request, session, response, auth }: HttpContextContract) { 10 | const { email, password, remember } = await request.validate(LoginValidator) 11 | 12 | try { 13 | await auth.attempt(email, password, remember) 14 | 15 | session.flash({ 16 | notification: { 17 | type: 'success', 18 | message: "Welcome back, you're now signed in.", 19 | }, 20 | }) 21 | 22 | return response.intended() 23 | } catch (error) { 24 | session.flash({ 25 | notification: { 26 | type: 'error', 27 | message: "We couldn't verify your credentials.", 28 | }, 29 | }) 30 | 31 | return response.redirect().back() 32 | } 33 | } 34 | 35 | public async destroy({ auth, session, response }: HttpContextContract) { 36 | await auth.logout() 37 | 38 | session.forget('password_confirmed_at') 39 | 40 | return response.redirect().toRoute('home') 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /templates/controllers/ConfirmPasswordController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { DateTime } from 'luxon' 3 | 4 | export default class ConfirmPasswordController { 5 | public create({ view }: HttpContextContract) { 6 | return view.render('auth/confirm-password') 7 | } 8 | 9 | public async store({ request, auth, session, response }: HttpContextContract) { 10 | try { 11 | await auth.verifyCredentials(auth.user!.email, request.input('password')) 12 | 13 | session.put('password_confirmed_at', DateTime.now().toSeconds()) 14 | 15 | return response.intended() 16 | } catch (error) { 17 | session.flash({ 18 | notification: { 19 | type: 'error', 20 | message: "We couldn't verify your credentials.", 21 | }, 22 | }) 23 | 24 | return response.redirect().back() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/controllers/EmailVerificationController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import { DateTime } from 'luxon' 4 | import EmailValidator from 'App/Validators/EmailValidator' 5 | import Event from '@ioc:Adonis/Core/Event' 6 | 7 | export default class EmailVerificationController { 8 | public async verify({ request, params, session, response }: HttpContextContract) { 9 | if (!request.hasValidSignature()) { 10 | session.flash({ 11 | notification: { 12 | type: 'error', 13 | message: 'Verification link is invalid or has expired.', 14 | }, 15 | }) 16 | 17 | return response.redirect('/verification/new') 18 | } 19 | 20 | const user = await User.findByOrFail('email', params.email) 21 | 22 | if (user.emailVerifiedAt) { 23 | session.flash({ 24 | notification: { 25 | type: 'info', 26 | message: 'Email address already verified.', 27 | }, 28 | }) 29 | 30 | return response.redirect('/login') 31 | } 32 | 33 | user.emailVerifiedAt = DateTime.now() 34 | await user.save() 35 | 36 | session.flash({ 37 | notification: { 38 | type: 'success', 39 | message: 'Email address verified.', 40 | }, 41 | }) 42 | 43 | return response.redirect('/login') 44 | } 45 | 46 | public create({ view }: HttpContextContract) { 47 | return view.render('auth/resend-verification') 48 | } 49 | 50 | public async store({ request, session, response }: HttpContextContract) { 51 | const { email } = await request.validate(EmailValidator) 52 | 53 | const user = await User.findBy('email', email) 54 | 55 | if (user?.emailVerifiedAt) { 56 | session.flash({ 57 | notification: { 58 | type: 'info', 59 | message: 'Email address arealdy verified.', 60 | }, 61 | }) 62 | 63 | return response.redirect('/login') 64 | } 65 | 66 | Event.emit('userRegistered', user!) 67 | 68 | session.flash({ 69 | notification: { 70 | type: 'success', 71 | message: 72 | 'A verification link has been sent to your email address, kindly follow the link to verify your email address.', 73 | }, 74 | }) 75 | 76 | return response.redirect().back() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /templates/controllers/PasswordResetController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import PasswordResetToken from 'App/Models/PasswordResetToken' 3 | import PasswordResetValidator from 'App/Validators/PasswordResetValidator' 4 | import Event from '@ioc:Adonis/Core/Event' 5 | 6 | export default class PasswordResetController { 7 | public async create({ params, view, session, response }: HttpContextContract) { 8 | try { 9 | const token = await PasswordResetToken.query() 10 | .where('token', decodeURIComponent(params.token)) 11 | .preload('user') 12 | .firstOrFail() 13 | 14 | return view.render('auth/reset-password', { 15 | token: token.token, 16 | email: token.user.email, 17 | }) 18 | } catch (error) { 19 | session.flash({ 20 | notification: { 21 | type: 'error', 22 | message: 'Invalid password reset token.', 23 | }, 24 | }) 25 | 26 | return response.redirect('/forgot-password') 27 | } 28 | } 29 | 30 | public async store({ request, session, response }: HttpContextContract) { 31 | const payload = await request.validate(PasswordResetValidator) 32 | 33 | try { 34 | const token = await PasswordResetToken.query() 35 | .where('token', payload.token) 36 | .preload('user') 37 | .firstOrFail() 38 | 39 | const user = token.user 40 | 41 | user.password = payload.password 42 | await user.save() 43 | 44 | await token.delete() 45 | 46 | Event.emit('passwordReset', user) 47 | 48 | session.flash({ 49 | notification: { 50 | type: 'success', 51 | message: 'Password reset successful.', 52 | }, 53 | }) 54 | 55 | return response.redirect('/login') 56 | } catch (error) { 57 | session.flash({ 58 | notification: { 59 | type: 'error', 60 | message: 'Invalid password reset token.', 61 | }, 62 | }) 63 | 64 | return response.redirect().back() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /templates/controllers/PasswordResetRequestController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import PasswordResetToken from 'App/Models/PasswordResetToken' 3 | import User from 'App/Models/User' 4 | import EmailValidator from 'App/Validators/EmailValidator' 5 | import { string } from '@ioc:Adonis/Core/Helpers' 6 | import Encryption from '@ioc:Adonis/Core/Encryption' 7 | import Event from '@ioc:Adonis/Core/Event' 8 | 9 | export default class PasswordResetRequestController { 10 | public create({ view }: HttpContextContract) { 11 | return view.render('auth/forgot-password') 12 | } 13 | 14 | public async store({ request, session, response }: HttpContextContract) { 15 | const { email } = await request.validate(EmailValidator) 16 | 17 | const user = await User.findByOrFail('email', email) 18 | 19 | await PasswordResetToken.query().where('user_id', user.id).delete() 20 | 21 | const { token } = await PasswordResetToken.create({ 22 | userId: user.id, 23 | token: Encryption.encrypt(string.generateRandom(32)), 24 | }) 25 | 26 | Event.emit('passwordResetRequested', { user, token }) 27 | 28 | session.flash({ 29 | notification: { 30 | type: 'success', 31 | message: 'A password reset link has been sent to your email address.', 32 | }, 33 | }) 34 | 35 | return response.redirect().back() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /templates/controllers/RegisterController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import RegisterValidator from 'App/Validators/RegisterValidator' 4 | import Event from '@ioc:Adonis/Core/Event' 5 | 6 | export default class RegisterController { 7 | public create({ view }: HttpContextContract) { 8 | return view.render('auth/register') 9 | } 10 | 11 | public async store({ request, auth, session, response }: HttpContextContract) { 12 | const payload = await request.validate(RegisterValidator) 13 | 14 | const user = await User.create(payload) 15 | 16 | Event.emit('userRegistered', user) 17 | 18 | session.flash({ 19 | notification: { 20 | type: 'success', 21 | message: 'Register successful!', 22 | }, 23 | }) 24 | 25 | await auth.login(user) 26 | 27 | return response.redirect().toRoute('dashboard') 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/middlewares/Auth.txt: -------------------------------------------------------------------------------- 1 | import { GuardsList } from '@ioc:Adonis/Addons/Auth' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import { AuthenticationException } from '@adonisjs/auth/build/standalone' 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(ctx: HttpContextContract, 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 ctx.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 | ctx.auth.defaultGuard = guard 45 | return true 46 | } 47 | } 48 | 49 | ctx.session.put('intended_url', ctx.request.url()) 50 | 51 | /** 52 | * Unable to authenticate using any guard 53 | */ 54 | throw new AuthenticationException( 55 | 'Unauthorized access', 56 | 'E_UNAUTHORIZED_ACCESS', 57 | guardLastAttempted, 58 | this.redirectTo 59 | ) 60 | } 61 | 62 | /** 63 | * Handle request 64 | */ 65 | public async handle( 66 | ctx: HttpContextContract, 67 | next: () => Promise, 68 | customGuards: (keyof GuardsList)[] 69 | ) { 70 | /** 71 | * Uses the user defined guards or the default guard mentioned in 72 | * the config file 73 | */ 74 | const guards = customGuards.length ? customGuards : [ctx.auth.name] 75 | await this.authenticate(ctx, guards) 76 | await next() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /templates/middlewares/ConfirmPassword.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { DateTime } from 'luxon' 3 | 4 | export default class ConfirmPassword { 5 | public async handle( 6 | { session, request, response }: HttpContextContract, 7 | next: () => Promise 8 | ) { 9 | const confirmedAt = session.get('password_confirmed_at', 0) 10 | 11 | if (DateTime.now().toSeconds() - confirmedAt > 60) { 12 | session.put('intended_url', request.url()) 13 | 14 | return response.redirect().toRoute('ConfirmPasswordController.create') 15 | } 16 | 17 | await next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /templates/middlewares/Guest.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | 3 | export default class Guest { 4 | public async handle({ auth, response }: HttpContextContract, next: () => Promise) { 5 | if (auth.isLoggedIn) { 6 | return response.redirect().toRoute('home') 7 | } 8 | 9 | await next() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/migrations/password_reset_tokens.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class PasswordResetTokens extends BaseSchema { 4 | protected tableName = 'password_reset_tokens' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table 10 | .integer('user_id') 11 | .unsigned() 12 | .notNullable() 13 | .references('users.id') 14 | .onDelete('CASCADE') 15 | .index() 16 | table.string('token').notNullable() 17 | 18 | /** 19 | * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL 20 | */ 21 | table.timestamp('created_at', { useTz: true }) 22 | table.timestamp('updated_at', { useTz: true }) 23 | }) 24 | } 25 | 26 | public async down() { 27 | this.schema.dropTable(this.tableName) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/migrations/users.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class UsersSchema extends BaseSchema { 4 | protected tableName = 'users' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table.string('name').notNullable() 10 | table.string('email', 255).unique().notNullable() 11 | table.string('password', 180).notNullable() 12 | table.string('remember_me_token').nullable() 13 | table.timestamp('email_verified_at').nullable() 14 | 15 | /** 16 | * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL 17 | */ 18 | table.timestamp('created_at', { useTz: true }).notNullable() 19 | table.timestamp('updated_at', { useTz: true }).notNullable() 20 | }) 21 | } 22 | 23 | public async down() { 24 | this.schema.dropTable(this.tableName) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /templates/models/PasswordResetToken.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 3 | import User from './User' 4 | 5 | export default class PasswordResetToken extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @column() 10 | public userId: number 11 | 12 | @column() 13 | public token: string 14 | 15 | @column.dateTime({ autoCreate: true }) 16 | public createdAt: DateTime 17 | 18 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 19 | public updatedAt: DateTime 20 | 21 | @belongsTo(() => User) 22 | public user: BelongsTo 23 | } 24 | -------------------------------------------------------------------------------- /templates/models/User.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import Hash from '@ioc:Adonis/Core/Hash' 3 | import { column, beforeSave, BaseModel } from '@ioc:Adonis/Lucid/Orm' 4 | 5 | export default class User extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @column() 10 | public name: string 11 | 12 | @column() 13 | public email: string 14 | 15 | @column({ serializeAs: null }) 16 | public password: string 17 | 18 | @column() 19 | public rememberMeToken?: string 20 | 21 | @column.dateTime() 22 | public emailVerifiedAt?: DateTime 23 | 24 | @column.dateTime({ autoCreate: true }) 25 | public createdAt: DateTime 26 | 27 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 28 | public updatedAt: DateTime 29 | 30 | @beforeSave() 31 | public static async hashPassword(user: User) { 32 | if (user.$dirty.password) { 33 | user.password = await Hash.make(user.password) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /templates/postcss.config.txt: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /templates/providers/AppProvider.txt: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class AppProvider { 4 | constructor(protected app: ApplicationContract) {} 5 | 6 | public register() { 7 | // Register your own bindings 8 | } 9 | 10 | public async boot() { 11 | const Response = this.app.container.use('Adonis/Core/Response') 12 | 13 | Response.macro('intended', function (defaultUri = '/') { 14 | const intendedUrl = this.ctx!.session.pull('intended_url', defaultUri) 15 | 16 | this.ctx!.response.redirect(intendedUrl) 17 | 18 | return this 19 | }) 20 | } 21 | 22 | public async ready() { 23 | // App is ready 24 | } 25 | 26 | public async shutdown() { 27 | // Cleanup, since app is going down 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/resources/css/app.txt: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /templates/resources/js/app.txt: -------------------------------------------------------------------------------- 1 | import '../css/app.css' 2 | import Alpine from 'alpinejs' 3 | 4 | window.Alpine = Alpine 5 | Alpine.start() 6 | -------------------------------------------------------------------------------- /templates/resources/views/auth/confirm-password.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Please confirm your password to continue. 8 |

9 | 10 |
11 | {{ csrfField() }} 12 | 13 |
14 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 26 |
27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /templates/resources/views/auth/forgot-password.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Enter your email address and we'll send you a password reset link. 8 |

9 | 10 |
11 | {{ csrfField() }} 12 | 13 |
14 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 26 |
27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /templates/resources/views/auth/login.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

Log in

7 | 8 |
9 | {{ csrfField() }} 10 | 11 |
12 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 | 35 |
36 | 37 | 42 |
43 | 44 |
45 | 48 |
49 |
50 | 55 |
56 |
57 | @endsection 58 | -------------------------------------------------------------------------------- /templates/resources/views/auth/register.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

Create a free account

7 | 8 |
9 | {{ csrfField() }} 10 | 11 |
12 | 15 |
16 | 17 |
18 | 19 | @if (flashMessages.has('errors.name')) 20 |

21 | {{ flashMessages.get('errors.name') }} 22 |

23 | @endif 24 |
25 | 26 |
27 | 30 |
31 | 32 |
33 | 34 | @if (flashMessages.has('errors.email')) 35 |

36 | {{ flashMessages.get('errors.email') }} 37 |

38 | @endif 39 |
40 | 41 |
42 | 45 |
46 | 47 |
48 | 49 | @if (flashMessages.has('errors.password')) 50 |

51 | {{ flashMessages.get('errors.password') }} 52 |

53 | @endif 54 |
55 | 56 |
57 | 60 |
61 | 62 |
63 | 64 | @if (flashMessages.has('errors.password_confirmation')) 65 |

66 | {{ flashMessages.get('errors.password_confirmation') }} 67 |

68 | @endif 69 |
70 | 71 |
72 | 75 |
76 |
77 |
78 |
79 | @endsection 80 | -------------------------------------------------------------------------------- /templates/resources/views/auth/resend-verification.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Didn't receive the verification link? Enter your email address and we'll resend it to you. 8 |

9 | 10 |
11 | {{ csrfField() }} 12 | 13 |
14 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 26 |
27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /templates/resources/views/auth/reset-password.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |
5 |
6 |

Set a new password reset.

7 | 8 |
9 | {{ csrfField() }} 10 | 11 | 12 | 13 |
14 | 17 |
18 | 28 |
29 |
30 | 31 |
32 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 44 |
45 | 46 |
47 |
48 | 49 |
50 | 53 |
54 |
55 |
56 |
57 | @endsection 58 | -------------------------------------------------------------------------------- /templates/resources/views/dashboard.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |

Dashboard

5 | 6 |
7 |
8 | Welcome back, {{ auth.user.name }}! 9 |
10 |
11 | @endsection 12 | -------------------------------------------------------------------------------- /templates/resources/views/home.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/app') 2 | 3 | @section('content') 4 |

Home

5 | @endsection 6 | -------------------------------------------------------------------------------- /templates/resources/views/layouts/app.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ env('APP_NAME') }} 8 | 9 | @entryPointStyles('app') 10 | @entryPointScripts('app') 11 | 12 | 13 | @if (flashMessages.has('notification')) 14 |
28 | @if(flashMessages.get('notification.type') === 'success') 29 |
30 | {{ flashMessages.get('notification.message') }} 31 |
32 | @elseif(flashMessages.get('notification.type') === 'error') 33 |
34 | {{ flashMessages.get('notification.message') }} 35 |
36 | @elseif(flashMessages.get('notification.type') === 'info') 37 |
38 | {{ flashMessages.get('notification.message') }} 39 |
40 | @endif 41 |
42 | @endif 43 | 44 | 132 | 133 |
134 | @!section('content') 135 |
136 | 137 | 138 | -------------------------------------------------------------------------------- /templates/start/routes.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.get('/', async ({ view }) => { 4 | return view.render('home') 5 | }).as('home') 6 | 7 | Route.get('dashboard', async ({ view }) => { 8 | return view.render('dashboard') 9 | }) 10 | .as('dashboard') 11 | .middleware('auth') 12 | 13 | Route.group(() => { 14 | Route.group(() => { 15 | Route.get('register', 'RegisterController.create') 16 | Route.post('register', 'RegisterController.store') 17 | 18 | Route.get('verification/new', 'EmailVerificationController.create') 19 | Route.post('verification', 'EmailVerificationController.store') 20 | Route.get('verification/:email', 'EmailVerificationController.verify').as('verification.verify') 21 | 22 | Route.get('login', 'AuthController.create') 23 | Route.post('login', 'AuthController.store') 24 | 25 | Route.get('forgot-password', 'PasswordResetRequestController.create') 26 | Route.post('forgot-password', 'PasswordResetRequestController.store') 27 | 28 | Route.get('reset-password/:token', 'PasswordResetController.create') 29 | Route.post('reset-password', 'PasswordResetController.store') 30 | }).middleware('guest') 31 | 32 | Route.group(() => { 33 | Route.get('confirm-password', 'ConfirmPasswordController.create') 34 | Route.post('confirm-password', 'ConfirmPasswordController.store') 35 | Route.post('logout', 'AuthController.destroy') 36 | }).middleware('auth') 37 | }).namespace('App/Controllers/Http/Auth') 38 | -------------------------------------------------------------------------------- /templates/tailwind.config.txt: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | const defaultTheme = require('tailwindcss/defaultTheme') 4 | 5 | module.exports = { 6 | content: ['./resources/views/**/*.edge'], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | sans: ['Poppins', ...defaultTheme.fontFamily.sans], 11 | }, 12 | }, 13 | }, 14 | plugins: [require('@tailwindcss/forms')], 15 | } 16 | -------------------------------------------------------------------------------- /templates/validators/EmailValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EmailValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string([ 9 | rules.trim(), 10 | rules.email(), 11 | rules.exists({ table: 'users', column: 'email' }), 12 | ]), 13 | }) 14 | 15 | public messages = { 16 | required: 'The {{ field }} field is required.', 17 | exists: 'No account exists for this email address.', 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /templates/validators/LoginValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class LoginValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string([rules.trim()]), 9 | password: schema.string([rules.trim()]), 10 | remember: schema.boolean.optional(), 11 | }) 12 | 13 | public messages = { 14 | required: 'The {{ field }} field is required.', 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/validators/PasswordResetValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class PasswordResetValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | token: schema.string([rules.trim()]), 9 | email: schema.string([ 10 | rules.trim(), 11 | rules.email(), 12 | rules.exists({ table: 'users', column: 'email' }), 13 | ]), 14 | password: schema.string([rules.trim(), rules.minLength(6), rules.confirmed()]), 15 | }) 16 | 17 | public messages = { 18 | 'required': 'The {{ field }} field is required.', 19 | 'exists': 'No account exists for this email address.', 20 | 'password.minLength': 'Password must be a minimum of 6 chracters.', 21 | 'password_confirmation.confirmed': 'Password confirmation does not match.', 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/validators/RegisterValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class RegisterValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string([rules.trim()]), 9 | email: schema.string([ 10 | rules.trim(), 11 | rules.email(), 12 | rules.normalizeEmail({ allLowercase: true }), 13 | rules.maxLength(255), 14 | rules.unique({ table: 'users', column: 'email' }), 15 | ]), 16 | password: schema.string([rules.trim(), rules.minLength(6), rules.confirmed()]), 17 | }) 18 | 19 | public messages = { 20 | 'required': 'The {{ field }} field is required.', 21 | 'unique': 'The {{ field }} has already been taken.', 22 | 'password.minLength': 'Password must be a minimum of 6 chracters.', 23 | 'password_confirmation.confirmed': 'Password confirmation does not match.', 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/webpack.config.txt: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const Encore = require('@symfony/webpack-encore') 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Encore runtime environment 7 | |-------------------------------------------------------------------------- 8 | */ 9 | if (!Encore.isRuntimeEnvironmentConfigured()) { 10 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev') 11 | } 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Output path 16 | |-------------------------------------------------------------------------- 17 | | 18 | | The output path for writing the compiled files. It should always 19 | | be inside the public directory, so that AdonisJS can serve it. 20 | | 21 | */ 22 | Encore.setOutputPath('./public/assets') 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Public URI 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The public URI to access the static files. It should always be 30 | | relative from the "public" directory. 31 | | 32 | */ 33 | Encore.setPublicPath('/assets') 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Entrypoints 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Entrypoints are script files that boots your frontend application. Ideally 41 | | a single entrypoint is used by majority of applications. However, feel 42 | | free to add more (if required). 43 | | 44 | | Also, make sure to read the docs on "Assets bundler" to learn more about 45 | | entrypoints. 46 | | 47 | */ 48 | Encore.addEntry('app', './resources/js/app.js') 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Copy assets 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Since the edge templates are not part of the Webpack compile lifecycle, any 56 | | images referenced by it will not be processed by Webpack automatically. Hence 57 | | we must copy them manually. 58 | | 59 | */ 60 | // Encore.copyFiles({ 61 | // from: './resources/images', 62 | // to: 'images/[path][name].[hash:8].[ext]', 63 | // }) 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Split shared code 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Instead of bundling duplicate code in all the bundles, generate a separate 71 | | bundle for the shared code. 72 | | 73 | | https://symfony.com/doc/current/frontend/encore/split-chunks.html 74 | | https://webpack.js.org/plugins/split-chunks-plugin/ 75 | | 76 | */ 77 | // Encore.splitEntryChunks() 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Isolated entrypoints 82 | |-------------------------------------------------------------------------- 83 | | 84 | | Treat each entry point and its dependencies as its own isolated module. 85 | | 86 | */ 87 | Encore.disableSingleRuntimeChunk() 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Cleanup output folder 92 | |-------------------------------------------------------------------------- 93 | | 94 | | It is always nice to cleanup the build output before creating a build. It 95 | | will ensure that all unused files from the previous build are removed. 96 | | 97 | */ 98 | Encore.cleanupOutputBeforeBuild() 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Source maps 103 | |-------------------------------------------------------------------------- 104 | | 105 | | Enable source maps in production 106 | | 107 | */ 108 | Encore.enableSourceMaps(!Encore.isProduction()) 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Assets versioning 113 | |-------------------------------------------------------------------------- 114 | | 115 | | Enable assets versioning to leverage lifetime browser and CDN cache 116 | | 117 | */ 118 | Encore.enableVersioning(Encore.isProduction()) 119 | 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | Configure dev server 123 | |-------------------------------------------------------------------------- 124 | | 125 | | Here we configure the dev server to enable live reloading for edge templates. 126 | | Remember edge templates are not processed by Webpack and hence we need 127 | | to watch them explicitly and livereload the browser. 128 | | 129 | */ 130 | Encore.configureDevServerOptions((options) => { 131 | /** 132 | * Normalize "options.static" property to an array 133 | */ 134 | if (!options.static) { 135 | options.static = [] 136 | } else if (!Array.isArray(options.static)) { 137 | options.static = [options.static] 138 | } 139 | 140 | /** 141 | * Enable live reload and add views directory 142 | */ 143 | options.liveReload = true 144 | options.static.push({ 145 | directory: join(__dirname, './resources/views'), 146 | watch: true, 147 | }) 148 | }) 149 | 150 | /* 151 | |-------------------------------------------------------------------------- 152 | | CSS precompilers support 153 | |-------------------------------------------------------------------------- 154 | | 155 | | Uncomment one of the following lines of code to enable support for your 156 | | favorite CSS precompiler 157 | | 158 | */ 159 | // Encore.enableSassLoader() 160 | // Encore.enableLessLoader() 161 | // Encore.enableStylusLoader() 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | CSS loaders 166 | |-------------------------------------------------------------------------- 167 | | 168 | | Uncomment one of the following line of code to enable support for 169 | | PostCSS or CSS. 170 | | 171 | */ 172 | Encore.enablePostCssLoader() 173 | // Encore.configureCssLoader(() => {}) 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Enable Vue loader 178 | |-------------------------------------------------------------------------- 179 | | 180 | | Uncomment the following lines of code to enable support for vue. Also make 181 | | sure to install the required dependencies. 182 | | 183 | */ 184 | // Encore.enableVueLoader(() => {}, { 185 | // version: 3, 186 | // runtimeCompilerBuild: false, 187 | // useJsx: false 188 | // }) 189 | 190 | /* 191 | |-------------------------------------------------------------------------- 192 | | Configure logging 193 | |-------------------------------------------------------------------------- 194 | | 195 | | To keep the terminal clean from unnecessary info statements , we only 196 | | log warnings and errors. If you want all the logs, you can change 197 | | the level to "info". 198 | | 199 | */ 200 | const config = Encore.getWebpackConfig() 201 | config.infrastructureLogging = { 202 | level: 'warn', 203 | } 204 | config.stats = 'errors-warnings' 205 | 206 | /* 207 | |-------------------------------------------------------------------------- 208 | | Export config 209 | |-------------------------------------------------------------------------- 210 | | 211 | | Export config for webpack to do its job 212 | | 213 | */ 214 | module.exports = config 215 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "files": [ 4 | "./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts", 5 | ], 6 | "compilerOptions": { 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------