├── .gitignore ├── .prettierignore ├── LICENSE.md ├── README.MD ├── appFiles.ts ├── instructions.md ├── instructions.ts ├── package-lock.json ├── package.json ├── scaffold.ts ├── templates ├── edge │ ├── app │ │ ├── Controllers │ │ │ └── Http │ │ │ │ ├── Auth │ │ │ │ ├── LoginController.txt │ │ │ │ ├── PasswordConfirmationController.txt │ │ │ │ ├── PasswordResetController.txt │ │ │ │ ├── PasswordResetRequestController.txt │ │ │ │ └── RegisterController.txt │ │ │ │ └── User │ │ │ │ ├── ApiTokensController.txt │ │ │ │ ├── ProfilesController.txt │ │ │ │ └── UsersController.txt │ │ ├── Enums │ │ │ ├── FlashMessage.txt │ │ │ ├── HttpStatusCodes.txt │ │ │ └── MailerPresets.txt │ │ ├── Exceptions │ │ │ └── Handler.txt │ │ ├── Middleware │ │ │ ├── ConfirmPassword.txt │ │ │ ├── Guest.txt │ │ │ ├── ShareProfile.txt │ │ │ └── VerificationCheck.txt │ │ ├── Models │ │ │ ├── ApiToken.txt │ │ │ ├── PasswordReset.txt │ │ │ ├── User.txt │ │ │ ├── UserProfile.txt │ │ │ └── UserSession.txt │ │ ├── Providers │ │ │ ├── EmailSendingProvider.txt │ │ │ └── ValidationRulesProvider.txt │ │ └── Validators │ │ │ ├── Auth │ │ │ └── RegisterValidator.txt │ │ │ └── User │ │ │ ├── PasswordUpdateValidator.txt │ │ │ ├── PasswordValidator.txt │ │ │ └── ProfileValidator.txt │ ├── config │ │ └── flow.txt │ ├── contracts │ │ └── events.txt │ ├── database │ │ └── migrations │ │ │ ├── 1697024229003_users.txt │ │ │ ├── 1697025049030_password_resets.txt │ │ │ ├── 1697025542544_user_sessions.txt │ │ │ ├── 1697027522393_api_tokens.txt │ │ │ └── 1697733327219_user_profiles.txt │ ├── env.txt │ ├── providers │ │ ├── AppProvider.txt │ │ └── SessionDriver │ │ │ └── index.txt │ ├── resources │ │ ├── css │ │ │ └── app.txt │ │ ├── js │ │ │ └── app.txt │ │ └── views │ │ │ ├── auth │ │ │ ├── confirm-password.txt │ │ │ ├── login.txt │ │ │ ├── password-reset-expired.txt │ │ │ ├── password-reset-invalid.txt │ │ │ ├── password-reset-request.txt │ │ │ ├── password-reset.txt │ │ │ ├── register.txt │ │ │ └── verification.txt │ │ │ ├── components │ │ │ ├── composites │ │ │ │ ├── breadcrumbs.txt │ │ │ │ └── profileNavigation.txt │ │ │ └── ui │ │ │ │ ├── dropdown.txt │ │ │ │ ├── logo.txt │ │ │ │ └── navbar.txt │ │ │ ├── dashboard │ │ │ ├── api-tokens.txt │ │ │ ├── index.txt │ │ │ └── profile │ │ │ │ └── edit.txt │ │ │ ├── emails │ │ │ ├── confirmation_template │ │ │ │ ├── index.txt │ │ │ │ └── plain.txt │ │ │ ├── verify_template │ │ │ │ ├── index.txt │ │ │ │ └── plain.txt │ │ │ └── welcome │ │ │ │ ├── index.txt │ │ │ │ └── plain.txt │ │ │ ├── errors │ │ │ ├── not-found.txt │ │ │ ├── server-error.txt │ │ │ └── unauthorized.txt │ │ │ ├── layouts │ │ │ ├── auth.txt │ │ │ └── main.txt │ │ │ └── partials │ │ │ ├── api-tokens │ │ │ ├── create-token.txt │ │ │ └── list-tokens.txt │ │ │ ├── flashMessages.txt │ │ │ ├── head.txt │ │ │ ├── header.txt │ │ │ └── profile │ │ │ ├── avatar.txt │ │ │ ├── delete.txt │ │ │ ├── details.txt │ │ │ ├── password.txt │ │ │ └── sessions.txt │ ├── start │ │ ├── events │ │ │ ├── auth.txt │ │ │ └── index.txt │ │ └── routes │ │ │ ├── api-tokens.txt │ │ │ ├── auth.txt │ │ │ ├── errors.txt │ │ │ └── profile.txt │ └── tailwind.config.txt └── vue │ ├── app │ ├── Controllers │ │ └── Http │ │ │ ├── Auth │ │ │ ├── LoginController.txt │ │ │ ├── PasswordConfirmationController.txt │ │ │ ├── PasswordResetController.txt │ │ │ ├── PasswordResetRequestController.txt │ │ │ └── RegisterController.txt │ │ │ └── User │ │ │ ├── ApiTokensController.txt │ │ │ ├── ProfilesController.txt │ │ │ └── UsersController.txt │ ├── Enums │ │ ├── FlashMessage.txt │ │ ├── HttpStatusCodes.txt │ │ └── MailerPresets.txt │ ├── Exceptions │ │ └── Handler.txt │ ├── Middleware │ │ ├── ConfirmPassword.txt │ │ ├── Guest.txt │ │ └── VerificationCheck.txt │ ├── Models │ │ ├── ApiToken.txt │ │ ├── PasswordReset.txt │ │ ├── User.txt │ │ ├── UserProfile.txt │ │ └── UserSession.txt │ ├── Providers │ │ ├── EmailSendingProvider.txt │ │ └── ValidationRulesProvider.txt │ └── Validators │ │ ├── Auth │ │ ├── EmailVerificationValidator.txt │ │ └── RegisterValidator.txt │ │ └── User │ │ ├── PasswordUpdateValidator.txt │ │ ├── PasswordValidator.txt │ │ └── ProfileValidator.txt │ ├── config │ └── flow.txt │ ├── contracts │ └── events.txt │ ├── database │ └── migrations │ │ ├── 1697024229003_users.txt │ │ ├── 1697025049030_password_resets.txt │ │ ├── 1697025542544_user_sessions.txt │ │ ├── 1697027522393_api_tokens.txt │ │ └── 1697733327219_user_profiles.txt │ ├── env.txt │ ├── providers │ ├── AppProvider.txt │ └── SessionDriver │ │ └── index.txt │ ├── resources │ ├── css │ │ └── app.txt │ ├── js │ │ ├── app.txt │ │ ├── index.d.txt │ │ ├── src │ │ │ ├── Components │ │ │ │ ├── Composites │ │ │ │ │ ├── EmailVerificationForm.txt │ │ │ │ │ ├── FlashMessages.txt │ │ │ │ │ ├── Navbar.txt │ │ │ │ │ ├── ProfileApiTokens.txt │ │ │ │ │ ├── ProfileAvatarForm.txt │ │ │ │ │ ├── ProfileDeleteForm.txt │ │ │ │ │ ├── ProfileDetailsForm.txt │ │ │ │ │ ├── ProfilePasswordForm.txt │ │ │ │ │ └── ProfileSessionsForm.txt │ │ │ │ ├── Overlays │ │ │ │ │ └── Terms.txt │ │ │ │ └── UI │ │ │ │ │ ├── FormInput.txt │ │ │ │ │ ├── Logo.txt │ │ │ │ │ ├── PageHeader.txt │ │ │ │ │ ├── ProfileMenu.txt │ │ │ │ │ └── UserAvatar.txt │ │ │ ├── Layouts │ │ │ │ ├── Auth.txt │ │ │ │ └── Default.txt │ │ │ └── Pages │ │ │ │ ├── Dashboard │ │ │ │ ├── Index.txt │ │ │ │ └── Profile.txt │ │ │ │ ├── Error.txt │ │ │ │ ├── Login.txt │ │ │ │ ├── Password-reset.txt │ │ │ │ ├── Password-update.txt │ │ │ │ ├── Register.txt │ │ │ │ └── Verification.txt │ │ ├── ssr.txt │ │ └── theme │ │ │ ├── avatar │ │ │ └── index.txt │ │ │ ├── button │ │ │ └── index.txt │ │ │ ├── card │ │ │ └── index.txt │ │ │ ├── checkbox │ │ │ └── index.txt │ │ │ ├── dialog │ │ │ └── index.txt │ │ │ ├── dropdown │ │ │ └── index.txt │ │ │ ├── global.txt │ │ │ ├── index.txt │ │ │ ├── inputtext │ │ │ └── index.txt │ │ │ ├── menu │ │ │ └── index.txt │ │ │ ├── menubar │ │ │ └── index.txt │ │ │ ├── overlaypanel │ │ │ └── index.txt │ │ │ └── toast │ │ │ └── index.txt │ └── views │ │ ├── app.txt │ │ ├── emails │ │ ├── confirmation_template │ │ │ ├── index.txt │ │ │ └── plain.txt │ │ ├── verify_template │ │ │ ├── index.txt │ │ │ └── plain.txt │ │ └── welcome │ │ │ ├── index.txt │ │ │ └── plain.txt │ │ ├── errors │ │ ├── not-found.txt │ │ ├── server-error.txt │ │ └── unauthorized.txt │ │ └── partials │ │ └── meta.txt │ ├── start │ ├── events │ │ ├── auth.txt │ │ └── index.txt │ └── routes │ │ ├── api-tokens.txt │ │ ├── auth.txt │ │ ├── errors.txt │ │ └── profile.txt │ └── tailwind.config.txt ├── tsconfig.json └── types.ts /.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 | .app 15 | TODO.txt 16 | *.tgz -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2023 Dylan Britz, 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 | -------------------------------------------------------------------------------- /appFiles.ts: -------------------------------------------------------------------------------- 1 | export const models = ['ApiToken', 'PasswordReset', 'User', 'UserProfile', 'UserSession'] 2 | export const authControllers = [ 3 | 'LoginController', 4 | 'PasswordResetController', 5 | 'RegisterController', 6 | 'PasswordResetRequestController', 7 | 'PasswordResetController', 8 | ] 9 | export const userControllers = ['ApiTokensController', 'ProfilesController', 'UsersController'] 10 | export const enums = ['FlashMessage', 'HttpStatusCodes', 'MailerPresets'] 11 | export const exceptions = ['Handler'] 12 | export const middleware = ['ConfirmPassword', 'Guest', 'ShareProfile', 'VerificationCheck'] 13 | export const services = ['EmailSendingProvider', 'ValidationRulesProvider'] 14 | export const authValidators = ['RegisterValidator'] 15 | export const userValidators = ['PasswordUpdateValidator', 'PasswordValidator', 'ProfileValidator'] 16 | // export const config = ['flow'] 17 | export const contracts = ['events'] 18 | export const migrations = [ 19 | '1697024229003_users', 20 | '1697025049030_password_resets', 21 | '1697025542544_user_sessions', 22 | '1697027522393_api_tokens', 23 | '1697733327219_user_profiles', 24 | ] 25 | export const providers = ['AppProvider', 'SessionDriver/index'] 26 | export const events = ['auth', 'index'] 27 | export const routes = ['api-tokens', 'auth', 'errors', 'profile'] 28 | 29 | const emails = [ 30 | 'emails/confirmation_template/index', 31 | 'emails/confirmation_template/plain', 32 | 'emails/verify_template/index', 33 | 'emails/verify_template/plain', 34 | 'emails/welcome/index', 35 | 'emails/welcome/plain', 36 | ] 37 | 38 | const errors = ['errors/not-found', 'errors/server-error', 'errors/unauthorized'] 39 | 40 | export const sharedViews = [...emails, ...errors] 41 | 42 | export const sharedJs = ['app'] 43 | 44 | export const sharedCss = ['app'] 45 | 46 | export const themeFiles = [ 47 | 'theme/avatar/index', 48 | 'theme/button/index', 49 | 'theme/card/index', 50 | 'theme/checkbox/index', 51 | 'theme/dialog/index', 52 | 'theme/inputText/index', 53 | 'theme/menu/index', 54 | 'theme/menuBar/index', 55 | 'theme/overlayPanel/index', 56 | 'theme/toast/index', 57 | 'theme/global', 58 | 'theme/index', 59 | ] 60 | 61 | export const vueFiles = [ 62 | 'src/Pages/Error', 63 | 'src/Pages/Login', 64 | 'src/Pages/Password-reset', 65 | 'src/Pages/Password-update', 66 | 'src/Pages/Register', 67 | 'src/Pages/Verification', 68 | 'src/Pages/Dashboard/Index', 69 | 'src/Pages/Dashboard/Profile', 70 | 'src/Layouts/Auth', 71 | 'src/Layouts/Default', 72 | 'src/Components/Composites/EmailVerificationForm', 73 | 'src/Components/Composites/FlashMessages', 74 | 'src/Components/Composites/Navbar', 75 | 'src/Components/Composites/ProfileApiTokens', 76 | 'src/Components/Composites/ProfileAvatarForm', 77 | 'src/Components/Composites/ProfileDeleteForm', 78 | 'src/Components/Composites/ProfileSessionsForm', 79 | 'src/Components/Composites/ProfileDetailsForm', 80 | 'src/Components/Composites/ProfilePasswordForm', 81 | 'src/Components/Overlays/Terms', 82 | 'src/Components/UI/FormInput', 83 | 'src/Components/UI/Logo', 84 | 'src/Components/UI/PageHeader', 85 | 'src/Components/UI/ProfileMenu', 86 | 'src/Components/UI/UserAvatar', 87 | ] 88 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | ## Done 2 | -------------------------------------------------------------------------------- /instructions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @britzdylan/adonisjs-flow 3 | * 4 | * (c) Dylan Britz 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 { ScaffoldOptions } from './types' 11 | import * as sinkStatic from '@adonisjs/sink' 12 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 13 | import { 14 | makeContracts, 15 | makeModels, 16 | makeEnums, 17 | makeException, 18 | makeControllers, 19 | makeEvents, 20 | makeMiddleware, 21 | makeMigrations, 22 | makeProviders, 23 | makeRoutes, 24 | makeServiceProviders, 25 | makeValidators, 26 | makeResources 27 | } from './scaffold' 28 | 29 | async function getStack(sink: typeof sinkStatic) { 30 | return sink.getPrompt().choice('Which stack would you like to scaffold?', ['edge', 'vue'], { 31 | default: 'edge', 32 | validate(choice) { 33 | return choice && choice.length ? true : 'Which stack would you like to scaffold?' 34 | }, 35 | }) 36 | } 37 | 38 | async function askShouldInstallPackage(sink: typeof sinkStatic) { 39 | return sink.getPrompt().confirm('Would you like to install all required dependencies?') 40 | } 41 | 42 | export default async function instructions( 43 | projectRoot: string, 44 | app: ApplicationContract, 45 | sink: typeof sinkStatic 46 | ) { 47 | const options: ScaffoldOptions = { 48 | stack: 'edge', 49 | } 50 | 51 | options.stack = await getStack(sink) 52 | 53 | makeControllers(projectRoot, app, sink, options) 54 | makeEnums(projectRoot, sink, options) 55 | makeException(projectRoot, app, sink, options) 56 | makeMiddleware(projectRoot, app, sink, options) 57 | makeModels(projectRoot, app, sink, options) 58 | makeServiceProviders(projectRoot, sink, options) 59 | makeValidators(projectRoot, sink, options) 60 | makeContracts(projectRoot, app, sink, options) 61 | makeMigrations(projectRoot, sink, options) 62 | makeProviders(projectRoot, app, sink, options) 63 | makeEvents(projectRoot, sink, options) 64 | makeRoutes(projectRoot, sink, options) 65 | makeResources(projectRoot, sink, options) 66 | 67 | const shouldInstallPackage = await askShouldInstallPackage(sink) 68 | 69 | if (shouldInstallPackage) { 70 | /** 71 | * Install required dependencies 72 | */ 73 | const pkg = new sink.files.PackageJsonFile(projectRoot) 74 | 75 | pkg.install('tailwindcss', undefined, true) 76 | pkg.install('autoprefixer', undefined, true) 77 | pkg.install('postcss', undefined, true) 78 | pkg.install('postcss-loader', undefined, true) 79 | pkg.install('ua-parser-js', undefined, false) 80 | pkg.install('ms', undefined, false) 81 | 82 | if (options.stack === 'edge') { 83 | pkg.install('daisyui', undefined, true) 84 | } 85 | 86 | if (options.stack === 'vue') { 87 | pkg.install('@eidellev/inertia-adonisjs', undefined, false) 88 | pkg.install('vue', undefined, false) 89 | pkg.install('vue-loader', undefined, false) 90 | pkg.install('@inertiajs/vue3', undefined, false) 91 | pkg.install('@vue/tsconfig', undefined, false) 92 | pkg.install('markdown-it', undefined, false) 93 | pkg.install('primeicons', undefined, false) 94 | pkg.install('primevue', undefined, false) 95 | } 96 | 97 | const logLines = [ 98 | `Installing: ${sink.logger.colors.gray(pkg.getInstalls().list.join(', '))}`, 99 | ] 100 | 101 | const spinner = sink.logger.await(logLines.join(' ')) 102 | 103 | try { 104 | await pkg.commitAsync() 105 | spinner.update('Packages installed') 106 | } catch (error) { 107 | spinner.update('Unable to install packages') 108 | sink.logger.fatal(error) 109 | } 110 | spinner.stop() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonisjs-flow", 3 | "version": "0.3.0", 4 | "description": "AdonisJS Flow is a application starter kit for AdonisJS that includes a lot of the common features you would need to build a web application.", 5 | "scripts": { 6 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 7 | "pretest": "npm run lint", 8 | "clean": "del-cli build", 9 | "format": "prettier --write .", 10 | "commit": "cz", 11 | "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", 12 | "test": "node -r @adonisjs/require-ts/build/register bin/test.ts", 13 | "compile": "npm run clean && tsc && npm run copyfiles", 14 | "build": "npm run compile", 15 | "prepublishOnly": "npm run build", 16 | "lint": "eslint . --ext=.ts", 17 | "release": "np --no-tests --message=\"chore(release): %s\"", 18 | "version": "npm run build", 19 | "pack": "npm run build && npm pack" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/britzdylan/adonis-flow.git" 24 | }, 25 | "author": "Dylan Britz", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/britzdylan/adonis-flow/issues" 29 | }, 30 | "homepage": "https://github.com/britzdylan/adonis-flow#readme", 31 | "devDependencies": { 32 | "@adonisjs/core": "^5.9.0", 33 | "@adonisjs/mrm-preset": "^5.0.3", 34 | "@adonisjs/require-ts": "^2.0.13", 35 | "@adonisjs/sink": "^5.4.3", 36 | "@types/node": "^20.8.7", 37 | "commitizen": "^4.3.0", 38 | "cz-conventional-changelog": "^3.3.0", 39 | "del-cli": "^5.1.0", 40 | "eslint": "^8.52.0", 41 | "eslint-plugin-adonis": "^2.1.1", 42 | "eslint-plugin-prettier": "^5.0.1", 43 | "mrm": "^4.1.22", 44 | "np": "^8.0.4", 45 | "prettier": "^3.0.3", 46 | "typescript": "^5.2.2", 47 | "copyfiles": "^2.4.1" 48 | }, 49 | "mrmConfig": { 50 | "core": false, 51 | "license": "MIT", 52 | "services": [], 53 | "minNodeVersion": "16.13.1", 54 | "probotApps": [] 55 | }, 56 | "files": [ 57 | "build/templates", 58 | "build/appFiles.js", 59 | "build/instructions.js", 60 | "build/scaffold.js", 61 | "build/types.js", 62 | "build/README.md", 63 | "build/instructions.md" 64 | ], 65 | "eslintConfig": { 66 | "extends": [ 67 | "plugin:adonis/typescriptPackage" 68 | ], 69 | "plugins": [ 70 | "prettier" 71 | ], 72 | "rules": { 73 | "prettier/prettier": [ 74 | "error", 75 | { 76 | "endOfLine": "auto" 77 | } 78 | ] 79 | } 80 | }, 81 | "eslintIgnore": [ 82 | "build" 83 | ], 84 | "prettier": { 85 | "trailingComma": "es5", 86 | "semi": false, 87 | "singleQuote": true, 88 | "useTabs": false, 89 | "quoteProps": "consistent", 90 | "bracketSpacing": true, 91 | "arrowParens": "always", 92 | "printWidth": 100 93 | }, 94 | "config": { 95 | "commitizen": { 96 | "path": "./node_modules/cz-conventional-changelog" 97 | } 98 | }, 99 | "np": { 100 | "yarn": false, 101 | "contents": "." 102 | }, 103 | "main": "index.js", 104 | "adonisjs": { 105 | "instructions": "./build/instructions.js", 106 | "instructionsMd": "./build/instructions.md" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/Auth/LoginController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import flowConfig from 'Config/flow' 4 | import FlashMessage from 'App/Enums/FlashMessage' 5 | 6 | const { login } = flowConfig.views 7 | const { LoginSuccess, LogoutSuccess } = FlashMessage 8 | 9 | /** 10 | * Controller responsible for handling user login and logout functionality. 11 | */ 12 | export default class LoginController { 13 | /** 14 | * Renders the login view. 15 | */ 16 | public async create({ view }: HttpContextContract): Promise { 17 | return view.render(login) 18 | } 19 | /** 20 | * Authenticates the user and logs them in. 21 | */ 22 | public async store(ctx: HttpContextContract): Promise { 23 | const { request, auth, response, session } = ctx 24 | const { email, password } = { 25 | email: request.input('email'), 26 | password: request.input('password'), 27 | } 28 | 29 | const user = await auth.use('web').attempt(email, password) 30 | 31 | Event.emit('user:login', { user, ctx }) 32 | session.flash('success', [LoginSuccess]) 33 | return response.redirect().toRoute('dashboard.index') 34 | } 35 | /** 36 | * Logs the user out. 37 | */ 38 | public async destroy(ctx: HttpContextContract): Promise { 39 | const { auth, response, session } = ctx 40 | await auth.logout() 41 | session.flash('success', [LogoutSuccess]) 42 | Event.emit('user:logout', { ctx }) 43 | return response.redirect().toRoute('login.create') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/Auth/PasswordConfirmationController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import flowConfig from 'Config/flow' 4 | import Encryption from '@ioc:Adonis/Core/Encryption' 5 | import { DateTime } from 'luxon' 6 | 7 | const { confirmPassword } = flowConfig.views 8 | 9 | /** 10 | * Controller for registering and verifying users. 11 | */ 12 | export default class PasswordConfirmationController { 13 | /** 14 | * Renders the registration view. 15 | */ 16 | public async create({ view, request }: HttpContextContract): Promise { 17 | return view.render(confirmPassword, { intendedRoute: request.params().intended }) 18 | } 19 | 20 | /** 21 | * Validates user password and redirect to intended route 22 | */ 23 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 24 | const { intended } = request.params() 25 | const password = await request.input('password') 26 | const user = auth.user as User 27 | const isSame = await user.verifyPassword(password) 28 | 29 | if (!isSame) { 30 | session.flash('error', 'Password does not match') 31 | return response.redirect().back() 32 | } 33 | 34 | session.put('password-confirmed', DateTime.now().toISO()!) 35 | 36 | return response.redirect(Encryption.decrypt(intended)!) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/Auth/PasswordResetController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { Exception } from '@adonisjs/core/build/standalone' 3 | import Event from '@ioc:Adonis/Core/Event' 4 | import PasswordReset from 'App/Models/PasswordReset' 5 | import PasswordValidator from 'App/Validators/User/PasswordValidator' 6 | import FlashMessage from 'App/Enums/FlashMessage' 7 | import flowConfig from 'Config/flow' 8 | 9 | const { passwordReset, signedUrlInvalid } = flowConfig.views 10 | const { PasswordResetSuccess } = FlashMessage 11 | 12 | export default class PasswordResetsController { 13 | /** 14 | * Renders the password reset view with the provided token. 15 | */ 16 | public async create({ view, request }: HttpContextContract): Promise { 17 | const token = request.param('token') 18 | const passwordResetRequest = await PasswordReset.findByOrFail('token', token) 19 | 20 | if (passwordResetRequest.isExpired()) { 21 | throw new Exception('URLhas expired', 403, 'E_INVALID_URL') 22 | } 23 | 24 | if (!(await passwordResetRequest.verifyToken(token))) { 25 | throw new Exception('Invalid URL', 403, 'E_INVALID_URL') 26 | } 27 | 28 | return view.render(passwordReset, { token: token }) 29 | } 30 | 31 | /** 32 | * Handles the password reset form submission. 33 | * */ 34 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 35 | const token = request.param('token') 36 | const passwordReset = await PasswordReset.findByOrFail('token', token) 37 | const { password } = await request.validate(PasswordValidator) 38 | const user = await passwordReset.related('user').query().firstOrFail() 39 | await auth.logout() 40 | 41 | user.merge({ password: password }) 42 | await user.save() 43 | 44 | Event.emit('user:resetPassword', { user, passwordReset }) 45 | 46 | session.flash({ 47 | success: [PasswordResetSuccess], 48 | }) 49 | 50 | return response.redirect().toRoute('login.create') 51 | } 52 | 53 | public async renderInvalidToken({ view, request }: HttpContextContract): Promise { 54 | const message = request.qs().message || 'Invalid URL' 55 | const status = 403 56 | return view.render(signedUrlInvalid, { message, status }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/Auth/PasswordResetRequestController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import User from 'App/Models/User' 4 | import PasswordReset from 'App/Models/PasswordReset' 5 | import flowConfig from 'Config/flow' 6 | import FlashMessage from 'App/Enums/FlashMessage' 7 | 8 | const { PasswordResetRequested } = FlashMessage 9 | const { passwordResetRequest } = flowConfig.views 10 | 11 | /** 12 | * Controller for handling password reset requests. 13 | */ 14 | export default class PasswordResetRequestController { 15 | /** 16 | * Renders the password reset request view. 17 | */ 18 | public async create({ view }: HttpContextContract): Promise { 19 | return view.render(passwordResetRequest) 20 | } 21 | 22 | /** 23 | * Handles the submission of a password reset request form. 24 | */ 25 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 26 | const user = await User.findBy('email', request.input('email')) 27 | if (user) { 28 | await PasswordReset.query().where('user_id', user.id).delete() 29 | 30 | const newRequest = new PasswordReset() 31 | newRequest.userId = user.id 32 | await newRequest.generateToken() 33 | await newRequest.save() 34 | 35 | Event.emit('user:resetPasswordRequest', { user, token: newRequest.token }) 36 | } 37 | session.flash({ 38 | success: [PasswordResetRequested], 39 | }) 40 | 41 | await auth.logout() 42 | 43 | return response.redirect().back() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/Auth/RegisterController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import User from 'App/Models/User' 4 | import RegisterValidator from 'App/Validators/Auth/RegisterValidator' 5 | import flowConfig from 'Config/flow' 6 | import FlashMessage from 'App/Enums/FlashMessage' 7 | 8 | const { register, verification } = flowConfig.views 9 | const { RegisterSuccess, EmailVerified, EmailVerificationResent } = FlashMessage 10 | 11 | /** 12 | * Controller for registering and verifying users. 13 | */ 14 | export default class RegistersController { 15 | /** 16 | * Renders the registration view. 17 | */ 18 | public async create({ view }: HttpContextContract): Promise { 19 | return view.render(register) 20 | } 21 | 22 | /** 23 | * Validates user data and creates a new user. 24 | */ 25 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 26 | const userData = await request.validate(RegisterValidator) 27 | const user = await User.create(userData) 28 | 29 | Event.emit('user:register', user) 30 | session.flash('success', [RegisterSuccess]) 31 | 32 | if (flowConfig.features.verification === 'strict') { 33 | return response.redirect().toRoute('login.create') 34 | } 35 | auth.login(user) 36 | return response.redirect().toRoute('dashboard.index') 37 | } 38 | 39 | /** 40 | * Verifies a user's email address. 41 | */ 42 | public async edit({ request, response, session }: HttpContextContract): Promise { 43 | const token = request.params().token 44 | const user = await User.findByOrFail('emailVerificationToken', token) 45 | 46 | const verified = user.verifyEmail(token) 47 | if (!verified) { 48 | return response.status(403).badRequest() 49 | } 50 | await user.save() 51 | session.flash('success', [EmailVerified]) 52 | return response.redirect().toRoute('login.create') 53 | } 54 | /** 55 | * Render email verification view. 56 | */ 57 | public async createVerification({ view, request }: HttpContextContract): Promise { 58 | return view.render(verification, { email: request.params().email }) 59 | } 60 | /** 61 | * Resend email verification. 62 | */ 63 | public async resendVerification({ 64 | request, 65 | response, 66 | session, 67 | }: HttpContextContract): Promise { 68 | const user = await User.findByOrFail('email', request.params().email) 69 | user.generateVerificationToken() 70 | await user.save() 71 | Event.emit('mail:sendEmailVerification', user) 72 | session.flash('success', [EmailVerificationResent]) 73 | return response.redirect().back() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/User/ApiTokensController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import ApiToken from 'App/Models/ApiToken' 4 | import FlashMessages from 'App/Enums/FlashMessage' 5 | 6 | const { ApiTokenDeleted } = FlashMessages 7 | 8 | export default class ApiTokensController { 9 | public async index({ view }: HttpContextContract) { 10 | const tokens = await ApiToken.all() 11 | return view.render('dashboard/api-tokens', { tokens }) 12 | } 13 | 14 | public async store({ request, response, auth, session }: HttpContextContract) { 15 | const { name, expiresIn } = request.all() 16 | const user = auth.user! 17 | const token = await auth.use('api').generate(user, { 18 | name, 19 | expiresIn, 20 | }) 21 | session.flash({ token: token }) 22 | Event.emit('api-token:created', token) 23 | response.redirect().toRoute('api-tokens.index') 24 | } 25 | 26 | public async destroy({ response, request, session }: HttpContextContract) { 27 | const token = await ApiToken.findOrFail(request.param('id')) 28 | await token.delete() 29 | Event.emit('api-token:deleted', token) 30 | session.flash({ success: [ApiTokenDeleted] }) 31 | response.redirect().toRoute('api-tokens.index') 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/User/ProfilesController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite' 3 | import Event from '@ioc:Adonis/Core/Event' 4 | import UserSession from 'App/Models/UserSession' 5 | import ProfileValidator from 'App/Validators/User/ProfileValidator' 6 | import FlashMessages from 'App/Enums/FlashMessage' 7 | import flowConfig from 'Config/flow' 8 | import User from 'App/Models/User' 9 | 10 | const { views } = flowConfig 11 | const { ProfileUpdated, FileTooLarge, EmailVerificationResent } = FlashMessages 12 | 13 | export default class ProfilesController { 14 | // view public profile 15 | // public async show({}: HttpContextContract) {} 16 | 17 | // view edit profile form 18 | public async edit({ auth, view, session }: HttpContextContract) { 19 | const profile = await auth.user?.related('profile').query().firstOrFail() 20 | const sessions = await auth.user?.related('sessions').query().orderBy('created_at', 'desc') 21 | let currentSession: string | undefined 22 | sessions?.forEach((ses) => { 23 | if (session.sessionId === ses.id) { 24 | currentSession = session.sessionId 25 | } 26 | }) 27 | return view.render(views.profile, { profile, sessions, currentSession }) 28 | } 29 | 30 | // handle update profile form submission 31 | public async update({ request, session, response, auth }: HttpContextContract) { 32 | const { email, firstName, lastName } = await request.validate(ProfileValidator) 33 | const user = auth.user! 34 | const userProfile = await user.related('profile').query().firstOrFail()! 35 | 36 | userProfile.merge({ firstName, lastName }) 37 | await userProfile.save() 38 | 39 | if (email !== user.email) { 40 | const isNotUnique = await User.findBy('email', email) 41 | if (isNotUnique) { 42 | session.flash('error', ['Email is not valid or already in use']) 43 | return response.redirect().back() 44 | } 45 | await user.newEmailResetRequest(email) 46 | await user.save() 47 | Event.emit('user:emailReset', user) 48 | } 49 | 50 | session.flash('success', [ProfileUpdated, EmailVerificationResent]) 51 | 52 | return response.redirect().back() 53 | } 54 | 55 | // handle update avatar form submission 56 | public async updateProfileAvatar({ request, response, auth, session }: HttpContextContract) { 57 | const avatar = request.file('avatar')! 58 | if (avatar.size > 2 * 1024 * 1024) { 59 | session.flash('error', [FileTooLarge]) 60 | return response.redirect().back() 61 | } 62 | 63 | const userProfile = await auth.user?.related('profile').query().firstOrFail()! 64 | userProfile.avatar = Attachment.fromFile(avatar) 65 | await userProfile.save() 66 | 67 | session.flash('success', [ProfileUpdated]) 68 | 69 | return response.redirect().back() 70 | } 71 | 72 | // handle delete avatar form submission 73 | public async deleteProfileAvatar({ response, auth, session }: HttpContextContract) { 74 | const userProfile = await auth.user?.related('profile').query().firstOrFail()! 75 | userProfile.avatar = null 76 | await userProfile.save() 77 | 78 | session.flash('success', [ProfileUpdated]) 79 | 80 | return response.redirect().back() 81 | } 82 | 83 | // handle delete session form submission 84 | public async destroySession({ request, response, session }: HttpContextContract) { 85 | const { id } = request.params() 86 | let userSession = await UserSession.findOrFail(id) 87 | await userSession.delete() 88 | session.flash('success', ['Session deleted']) 89 | return response.redirect().back() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /templates/edge/app/Controllers/Http/User/UsersController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import User from 'App/Models/User' 4 | import FlashMessages from 'App/Enums/FlashMessage' 5 | import PasswordUpdateValidator from 'App/Validators/User/PasswordUpdateValidator' 6 | 7 | const { EmailUpdated, PasswordValidationFailed } = FlashMessages 8 | export default class UsersController { 9 | public async index({}: HttpContextContract) {} 10 | 11 | public async create({}: HttpContextContract) {} 12 | 13 | public async store({}: HttpContextContract) {} 14 | 15 | public async show({}: HttpContextContract) {} 16 | 17 | public async edit({}: HttpContextContract) {} 18 | 19 | public async update({}: HttpContextContract) {} 20 | 21 | public async destroy({ auth, response }: HttpContextContract) { 22 | const user = auth.user! 23 | await auth.logout() 24 | await user.delete() 25 | Event.emit('user:delete', user) 26 | response.redirect().toRoute('login.create') 27 | } 28 | 29 | /** 30 | * Verifies users new email address and updates it. 31 | */ 32 | public async confirmEmailUpdateRequest({ 33 | request, 34 | response, 35 | session, 36 | auth, 37 | }: HttpContextContract) { 38 | const { email } = request.params() 39 | const user = await User.findByOrFail('emailResetRequest', email) 40 | const updatedEmail = user.updateEmail(email) 41 | if (!updatedEmail) { 42 | session.flash('error', ['Invalid email update request']) 43 | response.redirect().toRoute('login.create') 44 | } 45 | await user.save() 46 | await auth.logout() 47 | session.flash('success', [EmailUpdated]) 48 | return response.redirect().toRoute('login.create') 49 | } 50 | 51 | /** 52 | * Updates authenticated user's password. 53 | */ 54 | public async updatePassword({ request, response, session, auth }: HttpContextContract) { 55 | const { password, currentPassword } = await request.validate(PasswordUpdateValidator) 56 | const user = auth.user! 57 | if (!(await user.verifyPassword(currentPassword))) { 58 | session.flash('errors', [PasswordValidationFailed]) 59 | return response.redirect().back() 60 | } 61 | user.password = password 62 | await user.save() 63 | session.flash('success', ['Password updated successfully']) 64 | return response.redirect().toRoute('profile.edit') 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /templates/edge/app/Enums/FlashMessage.txt: -------------------------------------------------------------------------------- 1 | enum FlashMessages { 2 | // auth 3 | LoginSuccess = 'Login successful!', 4 | LoginFail = 'Invalid credentials, please try again.', 5 | LogoutSuccess = 'Logged out successfully.', 6 | RegisterSuccess = 'Registration successful! Please check your email to verify your account.', 7 | EmailVerified = 'Your email has been verified.', 8 | EmailNotVerified = 'Please verify your email address.', 9 | EmailUpdated = 'Your email has been updated.', 10 | EmailVerificationResent = 'Verification email resent, please check your email.', 11 | PasswordResetSuccess = 'Your password has been reset.', 12 | PasswordResetRequested = 'If your email exists in our system, you will receive a password reset link shortly.', 13 | PasswordValidationFailed = 'Password incorrect, please try again.', 14 | // profile 15 | ProfileUpdated = 'Profile updated successfully', 16 | 17 | // Files 18 | FileTooLarge = 'File too large, please try again.', 19 | 20 | // Api Tokens 21 | ApiTokenCreated = 'Api token created successfully.', 22 | ApiTokenDeleted = 'Api token deleted successfully.', 23 | } 24 | 25 | export default FlashMessages 26 | -------------------------------------------------------------------------------- /templates/edge/app/Enums/HttpStatusCodes.txt: -------------------------------------------------------------------------------- 1 | enum HttpStatusCodes { 2 | // 1xx Informational 3 | CONTINUE = 100, 4 | SWITCHING_PROTOCOLS = 101, 5 | PROCESSING = 102, 6 | 7 | // 2xx Success 8 | OK = 200, 9 | CREATED = 201, 10 | ACCEPTED = 202, 11 | NON_AUTHORITATIVE_INFORMATION = 203, 12 | NO_CONTENT = 204, 13 | RESET_CONTENT = 205, 14 | PARTIAL_CONTENT = 206, 15 | MULTI_STATUS = 207, 16 | ALREADY_REPORTED = 208, 17 | IM_USED = 226, 18 | 19 | // 3xx Redirection 20 | MULTIPLE_CHOICES = 300, 21 | MOVED_PERMANENTLY = 301, 22 | FOUND = 302, 23 | SEE_OTHER = 303, 24 | NOT_MODIFIED = 304, 25 | USE_PROXY = 305, 26 | TEMPORARY_REDIRECT = 307, 27 | PERMANENT_REDIRECT = 308, 28 | 29 | // 4xx Client errors 30 | BAD_REQUEST = 400, 31 | UNAUTHORIZED = 401, 32 | PAYMENT_REQUIRED = 402, 33 | FORBIDDEN = 403, 34 | NOT_FOUND = 404, 35 | METHOD_NOT_ALLOWED = 405, 36 | NOT_ACCEPTABLE = 406, 37 | PROXY_AUTHENTICATION_REQUIRED = 407, 38 | REQUEST_TIMEOUT = 408, 39 | CONFLICT = 409, 40 | GONE = 410, 41 | LENGTH_REQUIRED = 411, 42 | PRECONDITION_FAILED = 412, 43 | PAYLOAD_TOO_LARGE = 413, 44 | URI_TOO_LONG = 414, 45 | UNSUPPORTED_MEDIA_TYPE = 415, 46 | RANGE_NOT_SATISFIABLE = 416, 47 | EXPECTATION_FAILED = 417, 48 | IM_A_TEAPOT = 418, 49 | MISDIRECTED_REQUEST = 421, 50 | UNPROCESSABLE_ENTITY = 422, 51 | LOCKED = 423, 52 | FAILED_DEPENDENCY = 424, 53 | UPGRADE_REQUIRED = 426, 54 | PRECONDITION_REQUIRED = 428, 55 | TOO_MANY_REQUESTS = 429, 56 | REQUEST_HEADER_FIELDS_TOO_LARGE = 431, 57 | UNAVAILABLE_FOR_LEGAL_REASONS = 451, 58 | 59 | // 5xx Server errors 60 | INTERNAL_SERVER_ERROR = 500, 61 | NOT_IMPLEMENTED = 501, 62 | BAD_GATEWAY = 502, 63 | SERVICE_UNAVAILABLE = 503, 64 | GATEWAY_TIMEOUT = 504, 65 | HTTP_VERSION_NOT_SUPPORTED = 505, 66 | VARIANT_ALSO_NEGOTIATES = 506, 67 | INSUFFICIENT_STORAGE = 507, 68 | LOOP_DETECTED = 508, 69 | NOT_EXTENDED = 510, 70 | NETWORK_AUTHENTICATION_REQUIRED = 511, 71 | } 72 | 73 | export default HttpStatusCodes 74 | -------------------------------------------------------------------------------- /templates/edge/app/Enums/MailerPresets.txt: -------------------------------------------------------------------------------- 1 | enum MailerDefaults { 2 | FROM = 'britzdylan@gmail.com', 3 | CC = '', 4 | 5 | WELCOME_SUBJECT = 'Welcome aboard: Confirm your email', 6 | 7 | RESET_PASSWORD_SUBJECT = 'Reset your password', 8 | RESET_PASSWORD_TITLE = 'Reset your password', 9 | RESET_PASSWORD_MESSAGE = 'Click the following link to reset your password', 10 | RESET_PASSWORD_URL_TEXT = 'Reset password', 11 | 12 | RESET_PASSWORD_SUCCESS_SUBJECT = 'Your password has been reset', 13 | RESET_PASSWORD_SUCCESS_TITLE = 'Password reset successfully', 14 | RESET_PASSWORD_SUCCESS_MESSAGE = 'Your password has been reset', 15 | 16 | EMAIL_VERIFICATION_SUBJECT = 'Verify your email', 17 | EMAIL_VERIFICATION_TITLE = 'Verify your email', 18 | EMAIL_VERIFICATION_MESSAGE = 'Verify your email', 19 | 20 | EMAIL_UPDATE_SUBJECT = 'Confirmation: Update your email', 21 | EMAIL_UPDATE_TITLE = 'Confirm your new email', 22 | EMAIL_UPDATE_MESSAGE = 'Click the following link to confirm your new email', 23 | EMAIL_UPDATE_URL_TEXT = 'Confirm new email', 24 | 25 | USER_DELETE_SUBJECT = 'Confirmation of your account deletion', 26 | USER_DELETE_TITLE = 'Your account has been deleted', 27 | USER_DELETE_MESSAGE = 'Your account has been permanently deleted, and all your data has been removed from our servers.', 28 | } 29 | 30 | export default MailerDefaults 31 | -------------------------------------------------------------------------------- /templates/edge/app/Exceptions/Handler.txt: -------------------------------------------------------------------------------- 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 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 19 | import FlashMessages from 'App/Enums/FlashMessage' 20 | 21 | const { LoginFail } = FlashMessages 22 | 23 | export default class ExceptionHandler extends HttpExceptionHandler { 24 | protected disableStatusPagesInDevelopment = false 25 | protected statusPages = { 26 | '403': 'errors/unauthorized', 27 | '404': 'errors/not-found', 28 | '500..599': 'errors/server-error', 29 | } 30 | 31 | constructor() { 32 | super(Logger) 33 | } 34 | 35 | public async handle(error: any, ctx: HttpContextContract) { 36 | /** 37 | * Self handle the validation exception 38 | */ 39 | if (error.code === 'E_INVALID_AUTH_UID' || error.code === 'E_INVALID_AUTH_PASSWORD') { 40 | ctx.session.flash('errors', [LoginFail]) 41 | return ctx.response.redirect().back() 42 | } 43 | 44 | if (error.code === 'E_VALIDATION_FAILURE') { 45 | ctx.session.flash('errors', error.messages) 46 | } 47 | 48 | if (error.code === 'E_INVALID_URL') { 49 | return ctx.response.redirect().withQs({ message: error.message }).toRoute('error.invalidURL') 50 | } 51 | 52 | /** 53 | * Forward rest of the exceptions to the parent class 54 | */ 55 | return super.handle(error, ctx) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /templates/edge/app/Middleware/ConfirmPassword.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Encryption from '@ioc:Adonis/Core/Encryption' 3 | import { DateTime } from 'luxon' 4 | import flowConfig from 'Config/flow' 5 | 6 | const { passwordConfirmation } = flowConfig.features 7 | export default class ConfirmPassword { 8 | public async handle( 9 | { request, response, session }: HttpContextContract, 10 | next: () => Promise 11 | ) { 12 | const lastVerifiedAt = session.get('password-confirmed') 13 | const fromUrl = request 14 | .header('referer') 15 | ?.replace('http://', '') 16 | .replace('https://', '') 17 | .replace(request.header('host')!, '') 18 | 19 | const runRedirect = () => 20 | response.redirect().toRoute('password.confirm', [Encryption.encrypt(request.url())], { 21 | qs: { fromUrl }, 22 | }) 23 | 24 | if (!lastVerifiedAt) { 25 | return runRedirect() 26 | } 27 | 28 | const diff = DateTime.now().diff(DateTime.fromISO(lastVerifiedAt)) 29 | if (diff.minutes > passwordConfirmation) { 30 | return runRedirect() 31 | } 32 | 33 | await next() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/edge/app/Middleware/Guest.txt: -------------------------------------------------------------------------------- 1 | import type { 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('dashboard.index') 7 | } 8 | await next() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/edge/app/Middleware/ShareProfile.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | 3 | export default class ShareProfile { 4 | public async handle({ view, auth }: HttpContextContract, next: () => Promise) { 5 | const gProfile = await auth.user?.related('profile').query().firstOrFail() 6 | view.share({ 7 | gProfile, 8 | }) 9 | 10 | await next() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/edge/app/Middleware/VerificationCheck.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import FlashMessages from 'App/Enums/FlashMessage' 4 | 5 | const { EmailNotVerified } = FlashMessages 6 | 7 | export default class VerificationCheck { 8 | public async handle( 9 | { request, auth, response, session }: HttpContextContract, 10 | next: () => Promise 11 | ) { 12 | if (!auth.isLoggedIn) { 13 | const user = await User.findByOrFail('email', request.input('email')) 14 | if (!user.isVerified) { 15 | session.flash('errors', [EmailNotVerified]) 16 | return response.redirect().toRoute('verification.create', { email: user.email }) 17 | } 18 | } 19 | 20 | if (auth.user?.isVerified === false) { 21 | session.flash('errors', [EmailNotVerified]) 22 | return response.redirect().toRoute('verification.create', { email: auth.user.email }) 23 | } 24 | 25 | await next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/edge/app/Models/ApiToken.txt: -------------------------------------------------------------------------------- 1 | // app/Models/UserSession.ts 2 | 3 | import { DateTime } from 'luxon' 4 | import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' 5 | 6 | export default class ApiToken extends BaseModel { 7 | @column({ isPrimary: true }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @column() 14 | public name: string 15 | 16 | @column() 17 | public type: string 18 | 19 | @column() 20 | public token: string 21 | 22 | @column.dateTime() 23 | public expiresAt: DateTime 24 | 25 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 26 | public updatedAt: DateTime 27 | 28 | @column.dateTime({ autoCreate: true, autoUpdate: false }) 29 | public createdAt: DateTime 30 | } 31 | -------------------------------------------------------------------------------- /templates/edge/app/Models/PasswordReset.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column, belongsTo, BelongsTo, computed } from '@ioc:Adonis/Lucid/Orm' 3 | import { string } from '@ioc:Adonis/Core/Helpers' 4 | import User from 'App/Models/User' 5 | import Encryption from '@ioc:Adonis/Core/Encryption' 6 | export default class PasswordReset extends BaseModel { 7 | @column({ isPrimary: true }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @column() 14 | public token: string 15 | 16 | @column.dateTime({ autoCreate: true }) 17 | public createdAt: DateTime 18 | 19 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 20 | public updatedAt: DateTime 21 | 22 | @column.dateTime() 23 | public expiresAt: DateTime 24 | 25 | @belongsTo(() => User) 26 | public user: BelongsTo 27 | 28 | @computed() 29 | public isExpired(): boolean { 30 | return this.expiresAt.toMillis() < DateTime.local().toMillis() 31 | } 32 | 33 | public async generateToken(): Promise { 34 | this.token = Encryption.encrypt(string.generateRandom(40)) 35 | this.expiresAt = DateTime.local().plus({ hours: 2 }) 36 | } 37 | 38 | public async useToken(): Promise { 39 | this.expiresAt = DateTime.local().minus({ hours: 1 }) 40 | } 41 | 42 | public async verifyToken(token: string): Promise { 43 | return Encryption.decrypt(token) === Encryption.decrypt(this.token) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/edge/app/Models/User.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { 3 | BaseModel, 4 | column, 5 | beforeSave, 6 | beforeCreate, 7 | afterCreate, 8 | computed, 9 | hasOne, 10 | HasOne, 11 | hasMany, 12 | HasMany, 13 | } from '@ioc:Adonis/Lucid/Orm' 14 | import Hash from '@ioc:Adonis/Core/Hash' 15 | import Encryption from '@ioc:Adonis/Core/Encryption' 16 | import { string } from '@ioc:Adonis/Core/Helpers' 17 | import UserProfile from 'App/Models/UserProfile' 18 | import flowConfig from 'Config/flow' 19 | import UserSession from 'App/Models/UserSession' 20 | 21 | export default class User extends BaseModel { 22 | @column({ isPrimary: true }) 23 | public id: number 24 | 25 | @column() 26 | public email: string 27 | 28 | @column({ serializeAs: null }) 29 | public password: string 30 | 31 | @column() 32 | public rememberMeToken?: string 33 | 34 | @column({ serializeAs: null }) 35 | public emailVerificationToken: string | null 36 | 37 | @column({ serializeAs: null }) 38 | public emailResetRequest: string | null 39 | 40 | @column.dateTime() 41 | public emailVerifiedAt?: DateTime 42 | 43 | @column.dateTime({ autoCreate: true }) 44 | public createdAt: DateTime 45 | 46 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 47 | public updatedAt: DateTime 48 | 49 | @computed() 50 | public get isVerified() { 51 | if (!!this.emailVerifiedAt) { 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | /* 58 | / Relationships 59 | */ 60 | 61 | @hasOne(() => UserProfile) 62 | public profile: HasOne 63 | 64 | @hasMany(() => UserSession) 65 | public sessions: HasMany 66 | 67 | /* 68 | / Hooks 69 | */ 70 | @beforeCreate() 71 | public static generateVerificationToken(user: User) { 72 | if (flowConfig.features.verification === 'strict') { 73 | user.generateVerificationToken() 74 | } 75 | } 76 | 77 | @afterCreate() 78 | public static async createProfile(user: User) { 79 | const profile = await user.related('profile').create({ 80 | userId: user.id, 81 | }) 82 | await profile.save() 83 | } 84 | 85 | @beforeSave() 86 | public static async hashPassword(user: User) { 87 | if (user.$dirty.password) { 88 | user.password = await Hash.make(user.password) 89 | } 90 | } 91 | 92 | /* 93 | / Methods 94 | */ 95 | 96 | public verifyEmail(token: string): boolean { 97 | if (Encryption.decrypt(token) === Encryption.decrypt(this.emailVerificationToken ?? '')) { 98 | this.emailVerifiedAt = DateTime.now() 99 | this.emailVerificationToken = null 100 | return true 101 | } 102 | return false 103 | } 104 | 105 | public generateVerificationToken() { 106 | this.emailVerificationToken = Encryption.encrypt(string.generateRandom(40)) 107 | } 108 | 109 | public async newEmailResetRequest(email: string) { 110 | return (this.emailResetRequest = Encryption.encrypt(email, '2 hours')) 111 | } 112 | 113 | public async updateEmail(token: string) { 114 | const newEmail = Encryption.decrypt(token ?? '') 115 | if (!newEmail) { 116 | return false 117 | } 118 | this.email = newEmail! 119 | this.emailResetRequest = null 120 | this.emailVerifiedAt = DateTime.now() 121 | return true 122 | } 123 | 124 | public async verifyPassword(password: string): Promise { 125 | return Hash.verify(this.password, password) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /templates/edge/app/Models/UserProfile.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column, computed } from '@ioc:Adonis/Lucid/Orm' 3 | import { attachment, AttachmentContract } from '@ioc:Adonis/Addons/AttachmentLite' 4 | export default class UserProfile extends BaseModel { 5 | public static routeLookupKey = 'username' 6 | 7 | @column({ isPrimary: true, serializeAs: null }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @attachment({ folder: 'avatars', preComputeUrl: true }) 14 | public avatar: AttachmentContract | null 15 | 16 | @column() 17 | public firstName?: string | null 18 | 19 | @column() 20 | public lastName?: string | null 21 | 22 | @computed() 23 | public get fullName() { 24 | return `${this.firstName} ${this.lastName}` 25 | } 26 | 27 | @column.dateTime({ autoCreate: true }) 28 | public createdAt: DateTime 29 | 30 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 31 | public updatedAt: DateTime 32 | } 33 | -------------------------------------------------------------------------------- /templates/edge/app/Models/UserSession.txt: -------------------------------------------------------------------------------- 1 | // app/Models/UserSession.ts 2 | 3 | import { DateTime } from 'luxon' 4 | import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' 5 | import Encryption from '@ioc:Adonis/Core/Encryption' 6 | import type { UAParser } from 'ua-parser-js' 7 | export default class UserSession extends BaseModel { 8 | @column({ isPrimary: true }) 9 | public id: string 10 | 11 | @column() 12 | public userId: number | null 13 | 14 | @column() 15 | public ipAddress: string 16 | 17 | @column() 18 | public os: UAParser.IOS 19 | 20 | @column() 21 | public device: UAParser.IDevice 22 | 23 | @column() 24 | public browser: UAParser.IBrowser 25 | 26 | @column() 27 | public country: string | null 28 | 29 | @column({ 30 | prepare: (value: string) => Encryption.encrypt(JSON.stringify(value)), 31 | consume: (value: string) => JSON.parse(Encryption.decrypt(value) as string), 32 | }) 33 | public payload: string | object 34 | 35 | @column() 36 | public lastActivityAt: DateTime 37 | 38 | @column.dateTime({ autoCreate: true }) 39 | public createdAt: DateTime 40 | 41 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 42 | public updatedAt: DateTime 43 | } 44 | -------------------------------------------------------------------------------- /templates/edge/app/Providers/ValidationRulesProvider.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import flowConfig from 'Config/flow' 3 | 4 | export default class ValidationRulesProvider { 5 | private email = { 6 | required: schema.string({ trim: true }, [ 7 | rules.email(), 8 | rules.unique({ table: 'users', column: 'email' }), 9 | ]), 10 | optional: schema.string.optional({ trim: true }, [ 11 | rules.email(), 12 | rules.unique({ table: 'users', column: 'email' }), 13 | ]), 14 | notUnique: schema.string({ trim: true }, [rules.email()]), 15 | } 16 | 17 | private passwordConfirmed = { 18 | required: schema.string({}, [ 19 | rules.minLength(8), 20 | rules.alphaNum(), 21 | rules.trim(), 22 | rules.confirmed(), 23 | ]), 24 | } 25 | 26 | protected registerRules() { 27 | if (flowConfig.features.termsAndPrivacyPolicy) { 28 | return { 29 | email: this.email.required, 30 | password: this.passwordConfirmed.required, 31 | termsPrivacy: schema.boolean(), 32 | } 33 | } else { 34 | return { 35 | email: this.email.required, 36 | password: this.passwordConfirmed.required, 37 | termsPrivacy: schema.boolean.nullableAndOptional(), 38 | } 39 | } 40 | } 41 | 42 | protected passwordResetRules() { 43 | return { 44 | password: this.passwordConfirmed.required, 45 | } 46 | } 47 | 48 | protected passwordUpdateRules() { 49 | return { 50 | currentPassword: schema.string({ trim: true }, []), 51 | password: this.passwordConfirmed.required, 52 | } 53 | } 54 | 55 | protected profileUpdateRules() { 56 | return { 57 | firstName: schema.string({ trim: true }, [rules.maxLength(255)]), 58 | lastName: schema.string({ trim: true }, [rules.maxLength(255)]), 59 | email: this.email.notUnique, 60 | } 61 | } 62 | 63 | // Messages 64 | 65 | public messages = { 66 | 'email.required': 'Email is required', 67 | 'email.email': 'Email is not valid', 68 | 'password.required': 'Password is required', 69 | 'password.minLength': 'Password must be at least 8 characters long', 70 | 'unique': '{{ field }} is not available', 71 | 'confirmed': 'Passwords does not match', 72 | 'email.unique': 'Email is already in use', 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /templates/edge/app/Validators/Auth/RegisterValidator.txt: -------------------------------------------------------------------------------- 1 | // app/Validators/RegisterValidator.ts 2 | 3 | import { schema } from '@ioc:Adonis/Core/Validator' 4 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 5 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 6 | export default class RegisterValidator extends ValidationRulesProvider { 7 | constructor(protected ctx: HttpContextContract) { 8 | super() 9 | } 10 | 11 | public schema = schema.create({ 12 | ...this.registerRules(), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /templates/edge/app/Validators/User/PasswordUpdateValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class PasswordUpdateValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.passwordUpdateRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/edge/app/Validators/User/PasswordValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class PasswordValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.passwordResetRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/edge/app/Validators/User/ProfileValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class ProfileValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.profileUpdateRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/edge/config/flow.txt: -------------------------------------------------------------------------------- 1 | import type { MinuteNumbers } from 'luxon' 2 | 3 | const flowConfig: FlowConfig = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Stack Config - not implemented yet 7 | |-------------------------------------------------------------------------- 8 | | 9 | | The stack config is used to define the stack for which you want to 10 | | handle your views. The stack can be one of the following: edge, inertia 11 | | 12 | */ 13 | stack: 'edge', 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Features config 17 | |-------------------------------------------------------------------------- 18 | | 19 | | Configure optional features for AdonisJs Flow 20 | | 21 | */ 22 | features: { 23 | termsAndPrivacyPolicy: false, 24 | verification: 'strict', 25 | passwordConfirmation: 5, 26 | }, 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Views Config 30 | |-------------------------------------------------------------------------- 31 | | 32 | | The views config is to handle all your view template file locations 33 | | in one convenient place, without needing to go digging inside the controllers, providers etc.to make 34 | | small updates 35 | | 36 | */ 37 | views: { 38 | login: 'auth/login', 39 | register: 'auth/register', 40 | passwordResetRequest: 'auth/password-reset-request', 41 | passwordReset: 'auth/password-reset', 42 | verification: 'auth/verification', 43 | signedUrlInvalid: 'auth/signed-url-invalid', 44 | confirmPassword: 'auth/confirm-password', 45 | dashboard: 'dashboard/index', 46 | profile: 'dashboard/profile/edit', 47 | apiTokens: 'dashboard/api-tokens', 48 | }, 49 | } 50 | 51 | interface FlowFeatures { 52 | termsAndPrivacyPolicy: boolean 53 | verification: 'strict' | 'relaxed' 54 | passwordConfirmation: MinuteNumbers 55 | } 56 | 57 | interface FlowViews { 58 | login: string 59 | register: string 60 | verification: string 61 | passwordResetRequest: string 62 | passwordReset: string 63 | signedUrlInvalid: string 64 | confirmPassword: string 65 | dashboard: string 66 | profile: string 67 | apiTokens: string 68 | } 69 | 70 | interface FlowConfig { 71 | stack: 'edge' | 'inertia' 72 | features: FlowFeatures 73 | views: FlowViews 74 | } 75 | 76 | export default flowConfig 77 | -------------------------------------------------------------------------------- /templates/edge/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 PasswordReset from 'App/Models/PasswordReset' 9 | import type User from 'App/Models/User' 10 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 11 | 12 | declare module '@ioc:Adonis/Core/Event' { 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Define typed events 16 | |-------------------------------------------------------------------------- 17 | | 18 | | You can define types for events inside the following interface and 19 | | AdonisJS will make sure that all listeners and emit calls adheres 20 | | to the defined types. 21 | | 22 | | For example: 23 | | 24 | | interface EventsList { 25 | | 'new:user': UserModel 26 | | } 27 | | 28 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 29 | | an instance of the the UserModel only. 30 | | 31 | */ 32 | interface EventsList { 33 | 'user:register': User 34 | 'user:login': { user: User; ctx: HttpContextContract } 35 | 'user:logout': { ctx: HttpContextContract } 36 | 'user:resetPasswordRequest': { user: User; token: string } 37 | 'user:resetPassword': { user: User; passwordReset: PasswordReset } 38 | 'user:delete': User 39 | 'user:emailReset': User 40 | 'mail:sendEmailVerification': User 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /templates/edge/database/migrations/1697024229003_users.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class 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('email', 255).notNullable().unique().index() 10 | table.string('password', 180).notNullable() 11 | table.string('remember_me_token').nullable() 12 | table.timestamp('email_verified_at').nullable() 13 | table.string('email_verification_token').nullable().unique() 14 | table.string('email_reset_request').nullable().unique() 15 | 16 | /** 17 | * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL 18 | */ 19 | table.timestamp('created_at', { useTz: true }) 20 | table.timestamp('updated_at', { useTz: true }) 21 | }) 22 | } 23 | 24 | public async down() { 25 | this.schema.dropTable(this.tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/edge/database/migrations/1697025049030_password_resets.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'password_resets' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table 10 | .integer('user_id') 11 | .unsigned() 12 | .unique() 13 | .notNullable() 14 | .references('id') 15 | .inTable('users') 16 | .onDelete('CASCADE') 17 | table.string('token').notNullable().index() 18 | table.timestamp('created_at', { useTz: true }) 19 | table.timestamp('updated_at', { useTz: true }) 20 | table.timestamp('expires_at', { useTz: true }) 21 | }) 22 | } 23 | 24 | public async down() { 25 | this.schema.dropTable(this.tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/edge/database/migrations/1697025542544_user_sessions.txt: -------------------------------------------------------------------------------- 1 | // database/migrations/xxxx_xx_xx_xxxxxx_create_user_sessions_table.ts 2 | 3 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 4 | 5 | export default class CreateUserSessionsTable extends BaseSchema { 6 | protected tableName = 'user_sessions' 7 | 8 | public async up() { 9 | this.schema.createTable(this.tableName, (table) => { 10 | table.string('id').primary() 11 | table 12 | .integer('user_id') 13 | .unsigned() 14 | .nullable() 15 | .references('id') 16 | .inTable('users') 17 | .onDelete('CASCADE') 18 | .index() 19 | table.string('ip_address').nullable() 20 | table.string('os').nullable() 21 | table.string('device').nullable() 22 | table.string('browser').nullable() 23 | table.string('country').nullable() 24 | table.text('payload') 25 | table.timestamp('last_activity_at', { useTz: true }) 26 | table.timestamp('created_at', { useTz: true }) 27 | table.timestamp('updated_at', { useTz: true }) 28 | }) 29 | } 30 | 31 | public async down() { 32 | this.schema.dropTable(this.tableName) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/edge/database/migrations/1697027522393_api_tokens.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'api_tokens' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') 10 | table.string('name').notNullable() 11 | table.string('type').notNullable() 12 | table.string('token', 64).notNullable().unique() 13 | 14 | /** 15 | * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL 16 | */ 17 | table.timestamp('expires_at', { useTz: true }).nullable() 18 | table.timestamp('created_at', { useTz: true }).notNullable() 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/edge/database/migrations/1697733327219_user_profiles.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | export default class extends BaseSchema { 3 | protected tableName = 'user_profiles' 4 | 5 | public async up() { 6 | this.schema.createTable(this.tableName, (table) => { 7 | table.increments('id') 8 | table 9 | .integer('user_id') 10 | .unique() 11 | .references('id') 12 | .inTable('users') 13 | .onDelete('CASCADE') 14 | .index() 15 | table.json('avatar') 16 | table.string('first_name').nullable().defaultTo(null) 17 | table.string('last_name').nullable().defaultTo(null) 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/edge/env.txt: -------------------------------------------------------------------------------- 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 | APP_KEY: Env.schema.string(), 21 | APP_URL: Env.schema.string(), 22 | CACHE_VIEWS: Env.schema.boolean(), 23 | SESSION_DRIVER: Env.schema.string(), 24 | DRIVE_DISK: Env.schema.enum(['local'] as const), 25 | NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), 26 | DB_CONNECTION: Env.schema.string(), 27 | PG_HOST: Env.schema.string({ format: 'host' }), 28 | PG_PORT: Env.schema.number(), 29 | PG_USER: Env.schema.string(), 30 | PG_PASSWORD: Env.schema.string.optional(), 31 | PG_DB_NAME: Env.schema.string(), 32 | GITHUB_CLIENT_ID: Env.schema.string(), 33 | GITHUB_CLIENT_SECRET: Env.schema.string(), 34 | GOOGLE_CLIENT_ID: Env.schema.string(), 35 | GOOGLE_CLIENT_SECRET: Env.schema.string(), 36 | LINKEDIN_CLIENT_ID: Env.schema.string(), 37 | LINKEDIN_CLIENT_SECRET: Env.schema.string(), 38 | SMTP_HOST: Env.schema.string({ format: 'host' }), 39 | SMTP_PORT: Env.schema.number(), 40 | SMTP_USERNAME: Env.schema.string(), 41 | SMTP_PASSWORD: Env.schema.string(), 42 | }) 43 | -------------------------------------------------------------------------------- /templates/edge/providers/AppProvider.txt: -------------------------------------------------------------------------------- 1 | import type { 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 { DatabaseDriver } = await import('./SessionDriver') 12 | const Session = this.app.container.use('Adonis/Addons/Session') 13 | 14 | Session.extend('database', ({}, config, ctx) => { 15 | return new DatabaseDriver(ctx, config) 16 | }) 17 | } 18 | 19 | public async ready() { 20 | // App is ready 21 | } 22 | 23 | public async shutdown() { 24 | // Cleanup, since app is going down 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /templates/edge/providers/SessionDriver/index.txt: -------------------------------------------------------------------------------- 1 | import { SessionConfig, SessionDriverContract } from '@ioc:Adonis/Addons/Session' 2 | import UserSession from 'App/Models/UserSession' 3 | import { DateTime } from 'luxon' 4 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 5 | import ms from 'ms' 6 | import { UAParser } from 'ua-parser-js' 7 | export class DatabaseDriver implements SessionDriverContract { 8 | private config: SessionConfig 9 | constructor( 10 | protected ctx: HttpContextContract, 11 | config: SessionConfig 12 | ) { 13 | this.config = config 14 | } 15 | 16 | public async read(sessionId: string) { 17 | const session = await UserSession.find(sessionId) 18 | if (await this.expired(session as UserSession)) { 19 | return null 20 | } 21 | return session?.serialize().payload 22 | } 23 | 24 | public async write(sessionId: string, values: Record) { 25 | const parser = new UAParser(this.ctx.request.header('user-agent')) 26 | await UserSession.updateOrCreate( 27 | { id: sessionId }, 28 | { 29 | userId: this.ctx.auth.user?.id, 30 | ipAddress: this.ctx.request.ip(), 31 | browser: parser.getBrowser(), 32 | os: parser.getOS(), 33 | device: parser.getDevice(), 34 | lastActivityAt: DateTime.now(), 35 | payload: values, 36 | } 37 | ) 38 | } 39 | 40 | public async destroy(sessionId: string) { 41 | const session = await UserSession.find(sessionId) 42 | await session?.delete() 43 | } 44 | 45 | public async touch() {} 46 | 47 | protected async expired(session: UserSession) { 48 | return ( 49 | session?.lastActivityAt && 50 | ms(this.config.age) < DateTime.now().diff(session.lastActivityAt).milliseconds 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /templates/edge/resources/css/app.txt: -------------------------------------------------------------------------------- 1 | :root { 2 | --navbar-padding: 0.5rem; 3 | } 4 | 5 | @tailwind base; 6 | @tailwind components; 7 | @tailwind utilities; 8 | 9 | /* Path: resources/css/tailwind.css */ 10 | @layer base { 11 | .container { 12 | @apply lg:!max-w-screen-xl; 13 | } 14 | 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6 { 21 | @apply font-medium leading-5 mb-0; 22 | } 23 | h1 { 24 | @apply text-3xl; 25 | } 26 | 27 | h2 { 28 | @apply text-2xl; 29 | } 30 | 31 | h3 { 32 | @apply text-xl; 33 | } 34 | 35 | h4 { 36 | @apply text-lg; 37 | } 38 | 39 | h5 { 40 | @apply text-base; 41 | } 42 | 43 | h6 { 44 | @apply text-sm; 45 | } 46 | 47 | p { 48 | @apply leading-6; 49 | } 50 | } 51 | 52 | @layer components { 53 | .profileSection { 54 | @apply overflow-hidden grid grid-cols-2 gap-4 py-4; 55 | } 56 | 57 | .formWrapper { 58 | @apply bg-white rounded-lg shadow-lg p-4 flex flex-col gap-4 divide-y-2; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /templates/edge/resources/js/app.txt: -------------------------------------------------------------------------------- 1 | import '../css/app.css' 2 | -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/confirm-password.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | @set('backUrl', request.qs().fromUrl) 3 | 4 | @section('body') 5 |
6 |

Confirm Password

7 |
8 | 9 | 10 |
11 | Go Back 12 |
13 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/login.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

Login

6 |
7 | 9 | 11 | 12 |
13 | Need an account? Register here 14 | Forgot Password 15 |
16 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/password-reset-expired.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

Oops it seems like this link has expired, please try again

6 |
7 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/password-reset-invalid.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

This link is not valid

6 |
7 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/password-reset-request.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 | 6 |

Forgot your password?

7 | 8 |
9 | 10 | 12 | 13 |
14 |
15 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/password-reset.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 | 6 |

Reset your password

7 | 8 |
9 | 10 | 11 | 13 | 14 |
15 |
16 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/register.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

Register

6 | 7 |
8 | 10 | 12 | 14 | 15 |
16 | Click here to login 17 |
18 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/auth/verification.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 | 6 |

Please verify your email address

7 |
8 | 9 |
10 | 11 |
12 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/components/composites/breadcrumbs.txt: -------------------------------------------------------------------------------- 1 | @component('components/ui/navbar',{ 2 | classes: 'shadow-sm', 3 | }) 4 | 5 | @set('urls', request.url().split('/').filter(item => item !== '')) 6 | 7 | 27 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/components/composites/profileNavigation.txt: -------------------------------------------------------------------------------- 1 | @component('components.ui.dropdown',{ 2 | classes: 'dropdown-end', 3 | labelClass: 'btn btn-ghost btn-circle avatar', 4 | }) 5 | @slot('label') 6 | 7 |
8 | @if(gProfile.avatar == null) 9 | {{gProfile.fullName}} 10 | @else 11 | {{gProfile.fullName}} 12 | @endif 13 |
14 | @end 15 | @slot('main') 16 | 25 | @end 26 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/components/ui/dropdown.txt: -------------------------------------------------------------------------------- 1 | @set('attr', $props.serializeExcept(['labelClass','label', 'classes'])) 2 | -------------------------------------------------------------------------------- /templates/edge/resources/views/components/ui/logo.txt: -------------------------------------------------------------------------------- 1 | @set('title', capitalCase(sentenceCase(env('APP_NAME')))) 2 | 3 | {{title}} -------------------------------------------------------------------------------- /templates/edge/resources/views/components/ui/navbar.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/edge/resources/views/dashboard/api-tokens.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/main') 2 | 3 | @section('body') 4 |
5 |
6 |
7 |

Create API Token

8 |

API tokens allow third-party services to authenticate with our applications on your behalf

9 |
10 |
11 | @include('partials/api-tokens/create-token') 12 |
13 |
14 |
15 |
16 |

Manage API Tokens

17 |

You may delete any of your existing tokens if they are no longer needed.

18 |
19 |
20 | @include('partials/api-tokens/list-tokens') 21 |
22 |
23 | @if(flashMessages.has('token')) 24 |
25 | 39 |
40 | 41 | @end 42 |
43 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/dashboard/index.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/main') 2 | 3 | @section('body') 4 |
5 |
6 |

Adonis Flux

7 |

Welcome to your Flux project

8 |

9 | Adonis Flux is a beautifully designed application starter kit for AdonisJs and provides the perfect 10 | starting point for your next NodeJS application. Flux provides the implementation for your application's 11 | login, registration, email verification, two-factor authentication, session management, API and optional 12 | team 13 | management features. 14 | 15 | Flux is designed using TailwindCSS and offers your choice 16 | of EdgeJs or scaffolding. 17 |

18 |
19 |
20 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/dashboard/profile/edit.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/main') 2 | 3 | @section('body') 4 |
5 |
6 |
7 |

Profile Information

8 |

Update your accounts profile information and avatar

9 |
10 |
11 | @include('partials/profile/details') 12 |
13 |
14 |
15 |
16 |

Update Password

17 |

Ensure your account is using a long, random password to stay secure

18 |
19 |
20 | @include('partials/profile/password') 21 |
22 |
23 |
24 |
25 |

Browser sessions

26 |

Manage and logout your active sessions on other browsers and devices

27 |
28 |
29 | @include('partials/profile/sessions') 30 |
31 |
32 |
33 |
34 |

Delete Account

35 |

Permanently delete and close your account

36 |
37 |
38 | @include('partials/profile/delete') 39 |
40 |
41 |
42 | 43 | 44 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/emails/confirmation_template/index.txt: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 | 3 |

{{ description }}

4 | 5 | 6 | If you have any questions, please contact us. 7 | -------------------------------------------------------------------------------- /templates/edge/resources/views/emails/confirmation_template/plain.txt: -------------------------------------------------------------------------------- 1 | {{ title }} 2 | 3 | {{ description }} 4 | 5 | If you have any questions, please contact us. -------------------------------------------------------------------------------- /templates/edge/resources/views/emails/verify_template/index.txt: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 | 3 |

{{ description }} {{ urlText }}

4 | 5 | 6 |

7 | If you did not request a {{ title }}, ignore this message. 8 |

-------------------------------------------------------------------------------- /templates/edge/resources/views/emails/verify_template/plain.txt: -------------------------------------------------------------------------------- 1 | {{ title }} 2 | 3 | {{ description }} {{ urlText }} 4 | 5 | If you did not request a {{ title }}, ignore this message. -------------------------------------------------------------------------------- /templates/edge/resources/views/emails/welcome/index.txt: -------------------------------------------------------------------------------- 1 |

Welcome to {{ env('APP_NAME')}}

2 | 3 |

Thank you for creating an account. Please verify your email using this link: Click here

-------------------------------------------------------------------------------- /templates/edge/resources/views/emails/welcome/plain.txt: -------------------------------------------------------------------------------- 1 | Welcome to {{ env('APP_NAME')}} 2 | 3 | Thank you for creating an account. Please verify your email using this link: Click here -------------------------------------------------------------------------------- /templates/edge/resources/views/errors/not-found.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

404

6 |

Page not found

7 | Back to dashboard 8 |
9 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/errors/server-error.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | @set('message', request.qs().message.split(':')) 3 | 4 | @section('body') 5 |
6 |

500

7 |

{{message[message.length - 1]}}

8 | Back to dashboard 9 |
10 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/errors/unauthorized.txt: -------------------------------------------------------------------------------- 1 | @layout('layouts/auth') 2 | 3 | @section('body') 4 |
5 |

403

6 |

You are not authorized to access this page

7 | Back to dashboard 8 |
9 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/layouts/auth.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | @include('partials/head') 4 | 5 | 6 | @include('partials/flashMessages') 7 |
8 | @!section('body') 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/edge/resources/views/layouts/main.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | @include('partials/head') 4 | 5 | 6 | @include('partials/header') 7 | @include('partials/flashMessages') 8 |
9 | @!section('body') 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /templates/edge/resources/views/partials/api-tokens/create-token.txt: -------------------------------------------------------------------------------- 1 | @set('createTokenUrl', `/dashboard/api-tokens`) 2 |
3 | 5 | 6 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/api-tokens/list-tokens.txt: -------------------------------------------------------------------------------- 1 |
2 | @if(tokens.length == 0) 3 |
4 |

No Tokens created yet

5 |
6 | @end 7 | @each(item in tokens) 8 |
9 |

{{item.name}}

10 |
12 | 20 |
21 |
22 | @end 23 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/flashMessages.txt: -------------------------------------------------------------------------------- 1 | @if(flashMessages.has('errors')) 2 |
3 |
4 | 5 | 7 | 8 | 9 | @each(error in flashMessages.get('errors')) 10 | {{error}}
11 | @end 12 |
13 |
14 |
15 | @end 16 | 17 | @if(flashMessages.has('success')) 18 |
19 | 20 |
21 | 22 | 24 | 25 | 26 | @each(success in flashMessages.get('success')) 27 | {{success}}
28 | @end 29 |
30 |
31 |
32 | @end -------------------------------------------------------------------------------- /templates/edge/resources/views/partials/head.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @entryPointScripts('app') 5 | @entryPointStyles('app') 6 | {{ title ? title + ' | ' + capitalCase(sentenceCase(env('APP_NAME'))) : 7 | capitalCase(sentenceCase(env('APP_NAME')))}} 8 | 9 | -------------------------------------------------------------------------------- /templates/edge/resources/views/partials/header.txt: -------------------------------------------------------------------------------- 1 |
2 | @component('components/ui/navbar') 3 |
4 | @!ui.logo() 5 |
6 |
7 | @if(auth.isLoggedIn) 8 | @!composites.profileNavigation() 9 | @else 10 | Register 11 | Login 12 | @end 13 |
14 | @end 15 | @if(request.url() != '/') 16 | @!composites.breadcrumbs() 17 | @end 18 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/profile/avatar.txt: -------------------------------------------------------------------------------- 1 | @set('avatarPostURL', `/dashboard/profile/avatar?_method=PUT`) 2 | 3 |
4 | 5 | 6 | 7 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/profile/delete.txt: -------------------------------------------------------------------------------- 1 | @set('deleteURl', `/dashboard/profile?_method=DELETE`) 2 |

Please note this is a permanent action and all of your data will be wiped from our database. This is irreversible and 3 | make sure to backup any data you might need in the future

4 |
5 | 6 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/profile/details.txt: -------------------------------------------------------------------------------- 1 | @include('partials/profile/avatar') 2 | @set('profileUpdateUrl', `/dashboard/profile?_method=PUT`) 3 |
4 | 6 | 8 | 10 | 11 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/profile/password.txt: -------------------------------------------------------------------------------- 1 | @set('passwordUpdateUrl', `/dashboard/profile/password?_method=PUT`) 2 | 3 |
4 | 6 | 7 | 8 | 10 | 11 |
-------------------------------------------------------------------------------- /templates/edge/resources/views/partials/profile/sessions.txt: -------------------------------------------------------------------------------- 1 |

If neccessary you may logout of all your browser session accross your other devices. Of your active and most 2 | recent session will be listed below. If you feel your account has been compromised you should also update your 3 | password.

4 |
5 | 6 | @each(item in sessions) 7 |
8 | 10 | 12 | 13 |
14 | {{JSON.parse(item.os).name}} - {{JSON.parse(item.browser).name}} 15 | {{item.ipAddress}} 16 | @if(item.id === currentSession) 17 | - 18 | This Device 19 | @end 20 |
21 | @if(item.id != currentSession) 22 |
23 | 31 |
32 | @end 33 |
34 | @end 35 |
36 | {{-- 37 |
38 | 39 |
--}} -------------------------------------------------------------------------------- /templates/edge/start/events/auth.txt: -------------------------------------------------------------------------------- 1 | import Event from '@ioc:Adonis/Core/Event' 2 | import UserSession from 'App/Models/UserSession' 3 | import EmailSendingProvider from 'App/Providers/EmailSendingProvider' 4 | 5 | Event.on('user:register', (user) => { 6 | EmailSendingProvider.sendEmailVerificationLink(user) 7 | }) 8 | 9 | Event.on('user:login', ({}) => {}) 10 | 11 | Event.on('user:logout', ({}) => {}) 12 | 13 | Event.on('user:resetPasswordRequest', (data) => { 14 | EmailSendingProvider.sendPasswordResetLink(data) 15 | }) 16 | 17 | Event.on('user:resetPassword', (data) => { 18 | EmailSendingProvider.sendPasswordResetSuccess(data.user) 19 | data.passwordReset.useToken() 20 | data.passwordReset.save() 21 | UserSession.query().where('userId', data.user.id).delete() 22 | }) 23 | -------------------------------------------------------------------------------- /templates/edge/start/events/index.txt: -------------------------------------------------------------------------------- 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 Event from '@ioc:Adonis/Core/Event' 11 | import Mail from '@ioc:Adonis/Addons/Mail' 12 | import EmailSendingProvider from 'App/Providers/EmailSendingProvider' 13 | import './auth' 14 | 15 | Event.onError((event, error, eventData) => { 16 | console.log(event, error, eventData) 17 | }) 18 | 19 | // TODO log emails to database 20 | Event.on('mail:sent', Mail.prettyPrint) 21 | 22 | Event.on('mail:sendEmailVerification', (user) => { 23 | EmailSendingProvider.sendEmailVerificationLink(user) 24 | }) 25 | 26 | Event.on('user:delete', (user) => { 27 | EmailSendingProvider.sendAccountDeleteNotification(user) 28 | }) 29 | 30 | Event.on('user:emailReset', (user) => { 31 | EmailSendingProvider.sendNewEmailRequestConfirmation(user) 32 | }) 33 | -------------------------------------------------------------------------------- /templates/edge/start/routes/api-tokens.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.get('/', 'ApiTokensController.index').as('index') 5 | Route.post('/', 'ApiTokensController.store').as('store') 6 | Route.delete('/:id', 'ApiTokensController.destroy').as('destroy') 7 | }) 8 | .namespace('App/Controllers/Http/User') 9 | .middleware('auth') 10 | .as('api-tokens') 11 | .prefix('dashboard/api-tokens') 12 | -------------------------------------------------------------------------------- /templates/edge/start/routes/auth.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | import flowConfig from 'Config/flow' 3 | 4 | Route.group(() => { 5 | Route.group(() => { 6 | Route.get('/logout', 'LoginController.destroy').as('login.destroy') 7 | }).middleware('auth') 8 | 9 | Route.group(() => { 10 | if (flowConfig.features.verification === 'strict') { 11 | Route.post('/login', 'LoginController.store').as('login.store').middleware('verified') 12 | } else { 13 | Route.post('/login', 'LoginController.store').as('login.store') 14 | } 15 | 16 | Route.get('/register', 'RegisterController.create').as('register.create') 17 | Route.post('/register', 'RegisterController.store').as('register.store') 18 | 19 | Route.get('/login', 'LoginController.create').as('login.create') 20 | 21 | Route.get('/forgot-password', 'PasswordResetRequestController.create').as( 22 | 'password.createReset' 23 | ) 24 | Route.post('/forgot-password', 'PasswordResetRequestController.store').as('password.storeReset') 25 | 26 | Route.get('/reset-password/:token', 'PasswordResetController.create').as( 27 | 'password.createPassword' 28 | ) 29 | Route.post('/reset-password/:token', 'PasswordResetController.store').as( 30 | 'password.storePassword' 31 | ) 32 | }).middleware('guest') 33 | 34 | Route.get('/verification/:email', 'RegisterController.createVerification').as( 35 | 'verification.create' 36 | ) 37 | Route.post('/verification/:email', 'RegisterController.resendVerification').as( 38 | 'verification.store' 39 | ) 40 | Route.get('/verification/verify/:token', 'RegisterController.edit').as('verification.confirm') 41 | 42 | Route.get('/verification/password/:intended', 'PasswordConfirmationController.create').as( 43 | 'password.confirm' 44 | ) 45 | Route.post('/verification/password/:intended', 'PasswordConfirmationController.store').as( 46 | 'password.verify' 47 | ) 48 | }).namespace('App/Controllers/Http/Auth') 49 | -------------------------------------------------------------------------------- /templates/edge/start/routes/errors.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.get('/500', async ({ view }) => { 4 | return view.render('errors/505') 5 | }).as('error.invalidURL') 6 | -------------------------------------------------------------------------------- /templates/edge/start/routes/profile.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | Route.group(() => { 3 | Route.group(() => { 4 | Route.get('/', 'ProfilesController.edit').as('profile.edit') 5 | Route.put('/', 'ProfilesController.update').as('profile.update') 6 | Route.put('/avatar', 'ProfilesController.updateProfileAvatar').as('profile.updateAvatar') 7 | Route.put('/password', 'UsersController.updatePassword').as('password.update') 8 | Route.delete('/avatar', 'ProfilesController.deleteProfileAvatar').as('profile.deleteAvatar') 9 | Route.delete('/', 'UsersController.destroy').as('user.destroy') 10 | Route.delete('/sessions/:id', 'ProfilesController.destroySession').as('destroySession') 11 | }).middleware('auth') 12 | 13 | Route.get('/:email', 'UsersController.confirmEmailUpdateRequest').as('user.newEmail') 14 | }) 15 | .namespace('App/Controllers/Http/User') 16 | .prefix('/dashboard/profile') 17 | -------------------------------------------------------------------------------- /templates/edge/tailwind.config.txt: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./resources/**/*.{edge,js,ts,jsx,tsx,vue}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('daisyui')], 8 | daisyui: { 9 | themes: ['light'], 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/Auth/LoginController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import flowConfig from 'Config/flow' 4 | import FlashMessage from 'App/Enums/FlashMessage' 5 | import Route from '@ioc:Adonis/Core/Route' 6 | 7 | const { login } = flowConfig.views 8 | const { LoginSuccess, LogoutSuccess } = FlashMessage 9 | 10 | /** 11 | * Controller responsible for handling user login and logout functionality. 12 | */ 13 | export default class LoginController { 14 | /** 15 | * Renders the login view. 16 | */ 17 | public async create({ inertia }: HttpContextContract) { 18 | return inertia.render(login, { formUrl: Route.makeUrl('login.store') }) 19 | } 20 | /** 21 | * Authenticates the user and logs them in. 22 | */ 23 | public async store(ctx: HttpContextContract): Promise { 24 | const { request, auth, response, session } = ctx 25 | const { email, password } = { 26 | email: request.input('email'), 27 | password: request.input('password'), 28 | } 29 | const user = await auth.use('web').attempt(email, password) 30 | 31 | Event.emit('user:login', { user, ctx }) 32 | session.flash('success', [LoginSuccess]) 33 | return response.redirect().toRoute('dashboard.index') 34 | } 35 | /** 36 | * Logs the user out. 37 | */ 38 | public async destroy(ctx: HttpContextContract): Promise { 39 | const { auth, response, session } = ctx 40 | await auth.logout() 41 | session.flash('success', [LogoutSuccess]) 42 | Event.emit('user:logout', { ctx }) 43 | return response.redirect().toRoute('login.create') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/Auth/PasswordConfirmationController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import flowConfig from 'Config/flow' 4 | import Encryption from '@ioc:Adonis/Core/Encryption' 5 | import { DateTime } from 'luxon' 6 | 7 | const { confirmPassword } = flowConfig.views 8 | 9 | /** 10 | * Controller for registering and verifying users. 11 | */ 12 | export default class PasswordConfirmationController { 13 | /** 14 | * Renders the registration view. 15 | */ 16 | public async create({ view, request }: HttpContextContract): Promise { 17 | return view.render(confirmPassword, { intendedRoute: request.params().intended }) 18 | } 19 | 20 | /** 21 | * Validates user password and redirect to intended route 22 | */ 23 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 24 | const { intended } = request.params() 25 | const password = await request.input('password') 26 | const user = auth.user as User 27 | const isSame = await user.verifyPassword(password) 28 | 29 | if (!isSame) { 30 | session.flash('error', 'Password does not match') 31 | return response.redirect().back() 32 | } 33 | 34 | session.put('password-confirmed', DateTime.now().toISO()!) 35 | 36 | return response.redirect(Encryption.decrypt(intended)!) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/Auth/PasswordResetController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { Exception } from '@adonisjs/core/build/standalone' 3 | import Route from '@ioc:Adonis/Core/Route' 4 | import Event from '@ioc:Adonis/Core/Event' 5 | import PasswordReset from 'App/Models/PasswordReset' 6 | import PasswordValidator from 'App/Validators/User/PasswordValidator' 7 | import FlashMessage from 'App/Enums/FlashMessage' 8 | import flowConfig from 'Config/flow' 9 | 10 | const { passwordUpdate } = flowConfig.views 11 | const { PasswordResetSuccess } = FlashMessage 12 | 13 | export default class PasswordResetsController { 14 | /** 15 | * Renders the password reset view with the provided token. 16 | */ 17 | public async create({ inertia, request }: HttpContextContract) { 18 | const token = request.param('token') 19 | const passwordResetRequest = await PasswordReset.findByOrFail('token', token) 20 | 21 | if (passwordResetRequest.isExpired()) { 22 | throw new Exception( 23 | 'The URL you are trying to reach has unfortunately expired, please try again or contact support if you have any issues.', 24 | 403, 25 | 'E_INVALID_URL' 26 | ) 27 | } 28 | 29 | if (!(await passwordResetRequest.verifyToken(token))) { 30 | throw new Exception( 31 | 'The URL you are looking for does not exist, please try again or contact support if you have any issues.', 32 | 403, 33 | 'E_INVALID_URL' 34 | ) 35 | } 36 | 37 | return inertia.render(passwordUpdate, { 38 | formUrl: Route.makeUrl('password.updatePassword', { token: token }), 39 | }) 40 | } 41 | 42 | /** 43 | * Handles the password reset form submission. 44 | * */ 45 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 46 | // console.log(request.body()) 47 | const token = request.param('token') 48 | const { password } = await request.validate(PasswordValidator) 49 | const passwordReset = await PasswordReset.findByOrFail('token', token) 50 | const user = await passwordReset.related('user').query().firstOrFail() 51 | await auth.logout() 52 | 53 | user.merge({ password: password }) 54 | await user.save() 55 | 56 | Event.emit('user:resetPassword', { user, passwordReset }) 57 | 58 | session.flash({ 59 | success: [PasswordResetSuccess], 60 | }) 61 | 62 | return response.redirect().toRoute('login.create') 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/Auth/PasswordResetRequestController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import Route from '@ioc:Adonis/Core/Route' 4 | import User from 'App/Models/User' 5 | import PasswordReset from 'App/Models/PasswordReset' 6 | import flowConfig from 'Config/flow' 7 | import FlashMessage from 'App/Enums/FlashMessage' 8 | import EmailVerificationValidator from 'App/Validators/Auth/EmailVerificationValidator' 9 | 10 | const { PasswordResetRequested } = FlashMessage 11 | const { passwordResetRequest } = flowConfig.views 12 | 13 | /** 14 | * Controller for handling password reset requests. 15 | */ 16 | export default class PasswordResetRequestController { 17 | /** 18 | * Renders the password reset request view. 19 | */ 20 | public async create({ inertia }: HttpContextContract) { 21 | return inertia.render(passwordResetRequest, { formUrl: Route.makeUrl('password.storeReset') }) 22 | } 23 | 24 | /** 25 | * Handles the submission of a password reset request form. 26 | */ 27 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 28 | const { email } = await request.validate(EmailVerificationValidator) 29 | 30 | const user = await User.findBy('email', email) 31 | if (user) { 32 | await PasswordReset.query().where('user_id', user.id).delete() 33 | 34 | const newRequest = new PasswordReset() 35 | newRequest.userId = user.id 36 | await newRequest.generateToken() 37 | await newRequest.save() 38 | 39 | Event.emit('user:resetPasswordRequest', { user, token: newRequest.token }) 40 | } 41 | session.flash({ 42 | success: [PasswordResetRequested], 43 | }) 44 | 45 | await auth.logout() 46 | 47 | return response.redirect().back() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/Auth/RegisterController.txt: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import User from 'App/Models/User' 4 | import RegisterValidator from 'App/Validators/Auth/RegisterValidator' 5 | import flowConfig from 'Config/flow' 6 | import FlashMessage from 'App/Enums/FlashMessage' 7 | import Route from '@ioc:Adonis/Core/Route' 8 | import EmailVerificationValidator from 'App/Validators/Auth/EmailVerificationValidator' 9 | 10 | const { register, verification } = flowConfig.views 11 | const { RegisterSuccess, EmailVerified, EmailAlreadyVerified EmailVerificationResent } = FlashMessage 12 | 13 | /** 14 | * Controller for registering and verifying users. 15 | */ 16 | export default class RegistersController { 17 | /** 18 | * Renders the registration view. 19 | */ 20 | public async create({ inertia }: HttpContextContract) { 21 | return inertia.render(register, { formUrl: Route.makeUrl('register.store') }) 22 | } 23 | 24 | /** 25 | * Validates user data and creates a new user. 26 | */ 27 | public async store({ request, response, session, auth }: HttpContextContract): Promise { 28 | const userData = await request.validate(RegisterValidator) 29 | delete userData.terms 30 | const user = await User.create(userData) 31 | 32 | Event.emit('user:register', user) 33 | session.flash('success', [RegisterSuccess]) 34 | 35 | if (flowConfig.features.verification === 'strict') { 36 | return response.redirect().toRoute('login.create') 37 | } 38 | auth.login(user) 39 | return response.redirect().toRoute('dashboard.index') 40 | } 41 | 42 | /** 43 | * Verifies a user's email address. 44 | */ 45 | public async edit({ request, response, session }: HttpContextContract): Promise { 46 | const token = request.params().token 47 | const user = await User.findByOrFail('emailVerificationToken', token) 48 | 49 | const verified = user.verifyEmail(token) 50 | if (!verified) { 51 | return response.status(403).badRequest() 52 | } 53 | await user.save() 54 | session.flash('success', [EmailVerified]) 55 | return response.redirect().toRoute('login.create') 56 | } 57 | /** 58 | * Render email verification view. 59 | */ 60 | public async createVerification({ inertia }: HttpContextContract) { 61 | return inertia.render(verification) 62 | } 63 | /** 64 | * Resend email verification. 65 | */ 66 | public async resendVerification({ 67 | request, 68 | response, 69 | session, 70 | }: HttpContextContract): Promise { 71 | const { email } = await request.validate(EmailVerificationValidator) 72 | const user = await User.findByOrFail('email', email) 73 | if (user.isVerified) { 74 | session.flash('success', [EmailAlreadyVerified]) 75 | return response.redirect().back() 76 | } 77 | user.generateVerificationToken() 78 | await user.save() 79 | Event.emit('mail:sendEmailVerification', user) 80 | session.flash('success', [EmailVerificationResent]) 81 | return response.redirect().back() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/User/ApiTokensController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import ApiToken from 'App/Models/ApiToken' 4 | import FlashMessages from 'App/Enums/FlashMessage' 5 | 6 | const { ApiTokenDeleted } = FlashMessages 7 | 8 | export default class ApiTokensController { 9 | public async store({ request, response, auth, session }: HttpContextContract) { 10 | const { name, expiresIn } = request.all() 11 | console.log(name, expiresIn); 12 | if (!name || !expiresIn) { 13 | session.flash({ error: ['Name and expiration are required'] }) 14 | return response.redirect().back() 15 | } 16 | 17 | const user = auth.user! 18 | const token = await auth.use('api').generate(user, { 19 | name, 20 | expiresIn, 21 | }) 22 | session.flash({ token: token }) 23 | Event.emit('api-token:created', token) 24 | response.redirect().back() 25 | } 26 | 27 | public async destroy({ response, request, session }: HttpContextContract) { 28 | const token = await ApiToken.findOrFail(request.param('id')) 29 | await token.delete() 30 | Event.emit('api-token:deleted', token) 31 | session.flash({ success: [ApiTokenDeleted] }) 32 | response.redirect().back() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/vue/app/Controllers/Http/User/UsersController.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Event from '@ioc:Adonis/Core/Event' 3 | import User from 'App/Models/User' 4 | import FlashMessages from 'App/Enums/FlashMessage' 5 | import PasswordUpdateValidator from 'App/Validators/User/PasswordUpdateValidator' 6 | 7 | const { EmailUpdated, PasswordValidationFailed } = FlashMessages 8 | export default class UsersController { 9 | public async index({}: HttpContextContract) {} 10 | 11 | public async create({}: HttpContextContract) {} 12 | 13 | public async store({}: HttpContextContract) {} 14 | 15 | public async show({}: HttpContextContract) {} 16 | 17 | public async edit({}: HttpContextContract) {} 18 | 19 | public async update({}: HttpContextContract) {} 20 | 21 | public async destroy({ auth, response }: HttpContextContract) { 22 | const user = auth.user! 23 | await auth.logout() 24 | await user.delete() 25 | Event.emit('user:delete', user) 26 | response.redirect().toRoute('login.create') 27 | } 28 | 29 | /** 30 | * Verifies users new email address and updates it. 31 | */ 32 | public async confirmEmailUpdateRequest({ 33 | request, 34 | response, 35 | session, 36 | auth, 37 | }: HttpContextContract) { 38 | const { email } = request.params() 39 | const user = await User.findByOrFail('emailResetRequest', email) 40 | const updatedEmail = user.updateEmail(email) 41 | if (!updatedEmail) { 42 | session.flash('error', ['Invalid email update request']) 43 | response.redirect().toRoute('login.create') 44 | } 45 | await user.save() 46 | await auth.logout() 47 | session.flash('success', [EmailUpdated]) 48 | return response.redirect().toRoute('login.create') 49 | } 50 | 51 | /** 52 | * Updates authenticated user's password. 53 | */ 54 | public async updatePassword({ request, response, session, auth }: HttpContextContract) { 55 | const { password, currentPassword } = await request.validate(PasswordUpdateValidator) 56 | const user = auth.user! 57 | if (!(await user.verifyPassword(currentPassword))) { 58 | session.flash('errors', [PasswordValidationFailed]) 59 | return response.redirect().back() 60 | } 61 | user.password = password 62 | await user.save() 63 | session.flash('success', ['Password updated successfully']) 64 | return response.redirect().toRoute('profile.edit') 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /templates/vue/app/Enums/FlashMessage.txt: -------------------------------------------------------------------------------- 1 | enum FlashMessages { 2 | // auth 3 | LoginSuccess = 'Login successful!', 4 | LoginFail = 'Invalid credentials, please try again.', 5 | LogoutSuccess = 'Logged out successfully.', 6 | RegisterSuccess = 'Registration successful! Please check your email to verify your account.', 7 | EmailVerified = 'Your email has been verified.', 8 | EmailNotVerified = 'Please verify your email address.', 9 | EmailUpdated = 'Your email has been updated.', 10 | EmailVerificationResent = 'Verification email resent, please check your email.', 11 | EmailAlreadyVerified = 'Your email has already been verified.', 12 | PasswordResetSuccess = 'Your password has been reset.', 13 | PasswordResetRequested = 'If your email exists in our system, you will receive a password reset link shortly.', 14 | PasswordValidationFailed = 'Password incorrect, please try again.', 15 | // profile 16 | ProfileUpdated = 'Profile updated successfully', 17 | 18 | // Files 19 | FileTooLarge = 'File too large, please try again.', 20 | 21 | // Api Tokens 22 | ApiTokenCreated = 'Api token created successfully.', 23 | ApiTokenDeleted = 'Api token deleted successfully.', 24 | } 25 | 26 | export default FlashMessages 27 | -------------------------------------------------------------------------------- /templates/vue/app/Enums/HttpStatusCodes.txt: -------------------------------------------------------------------------------- 1 | enum HttpStatusCodes { 2 | // 1xx Informational 3 | CONTINUE = 100, 4 | SWITCHING_PROTOCOLS = 101, 5 | PROCESSING = 102, 6 | 7 | // 2xx Success 8 | OK = 200, 9 | CREATED = 201, 10 | ACCEPTED = 202, 11 | NON_AUTHORITATIVE_INFORMATION = 203, 12 | NO_CONTENT = 204, 13 | RESET_CONTENT = 205, 14 | PARTIAL_CONTENT = 206, 15 | MULTI_STATUS = 207, 16 | ALREADY_REPORTED = 208, 17 | IM_USED = 226, 18 | 19 | // 3xx Redirection 20 | MULTIPLE_CHOICES = 300, 21 | MOVED_PERMANENTLY = 301, 22 | FOUND = 302, 23 | SEE_OTHER = 303, 24 | NOT_MODIFIED = 304, 25 | USE_PROXY = 305, 26 | TEMPORARY_REDIRECT = 307, 27 | PERMANENT_REDIRECT = 308, 28 | 29 | // 4xx Client errors 30 | BAD_REQUEST = 400, 31 | UNAUTHORIZED = 401, 32 | PAYMENT_REQUIRED = 402, 33 | FORBIDDEN = 403, 34 | NOT_FOUND = 404, 35 | METHOD_NOT_ALLOWED = 405, 36 | NOT_ACCEPTABLE = 406, 37 | PROXY_AUTHENTICATION_REQUIRED = 407, 38 | REQUEST_TIMEOUT = 408, 39 | CONFLICT = 409, 40 | GONE = 410, 41 | LENGTH_REQUIRED = 411, 42 | PRECONDITION_FAILED = 412, 43 | PAYLOAD_TOO_LARGE = 413, 44 | URI_TOO_LONG = 414, 45 | UNSUPPORTED_MEDIA_TYPE = 415, 46 | RANGE_NOT_SATISFIABLE = 416, 47 | EXPECTATION_FAILED = 417, 48 | IM_A_TEAPOT = 418, 49 | MISDIRECTED_REQUEST = 421, 50 | UNPROCESSABLE_ENTITY = 422, 51 | LOCKED = 423, 52 | FAILED_DEPENDENCY = 424, 53 | UPGRADE_REQUIRED = 426, 54 | PRECONDITION_REQUIRED = 428, 55 | TOO_MANY_REQUESTS = 429, 56 | REQUEST_HEADER_FIELDS_TOO_LARGE = 431, 57 | UNAVAILABLE_FOR_LEGAL_REASONS = 451, 58 | 59 | // 5xx Server errors 60 | INTERNAL_SERVER_ERROR = 500, 61 | NOT_IMPLEMENTED = 501, 62 | BAD_GATEWAY = 502, 63 | SERVICE_UNAVAILABLE = 503, 64 | GATEWAY_TIMEOUT = 504, 65 | HTTP_VERSION_NOT_SUPPORTED = 505, 66 | VARIANT_ALSO_NEGOTIATES = 506, 67 | INSUFFICIENT_STORAGE = 507, 68 | LOOP_DETECTED = 508, 69 | NOT_EXTENDED = 510, 70 | NETWORK_AUTHENTICATION_REQUIRED = 511, 71 | } 72 | 73 | export default HttpStatusCodes 74 | -------------------------------------------------------------------------------- /templates/vue/app/Enums/MailerPresets.txt: -------------------------------------------------------------------------------- 1 | enum MailerDefaults { 2 | FROM = 'britzdylan@gmail.com', 3 | CC = '', 4 | 5 | WELCOME_SUBJECT = 'Welcome aboard: Confirm your email', 6 | 7 | RESET_PASSWORD_SUBJECT = 'Reset your password', 8 | RESET_PASSWORD_TITLE = 'Reset your password', 9 | RESET_PASSWORD_MESSAGE = 'Click the following link to reset your password', 10 | RESET_PASSWORD_URL_TEXT = 'Reset password', 11 | 12 | RESET_PASSWORD_SUCCESS_SUBJECT = 'Your password has been reset', 13 | RESET_PASSWORD_SUCCESS_TITLE = 'Password reset successfully', 14 | RESET_PASSWORD_SUCCESS_MESSAGE = 'Your password has been reset', 15 | 16 | EMAIL_VERIFICATION_SUBJECT = 'Verify your email', 17 | EMAIL_VERIFICATION_TITLE = 'Verify your email', 18 | EMAIL_VERIFICATION_MESSAGE = 'Verify your email', 19 | 20 | EMAIL_UPDATE_SUBJECT = 'Confirmation: Update your email', 21 | EMAIL_UPDATE_TITLE = 'Confirm your new email', 22 | EMAIL_UPDATE_MESSAGE = 'Click the following link to confirm your new email', 23 | EMAIL_UPDATE_URL_TEXT = 'Confirm new email', 24 | 25 | USER_DELETE_SUBJECT = 'Confirmation of your account deletion', 26 | USER_DELETE_TITLE = 'Your account has been deleted', 27 | USER_DELETE_MESSAGE = 'Your account has been permanently deleted, and all your data has been removed from our servers.', 28 | } 29 | 30 | export default MailerDefaults 31 | -------------------------------------------------------------------------------- /templates/vue/app/Exceptions/Handler.txt: -------------------------------------------------------------------------------- 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 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 19 | import FlashMessages from 'App/Enums/FlashMessage' 20 | import flowConfig from 'Config/flow' 21 | 22 | const { LoginFail } = FlashMessages 23 | const { errors } = flowConfig 24 | 25 | export default class ExceptionHandler extends HttpExceptionHandler { 26 | private customErrors: string[] = Object.keys(errors) 27 | 28 | protected disableStatusPagesInDevelopment = false 29 | protected statusPages = { 30 | '403': 'errors/unauthorized', 31 | '404': 'errors/not-found', 32 | '500..599': 'errors/server-error', 33 | } 34 | 35 | constructor() { 36 | super(Logger) 37 | } 38 | 39 | public async handle(error: any, ctx: HttpContextContract) { 40 | /** 41 | * Self handle the validation exception 42 | */ 43 | if (error.code === 'E_INVALID_AUTH_UID' || error.code === 'E_INVALID_AUTH_PASSWORD') { 44 | ctx.session.flash('errors', [LoginFail]) 45 | return ctx.response.redirect().back() 46 | } 47 | 48 | if (error.code === 'E_VALIDATION_FAILURE') { 49 | ctx.session.flash('errors', error.messages) 50 | } 51 | 52 | if (this.customErrors.includes(error.code)) { 53 | const { title, message } = errors[error.code] 54 | return ctx.response.redirect().withQs({ title, message, code: error.code }).toRoute('error') 55 | } 56 | 57 | /** 58 | * Forward rest of the exceptions to the parent class 59 | */ 60 | return super.handle(error, ctx) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /templates/vue/app/Middleware/ConfirmPassword.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Encryption from '@ioc:Adonis/Core/Encryption' 3 | import { DateTime } from 'luxon' 4 | import flowConfig from 'Config/flow' 5 | 6 | const { passwordConfirmation } = flowConfig.features 7 | export default class ConfirmPassword { 8 | public async handle( 9 | { request, response, session }: HttpContextContract, 10 | next: () => Promise 11 | ) { 12 | const lastVerifiedAt = session.get('password-confirmed') 13 | const fromUrl = request 14 | .header('referer') 15 | ?.replace('http://', '') 16 | .replace('https://', '') 17 | .replace(request.header('host')!, '') 18 | 19 | const runRedirect = () => 20 | response.redirect().toRoute('password.confirm', [Encryption.encrypt(request.url())], { 21 | qs: { fromUrl }, 22 | }) 23 | 24 | if (!lastVerifiedAt) { 25 | return runRedirect() 26 | } 27 | 28 | const diff = DateTime.now().diff(DateTime.fromISO(lastVerifiedAt)) 29 | if (diff.minutes > passwordConfirmation) { 30 | return runRedirect() 31 | } 32 | 33 | await next() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/vue/app/Middleware/Guest.txt: -------------------------------------------------------------------------------- 1 | import type { 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('dashboard.index') 7 | } 8 | await next() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/vue/app/Middleware/VerificationCheck.txt: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import FlashMessages from 'App/Enums/FlashMessage' 4 | import { Exception } from '@adonisjs/core/build/standalone' 5 | 6 | const { EmailNotVerified } = FlashMessages 7 | 8 | export default class VerificationCheck { 9 | public async handle({ request, auth }: HttpContextContract, next: () => Promise) { 10 | if (!auth.isLoggedIn) { 11 | const user = await User.findByOrFail('email', request.input('email')) 12 | if (!user.isVerified) { 13 | throw new Exception(EmailNotVerified, 403, 'E_VERIFICATION_REQUIRED') 14 | } 15 | } 16 | 17 | if (auth.user?.isVerified === false) { 18 | throw new Exception(EmailNotVerified, 403, 'E_VERIFICATION_REQUIRED') 19 | } 20 | 21 | await next() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/vue/app/Models/ApiToken.txt: -------------------------------------------------------------------------------- 1 | // app/Models/UserSession.ts 2 | 3 | import { DateTime } from 'luxon' 4 | import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' 5 | 6 | export default class ApiToken extends BaseModel { 7 | @column({ isPrimary: true }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @column() 14 | public name: string 15 | 16 | @column() 17 | public type: string 18 | 19 | @column() 20 | public token: string 21 | 22 | @column.dateTime() 23 | public expiresAt: DateTime 24 | 25 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 26 | public updatedAt: DateTime 27 | 28 | @column.dateTime({ autoCreate: true, autoUpdate: false }) 29 | public createdAt: DateTime 30 | } 31 | -------------------------------------------------------------------------------- /templates/vue/app/Models/PasswordReset.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column, belongsTo, BelongsTo, computed } from '@ioc:Adonis/Lucid/Orm' 3 | import { string } from '@ioc:Adonis/Core/Helpers' 4 | import User from 'App/Models/User' 5 | import Encryption from '@ioc:Adonis/Core/Encryption' 6 | export default class PasswordReset extends BaseModel { 7 | @column({ isPrimary: true }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @column() 14 | public token: string 15 | 16 | @column.dateTime({ autoCreate: true }) 17 | public createdAt: DateTime 18 | 19 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 20 | public updatedAt: DateTime 21 | 22 | @column.dateTime() 23 | public expiresAt: DateTime 24 | 25 | @belongsTo(() => User) 26 | public user: BelongsTo 27 | 28 | @computed() 29 | public isExpired(): boolean { 30 | return this.expiresAt.toMillis() < DateTime.local().toMillis() 31 | } 32 | 33 | public async generateToken(): Promise { 34 | this.token = Encryption.encrypt(string.generateRandom(40)) 35 | this.expiresAt = DateTime.local().plus({ hours: 2 }) 36 | } 37 | 38 | public async useToken(): Promise { 39 | this.expiresAt = DateTime.local().minus({ hours: 1 }) 40 | } 41 | 42 | public async verifyToken(token: string): Promise { 43 | return Encryption.decrypt(token) === Encryption.decrypt(this.token) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/vue/app/Models/User.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { 3 | BaseModel, 4 | column, 5 | beforeSave, 6 | beforeCreate, 7 | afterCreate, 8 | computed, 9 | hasOne, 10 | HasOne, 11 | hasMany, 12 | HasMany, 13 | } from '@ioc:Adonis/Lucid/Orm' 14 | import Hash from '@ioc:Adonis/Core/Hash' 15 | import Encryption from '@ioc:Adonis/Core/Encryption' 16 | import { string } from '@ioc:Adonis/Core/Helpers' 17 | import UserProfile from 'App/Models/UserProfile' 18 | import flowConfig from 'Config/flow' 19 | import UserSession from 'App/Models/UserSession' 20 | 21 | export default class User extends BaseModel { 22 | @column({ isPrimary: true }) 23 | public id: number 24 | 25 | @column() 26 | public email: string 27 | 28 | @column({ serializeAs: null }) 29 | public password: string 30 | 31 | @column() 32 | public rememberMeToken?: string 33 | 34 | @column({ serializeAs: null }) 35 | public emailVerificationToken: string | null 36 | 37 | @column({ serializeAs: null }) 38 | public emailResetRequest: string | null 39 | 40 | @column.dateTime() 41 | public emailVerifiedAt?: DateTime 42 | 43 | @column.dateTime({ autoCreate: true }) 44 | public createdAt: DateTime 45 | 46 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 47 | public updatedAt: DateTime 48 | 49 | @computed() 50 | public get isVerified() { 51 | if (!!this.emailVerifiedAt) { 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | /* 58 | / Relationships 59 | */ 60 | 61 | @hasOne(() => UserProfile) 62 | public profile: HasOne 63 | 64 | @hasMany(() => UserSession) 65 | public sessions: HasMany 66 | 67 | /* 68 | / Hooks 69 | */ 70 | @beforeCreate() 71 | public static generateVerificationToken(user: User) { 72 | if (flowConfig.features.verification === 'strict') { 73 | user.generateVerificationToken() 74 | } 75 | } 76 | 77 | @afterCreate() 78 | public static async createProfile(user: User) { 79 | const profile = await user.related('profile').create({ 80 | userId: user.id, 81 | }) 82 | await profile.save() 83 | } 84 | 85 | @beforeSave() 86 | public static async hashPassword(user: User) { 87 | if (user.$dirty.password) { 88 | user.password = await Hash.make(user.password) 89 | } 90 | } 91 | 92 | /* 93 | / Methods 94 | */ 95 | 96 | public verifyEmail(token: string): boolean { 97 | if (Encryption.decrypt(token) === Encryption.decrypt(this.emailVerificationToken ?? '')) { 98 | this.emailVerifiedAt = DateTime.now() 99 | this.emailVerificationToken = null 100 | return true 101 | } 102 | return false 103 | } 104 | 105 | public generateVerificationToken() { 106 | this.emailVerificationToken = Encryption.encrypt(string.generateRandom(40)) 107 | } 108 | 109 | public async newEmailResetRequest(email: string) { 110 | return (this.emailResetRequest = Encryption.encrypt(email, '2 hours')) 111 | } 112 | 113 | public async updateEmail(token: string) { 114 | const newEmail = Encryption.decrypt(token ?? '') 115 | if (!newEmail) { 116 | return false 117 | } 118 | this.email = newEmail! 119 | this.emailResetRequest = null 120 | this.emailVerifiedAt = DateTime.now() 121 | return true 122 | } 123 | 124 | public async verifyPassword(password: string): Promise { 125 | return Hash.verify(this.password, password) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /templates/vue/app/Models/UserProfile.txt: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column, computed } from '@ioc:Adonis/Lucid/Orm' 3 | import { attachment, AttachmentContract } from '@ioc:Adonis/Addons/AttachmentLite' 4 | export default class UserProfile extends BaseModel { 5 | public static routeLookupKey = 'username' 6 | 7 | @column({ isPrimary: true, serializeAs: null }) 8 | public id: number 9 | 10 | @column() 11 | public userId: number 12 | 13 | @attachment({ folder: 'avatars', preComputeUrl: true }) 14 | public avatar: AttachmentContract | null 15 | 16 | @column() 17 | public firstName?: string | null 18 | 19 | @column() 20 | public lastName?: string | null 21 | 22 | @computed() 23 | public get fullName() { 24 | return `${this.firstName} ${this.lastName}` 25 | } 26 | 27 | @column.dateTime({ autoCreate: true }) 28 | public createdAt: DateTime 29 | 30 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 31 | public updatedAt: DateTime 32 | } 33 | -------------------------------------------------------------------------------- /templates/vue/app/Models/UserSession.txt: -------------------------------------------------------------------------------- 1 | // app/Models/UserSession.ts 2 | 3 | import { DateTime } from 'luxon' 4 | import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' 5 | import Encryption from '@ioc:Adonis/Core/Encryption' 6 | import type { UAParser } from 'ua-parser-js' 7 | export default class UserSession extends BaseModel { 8 | @column({ isPrimary: true }) 9 | public id: string 10 | 11 | @column() 12 | public userId: number | null 13 | 14 | @column() 15 | public ipAddress: string 16 | 17 | @column() 18 | public os: UAParser.IOS 19 | 20 | @column() 21 | public device: UAParser.IDevice 22 | 23 | @column() 24 | public browser: UAParser.IBrowser 25 | 26 | @column() 27 | public country: string | null 28 | 29 | @column({ 30 | prepare: (value: string) => Encryption.encrypt(JSON.stringify(value)), 31 | consume: (value: string) => JSON.parse(Encryption.decrypt(value) as string), 32 | }) 33 | public payload: string | object 34 | 35 | @column() 36 | public lastActivityAt: DateTime 37 | 38 | @column.dateTime({ autoCreate: true }) 39 | public createdAt: DateTime 40 | 41 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 42 | public updatedAt: DateTime 43 | } 44 | -------------------------------------------------------------------------------- /templates/vue/app/Providers/ValidationRulesProvider.txt: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import flowConfig from 'Config/flow' 3 | 4 | export default class ValidationRulesProvider { 5 | private email = { 6 | required: schema.string({ trim: true }, [ 7 | rules.email(), 8 | rules.unique({ table: 'users', column: 'email' }), 9 | ]), 10 | optional: schema.string.optional({ trim: true }, [ 11 | rules.email(), 12 | rules.unique({ table: 'users', column: 'email' }), 13 | ]), 14 | notUnique: schema.string({ trim: true }, [rules.email()]), 15 | } 16 | 17 | private password = { 18 | requiredConfirmed: schema.string({}, [ 19 | rules.minLength(8), 20 | rules.alphaNum(), 21 | rules.trim(), 22 | rules.confirmed(), 23 | ]), 24 | required: schema.string({}, [rules.minLength(8), rules.alphaNum(), rules.trim()]), 25 | } 26 | 27 | protected registerRules() { 28 | if (flowConfig.features.termsAndPrivacyPolicy) { 29 | return { 30 | email: this.email.required, 31 | password: this.password.requiredConfirmed, 32 | terms: schema.array([rules.maxLength(1), rules.minLength(1)]).members(schema.boolean()), 33 | } 34 | } else { 35 | return { 36 | email: this.email.required, 37 | password: this.password.requiredConfirmed, 38 | terms: schema.boolean.nullableAndOptional(), 39 | } 40 | } 41 | } 42 | 43 | protected passwordResetRules() { 44 | return { 45 | password: this.password.required, 46 | } 47 | } 48 | 49 | protected passwordUpdateRules() { 50 | return { 51 | currentPassword: schema.string({ trim: true }, []), 52 | password: this.password.required, 53 | } 54 | } 55 | 56 | protected profileUpdateRules() { 57 | return { 58 | firstName: schema.string({ trim: true }, [rules.maxLength(255)]), 59 | lastName: schema.string({ trim: true }, [rules.maxLength(255)]), 60 | email: this.email.notUnique, 61 | } 62 | } 63 | 64 | protected emailVerificationRules() { 65 | return { 66 | email: this.email.notUnique, 67 | } 68 | } 69 | 70 | protected apiTokenRules() { 71 | return { 72 | name: schema.string({ trim: true }, [rules.maxLength(255)]), 73 | expiresIn: schema.enum(['1 day', '7 days', '30 days', '90 days', null] as const), 74 | } 75 | } 76 | 77 | // Messages 78 | 79 | public messages = { 80 | 'email.required': 'Email is required', 81 | 'email.email': 'Email is not valid', 82 | 'password.required': 'Password is required', 83 | 'password.minLength': 'Password must be at least 8 characters long', 84 | 'unique': '{{ field }} is not available', 85 | 'confirmed': 'Passwords does not match', 86 | 'email.unique': 'Email is already in use', 87 | 'terms.required': 'You must agree to the terms and conditions', 88 | 'terms.minLength': 'You must agree to the terms and conditions', 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /templates/vue/app/Validators/Auth/EmailVerificationValidator.txt: -------------------------------------------------------------------------------- 1 | // app/Validators/RegisterValidator.ts 2 | 3 | import { schema } from '@ioc:Adonis/Core/Validator' 4 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 5 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 6 | export default class EmailVerificationValidator extends ValidationRulesProvider { 7 | constructor(protected ctx: HttpContextContract) { 8 | super() 9 | } 10 | 11 | public schema = schema.create({ 12 | ...this.emailVerificationRules(), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /templates/vue/app/Validators/Auth/RegisterValidator.txt: -------------------------------------------------------------------------------- 1 | // app/Validators/RegisterValidator.ts 2 | 3 | import { schema } from '@ioc:Adonis/Core/Validator' 4 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 5 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 6 | export default class RegisterValidator extends ValidationRulesProvider { 7 | constructor(protected ctx: HttpContextContract) { 8 | super() 9 | } 10 | 11 | public schema = schema.create({ 12 | ...this.registerRules(), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /templates/vue/app/Validators/User/PasswordUpdateValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class PasswordUpdateValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.passwordUpdateRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/vue/app/Validators/User/PasswordValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class PasswordValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.passwordResetRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/vue/app/Validators/User/ProfileValidator.txt: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | import ValidationRulesProvider from 'App/Providers/ValidationRulesProvider' 4 | 5 | export default class ProfileValidator extends ValidationRulesProvider { 6 | constructor(protected ctx: HttpContextContract) { 7 | super() 8 | } 9 | 10 | public schema = schema.create({ 11 | ...this.profileUpdateRules(), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /templates/vue/config/flow.txt: -------------------------------------------------------------------------------- 1 | import type { TFlowConfig } from 'Types/*' 2 | 3 | const flowConfig: TFlowConfig = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Features config 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Configure optional features for AdonisJs Flow 10 | | 11 | */ 12 | features: { 13 | termsAndPrivacyPolicy: true, 14 | verification: 'strict', 15 | passwordConfirmation: 5, 16 | }, 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Views Config 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The views config is to handle all your view template file locations 23 | | in one convenient place, without needing to go digging inside the controllers, providers etc.to make 24 | | small updates 25 | | 26 | */ 27 | views: { 28 | login: 'Login', 29 | register: 'Register', 30 | passwordResetRequest: 'Password-reset', 31 | passwordUpdate: 'Password-update', 32 | verification: 'Verification', 33 | errorPage: 'Error', 34 | confirmPassword: 'auth/confirm-password', 35 | dashboard: 'Dashboard/Index', 36 | profile: 'Dashboard/Profile', 37 | }, 38 | errors: { 39 | E_INVALID_URL: { 40 | title: 'Invalid URL', 41 | message: 42 | 'This URL is no longer valid or has expired, if this problem persists please contact support.', 43 | }, 44 | E_VERIFICATION_REQUIRED: { 45 | title: 'Account verification required', 46 | message: 47 | 'Your account has not yet been verified, please verify your account before trying to login.', 48 | }, 49 | }, 50 | } 51 | 52 | export default flowConfig 53 | -------------------------------------------------------------------------------- /templates/vue/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 PasswordReset from 'App/Models/PasswordReset' 9 | import type User from 'App/Models/User' 10 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 11 | 12 | declare module '@ioc:Adonis/Core/Event' { 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Define typed events 16 | |-------------------------------------------------------------------------- 17 | | 18 | | You can define types for events inside the following interface and 19 | | AdonisJS will make sure that all listeners and emit calls adheres 20 | | to the defined types. 21 | | 22 | | For example: 23 | | 24 | | interface EventsList { 25 | | 'new:user': UserModel 26 | | } 27 | | 28 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 29 | | an instance of the the UserModel only. 30 | | 31 | */ 32 | interface EventsList { 33 | 'user:register': User 34 | 'user:login': { user: User; ctx: HttpContextContract } 35 | 'user:logout': { ctx: HttpContextContract } 36 | 'user:resetPasswordRequest': { user: User; token: string } 37 | 'user:resetPassword': { user: User; passwordReset: PasswordReset } 38 | 'user:delete': User 39 | 'user:emailReset': User 40 | 'mail:sendEmailVerification': User 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /templates/vue/database/migrations/1697024229003_users.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class 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('email', 255).notNullable().unique().index() 10 | table.string('password', 180).notNullable() 11 | table.string('remember_me_token').nullable() 12 | table.timestamp('email_verified_at').nullable() 13 | table.string('email_verification_token').nullable().unique() 14 | table.string('email_reset_request').nullable().unique() 15 | 16 | /** 17 | * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL 18 | */ 19 | table.timestamp('created_at', { useTz: true }) 20 | table.timestamp('updated_at', { useTz: true }) 21 | }) 22 | } 23 | 24 | public async down() { 25 | this.schema.dropTable(this.tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/vue/database/migrations/1697025049030_password_resets.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'password_resets' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table 10 | .integer('user_id') 11 | .unsigned() 12 | .unique() 13 | .notNullable() 14 | .references('id') 15 | .inTable('users') 16 | .onDelete('CASCADE') 17 | table.string('token').notNullable().index() 18 | table.timestamp('created_at', { useTz: true }) 19 | table.timestamp('updated_at', { useTz: true }) 20 | table.timestamp('expires_at', { useTz: true }) 21 | }) 22 | } 23 | 24 | public async down() { 25 | this.schema.dropTable(this.tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/vue/database/migrations/1697025542544_user_sessions.txt: -------------------------------------------------------------------------------- 1 | // database/migrations/xxxx_xx_xx_xxxxxx_create_user_sessions_table.ts 2 | 3 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 4 | 5 | export default class CreateUserSessionsTable extends BaseSchema { 6 | protected tableName = 'user_sessions' 7 | 8 | public async up() { 9 | this.schema.createTable(this.tableName, (table) => { 10 | table.string('id').primary() 11 | table 12 | .integer('user_id') 13 | .unsigned() 14 | .nullable() 15 | .references('id') 16 | .inTable('users') 17 | .onDelete('CASCADE') 18 | .index() 19 | table.string('ip_address').nullable() 20 | table.string('os').nullable() 21 | table.string('device').nullable() 22 | table.string('browser').nullable() 23 | table.string('country').nullable() 24 | table.text('payload') 25 | table.timestamp('last_activity_at', { useTz: true }) 26 | table.timestamp('created_at', { useTz: true }) 27 | table.timestamp('updated_at', { useTz: true }) 28 | }) 29 | } 30 | 31 | public async down() { 32 | this.schema.dropTable(this.tableName) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/vue/database/migrations/1697027522393_api_tokens.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'api_tokens' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') 10 | table.string('name').notNullable() 11 | table.string('type').notNullable() 12 | table.string('token', 64).notNullable().unique() 13 | 14 | /** 15 | * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL 16 | */ 17 | table.timestamp('expires_at', { useTz: true }).nullable() 18 | table.timestamp('created_at', { useTz: true }).notNullable() 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/vue/database/migrations/1697733327219_user_profiles.txt: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | export default class extends BaseSchema { 3 | protected tableName = 'user_profiles' 4 | 5 | public async up() { 6 | this.schema.createTable(this.tableName, (table) => { 7 | table.increments('id') 8 | table 9 | .integer('user_id') 10 | .unique() 11 | .references('id') 12 | .inTable('users') 13 | .onDelete('CASCADE') 14 | .index() 15 | table.json('avatar') 16 | table.string('first_name').nullable().defaultTo(null) 17 | table.string('last_name').nullable().defaultTo(null) 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/vue/env.txt: -------------------------------------------------------------------------------- 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 | APP_KEY: Env.schema.string(), 21 | APP_URL: Env.schema.string(), 22 | CACHE_VIEWS: Env.schema.boolean(), 23 | SESSION_DRIVER: Env.schema.string(), 24 | DRIVE_DISK: Env.schema.enum(['local'] as const), 25 | NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), 26 | DB_CONNECTION: Env.schema.string(), 27 | PG_HOST: Env.schema.string({ format: 'host' }), 28 | PG_PORT: Env.schema.number(), 29 | PG_USER: Env.schema.string(), 30 | PG_PASSWORD: Env.schema.string.optional(), 31 | PG_DB_NAME: Env.schema.string(), 32 | GITHUB_CLIENT_ID: Env.schema.string(), 33 | GITHUB_CLIENT_SECRET: Env.schema.string(), 34 | GOOGLE_CLIENT_ID: Env.schema.string(), 35 | GOOGLE_CLIENT_SECRET: Env.schema.string(), 36 | LINKEDIN_CLIENT_ID: Env.schema.string(), 37 | LINKEDIN_CLIENT_SECRET: Env.schema.string(), 38 | SMTP_HOST: Env.schema.string({ format: 'host' }), 39 | SMTP_PORT: Env.schema.number(), 40 | SMTP_USERNAME: Env.schema.string(), 41 | SMTP_PASSWORD: Env.schema.string(), 42 | }) 43 | -------------------------------------------------------------------------------- /templates/vue/providers/AppProvider.txt: -------------------------------------------------------------------------------- 1 | import type { 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 { DatabaseDriver } = await import('./SessionDriver') 12 | const Session = this.app.container.use('Adonis/Addons/Session') 13 | 14 | Session.extend('database', ({}, config, ctx) => { 15 | return new DatabaseDriver(ctx, config) 16 | }) 17 | } 18 | 19 | public async ready() { 20 | // App is ready 21 | } 22 | 23 | public async shutdown() { 24 | // Cleanup, since app is going down 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /templates/vue/providers/SessionDriver/index.txt: -------------------------------------------------------------------------------- 1 | import { SessionConfig, SessionDriverContract } from '@ioc:Adonis/Addons/Session' 2 | import UserSession from 'App/Models/UserSession' 3 | import { DateTime } from 'luxon' 4 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 5 | import ms from 'ms' 6 | import { UAParser } from 'ua-parser-js' 7 | export class DatabaseDriver implements SessionDriverContract { 8 | private config: SessionConfig 9 | constructor( 10 | protected ctx: HttpContextContract, 11 | config: SessionConfig 12 | ) { 13 | this.config = config 14 | } 15 | 16 | public async read(sessionId: string) { 17 | const session = await UserSession.find(sessionId) 18 | if (await this.expired(session as UserSession)) { 19 | return null 20 | } 21 | return session?.serialize().payload 22 | } 23 | 24 | public async write(sessionId: string, values: Record) { 25 | const parser = new UAParser(this.ctx.request.header('user-agent')) 26 | await UserSession.updateOrCreate( 27 | { id: sessionId }, 28 | { 29 | userId: this.ctx.auth.user?.id, 30 | ipAddress: this.ctx.request.ip(), 31 | browser: parser.getBrowser(), 32 | os: parser.getOS(), 33 | device: parser.getDevice(), 34 | lastActivityAt: DateTime.now(), 35 | payload: values, 36 | } 37 | ) 38 | } 39 | 40 | public async destroy(sessionId: string) { 41 | const session = await UserSession.find(sessionId) 42 | await session?.delete() 43 | } 44 | 45 | public async touch() {} 46 | 47 | protected async expired(session: UserSession) { 48 | return ( 49 | session?.lastActivityAt && 50 | ms(this.config.age) < DateTime.now().diff(session.lastActivityAt).milliseconds 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /templates/vue/resources/css/app.txt: -------------------------------------------------------------------------------- 1 | @layer tailwind-base, primevue, tailwind-utilities; 2 | 3 | @layer tailwind-base { 4 | @tailwind base; 5 | } 6 | 7 | @layer tailwind-utilities { 8 | @tailwind components; 9 | @tailwind utilities; 10 | } 11 | 12 | @layer utilities { 13 | .hide-before { 14 | @apply before:!opacity-0; 15 | } 16 | } 17 | 18 | @layer components { 19 | .page-content { 20 | @apply py-6 flex-grow; 21 | } 22 | 23 | .link { 24 | @apply text-zinc-600 dark:text-zinc-300 hover:underline underline-offset-2 cursor-pointer inline-block; 25 | } 26 | 27 | .input-wrapper { 28 | @apply flex flex-col gap-1; 29 | } 30 | 31 | .checkbox-wrapper { 32 | @apply flex flex-row items-center; 33 | } 34 | 35 | .input-wrapper label, 36 | .checkbox-wrapper label { 37 | @apply text-sm font-medium; 38 | } 39 | 40 | .main-nav { 41 | @apply flex flex-row items-center w-full py-4 gap-4; 42 | } 43 | 44 | .main-nav ul { 45 | @apply mr-auto ml-8; 46 | } 47 | 48 | .centered-content { 49 | @apply flex flex-col items-center justify-center gap-4 md:min-w-[384px] m-auto; 50 | } 51 | } 52 | 53 | /* Initial Values */ 54 | :root { 55 | --primary-50: 236 238 255; 56 | --primary-100: 221 224 255; 57 | --primary-200: 194 199 255; 58 | --primary-300: 156 161 255; 59 | --primary-400: 122 117 255; 60 | --primary-500: 90 69 255; 61 | --primary-600: 91 54 245; 62 | --primary-700: 78 42 216; 63 | --primary-800: 63 37 174; 64 | --primary-900: 54 38 137; 65 | --primary-950: 34 22 80; 66 | --surface-0: 255 255 255; 67 | --surface-50: 202 207 216; 68 | --surface-100: 188 190 200; 69 | --surface-200: 164 167 183; 70 | --surface-300: 128 132 153; 71 | --surface-400: 89 92 110; 72 | --surface-500: 65 67 83; 73 | --surface-600: 49 50 63; 74 | --surface-700: 39 40 48; 75 | --surface-800: 30 30 36; 76 | --surface-900: 24 24 27; 77 | --surface-950: 9 9 11; 78 | } 79 | 80 | #app { 81 | @apply min-h-screen mx-auto container flex flex-col px-4; 82 | } 83 | 84 | #profile-overlay { 85 | @apply hide-before; 86 | } 87 | -------------------------------------------------------------------------------- /templates/vue/resources/js/app.txt: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from 'vue' 2 | import { createApp, h } from 'vue' 3 | import { createInertiaApp } from '@inertiajs/vue3' 4 | import { Link } from '@inertiajs/vue3' 5 | import PrimeVue from 'primevue/config' 6 | 7 | // CSS 8 | import 'primeicons/primeicons.css' 9 | import '../css/app.css' 10 | 11 | // Layouts 12 | import Theme from 'Theme/' 13 | import DefaultLayout from 'Layouts/Default.vue' 14 | 15 | // Prime Components 16 | import Button from 'primevue/button' 17 | import InputText from 'primevue/inputtext' 18 | import Menubar from 'primevue/menubar' 19 | import ToastService from 'primevue/toastservice' 20 | import Card from 'primevue/card' 21 | import Avatar from 'primevue/avatar' 22 | import Menu from 'primevue/menu' 23 | import OverlayPanel from 'primevue/overlaypanel' 24 | import Checkbox from 'primevue/checkbox' 25 | import Dialog from 'primevue/dialog' 26 | import FormInput from 'Components/UI/formInput.vue' 27 | import PageHeader from 'Components/UI/pageHeader.vue' 28 | import Dropdown from 'primevue/dropdown' 29 | 30 | createInertiaApp({ 31 | resolve: (name) => { 32 | const page: DefineComponent = require(`./src/Pages/${name}`).default 33 | if (!page.layout) { 34 | page.layout = DefaultLayout 35 | } 36 | return page 37 | }, 38 | setup({ el, App, props, plugin }) { 39 | createApp({ render: () => h(App, props) }) 40 | .use(plugin) 41 | .use(PrimeVue, { 42 | unstyled: true, 43 | pt: Theme, 44 | }) 45 | .use(ToastService) 46 | .component('Link', Link) 47 | .component('Button', Button) 48 | .component('Menubar', Menubar) 49 | .component('Card', Card) 50 | .component('Avatar', Avatar) 51 | .component('Menu', Menu) 52 | .component('OverlayPanel', OverlayPanel) 53 | .component('InputText', InputText) 54 | .component('Checkbox', Checkbox) 55 | .component('Dialog', Dialog) 56 | .component('Dropdown', Dropdown) 57 | .component('FormInput', FormInput) 58 | .component('PageHeader', PageHeader) 59 | .mount(el) 60 | }, 61 | }) 62 | -------------------------------------------------------------------------------- /templates/vue/resources/js/index.d.txt: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | declare module '*.js' 8 | 9 | declare module 'Theme/*' 10 | 11 | declare module 'Layouts/*' 12 | 13 | declare module 'Components/*' 14 | 15 | declare module 'Types/*' 16 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/EmailVerificationForm.txt: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/FlashMessages.txt: -------------------------------------------------------------------------------- 1 | 6 | 7 | 51 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/Navbar.txt: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/ProfileAvatarForm.txt: -------------------------------------------------------------------------------- 1 | 22 | 23 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/ProfileDeleteForm.txt: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/ProfileDetailsForm.txt: -------------------------------------------------------------------------------- 1 | 40 | 41 | 81 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/ProfilePasswordForm.txt: -------------------------------------------------------------------------------- 1 | 40 | 41 | 76 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Composites/ProfileSessionsForm.txt: -------------------------------------------------------------------------------- 1 | 63 | 64 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/Overlays/Terms.txt: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/UI/FormInput.txt: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/UI/Logo.txt: -------------------------------------------------------------------------------- 1 | 21 | 22 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/UI/PageHeader.txt: -------------------------------------------------------------------------------- 1 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/UI/ProfileMenu.txt: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Components/UI/UserAvatar.txt: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Layouts/Auth.txt: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Layouts/Default.txt: -------------------------------------------------------------------------------- 1 | 10 | 14 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Dashboard/Index.txt: -------------------------------------------------------------------------------- 1 | 2 | 18 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Error.txt: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Login.txt: -------------------------------------------------------------------------------- 1 | 41 | 42 | 60 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Password-reset.txt: -------------------------------------------------------------------------------- 1 | 35 | 36 | 66 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Password-update.txt: -------------------------------------------------------------------------------- 1 | 35 | 36 | 66 | -------------------------------------------------------------------------------- /templates/vue/resources/js/src/Pages/Verification.txt: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /templates/vue/resources/js/ssr.txt: -------------------------------------------------------------------------------- 1 | import { createSSRApp, h } from 'vue' 2 | import type { DefineComponent } from "vue"; 3 | import { renderToString } from '@vue/server-renderer' 4 | import { createInertiaApp } from '@inertiajs/vue3' 5 | export default function render(page) { 6 | return createInertiaApp({ 7 | page, 8 | render: renderToString, 9 | resolve: (name) => { 10 | const page : DefineComponent = require(`./Pages/${name}`) 11 | return page 12 | }, 13 | setup({ App, props, plugin }) { 14 | return createSSRApp({ 15 | render: () => h(App, props), 16 | }).use(plugin) 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/avatar/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props, parent }) => ({ 3 | class: [ 4 | // Font 5 | { 6 | 'text-xl': props.size == 'large', 7 | 'text-2xl': props.size == 'xlarge' 8 | }, 9 | 10 | // Alignments 11 | 'inline-flex items-center justify-center', 12 | 'relative', 13 | 14 | // Sizes 15 | { 16 | 'h-8 w-8': props.size == null || props.size == 'normal', 17 | 'w-12 h-12': props.size == 'large', 18 | 'w-16 h-16': props.size == 'xlarge' 19 | }, 20 | { '-ml-4': parent.instance.$style?.name == 'avatargroup' }, 21 | 22 | // Shapes 23 | { 24 | 'rounded-lg': props.shape == 'square', 25 | 'rounded-full': props.shape == 'circle' 26 | }, 27 | { 'border-2': parent.instance.$style?.name == 'avatargroup' }, 28 | 29 | // Colors 30 | 'bg-surface-700 dark:bg-surface-300', 31 | { 'border-white dark:border-surface-800': parent.instance.$style?.name == 'avatargroup' } 32 | ] 33 | }), 34 | image: { 35 | class: 'h-full w-full' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/card/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | //Shape 5 | 'rounded-md', 6 | 'shadow-sm', 7 | 8 | //Color 9 | 'bg-surface-0 dark:bg-surface-950', 10 | 'text-surface-700 dark:text-surface-0', 11 | 'border border-surface-50/50 dark:border-surface-700', 12 | ], 13 | }, 14 | body: { 15 | class: 'p-5', 16 | }, 17 | title: { 18 | class: 'text-2xl font-semibold mb-2', 19 | }, 20 | subtitle: { 21 | class: [ 22 | //Font 23 | 'font-normal', 24 | 25 | //Spacing 26 | 'mb-2', 27 | 28 | //Color 29 | 'text-surface-600 dark:text-surface-0/60', 30 | ], 31 | }, 32 | content: { 33 | class: 'py-5', // Vertical padding. 34 | }, 35 | footer: { 36 | class: 'pt-5', // Top padding. 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/checkbox/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | 'relative', 5 | 6 | // Alignment 7 | 'inline-flex', 8 | 'align-bottom', 9 | 10 | // Size 11 | 'w-5', 12 | 'h-5', 13 | 14 | // Misc 15 | 'cursor-pointer', 16 | 'select-none', 17 | ], 18 | }, 19 | input: ({ props, context }) => ({ 20 | class: [ 21 | // Alignment 22 | 'flex', 23 | 'items-center', 24 | 'justify-center', 25 | 26 | // Size 27 | 'w-5', 28 | 'h-5', 29 | 30 | // Shape 31 | 'rounded-md', 32 | 'border-2', 33 | 34 | // Colors 35 | 'text-surface-600', 36 | { 37 | 'border-surface-200 bg-surface-0 dark:border-surface-700 dark:bg-surface-900': 38 | !context.checked, 39 | 'border-surface-800 bg-surface-900 dark:border-surface-100 dark:bg-surface-50': 40 | context.checked, 41 | }, 42 | 43 | // States 44 | 'focus:outline-none focus:outline-offset-0', 45 | { 46 | 'hover:border-surface-500 dark:hover:border-surface-400': !props.disabled, 47 | 'ring-none': !props.disabled && context.focused, 48 | 'cursor-default opacity-60': props.disabled, 49 | }, 50 | 51 | // Transitions 52 | 'transition-colors', 53 | 'duration-200', 54 | ], 55 | }), 56 | icon: { 57 | class: [ 58 | // Font 59 | 'text-normal', 60 | 61 | // Size 62 | 'w-3', 63 | 'h-3', 64 | 65 | // Colors 66 | 'text-white dark:text-surface-900', 67 | 68 | // Transitions 69 | 'transition-all', 70 | 'duration-200', 71 | ], 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/global.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | css: ` 3 | *[data-pd-ripple="true"]{ 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | span[data-p-ink-active="true"]{ 8 | animation: ripple 0.4s linear; 9 | } 10 | @keyframes ripple { 11 | 100% { 12 | opacity: 0; 13 | transform: scale(2.5); 14 | } 15 | } 16 | 17 | .progress-spinner-circle { 18 | stroke-dasharray: 89, 200; 19 | stroke-dashoffset: 0; 20 | animation: p-progress-spinner-dash 1.5s ease-in-out infinite, p-progress-spinner-color 6s ease-in-out infinite; 21 | stroke-linecap: round; 22 | } 23 | 24 | @keyframes p-progress-spinner-dash{ 25 | 0% { 26 | stroke-dasharray: 1, 200; 27 | stroke-dashoffset: 0; 28 | } 29 | 30 | 50% { 31 | stroke-dasharray: 89, 200; 32 | stroke-dashoffset: -35px; 33 | } 34 | 100% { 35 | stroke-dasharray: 89, 200; 36 | stroke-dashoffset: -124px; 37 | } 38 | } 39 | @keyframes p-progress-spinner-color { 40 | 100%, 0% { 41 | stroke: #ff5757; 42 | } 43 | 40% { 44 | stroke: #696cff; 45 | } 46 | 66% { 47 | stroke: #1ea97c; 48 | } 49 | 80%, 90% { 50 | stroke: #cc8925; 51 | } 52 | } 53 | 54 | .progressbar-value-animate::after { 55 | will-change: left, right; 56 | animation: p-progressbar-indeterminate-anim-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 57 | } 58 | .progressbar-value-animate::before { 59 | will-change: left, right; 60 | animation: p-progressbar-indeterminate-anim 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 61 | } 62 | @keyframes p-progressbar-indeterminate-anim { 63 | 0% { 64 | left: -35%; 65 | right: 100%; 66 | } 67 | 60% { 68 | left: 100%; 69 | right: -90%; 70 | } 71 | 100% { 72 | left: 100%; 73 | right: -90%; 74 | } 75 | } 76 | 77 | .p-fadein { 78 | animation: p-fadein 250ms linear; 79 | } 80 | 81 | @keyframes p-fadein { 82 | 0% { 83 | opacity: 0; 84 | } 85 | 100% { 86 | opacity: 1; 87 | } 88 | } 89 | ` 90 | }; 91 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/index.txt: -------------------------------------------------------------------------------- 1 | import global from './global.js'; 2 | import button from './button'; 3 | import inputtext from './inputtext'; 4 | import menubar from './menubar'; 5 | import toast from './toast'; 6 | import card from './card'; 7 | import avatar from './avatar'; 8 | import menu from './menu'; 9 | import overlaypanel from './overlaypanel'; 10 | import checkbox from './checkbox'; 11 | import dialog from './dialog'; 12 | import dropdown from './dropdown'; 13 | 14 | export default { 15 | global, 16 | button, 17 | inputtext, 18 | menubar, 19 | toast, 20 | card, 21 | avatar, 22 | menu, 23 | overlaypanel, 24 | checkbox, 25 | dialog, 26 | dropdown 27 | } 28 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/inputtext/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props, context }) => ({ 3 | class: [ 4 | // Font 5 | 'font-sans leading-none text-sm', 6 | 7 | // Spacing 8 | 'm-0', 9 | { 10 | 'px-4 py-4': props.size == 'large', 11 | 'px-2 py-1.5': props.size == 'small', 12 | 'px-3 py-2': props.size == null, 13 | }, 14 | 15 | // Colors 16 | 'text-surface-600 dark:text-surface-200', 17 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500', 18 | 'bg-transparent', 19 | 'border border-surface-50/50 dark:border-surface-700', 20 | 21 | // States 22 | { 23 | 'focus-visible:outline-none focus-visible:outline-offset-0 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-surface-50 dark:focus-visible:ring-surface-900': 24 | !context.disabled, 25 | 'opacity-60 select-none pointer-events-none cursor-default': context.disabled, 26 | }, 27 | 28 | // Misc 29 | 'rounded-md', 30 | 'appearance-none', 31 | 'transition-colors duration-200', 32 | ], 33 | }), 34 | } 35 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/menu/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Sizing and Shape 5 | 'min-w-[12rem]', 6 | 'rounded-md', 7 | // Spacing 8 | 'p-0', 9 | // Colors 10 | 'bg-surface-0 dark:bg-surface-800', 11 | 'text-surface-700 dark:text-white/80', 12 | 'border border-surface-50/50 dark:border-surface-700' 13 | ] 14 | }, 15 | menu: { 16 | class: [ 17 | // Spacings and Shape 18 | 'list-none', 19 | 'm-0', 20 | 'p-0', 21 | 'outline-none' 22 | ] 23 | }, 24 | content: ({ context }) => ({ 25 | class: [ 26 | //Shape 27 | 'rounded-none', 28 | // Colors 29 | 'text-surface-700 dark:text-white/80', 30 | { 31 | 'bg-surface-200 text-surface-700 dark:bg-surface-300/10 dark:text-white': context.focused 32 | }, 33 | // Transitions 34 | 'transition-shadow', 35 | 'duration-200', 36 | // States 37 | 'hover:text-surface-700 dark:hover:text-white/80', 38 | 'hover:bg-surface-50/50 dark:bg-surface-700 dark:hover:bg-surface-400/30' 39 | ] 40 | }), 41 | action: { 42 | class: [ 43 | 'relative', 44 | // Flexbox 45 | 46 | 'flex', 47 | 'items-center', 48 | 49 | // Spacing 50 | 'py-3', 51 | 'px-5', 52 | 53 | // Color 54 | 'text-surface-700 dark:text-white/80', 55 | 56 | // Misc 57 | 'no-underline', 58 | 'overflow-hidden', 59 | 'cursor-pointer', 60 | 'select-none' 61 | ] 62 | }, 63 | icon: { 64 | class: [ 65 | // Spacing 66 | 'mr-2', 67 | 68 | // Color 69 | 'text-surface-600 dark:text-white/70' 70 | ] 71 | }, 72 | label: { 73 | class: ['leading-none'] 74 | }, 75 | submenuheader: { 76 | class: [ 77 | // Font 78 | 'font-bold', 79 | // Spacing 80 | 'm-0', 81 | 'py-3 px-5', 82 | // Shape 83 | 'rounded-tl-none', 84 | 'rounded-tr-none', 85 | // Colors 86 | 'bg-surface-0 dark:bg-surface-700', 87 | 'text-surface-700 dark:text-white' 88 | ] 89 | }, 90 | transition: { 91 | enterFromClass: 'opacity-0 scale-y-[0.8]', 92 | enterActiveClass: 'transition-[transform,opacity] duration-[120ms] ease-[cubic-bezier(0,0,0.2,1)]', 93 | leaveActiveClass: 'transition-opacity duration-100 ease-linear', 94 | leaveToClass: 'opacity-0' 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/overlaypanel/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Shape 5 | 'rounded-md shadow-lg', 6 | 'border-0 dark:border', 7 | 8 | // Position 9 | 'absolute left-0 top-0 mt-2', 10 | 'z-40 transform origin-center', 11 | 12 | // Color 13 | 'bg-surface-0 dark:bg-surface-800', 14 | 'text-surface-700 dark:text-surface-0/80', 15 | 'dark:border-surface-700', 16 | 17 | // Before: Triangle 18 | 'before:absolute before:-top-2 before:ml-4', 19 | 'before:w-0 before:h-0', 20 | 'before:border-transparent before:border-solid', 21 | 'before:border-x-[0.5rem] before:border-b-[0.5rem]', 22 | 'before:border-t-0 before:border-b-surface-0 dark:before:border-b-surface-800' 23 | ] 24 | }, 25 | content: { 26 | class: 'p-0 items-center flex' 27 | }, 28 | transition: { 29 | enterFromClass: 'opacity-0 scale-y-[0.8]', 30 | enterActiveClass: 'transition-[transform,opacity] duration-[120ms] ease-[cubic-bezier(0,0,0.2,1)]', 31 | leaveActiveClass: 'transition-opacity duration-100 ease-linear', 32 | leaveToClass: 'opacity-0' 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /templates/vue/resources/js/theme/toast/index.txt: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | //Size and Shape 5 | 'w-96 rounded-md', 6 | 7 | // Positioning 8 | { '-translate-x-2/4': props.position == 'top-center' || props.position == 'bottom-center' } 9 | ] 10 | }), 11 | container: ({ props }) => ({ 12 | class: [ 13 | 'my-4 rounded-md w-full', 14 | 'border-solid border-0 border-l-[6px]', 15 | 'backdrop-blur-[10px] shadow-md', 16 | 17 | // Colors 18 | { 19 | 'bg-blue-100/70 dark:bg-blue-500/20': props.message.severity == 'info', 20 | 'bg-green-100/70 dark:bg-green-500/20': props.message.severity == 'success', 21 | 'bg-orange-100/70 dark:bg-orange-500/20': props.message.severity == 'warn', 22 | 'bg-red-100/70 dark:bg-red-500/20': props.message.severity == 'error' 23 | }, 24 | { 25 | 'border-blue-500 dark:border-blue-400': props.message.severity == 'info', 26 | 'border-green-500 dark:border-green-400': props.message.severity == 'success', 27 | 'border-orange-500 dark:border-orange-400': props.message.severity == 'warn', 28 | 'border-red-500 dark:border-red-400': props.message.severity == 'error' 29 | }, 30 | { 31 | 'text-blue-700 dark:text-blue-300': props.message.severity == 'info', 32 | 'text-green-700 dark:text-green-300': props.message.severity == 'success', 33 | 'text-orange-700 dark:text-orange-300': props.message.severity == 'warn', 34 | 'text-red-700 dark:text-red-300': props.message.severity == 'error' 35 | } 36 | ] 37 | }), 38 | content: { 39 | class: 'flex items-start p-4' 40 | }, 41 | icon: { 42 | class: [ 43 | // Sizing and Spacing 44 | 'w-6 h-6', 45 | 'text-lg leading-none mr-2 shrink-0' 46 | ] 47 | }, 48 | text: { 49 | class: [ 50 | // Font and Text 51 | 'text-base leading-none', 52 | 'ml-2', 53 | 'flex-1' 54 | ] 55 | }, 56 | summary: { 57 | class: 'font-bold block' 58 | }, 59 | detail: { 60 | class: 'mt-2 block' 61 | }, 62 | closebutton: { 63 | class: [ 64 | // Flexbox 65 | 'flex items-center justify-center', 66 | 67 | // Size 68 | 'w-8 h-8', 69 | 70 | // Spacing and Misc 71 | 'ml-auto relative', 72 | 73 | // Shape 74 | 'rounded-full', 75 | 76 | // Colors 77 | 'bg-transparent', 78 | 79 | // Transitions 80 | 'transition duration-200 ease-in-out', 81 | 82 | // States 83 | 'hover:bg-surface-0/50 dark:hover:bg-surface-0/10', 84 | 85 | // Misc 86 | 'overflow-hidden' 87 | ] 88 | }, 89 | transition: { 90 | enterFromClass: 'opacity-0 translate-y-2/4', 91 | enterActiveClass: 'transition-[transform,opacity] duration-300', 92 | leaveFromClass: 'max-h-[1000px]', 93 | leaveActiveClass: '!transition-[max-height_.45s_cubic-bezier(0,1,0,1),opacity_.3s,margin-bottom_.3s] overflow-hidden', 94 | leaveToClass: 'max-h-0 opacity-0 mb-0' 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /templates/vue/resources/views/app.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @include('partials/meta') 6 | @inertiaHead 7 | 8 | 9 | 10 | @inertia 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/vue/resources/views/emails/confirmation_template/index.txt: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 | 3 |

{{ description }}

4 | 5 | 6 | If you have any questions, please contact us. 7 | -------------------------------------------------------------------------------- /templates/vue/resources/views/emails/confirmation_template/plain.txt: -------------------------------------------------------------------------------- 1 | {{ title }} 2 | 3 | {{ description }} 4 | 5 | If you have any questions, please contact us. -------------------------------------------------------------------------------- /templates/vue/resources/views/emails/verify_template/index.txt: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 | 3 |

{{ description }} {{ urlText }}

4 | 5 | 6 |

7 | If you did not request a {{ title }}, ignore this message. 8 |

-------------------------------------------------------------------------------- /templates/vue/resources/views/emails/verify_template/plain.txt: -------------------------------------------------------------------------------- 1 | {{ title }} 2 | 3 | {{ description }} {{ urlText }} 4 | 5 | If you did not request a {{ title }}, ignore this message. -------------------------------------------------------------------------------- /templates/vue/resources/views/emails/welcome/index.txt: -------------------------------------------------------------------------------- 1 |

Welcome to {{ env('APP_NAME')}}

2 | 3 |

Thank you for creating an account. Please verify your email using this link: Click here

-------------------------------------------------------------------------------- /templates/vue/resources/views/emails/welcome/plain.txt: -------------------------------------------------------------------------------- 1 | Welcome to {{ env('APP_NAME')}} 2 | 3 | Thank you for creating an account. Please verify your email using this link: Click here -------------------------------------------------------------------------------- /templates/vue/resources/views/errors/not-found.txt: -------------------------------------------------------------------------------- 1 | @section('body') 2 |
3 |

404

4 |

Page not found

5 | Back to dashboard 6 |
7 | @end -------------------------------------------------------------------------------- /templates/vue/resources/views/errors/server-error.txt: -------------------------------------------------------------------------------- 1 | @set('message', request.qs().message.split(':')) 2 | 3 | @section('body') 4 |
5 |

500

6 |

{{message[message.length - 1]}}

7 | Back to dashboard 8 |
9 | @end -------------------------------------------------------------------------------- /templates/vue/resources/views/errors/unauthorized.txt: -------------------------------------------------------------------------------- 1 | @section('body') 2 |
3 |

403

4 |

You are not authorized to access this page

5 | Back to dashboard 6 |
7 | @end -------------------------------------------------------------------------------- /templates/vue/resources/views/partials/meta.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @entryPointScripts('app') 6 | @entryPointStyles('app') 7 | {{ title ? title + ' | ' + capitalCase(sentenceCase(env('APP_NAME'))) : 8 | capitalCase(sentenceCase(env('APP_NAME')))}} 9 | -------------------------------------------------------------------------------- /templates/vue/start/events/auth.txt: -------------------------------------------------------------------------------- 1 | import Event from '@ioc:Adonis/Core/Event' 2 | import UserSession from 'App/Models/UserSession' 3 | import EmailSendingProvider from 'App/Providers/EmailSendingProvider' 4 | 5 | Event.on('user:register', (user) => { 6 | EmailSendingProvider.sendEmailVerificationLink(user) 7 | }) 8 | 9 | Event.on('user:login', ({}) => {}) 10 | 11 | Event.on('user:logout', ({}) => {}) 12 | 13 | Event.on('user:resetPasswordRequest', (data) => { 14 | EmailSendingProvider.sendPasswordResetLink(data) 15 | }) 16 | 17 | Event.on('user:resetPassword', (data) => { 18 | EmailSendingProvider.sendPasswordResetSuccess(data.user) 19 | data.passwordReset.useToken() 20 | data.passwordReset.save() 21 | UserSession.query().where('userId', data.user.id).delete() 22 | }) 23 | -------------------------------------------------------------------------------- /templates/vue/start/events/index.txt: -------------------------------------------------------------------------------- 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 Event from '@ioc:Adonis/Core/Event' 11 | import Mail from '@ioc:Adonis/Addons/Mail' 12 | import EmailSendingProvider from 'App/Providers/EmailSendingProvider' 13 | import './auth' 14 | 15 | Event.onError((event, error, eventData) => { 16 | console.log(event, error, eventData) 17 | }) 18 | 19 | // TODO log emails to database 20 | Event.on('mail:sent', Mail.prettyPrint) 21 | 22 | Event.on('mail:sendEmailVerification', (user) => { 23 | EmailSendingProvider.sendEmailVerificationLink(user) 24 | }) 25 | 26 | Event.on('user:delete', (user) => { 27 | EmailSendingProvider.sendAccountDeleteNotification(user) 28 | }) 29 | 30 | Event.on('user:emailReset', (user) => { 31 | EmailSendingProvider.sendNewEmailRequestConfirmation(user) 32 | }) 33 | -------------------------------------------------------------------------------- /templates/vue/start/routes/api-tokens.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.post('/', 'ApiTokensController.store').as('store') 5 | Route.delete('/:id', 'ApiTokensController.destroy').as('destroy') 6 | }) 7 | .namespace('App/Controllers/Http/User') 8 | .middleware('auth') 9 | .as('api-tokens') 10 | .prefix('dashboard/api-tokens') 11 | -------------------------------------------------------------------------------- /templates/vue/start/routes/auth.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | import flowConfig from 'Config/flow' 3 | 4 | Route.group(() => { 5 | Route.group(() => { 6 | Route.get('/logout', 'LoginController.destroy').as('login.destroy') 7 | }).middleware('auth') 8 | 9 | Route.group(() => { 10 | if (flowConfig.features.verification === 'strict') { 11 | Route.post('/login', 'LoginController.store').as('login.store').middleware('verified') 12 | } else { 13 | Route.post('/login', 'LoginController.store').as('login.store') 14 | } 15 | 16 | Route.get('/register', 'RegisterController.create').as('register.create') 17 | Route.post('/register', 'RegisterController.store').as('register.store') 18 | 19 | Route.get('/login', 'LoginController.create').as('login.create') 20 | 21 | Route.get('/forgot-password', 'PasswordResetRequestController.create').as( 22 | 'password.createReset' 23 | ) 24 | Route.post('/forgot-password', 'PasswordResetRequestController.store').as('password.storeReset') 25 | 26 | Route.get('/reset-password/:token', 'PasswordResetController.create').as( 27 | 'password.createPassword' 28 | ) 29 | Route.post('/reset-password/:token', 'PasswordResetController.store').as( 30 | 'password.updatePassword' 31 | ) 32 | }).middleware('guest') 33 | 34 | Route.get('/verification', 'RegisterController.createVerification').as('verification.create') 35 | Route.post('/verification', 'RegisterController.resendVerification').as('verification.store') 36 | Route.get('/verification/verify/:token', 'RegisterController.edit').as('verification.confirm') 37 | 38 | Route.get('/verification/password/:intended', 'PasswordConfirmationController.create').as( 39 | 'password.confirm' 40 | ) 41 | Route.post('/verification/password/:intended', 'PasswordConfirmationController.store').as( 42 | 'password.verify' 43 | ) 44 | }).namespace('App/Controllers/Http/Auth') 45 | -------------------------------------------------------------------------------- /templates/vue/start/routes/errors.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | import flowConfig from 'Config/flow' 3 | const { views } = flowConfig 4 | Route.get('/oops', async ({ request, inertia }) => { 5 | const message = request.qs().message || 'please try again later or contact support.' 6 | const code = request.qs().code 7 | return inertia.render(views.errorPage, { title: 'Oops, something went wrong', message, code }) 8 | }).as('error') 9 | -------------------------------------------------------------------------------- /templates/vue/start/routes/profile.txt: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | Route.group(() => { 3 | Route.group(() => { 4 | Route.get('/', 'ProfilesController.edit').as('profile.edit') 5 | Route.put('/', 'ProfilesController.update').as('profile.update') 6 | Route.put('/avatar', 'ProfilesController.updateProfileAvatar').as('profile.updateAvatar') 7 | Route.put('/password', 'UsersController.updatePassword').as('password.update') 8 | Route.delete('/avatar', 'ProfilesController.deleteProfileAvatar').as('profile.deleteAvatar') 9 | Route.delete('/', 'UsersController.destroy').as('user.destroy') 10 | Route.delete('/sessions/:id', 'ProfilesController.destroySession').as('destroySession') 11 | }).middleware('auth') 12 | 13 | Route.get('/:email', 'UsersController.confirmEmailUpdateRequest').as('user.newEmail') 14 | }) 15 | .namespace('App/Controllers/Http/User') 16 | .prefix('/dashboard/profile') 17 | -------------------------------------------------------------------------------- /templates/vue/tailwind.config.txt: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './resources/**/*.{edge,js,ts,jsx,tsx,vue}', 5 | './node_modules/primevue/**/*.{vue,js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'primary-50': 'rgb(var(--primary-50))', 11 | 'primary-100': 'rgb(var(--primary-100))', 12 | 'primary-200': 'rgb(var(--primary-200))', 13 | 'primary-300': 'rgb(var(--primary-300))', 14 | 'primary-400': 'rgb(var(--primary-400))', 15 | 'primary-500': 'rgb(var(--primary-500))', 16 | 'primary-600': 'rgb(var(--primary-600))', 17 | 'primary-700': 'rgb(var(--primary-700))', 18 | 'primary-800': 'rgb(var(--primary-800))', 19 | 'primary-900': 'rgb(var(--primary-900))', 20 | 'primary-950': 'rgb(var(--primary-950))', 21 | 'surface-0': 'rgb(var(--surface-0))', 22 | 'surface-50': 'rgb(var(--surface-50))', 23 | 'surface-100': 'rgb(var(--surface-100))', 24 | 'surface-200': 'rgb(var(--surface-200))', 25 | 'surface-300': 'rgb(var(--surface-300))', 26 | 'surface-400': 'rgb(var(--surface-400))', 27 | 'surface-500': 'rgb(var(--surface-500))', 28 | 'surface-600': 'rgb(var(--surface-600))', 29 | 'surface-700': 'rgb(var(--surface-700))', 30 | 'surface-800': 'rgb(var(--surface-800))', 31 | 'surface-900': 'rgb(var(--surface-900))', 32 | 'surface-950': 'rgb(var(--surface-950))', 33 | }, 34 | }, 35 | }, 36 | plugins: [require('@tailwindcss/typography')], 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "files": ["./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts"], 4 | "compilerOptions": { 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "skipLibCheck": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface ScaffoldOptions { 2 | stack: 'edge' | 'vue' 3 | } 4 | --------------------------------------------------------------------------------