├── .editorconfig ├── .env.example ├── .env.testing ├── .gitignore ├── README.md ├── ace ├── app ├── Controllers │ └── Http │ │ ├── PostController.js │ │ ├── SessionController.js │ │ └── UserController.js ├── Exceptions │ └── Handler.js ├── Middleware │ └── RedirectIfAuthenticated.js └── Models │ ├── Hooks │ └── User.js │ ├── Post.js │ └── User.js ├── config ├── app.js ├── auth.js ├── bodyParser.js ├── cors.js ├── database.js ├── session.js └── shield.js ├── database ├── factory.js └── migrations │ ├── 1503248427885_user.js │ └── 1507839709797_post_schema.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── resources ├── css │ └── app.css └── views │ ├── components │ ├── alert.edge │ ├── input.edge │ ├── label.edge │ ├── panel.edge │ └── textarea.edge │ ├── layout │ ├── app.edge │ └── partials │ │ ├── header.edge │ │ └── hero.edge │ ├── posts │ ├── create.edge │ ├── edit.edge │ ├── index.edge │ └── partials │ │ ├── form.edge │ │ └── post-card.edge │ ├── session │ └── create.edge │ └── user │ └── create.edge ├── server.js ├── start ├── app.js ├── kernel.js └── routes.js ├── tailwind.config.js ├── test └── functional │ ├── authentication.spec.js │ ├── delete-post.spec.js │ ├── read-post.spec.js │ ├── register.spec.js │ ├── update-post.spec.js │ └── write-post.spec.js └── vowfile.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOST=127.0.0.1 2 | PORT=3333 3 | NODE_ENV=development 4 | CACHE_VIEWS=false 5 | APP_KEY= 6 | 7 | DB_FILENAME=development.sqlite 8 | 9 | SESSION_DRIVER=cookie 10 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | HOST=127.0.0.1 2 | PORT=4000 3 | NODE_ENV=testing 4 | CACHE_VIEWS=false 5 | 6 | DB_FILENAME=testing.sqlite 7 | 8 | ENABLE_CSRF_CHECK=false 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | .env 4 | database/*.sqlite 5 | .cache 6 | 7 | # Don't push the public folder 8 | public 9 | 10 | # Keep only images 11 | !public/img 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adonis Blog Demo :triangular_ruler: 2 | 3 | This repo contains an example application of the Adonis Framework. You must checkout the source code or the API docs on official website to learn more. 4 | 5 |
6 |
7 |
8 | 9 | 10 | 11 | ## What's in the box? 12 | 13 | 1. Authentication System 14 | 2. Blogging System 15 | 3. API Testing 16 | 4. Browser Testing 17 | 5. Assets management 18 | 19 | ## How to run 20 | 21 | - Run `npm install` to install all dependencies 22 | - Make a copy of `.env.example` rename it to `.env` 23 | - Run `adonis key:generate` to generate the secret key 24 | - Run `adonis migration:run` to setup the database 25 | - Run `npm run build` to build static assets 26 | - Run `adonis serve --dev` to run the application 27 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Ace Commands 6 | |-------------------------------------------------------------------------- 7 | | 8 | | The ace file is just a regular Javascript file but with no extension. You 9 | | can call `node ace` followed by the command name and it just works. 10 | | 11 | | Also you can use `adonis` followed by the command name, since the adonis 12 | | global proxy all the ace commands. 13 | | 14 | */ 15 | 16 | const { Ignitor } = require('@adonisjs/ignitor') 17 | 18 | new Ignitor(require('@adonisjs/fold')) 19 | .appRoot(__dirname) 20 | .fireAce() 21 | .catch(console.error) 22 | -------------------------------------------------------------------------------- /app/Controllers/Http/PostController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Post = use('App/Models/Post') 4 | const { validateAll } = use('Validator') 5 | 6 | class PostController { 7 | async index ({ view }) { 8 | /** 9 | * Fetch all posts inside our database. 10 | * 11 | * ref: http://adonisjs.com/docs/4.1/lucid#_all 12 | */ 13 | const posts = await Post.all() 14 | 15 | /** 16 | * Render the view 'posts.index' 17 | * with the posts fetched as data. 18 | * 19 | * ref: http://adonisjs.com/docs/4.1/views 20 | */ 21 | return view.render('posts.index', { posts: posts.toJSON() }) 22 | } 23 | 24 | create ({ view }) { 25 | /** 26 | * Render the view 'posts.create'. 27 | * 28 | * ref: http://adonisjs.com/docs/4.1/views 29 | */ 30 | return view.render('posts.create') 31 | } 32 | 33 | async store ({ auth, session, request, response }) { 34 | /** 35 | * Getting needed parameters. 36 | * 37 | * ref: http://adonisjs.com/docs/4.1/request#_only 38 | */ 39 | const data = request.only(['title', 'body']) 40 | 41 | /** 42 | * Validating our data. 43 | * 44 | * ref: http://adonisjs.com/docs/4.1/validator 45 | */ 46 | const validation = await validateAll(data, { 47 | title: 'required', 48 | body: 'required', 49 | }) 50 | 51 | /** 52 | * If validation fails, early returns with validation message. 53 | */ 54 | if (validation.fails()) { 55 | session 56 | .withErrors(validation.messages()) 57 | .flashAll() 58 | 59 | return response.redirect('back') 60 | } 61 | 62 | /** 63 | * Creating a new post through the logged in user 64 | * into the database. 65 | * 66 | * ref: http://adonisjs.com/docs/4.1/lucid#_create 67 | */ 68 | const currentUser = await auth.getUser() 69 | await currentUser.posts().create(data) 70 | 71 | return response.redirect('/') 72 | } 73 | 74 | async edit ({ params, view }) { 75 | /** 76 | * Finding the post. 77 | * 78 | * ref: http://adonisjs.com/docs/4.1/lucid#_findorfail 79 | */ 80 | const post = await Post.findOrFail(params.id) 81 | 82 | return view.render('posts.edit', { post: post.toJSON() }) 83 | } 84 | 85 | async update ({ params, session, request, response }) { 86 | /** 87 | * Getting needed parameters. 88 | * 89 | * ref: http://adonisjs.com/docs/4.1/request#_only 90 | */ 91 | const data = request.only(['title', 'body']) 92 | 93 | /** 94 | * Validating our data. 95 | * 96 | * ref: http://adonisjs.com/docs/4.1/validator 97 | */ 98 | const validation = await validateAll(data, { 99 | title: 'required', 100 | body: 'required', 101 | }) 102 | 103 | /** 104 | * If validation fails, early returns with validation message. 105 | */ 106 | if (validation.fails()) { 107 | session 108 | .withErrors(validation.messages()) 109 | .flashAll() 110 | 111 | return response.redirect('back') 112 | } 113 | 114 | /** 115 | * Finding the post and updating fields on it 116 | * before saving it to the database. 117 | * 118 | * ref: http://adonisjs.com/docs/4.1/lucid#_inserts_updates 119 | */ 120 | const post = await Post.findOrFail(params.id) 121 | post.merge(data) 122 | await post.save() 123 | 124 | return response.redirect('/') 125 | } 126 | 127 | async delete ({ params, response }) { 128 | /** 129 | * Finding the post and deleting it 130 | * 131 | * ref: http://adonisjs.com/docs/4.1/lucid#_deletes 132 | */ 133 | const post = await Post.findOrFail(params.id) 134 | await post.delete() 135 | 136 | return response.redirect('/') 137 | } 138 | } 139 | 140 | module.exports = PostController 141 | -------------------------------------------------------------------------------- /app/Controllers/Http/SessionController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SessionController { 4 | create ({ view }) { 5 | /** 6 | * Render the view 'sessions.create'. 7 | * 8 | * ref: http://adonisjs.com/docs/4.0/views 9 | */ 10 | return view.render('session.create') 11 | } 12 | 13 | /** 14 | * Store a session. 15 | */ 16 | async store ({ auth, request, response, session }) { 17 | /** 18 | * Getting needed parameters. 19 | * 20 | * ref: http://adonisjs.com/docs/4.0/request#_all 21 | */ 22 | const { username, password } = request.all() 23 | 24 | /** 25 | * Wrapping the authentication in order to 26 | * handle errors when authentication fail. 27 | * 28 | * ref: http://adonisjs.com/docs/4.1/authentication#_attempt_uid_password 29 | */ 30 | try { 31 | await auth.attempt(username, password) 32 | } catch (e) { 33 | /** 34 | * Add flash message to the session with the content of 35 | * the form except password field. 36 | * 37 | * ref: http://adonisjs.com/docs/4.1/sessions#_flash_messages 38 | */ 39 | session.flashExcept(['password']) 40 | 41 | /** 42 | * Add a custom object to the session store. 43 | * 44 | * ref: http://adonisjs.com/docs/4.1/sessions#_flash 45 | */ 46 | session.flash({ error: 'We cannot find any account with these credentials.' }) 47 | 48 | /** 49 | * Since the authentication failed we redirect 50 | * our user back to the form. 51 | * 52 | * ref: http://adonisjs.com/docs/4.1/response#_redirects 53 | */ 54 | return response.redirect('login') 55 | } 56 | 57 | /** 58 | * We are authenticated. 59 | */ 60 | return response.redirect('/') 61 | } 62 | 63 | async delete ({ auth, response }) { 64 | /** 65 | * Logout the user. 66 | * 67 | * ref: http://adonisjs.com/docs/4.1/authentication#_logout 68 | */ 69 | await auth.logout() 70 | 71 | return response.redirect('/') 72 | } 73 | } 74 | 75 | module.exports = SessionController 76 | -------------------------------------------------------------------------------- /app/Controllers/Http/UserController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const User = use('App/Models/User') 4 | const { validateAll } = use('Validator') 5 | 6 | class UserController { 7 | create ({ view }) { 8 | /** 9 | * Render the view 'user.create'. 10 | * 11 | * ref: http://adonisjs.com/docs/4.1/views 12 | */ 13 | return view.render('user.create') 14 | } 15 | 16 | async store ({ auth, session, request, response }) { 17 | /** 18 | * Getting needed parameters. 19 | * 20 | * ref: http://adonisjs.com/docs/4.1/request#_only 21 | */ 22 | const data = request.only(['username', 'email', 'password', 'password_confirmation']) 23 | 24 | /** 25 | * Validating our data. 26 | * 27 | * ref: http://adonisjs.com/docs/4.1/validator 28 | */ 29 | const validation = await validateAll(data, { 30 | username: 'required|unique:users', 31 | email: 'required|email|unique:users', 32 | password: 'required', 33 | password_confirmation: 'required_if:password|same:password', 34 | }) 35 | 36 | /** 37 | * If validation fails, early returns with validation message. 38 | */ 39 | if (validation.fails()) { 40 | session 41 | .withErrors(validation.messages()) 42 | .flashExcept(['password']) 43 | 44 | return response.redirect('back') 45 | } 46 | 47 | // Deleting the confirmation field since we don't 48 | // want to save it 49 | delete data.password_confirmation 50 | 51 | /** 52 | * Creating a new user into the database. 53 | * 54 | * ref: http://adonisjs.com/docs/4.1/lucid#_create 55 | */ 56 | const user = await User.create(data) 57 | 58 | // Authenticate the user 59 | await auth.login(user) 60 | 61 | return response.redirect('/') 62 | } 63 | } 64 | 65 | module.exports = UserController 66 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseExceptionHandler = use('BaseExceptionHandler') 4 | 5 | /** 6 | * This class handles all exceptions thrown during 7 | * the HTTP request lifecycle. 8 | * 9 | * @class ExceptionHandler 10 | */ 11 | class ExceptionHandler extends BaseExceptionHandler { 12 | /** 13 | * Handle exception thrown during the HTTP lifecycle 14 | * 15 | * @method handle 16 | * 17 | * @param {Object} error 18 | * @param {Object} options.request 19 | * @param {Object} options.response 20 | * 21 | * @return {void} 22 | */ 23 | async handle (error, { request, response, session, view }) { 24 | if (error.code === 'E_INVALID_SESSION') { 25 | session.flash({ error: 'You must be authenticated to access this page!' }) 26 | 27 | return response.redirect('/') 28 | } 29 | 30 | return super.handle(...arguments) 31 | } 32 | 33 | /** 34 | * Report exception for logging or debugging. 35 | * 36 | * @method report 37 | * 38 | * @param {Object} error 39 | * @param {Object} options.request 40 | * 41 | * @return {void} 42 | */ 43 | async report (error, { request }) { 44 | } 45 | } 46 | 47 | module.exports = ExceptionHandler 48 | -------------------------------------------------------------------------------- /app/Middleware/RedirectIfAuthenticated.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class RedirectIfAuthenticated { 4 | async handle ({ auth, request, response }, next) { 5 | /** 6 | * Verify if we are logged in. 7 | * 8 | * ref: http://adonisjs.com/docs/4.0/authentication#_check 9 | */ 10 | try { 11 | await auth.check() 12 | 13 | return response.redirect('/') 14 | } catch (e) {} 15 | 16 | await next() 17 | 18 | } 19 | } 20 | 21 | module.exports = RedirectIfAuthenticated 22 | -------------------------------------------------------------------------------- /app/Models/Hooks/User.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Hash = use('Hash') 4 | 5 | const UserHook = module.exports = {} 6 | 7 | /** 8 | * Hash using password as a hook. 9 | * 10 | * @method 11 | * 12 | * @param {Object} userInstance 13 | * 14 | * @return {void} 15 | */ 16 | UserHook.hashPassword = async (userInstance) => { 17 | if (userInstance.password) { 18 | userInstance.password = await Hash.make(userInstance.password) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Models/Post.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Model = use('Model') 4 | 5 | class Post extends Model { 6 | author () { 7 | return this.belongsTo('App/Models/User', 'user_id') 8 | } 9 | } 10 | 11 | module.exports = Post 12 | -------------------------------------------------------------------------------- /app/Models/User.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Model = use('Model') 4 | 5 | class User extends Model { 6 | static boot () { 7 | super.boot() 8 | 9 | /** 10 | * A hook to hash the user password before saving 11 | * it to the database. 12 | * 13 | * Look at `app/Models/Hooks/User.js` file to 14 | * check the hashPassword method 15 | */ 16 | this.addHook('beforeCreate', 'User.hashPassword') 17 | } 18 | 19 | posts () { 20 | return this.hasMany('App/Models/Post') 21 | } 22 | } 23 | 24 | module.exports = User 25 | -------------------------------------------------------------------------------- /config/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | 5 | module.exports = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | App Key 9 | |-------------------------------------------------------------------------- 10 | | 11 | | App key is a randomly generated 16 or 32 characters long string required 12 | | to encrypted cookies, sessions and other sensitive data. 13 | | 14 | */ 15 | appKey: Env.get('APP_KEY'), 16 | 17 | http: { 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Allow Method Spoofing 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Method spoofing allows to make requests by spoofing the http verb. 24 | | Which means you can make a GET request but instruct the server to 25 | | treat as a POST or PUT request. If you want this feature, set the 26 | | below value to true. 27 | | 28 | */ 29 | allowMethodSpoofing: true, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Trust Proxy 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Trust proxy defines whether X-Forwaded-* headers should be trusted or not. 37 | | When your application is behind a proxy server like nginx, these values 38 | | are set automatically and should be trusted. Apart from setting it 39 | | to true or false Adonis supports handful or ways to allow proxy 40 | | values. Read documentation for that. 41 | | 42 | */ 43 | trustProxy: false, 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Subdomains 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Offset to be used for returning subdomains for a given request.For 51 | | majority of applications it will be 2, until you have nested 52 | | sudomains. 53 | | cheatsheet.adonisjs.com - offset - 2 54 | | virk.cheatsheet.adonisjs.com - offset - 3 55 | | 56 | */ 57 | subdomainOffset: 2, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | JSONP Callback 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Default jsonp callback to be used when callback query string is missing 65 | | in request url. 66 | | 67 | */ 68 | jsonpCallback: 'callback', 69 | 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Etag 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Set etag on all HTTP response. In order to disable for selected routes, 77 | | you can call the `response.send` with an options object as follows. 78 | | 79 | | response.send('Hello', { ignoreEtag: true }) 80 | | 81 | */ 82 | etag: true 83 | }, 84 | 85 | views: { 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Cache Views 89 | |-------------------------------------------------------------------------- 90 | | 91 | | Define whether or not to cache the compiled view. Set it to true in 92 | | production to optimize view loading time. 93 | | 94 | */ 95 | cache: Env.get('CACHE_VIEWS', true) 96 | }, 97 | 98 | static: { 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Dot Files 102 | |-------------------------------------------------------------------------- 103 | | 104 | | Define how to treat dot files when trying to server static resources. 105 | | By default it is set to ignore, which will pretend that dotfiles 106 | | does not exists. 107 | | 108 | | Can be one of the following 109 | | ignore, deny, allow 110 | | 111 | */ 112 | dotfiles: 'ignore', 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | ETag 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Enable or disable etag generation 120 | | 121 | */ 122 | etag: true, 123 | 124 | /* 125 | |-------------------------------------------------------------------------- 126 | | Extensions 127 | |-------------------------------------------------------------------------- 128 | | 129 | | Set file extension fallbacks. When set, if a file is not found, the given 130 | | extensions will be added to the file name and search for. The first 131 | | that exists will be served. Example: ['html', 'htm']. 132 | | 133 | */ 134 | extensions: false 135 | }, 136 | 137 | locales: { 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Driver 141 | |-------------------------------------------------------------------------- 142 | | 143 | | The driver to be used for fetching and updating locales. Below is the 144 | | list of available options. 145 | | 146 | | file, database 147 | | 148 | */ 149 | driver: 'file', 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | Default Locale 154 | |-------------------------------------------------------------------------- 155 | | 156 | | Default locale to be used by Antl provider. You can always switch drivers 157 | | in runtime or use the official Antl middleware to detect the driver 158 | | based on HTTP headers/query string. 159 | | 160 | */ 161 | locale: 'en' 162 | }, 163 | 164 | logger: { 165 | /* 166 | |-------------------------------------------------------------------------- 167 | | Transport 168 | |-------------------------------------------------------------------------- 169 | | 170 | | Transport to be used for logging messages. You can have multiple 171 | | transports using same driver. 172 | | 173 | | Available drivers are: `file` and `console`. 174 | | 175 | */ 176 | transport: 'console', 177 | 178 | /* 179 | |-------------------------------------------------------------------------- 180 | | Console Transport 181 | |-------------------------------------------------------------------------- 182 | | 183 | | Using `console` driver for logging. This driver writes to `stdout` 184 | | and `stderr` 185 | | 186 | */ 187 | console: { 188 | driver: 'console', 189 | name: 'adonis-app', 190 | level: 'info' 191 | }, 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | File Transport 196 | |-------------------------------------------------------------------------- 197 | | 198 | | File transport uses file driver and writes log messages for a given 199 | | file inside `tmp` directory for your app. 200 | | 201 | | For a different directory, set an absolute path for the filename. 202 | | 203 | */ 204 | file: { 205 | driver: 'file', 206 | name: 'adonis-app', 207 | filename: 'adonis.log', 208 | level: 'info' 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /config/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Authenticator 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Authentication is a combination of serializer and scheme with extra 10 | | config to define on how to authenticate a user. 11 | | 12 | | Available Schemes - basic, session, jwt, api 13 | | Available Serializers - lucid, database 14 | | 15 | */ 16 | authenticator: 'session', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Session 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Session authenticator makes use of sessions to authenticate a user. 24 | | Session authentication is always persistent. 25 | | 26 | */ 27 | session: { 28 | serializer: 'lucid', 29 | model: 'App/Models/User', 30 | scheme: 'session', 31 | uid: 'username', 32 | password: 'password' 33 | }, 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Basic Auth 38 | |-------------------------------------------------------------------------- 39 | | 40 | | The basic auth authenticator uses basic auth header to authenticate a 41 | | user. 42 | | 43 | | NOTE: 44 | | This scheme is not persistent and users are supposed to pass 45 | | login credentials on each request. 46 | | 47 | */ 48 | basic: { 49 | serializer: 'lucid', 50 | model: 'App/Models/User', 51 | scheme: 'basic', 52 | uid: 'email', 53 | password: 'password' 54 | }, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Jwt 59 | |-------------------------------------------------------------------------- 60 | | 61 | | The jwt authenticator works by passing a jwt token on each HTTP request 62 | | via HTTP `Authorization` header. 63 | | 64 | */ 65 | jwt: { 66 | serializer: 'lucid', 67 | model: 'App/Models/User', 68 | scheme: 'jwt', 69 | uid: 'email', 70 | password: 'password', 71 | options: { 72 | secret: 'self::app.appKey' 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/bodyParser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | JSON Parser 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Below settings are applied when request body contains JSON payload. If 10 | | you want body parser to ignore JSON payload, then simply set `types` 11 | | to an empty array. 12 | */ 13 | json: { 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | limit 17 | |-------------------------------------------------------------------------- 18 | | 19 | | Defines the limit of JSON that can be sent by the client. If payload 20 | | is over 1mb it will not be processed. 21 | | 22 | */ 23 | limit: '1mb', 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | strict 28 | |-------------------------------------------------------------------------- 29 | | 30 | | When `scrict` is set to true, body parser will only parse Arrays and 31 | | Object. Otherwise everything parseable by `JSON.parse` is parsed. 32 | | 33 | */ 34 | strict: true, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | types 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Which content types are processed as JSON payloads. You are free to 42 | | add your own types here, but the request body should be parseable 43 | | by `JSON.parse` method. 44 | | 45 | */ 46 | types: [ 47 | 'application/json', 48 | 'application/json-patch+json', 49 | 'application/vnd.api+json', 50 | 'application/csp-report' 51 | ] 52 | }, 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Raw Parser 57 | |-------------------------------------------------------------------------- 58 | | 59 | | 60 | | 61 | */ 62 | raw: { 63 | types: [ 64 | 'text/*' 65 | ] 66 | }, 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Form Parser 71 | |-------------------------------------------------------------------------- 72 | | 73 | | 74 | | 75 | */ 76 | form: { 77 | types: [ 78 | 'application/x-www-form-urlencoded' 79 | ] 80 | }, 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Files Parser 85 | |-------------------------------------------------------------------------- 86 | | 87 | | 88 | | 89 | */ 90 | files: { 91 | types: [ 92 | 'multipart/form-data' 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Max Size 98 | |-------------------------------------------------------------------------- 99 | | 100 | | Below value is the max size of all the files uploaded to the server. It 101 | | is validated even before files have been processed and hard exception 102 | | is thrown. 103 | | 104 | | Consider setting a reasonable value here, otherwise people may upload GB's 105 | | of files which will keep your server busy. 106 | | 107 | | Also this value is considered when `autoProcess` is set to true. 108 | | 109 | */ 110 | maxSize: '20mb', 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Auto Process 115 | |-------------------------------------------------------------------------- 116 | | 117 | | Whether or not to auto-process files. Since HTTP servers handle files via 118 | | couple of specific endpoints. It is better to set this value off and 119 | | manually process the files when required. 120 | | 121 | | This value can contain a boolean or an array of route patterns 122 | | to be autoprocessed. 123 | */ 124 | autoProcess: true, 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Process Manually 129 | |-------------------------------------------------------------------------- 130 | | 131 | | The list of routes that should not process files and instead rely on 132 | | manual process. This list should only contain routes when autoProcess 133 | | is to true. Otherwise everything is processed manually. 134 | | 135 | */ 136 | processManually: [] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /config/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Origin 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Set a list of origins to be allowed. The value can be one of the following 10 | | 11 | | Boolean: true - Allow current request origin 12 | | Boolean: false - Disallow all 13 | | String - Comma seperated list of allowed origins 14 | | Array - An array of allowed origins 15 | | String: * - A wildcard to allow current request origin 16 | | Function - Receives the current origin and should return one of the above values. 17 | | 18 | */ 19 | origin: false, 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Methods 24 | |-------------------------------------------------------------------------- 25 | | 26 | | HTTP methods to be allowed. The value can be one of the following 27 | | 28 | | String - Comma seperated list of allowed methods 29 | | Array - An array of allowed methods 30 | | 31 | */ 32 | methods: ['GET', 'PUT', 'POST'], 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Headers 37 | |-------------------------------------------------------------------------- 38 | | 39 | | List of headers to be allowed via Access-Control-Request-Headers header. 40 | | The value can be on of the following. 41 | | 42 | | Boolean: true - Allow current request headers 43 | | Boolean: false - Disallow all 44 | | String - Comma seperated list of allowed headers 45 | | Array - An array of allowed headers 46 | | String: * - A wildcard to allow current request headers 47 | | Function - Receives the current header and should return one of the above values. 48 | | 49 | */ 50 | headers: true, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Expose Headers 55 | |-------------------------------------------------------------------------- 56 | | 57 | | A list of headers to be exposed via `Access-Control-Expose-Headers` 58 | | header. The value can be on of the following. 59 | | 60 | | Boolean: false - Disallow all 61 | | String: Comma seperated list of allowed headers 62 | | Array - An array of allowed headers 63 | | 64 | */ 65 | exposeHeaders: false, 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Credentials 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Define Access-Control-Allow-Credentials header. It should always be a 73 | | boolean. 74 | | 75 | */ 76 | credentials: false, 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | MaxAge 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Define Access-Control-Allow-Max-Age 84 | | 85 | */ 86 | maxAge: 90 87 | } 88 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | const Helpers = use('Helpers') 5 | 6 | module.exports = { 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Default Connection 10 | |-------------------------------------------------------------------------- 11 | | 12 | | Connection defines the default connection settings to be used while 13 | | interacting with SQL databases. 14 | | 15 | */ 16 | connection: Env.get('DB_CONNECTION', 'sqlite'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Sqlite 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Sqlite is a flat file database and can be good choice under development 24 | | environment. 25 | | 26 | | npm i --save sqlite3 27 | | 28 | */ 29 | sqlite: { 30 | client: 'sqlite3', 31 | connection: { 32 | filename: Helpers.databasePath(Env.get('DB_FILENAME', 'development.sqlite')) 33 | }, 34 | useNullAsDefault: true 35 | }, 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | MySQL 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Here we define connection settings for MySQL database. 43 | | 44 | | npm i --save mysql 45 | | 46 | */ 47 | mysql: { 48 | client: 'mysql', 49 | connection: { 50 | host: Env.get('DB_HOST', 'localhost'), 51 | port: Env.get('DB_PORT', ''), 52 | user: Env.get('DB_USER', 'root'), 53 | password: Env.get('DB_PASSWORD', ''), 54 | database: Env.get('DB_DATABASE', 'adonis') 55 | } 56 | }, 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | PostgreSQL 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here we define connection settings for PostgreSQL database. 64 | | 65 | | npm i --save pg 66 | | 67 | */ 68 | pg: { 69 | client: 'pg', 70 | connection: { 71 | host: Env.get('DB_HOST', 'localhost'), 72 | port: Env.get('DB_PORT', ''), 73 | user: Env.get('DB_USER', 'root'), 74 | password: Env.get('DB_PASSWORD', ''), 75 | database: Env.get('DB_DATABASE', 'adonis') 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | 5 | module.exports = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Session Driver 9 | |-------------------------------------------------------------------------- 10 | | 11 | | The session driver to be used for storing session values. It can be 12 | | cookie, file or redis. 13 | | 14 | | For `redis` driver, make sure to install and register `@adonisjs/redis` 15 | | 16 | */ 17 | driver: Env.get('SESSION_DRIVER', 'cookie'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Cookie Name 22 | |-------------------------------------------------------------------------- 23 | | 24 | | The name of the cookie to be used for saving session id. Session ids 25 | | are signed and encrypted. 26 | | 27 | */ 28 | cookieName: 'adonis-session', 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Clear session when browser closes 33 | |-------------------------------------------------------------------------- 34 | | 35 | | If this value is true, the session cookie will be temporary and will be 36 | | removed when browser closes. 37 | | 38 | */ 39 | clearWithBrowser: true, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Session age 44 | |-------------------------------------------------------------------------- 45 | | 46 | | This value is only used when `clearWithBrowser` is set to false. The 47 | | age must be a valid https://npmjs.org/package/ms string or should 48 | | be in milliseconds. 49 | | 50 | | Valid values are: 51 | | '2h', '10d', '5y', '2.5 hrs' 52 | | 53 | */ 54 | age: '2h', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Cookie options 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Cookie options defines the options to be used for setting up session 62 | | cookie 63 | | 64 | */ 65 | cookie: { 66 | httpOnly: true, 67 | sameSite: true, 68 | path: '/' 69 | }, 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Sessions location 74 | |-------------------------------------------------------------------------- 75 | | 76 | | If driver is set to file, we need to define the relative location from 77 | | the temporary path or absolute url to any location. 78 | | 79 | */ 80 | file: { 81 | location: 'sessions' 82 | }, 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Redis config 87 | |-------------------------------------------------------------------------- 88 | | 89 | | The configuration for the redis driver. By default we reference it from 90 | | the redis file. But you are free to define an object here too. 91 | | 92 | */ 93 | redis: 'self::redis.default' 94 | } 95 | -------------------------------------------------------------------------------- /config/shield.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | 5 | module.exports = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Content Security Policy 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Content security policy filters out the origins not allowed to execute 12 | | and load resources like scripts, styles and fonts. There are wide 13 | | variety of options to choose from. 14 | */ 15 | csp: { 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Directives 19 | |-------------------------------------------------------------------------- 20 | | 21 | | All directives are defined in camelCase and here is the list of 22 | | available directives and their possible values. 23 | | 24 | | https://content-security-policy.com 25 | | 26 | | @example 27 | | directives: { 28 | | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] 29 | | } 30 | | 31 | */ 32 | directives: { 33 | }, 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Report only 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Setting `reportOnly=true` will not block the scripts from running and 40 | | instead report them to a URL. 41 | | 42 | */ 43 | reportOnly: false, 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Set all headers 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Headers staring with `X` have been depreciated, since all major browsers 50 | | supports the standard CSP header. So its better to disable deperciated 51 | | headers, unless you want them to be set. 52 | | 53 | */ 54 | setAllHeaders: false, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Disable on android 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Certain versions of android are buggy with CSP policy. So you can set 62 | | this value to true, to disable it for Android versions with buggy 63 | | behavior. 64 | | 65 | | Here is an issue reported on a different package, but helpful to read 66 | | if you want to know the behavior. https://github.com/helmetjs/helmet/pull/82 67 | | 68 | */ 69 | disableAndroid: true 70 | }, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | X-XSS-Protection 75 | |-------------------------------------------------------------------------- 76 | | 77 | | X-XSS Protection saves from applications from XSS attacks. It is adopted 78 | | by IE and later followed by some other browsers. 79 | | 80 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 81 | | 82 | */ 83 | xss: { 84 | enabled: true, 85 | enableOnOldIE: false 86 | }, 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Iframe Options 91 | |-------------------------------------------------------------------------- 92 | | 93 | | xframe defines whether or not your website can be embedded inside an 94 | | iframe. Choose from one of the following options. 95 | | @available options 96 | | DENY, SAMEORIGIN, ALLOW-FROM http://example.com 97 | | 98 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 99 | */ 100 | xframe: 'DENY', 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | No Sniff 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Browsers have a habit of sniffing content-type of a response. Which means 108 | | files with .txt extension containing Javascript code will be executed as 109 | | Javascript. You can disable this behavior by setting nosniff to false. 110 | | 111 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 112 | | 113 | */ 114 | nosniff: true, 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | No Open 119 | |-------------------------------------------------------------------------- 120 | | 121 | | IE users can execute webpages in the context of your website, which is 122 | | a serious security risk. Below option will manage this for you. 123 | | 124 | */ 125 | noopen: true, 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | CSRF Protection 130 | |-------------------------------------------------------------------------- 131 | | 132 | | CSRF Protection adds another layer of security by making sure, actionable 133 | | routes does have a valid token to execute an action. 134 | | 135 | */ 136 | csrf: { 137 | enable: Env.get('ENABLE_CSRF_CHECK', true) === 'true', 138 | methods: ['POST', 'PUT', 'DELETE'], 139 | filterUris: [], 140 | cookieOptions: { 141 | httpOnly: false, 142 | sameSite: true, 143 | path: '/', 144 | maxAge: 7200 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /database/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Factory 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Factories are used to define blueprints for database tables or Lucid 9 | | models. Later you can use these blueprints to seed your database 10 | | with dummy data. 11 | | 12 | */ 13 | 14 | const Factory = use('Factory') 15 | 16 | Factory.blueprint('App/Models/User', (faker, index, data) => { 17 | const defaultValue = { 18 | username: faker.username(), 19 | email: faker.email(), 20 | password: 'secret', 21 | } 22 | 23 | return Object.assign(defaultValue, data) 24 | }) 25 | 26 | Factory.blueprint('App/Models/Post', (faker) => { 27 | return { 28 | title: faker.sentence(), 29 | body: faker.paragraph(), 30 | user_id: async () => { 31 | return (await Factory.model('App/Models/User').create()).id 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /database/migrations/1503248427885_user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class UserSchema extends Schema { 6 | up () { 7 | this.create('users', (table) => { 8 | table.increments() 9 | table.timestamps() 10 | table.string('username', 80).notNullable().unique() 11 | table.string('email', 254).notNullable().unique() 12 | table.string('password', 60).notNullable() 13 | }) 14 | } 15 | 16 | down () { 17 | this.drop('users') 18 | } 19 | } 20 | 21 | module.exports = UserSchema 22 | -------------------------------------------------------------------------------- /database/migrations/1507839709797_post_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class PostSchema extends Schema { 6 | up () { 7 | this.create('posts', (table) => { 8 | table.increments() 9 | table.timestamps() 10 | table.string('title') 11 | table.text('body') 12 | table.integer('user_id').unsigned() 13 | }) 14 | } 15 | 16 | down () { 17 | this.drop('posts') 18 | } 19 | } 20 | 21 | module.exports = PostSchema 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonis-blog-demo", 3 | "version": "4.0.0", 4 | "adonis-version": "4.0.0", 5 | "description": "AdonisJS Blog Demo Application", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "node ace test --timeout=0", 9 | "build": "postcss -o public/app.css resources/css/app.css", 10 | "build:prod": "cross-env NODE_ENV=production postcss -o public/app.css resources/css/app.css" 11 | }, 12 | "license": "UNLICENSED", 13 | "private": true, 14 | "dependencies": { 15 | "@adonisjs/ace": "^5.0.2", 16 | "@adonisjs/auth": "^3.0.5", 17 | "@adonisjs/bodyparser": "^2.0.3", 18 | "@adonisjs/cors": "^1.0.6", 19 | "@adonisjs/fold": "^4.0.8", 20 | "@adonisjs/framework": "^5.0.8", 21 | "@adonisjs/ignitor": "^2.0.6", 22 | "@adonisjs/lucid": "^5.0.4", 23 | "@adonisjs/session": "^1.0.25", 24 | "@adonisjs/shield": "^1.0.6", 25 | "@adonisjs/validator": "^5.0.3", 26 | "@adonisjs/vow": "^1.0.15", 27 | "@adonisjs/vow-browser": "^1.0.6" 28 | }, 29 | "devDependencies": { 30 | "@fullhuman/postcss-purgecss": "^2.1.2", 31 | "@tailwindcss/ui": "^0.1.3", 32 | "autoprefixer": "^9.7.6", 33 | "cross-env": "^7.0.2", 34 | "postcss-cli": "^7.1.0", 35 | "postcss-import": "^12.0.1", 36 | "postcss-nested": "^4.2.1", 37 | "sqlite3": "^4.0.1", 38 | "tailwindcss": "^1.2.0" 39 | }, 40 | "autoload": { 41 | "App": "./app" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwind = require('tailwindcss') 2 | const autoprefixer = require('autoprefixer') 3 | const postCSSImport = require('postcss-import') 4 | const postCSSNested = require('postcss-nested') 5 | const purgeCSS = require('@fullhuman/postcss-purgecss') 6 | 7 | const isProduction = process.env.NODE_ENV === 'production' 8 | 9 | const productionPlugins = [ 10 | purgeCSS({ 11 | content: ['./resources/views/**/*.edge'], 12 | whitelist: ['body', 'html'], 13 | whitelistPatterns: [], 14 | defaultExtractor: content => content.match(/[\w-/:]+(? 2 |
3 |
4 | @if(type === 'info') 5 | 6 | 7 | 8 | @elseif(type === 'danger') 9 | 10 | 11 | 12 | @endif 13 |
14 |
15 |

16 | @!yield($slot.main) 17 |

18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /resources/views/components/input.edge: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 | -------------------------------------------------------------------------------- /resources/views/components/label.edge: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/panel.edge: -------------------------------------------------------------------------------- 1 |
2 |
3 | @!yield($slot.main) 4 |
5 |
6 | -------------------------------------------------------------------------------- /resources/views/components/textarea.edge: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | -------------------------------------------------------------------------------- /resources/views/layout/app.edge: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{-- Meta Information --}} 5 | 6 | 7 | 8 | 9 | {{-- Title --}} 10 | {{ title }} | Adonis 11 | 12 | {{-- Scripts --}} 13 | 14 | 15 | {{-- CSS --}} 16 | 17 | 18 | 19 | {{-- 20 | Include a partial into the view 21 | ref: http://edge.adonisjs.com/docs/partials 22 | --}} 23 | @include('layout.partials.header') 24 | 25 |
26 | {{-- 27 | Display the section content 28 | ref: http://edge.adonisjs.com/docs/layouts#_basic_example 29 | --}} 30 | @!section('content') 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/views/layout/partials/header.edge: -------------------------------------------------------------------------------- 1 |
2 | {{-- Dots Background --}} 3 | 23 | 24 |
25 |
26 | 80 |
81 | 82 | {{-- Hero --}} 83 | @include('layout.partials.hero') 84 |
85 |
86 | -------------------------------------------------------------------------------- /resources/views/layout/partials/hero.edge: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | A Modern Web Framework 5 |
6 | for Node.js 7 |

8 |

9 | AdonisJS is a fully-featured MVC framework for Node.js. It takes care of most of your web development hassles, offering you a clean and stable API to build web apps or microservices. 10 |

11 |
12 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /resources/views/posts/create.edge: -------------------------------------------------------------------------------- 1 | @layout('layout.app') 2 | 3 | @section('content') 4 | @set('title', 'Create a post') 5 | 6 |
7 | 8 | {{-- 9 | We are here checking if the key 'error' is in the session. 10 | If it is it means that our validation fails. 11 | 12 | ref: http://adonisjs.com/docs/4.0/sessions#_view_helpers 13 | ref: http://edge.adonisjs.com/docs/conditionals#_if 14 | --}} 15 | @if(flashMessage('error')) 16 | @component('components.alert', { type: 'danger', dismissible: true }) 17 | {{ flashMessage('error') }} 18 | @endcomponent 19 | @endif 20 | 21 | @include('posts.partials.form') 22 | 23 |
24 | @endsection 25 | -------------------------------------------------------------------------------- /resources/views/posts/edit.edge: -------------------------------------------------------------------------------- 1 | @layout('layout.app') 2 | 3 | @section('content') 4 | @set('title', 'Edit the post ' + post.id) 5 | 6 |
7 | 8 | {{-- 9 | We are here checking if the key 'error' is in the session. 10 | If it is it means that our validation fails. 11 | 12 | ref: http://adonisjs.com/docs/4.0/sessions#_view_helpers 13 | ref: http://edge.adonisjs.com/docs/conditionals#_if 14 | --}} 15 | @if(flashMessage('error')) 16 | @component('components.alert', { type: 'danger', color: 'red' }) 17 | {{ flashMessage('error') }} 18 | @endcomponent 19 | @endif 20 | 21 | @include('posts.partials.form') 22 | 23 |
24 | @endsection 25 | -------------------------------------------------------------------------------- /resources/views/posts/index.edge: -------------------------------------------------------------------------------- 1 | @layout('layout.app') 2 | 3 | @section('content') 4 | @set('title', 'Blog Demo') 5 | 6 |
7 |
8 |
9 |
10 | 11 |
12 | {{-- Section Title --}} 13 |
14 |

15 | Posts 16 |

17 |
18 | 19 |
20 | {{-- 21 | Looping over all posts. 22 | 23 | ref: http://edge.adonisjs.com/docs/iteration 24 | --}} 25 | @each(post in posts, include = 'posts.partials.post-card') 26 | @else 27 |
28 | @component('components.alert', { type: 'info', color: 'blue' }) 29 | There's no post available! 30 | @endcomponent 31 |
32 | @endeach 33 |
34 |
35 |
36 | @endsection 37 | -------------------------------------------------------------------------------- /resources/views/posts/partials/form.edge: -------------------------------------------------------------------------------- 1 | {{-- 2 | AdonisJs provides CSRF Protection by default. 3 | This mean that we need to send a csrf token for every POST, PUT or DELETE request. 4 | 5 | ref: http://adonisjs.com/docs/4.0/csrf 6 | --}} 7 | {{ csrfField() }} 8 | 9 |
10 |

11 | New Post 12 |

13 |

14 | Feel free to write whatever you want. 15 |

16 |
17 | 18 |
19 | {{-- Title Field --}} 20 |
21 | @!component('components.label', { text: 'Title', related: 'title' }) 22 | 23 |
24 | @!component('components.input', { name: 'title', value: old('title', post.title || ''), required: true }) 25 | {{ elIf('

$self

', getErrorFor('title'), hasErrorFor('title')) }} 26 |
27 |
28 | 29 | {{-- Body Field --}} 30 |
31 | @!component('components.label', { text: 'Body', related: 'body' }) 32 | 33 |
34 | @!component('components.textarea', { name: 'body', value: old('body', post.body || '') }) 35 | {{ elIf('

$self

', getErrorFor('body'), hasErrorFor('body')) }} 36 |
37 |
38 | 39 |
40 |
41 | 44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /resources/views/posts/partials/post-card.edge: -------------------------------------------------------------------------------- 1 |
2 | {{-- Illustration --}} 3 |
4 | 5 |
6 | 7 | {{-- Card Body --}} 8 |
9 | 10 |

11 | {{ post.title }} 12 |

13 |

14 | {{{ truncate(post.body, 25, '...') }}} 15 |

16 |
17 |
18 | 19 | {{-- 20 | This tag let us know if the current user is logged in or not. 21 | 22 | ref: http://adonisjs.com/docs/4.0/views#_loggedin 23 | ref: http://adonisjs.com/docs/4.0/authentication#_loggedin 24 | --}} 25 | @loggedIn 26 |
27 | 28 | Edit 29 | 30 | 31 | Delete 32 | 33 |
34 | @endloggedIn 35 |
36 | -------------------------------------------------------------------------------- /resources/views/session/create.edge: -------------------------------------------------------------------------------- 1 | @layout('layout.app') 2 | 3 | @section('content') 4 | @set('title', 'Login') 5 | 6 |
7 | @component('components.panel') 8 |
9 | {{-- 10 | We are here checking if the key 'error' is in the session. 11 | If it is it means that our validation fails and credentials are incorect. 12 | 13 | ref: http://adonisjs.com/docs/4.1/sessions#_view_helpers 14 | ref: http://edge.adonisjs.com/docs/conditionals#_if 15 | --}} 16 | @if(flashMessage('error')) 17 |
18 | @component('components.alert', { type: 'danger', color: 'red' }) 19 | {{ flashMessage('error') }} 20 | @endcomponent 21 |
22 | @endif 23 | 24 | {{-- 25 | AdonisJs provides CSRF Protection by default. 26 | This mean that we need to send a csrf token for every POST, PUT or DELETE request. 27 | 28 | ref: http://adonisjs.com/docs/4.1/csrf 29 | --}} 30 | {{ csrfField() }} 31 | 32 |
33 |

34 | Sign In 35 |

36 |
37 | 38 |
39 | {{-- Username Field --}} 40 |
41 | @!component('components.label', { text: 'Username', related: 'username' }) 42 | 43 |
44 | @!component('components.input', { name: 'username', value: old('username'), required: true }) 45 |
46 |
47 | 48 | {{-- Password Field --}} 49 |
50 | @!component('components.label', text = 'Password', related = 'password') 51 | 52 |
53 | @!component('components.input', name = 'password', type = 'password', required = true) 54 |
55 |
56 | 57 |
58 |
59 | 62 |
63 |
64 |
65 |
66 | @endcomponent 67 |
68 | @endsection 69 | -------------------------------------------------------------------------------- /resources/views/user/create.edge: -------------------------------------------------------------------------------- 1 | @layout('layout.app') 2 | 3 | @section('content') 4 | @set('title', 'Register') 5 | 6 |
7 | @component('components.panel') 8 |
9 | {{-- 10 | We are here checking if the key 'error' is in the session. 11 | If it is it means that our validation fails and credentials are incorect. 12 | 13 | ref: http://adonisjs.com/docs/4.1/sessions#_view_helpers 14 | ref: http://edge.adonisjs.com/docs/conditionals#_if 15 | --}} 16 | @if(flashMessage('error')) 17 |
18 | @component('components.alert', { type: 'danger', color: 'red' }) 19 | {{ flashMessage('error') }} 20 | @endcomponent 21 |
22 | @endif 23 | 24 | {{-- 25 | AdonisJs provides CSRF Protection by default. 26 | This mean that we need to send a csrf token for every POST, PUT or DELETE request. 27 | 28 | ref: http://adonisjs.com/docs/4.1/csrf 29 | --}} 30 | {{ csrfField() }} 31 | 32 |
33 |

34 | Register 35 |

36 |
37 | 38 |
39 | {{-- Username Field --}} 40 |
41 | @!component('components.label', { text: 'Username', related: 'username' }) 42 | 43 |
44 | @!component('components.input', { name: 'username', value: old('username'), required: true }) 45 | {{ elIf('

$self

', getErrorFor('username'), hasErrorFor('username')) }} 46 |
47 |
48 | 49 | {{-- Email Field --}} 50 |
51 | @!component('components.label', { text: 'Email', related: 'email' }) 52 | 53 |
54 | @!component('components.input', { name: 'email', type: 'email', value: old('email'), required: true }) 55 | {{ elIf('

$self

', getErrorFor('email'), hasErrorFor('email')) }} 56 |
57 |
58 | 59 | {{-- Password Field --}} 60 |
61 | @!component('components.label', { text: 'Password', related: 'password' }) 62 | 63 |
64 | @!component('components.input', { name: 'password', type: 'password', required: true }) 65 | {{ elIf('

$self

', getErrorFor('password'), hasErrorFor('password')) }} 66 |
67 |
68 | 69 | {{-- Password Confirmation Field --}} 70 |
71 | @!component('components.label', { text: 'Password Confirmation', related: 'password_confirmation' }) 72 | 73 |
74 | @!component('components.input', { name: 'password_confirmation', type: 'password', required: true }) 75 | {{ elIf('

$self

', getErrorFor('password_confirmation'), hasErrorFor('password_confirmation')) }} 76 |
77 |
78 | 79 |
80 |
81 | 84 |
85 |
86 |
87 |
88 | @endcomponent 89 |
90 | @endsection 91 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Http server 6 | |-------------------------------------------------------------------------- 7 | | 8 | | This file bootstrap Adonisjs to start the HTTP server. You are free to 9 | | customize the process of booting the http server. 10 | | 11 | | """ Loading ace commands """ 12 | | At times you may want to load ace commands when starting the HTTP server. 13 | | Same can be done by chaining `loadCommands()` method after 14 | | 15 | | """ Preloading files """ 16 | | Also you can preload files by calling `preLoad('path/to/file')` method. 17 | | Make sure to pass relative path from the project root. 18 | */ 19 | 20 | const { Ignitor } = require('@adonisjs/ignitor') 21 | 22 | new Ignitor(require('@adonisjs/fold')) 23 | .appRoot(__dirname) 24 | .fireHttpServer() 25 | .catch(console.error) 26 | -------------------------------------------------------------------------------- /start/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Providers 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Providers are building blocks for your Adonis app. Anytime you install 9 | | a new Adonis specific package, chances are you will register the 10 | | provider here. 11 | | 12 | */ 13 | const providers = [ 14 | '@adonisjs/framework/providers/AppProvider', 15 | '@adonisjs/framework/providers/ViewProvider', 16 | '@adonisjs/lucid/providers/LucidProvider', 17 | '@adonisjs/bodyparser/providers/BodyParserProvider', 18 | '@adonisjs/cors/providers/CorsProvider', 19 | '@adonisjs/shield/providers/ShieldProvider', 20 | '@adonisjs/session/providers/SessionProvider', 21 | '@adonisjs/auth/providers/AuthProvider', 22 | '@adonisjs/vow/providers/VowProvider', 23 | '@adonisjs/vow-browser/providers/VowBrowserProvider', 24 | '@adonisjs/validator/providers/ValidatorProvider', 25 | ] 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Ace Providers 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Ace providers are required only when running ace commands. For example 33 | | Providers for migrations, tests etc. 34 | | 35 | */ 36 | const aceProviders = [ 37 | '@adonisjs/lucid/providers/MigrationsProvider' 38 | ] 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Aliases 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Aliases are short unique names for IoC container bindings. You are free 46 | | to create your own aliases. 47 | | 48 | | For example: 49 | | { Route: 'Adonis/Src/Route' } 50 | | 51 | */ 52 | const aliases = {} 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Commands 57 | |-------------------------------------------------------------------------- 58 | | 59 | | Here you store ace commands for your package 60 | | 61 | */ 62 | const commands = [] 63 | 64 | module.exports = { providers, aceProviders, aliases, commands } 65 | -------------------------------------------------------------------------------- /start/kernel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Server = use('Server') 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Global Middleware 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Global middleware are executed on each http request only when the routes 11 | | match. 12 | | 13 | */ 14 | const globalMiddleware = [ 15 | 'Adonis/Middleware/BodyParser', 16 | 'Adonis/Middleware/Session', 17 | 'Adonis/Middleware/Shield', 18 | 'Adonis/Middleware/AuthInit' 19 | ] 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Named Middleware 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Named middleware is key/value object to conditionally add middleware on 27 | | specific routes or group of routes. 28 | | 29 | | // define 30 | | { 31 | | auth: 'Adonis/Middleware/Auth' 32 | | } 33 | | 34 | | // use 35 | | Route.get().middleware('auth') 36 | | 37 | */ 38 | const namedMiddleware = { 39 | auth: 'Adonis/Middleware/Auth', 40 | guest: 'App/Middleware/RedirectIfAuthenticated', 41 | } 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Server Middleware 46 | |-------------------------------------------------------------------------- 47 | | 48 | | Server levl middleware are executed even when route for a given URL is 49 | | not registered. Features like `static assets` and `cors` needs better 50 | | control over request lifecycle. 51 | | 52 | */ 53 | const serverMiddleware = [ 54 | 'Adonis/Middleware/Static', 55 | 'Adonis/Middleware/Cors' 56 | ] 57 | 58 | Server 59 | .registerGlobal(globalMiddleware) 60 | .registerNamed(namedMiddleware) 61 | .use(serverMiddleware) 62 | -------------------------------------------------------------------------------- /start/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Routes 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Http routes are entry points to your web application. You can create 9 | | routes for different URL's and bind Controller actions to them. 10 | | 11 | | A complete guide on routing is available here. 12 | | http://adonisjs.com/docs/4.0/routing 13 | | 14 | */ 15 | 16 | const Route = use('Route') 17 | 18 | Route.get('/', 'PostController.index') 19 | 20 | // Those routes should be only accessible 21 | // when you are not logged in 22 | Route.group(() => { 23 | Route.get('login', 'SessionController.create') 24 | Route.post('login', 'SessionController.store') 25 | 26 | Route.get('register', 'UserController.create') 27 | Route.post('register', 'UserController.store') 28 | }).middleware(['guest']) 29 | 30 | // Those routes should be only accessible 31 | // when you are logged in 32 | Route.group(() => { 33 | Route.get('logout', 'SessionController.delete') 34 | 35 | Route.get('posts/create', 'PostController.create') 36 | Route.post('posts', 'PostController.store') 37 | Route.get('posts/:id/edit', 'PostController.edit') 38 | Route.get('posts/:id/delete', 'PostController.delete') 39 | Route.put('posts/:id', 'PostController.update') 40 | }).middleware(['auth']) 41 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: {}, 3 | variants: {}, 4 | plugins: [ 5 | require('@tailwindcss/ui')(), 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /test/functional/authentication.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const { test, trait } = use('Test/Suite')('Authentication') 5 | 6 | trait('Test/Browser') 7 | trait('DatabaseTransactions') 8 | 9 | test('should display an error when crendentials are incorect', async ({ browser }) => { 10 | // Given we have no user 11 | 12 | // And we are on the login page 13 | const page = await browser.visit('/login') 14 | 15 | // When we fill and send the login form 16 | await page 17 | .type('[name="username"]', 'romain.lanz') 18 | .type('[name="password"]', 'secret') 19 | .submitForm('form') 20 | .waitForNavigation() 21 | 22 | // We expect to be again on the login page 23 | await page.assertPath('/login') 24 | 25 | // And we expect to see an alert message 26 | await page.assertExists('div[role="alert"]') 27 | 28 | // And to see the username filled 29 | await page.assertValue('[name="username"]', 'romain.lanz') 30 | }).timeout(0) 31 | 32 | test('a user can log in inside the application', async ({ browser }) => { 33 | // Given we have a user 34 | const user = await Factory.model('App/Models/User').create({ password: 'secret' }) 35 | 36 | // And we are on the login page 37 | const page = await browser.visit('/login') 38 | 39 | // When we fill and send the login form 40 | await page 41 | .type('[name="username"]', user.username) 42 | .type('[name="password"]', 'secret') 43 | .submitForm('form') 44 | .waitForNavigation() 45 | 46 | // We expect to be on the homepage 47 | await page.assertPath('/') 48 | }).timeout(0) 49 | -------------------------------------------------------------------------------- /test/functional/delete-post.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const Post = use('App/Models/Post') 5 | const { test, trait } = use('Test/Suite')('Delete Post') 6 | 7 | trait('Auth/Client') 8 | trait('Test/Browser') 9 | trait('Session/Client') 10 | trait('DatabaseTransactions') 11 | 12 | test('we can delete a post', async ({ assert, browser }) => { 13 | // Given we have a post 14 | const post = await Factory.model('App/Models/Post').create() 15 | 16 | // And we have a user 17 | const user = await Factory.model('App/Models/User').create() 18 | 19 | // And we are logged on the homepage 20 | const page = await browser.visit(`/`, (request) => { 21 | request.loginVia(user) 22 | }) 23 | 24 | // When we click on the trash icon 25 | await page 26 | .click('.trash') 27 | .waitForNavigation() 28 | 29 | // We expect to be again on the homepage 30 | await page.assertPath('/') 31 | 32 | // And we expect that no post exists on the database 33 | try { 34 | await Post.findOrFail(post.id) 35 | assert.isTrue(false) 36 | } catch (e) {} 37 | }) 38 | -------------------------------------------------------------------------------- /test/functional/read-post.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const { test, trait } = use('Test/Suite')('Read Post') 5 | 6 | trait('Auth/Client') 7 | trait('Session/Client') 8 | trait('Test/ApiClient') 9 | trait('DatabaseTransactions') 10 | 11 | test("should see an information when there's no post", async ({ assert, client }) => { 12 | // Given we have no post 13 | 14 | // When we fetch the home page 15 | const response = await client.get('/').end() 16 | 17 | // The request should be good 18 | response.assertStatus(200) 19 | 20 | // And we expect to see the message "There's no post available!" 21 | assert.include(response.text, "There's no post available!") 22 | }) 23 | 24 | test('should see post on home page', async ({ assert, client }) => { 25 | // Given we already have a post 26 | const post = await Factory.model('App/Models/Post').create() 27 | 28 | // When we fetch the home page 29 | const response = await client.get('/').end() 30 | 31 | // The request should be good 32 | response.assertStatus(200) 33 | 34 | // And we expect to see the title of the created post 35 | assert.include(response.text, post.title) 36 | }) 37 | -------------------------------------------------------------------------------- /test/functional/register.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const User = use('App/Models/User') 5 | const { test, trait } = use('Test/Suite')('Register') 6 | 7 | trait('Test/Browser') 8 | trait('DatabaseTransactions') 9 | 10 | test('we can register a new user', async ({ assert, browser }) => { 11 | // Given we are on the register page 12 | const page = await browser.visit('/register') 13 | 14 | // When we fill and send the login form 15 | await page 16 | .type('[name="username"]', 'romain.lanz') 17 | .type('[name="email"]', 'romain.lanz@slynova.ch') 18 | .type('[name="password"]', 'secret') 19 | .type('[name="password_confirmation"]', 'secret') 20 | .submitForm('form') 21 | .waitForNavigation() 22 | 23 | // We expect to be on the homepage 24 | await page.assertPath('/') 25 | 26 | // And to have a user into the database 27 | const user = await User.findBy('username', 'romain.lanz') 28 | assert.isNotNull(user) 29 | }).timeout(0) 30 | 31 | test('we need to provide a valid email', async ({ assert, browser }) => { 32 | // Given we are on the register page 33 | const page = await browser.visit('/register') 34 | 35 | // When we fill and send the login form 36 | await page 37 | .type('[name="username"]', 'romain.lanz') 38 | .type('[name="email"]', 'romain.lanz') 39 | .type('[name="password"]', 'secret') 40 | .type('[name="password_confirmation"]', 'secret') 41 | .submitForm('form') 42 | .waitForNavigation() 43 | 44 | // We expect to be again on the register page 45 | await page.assertPath('/register') 46 | 47 | // And to see the username filled 48 | await page.assertValue('[name="username"]', 'romain.lanz') 49 | 50 | // And to see the email filled 51 | await page.assertValue('[name="email"]', 'romain.lanz') 52 | 53 | // And we expect to see a form error 54 | await page.assertExists('small.text-xs') 55 | }) 56 | 57 | test('we need to provide a identical password', async ({ assert, browser }) => { 58 | // Given we are on the register page 59 | const page = await browser.visit('/register') 60 | 61 | // When we fill and send the login form 62 | await page 63 | .type('[name="username"]', 'romain.lanz') 64 | .type('[name="email"]', 'romain.lanz@slynova.ch') 65 | .type('[name="password"]', 'secret') 66 | .type('[name="password_confirmation"]', 'secret2') 67 | .submitForm('form') 68 | .waitForNavigation() 69 | 70 | // We expect to be again on the register page 71 | await page.assertPath('/register') 72 | 73 | // And to see the username filled 74 | await page.assertValue('[name="username"]', 'romain.lanz') 75 | 76 | // And to see the email filled 77 | await page.assertValue('[name="email"]', 'romain.lanz@slynova.ch') 78 | 79 | // And we expect to see a form error 80 | await page.assertExists('small.text-xs') 81 | }) 82 | 83 | test('we cannot have two same username', async ({ assert, browser }) => { 84 | // Given we have a user 85 | const user = await Factory.model('App/Models/User').create() 86 | 87 | // And we are on the register page 88 | const page = await browser.visit('/register') 89 | 90 | // When we fill and send the login form 91 | await page 92 | .type('[name="username"]', user.username) 93 | .type('[name="email"]', user.email) 94 | .type('[name="password"]', 'secret') 95 | .type('[name="password_confirmation"]', 'secret') 96 | .submitForm('form') 97 | .waitForNavigation() 98 | 99 | // We expect to be again on the register page 100 | await page.assertPath('/register') 101 | 102 | // And to see the username filled 103 | await page.assertValue('[name="username"]', user.username) 104 | 105 | // And to see the email filled 106 | await page.assertValue('[name="email"]', user.email) 107 | 108 | // And we expect to see a form error 109 | await page.assertExists('small.text-xs') 110 | }) 111 | -------------------------------------------------------------------------------- /test/functional/update-post.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const { test, trait } = use('Test/Suite')('Update Post') 5 | 6 | trait('Auth/Client') 7 | trait('Test/Browser') 8 | trait('Session/Client') 9 | trait('DatabaseTransactions') 10 | 11 | test('fields are filled when editing a post', async ({ browser }) => { 12 | // Given we have a post 13 | const post = await Factory.model('App/Models/Post').create() 14 | 15 | // And we have a user 16 | const user = await Factory.model('App/Models/User').create() 17 | 18 | // And we are logged on the post update form page 19 | const page = await browser.visit(`/posts/${post.id}/edit`, (request) => { 20 | request.loginVia(user) 21 | }) 22 | 23 | // We expect to see the title filled 24 | await page.assertValue('[name="title"]', post.title) 25 | 26 | // And to see the body filled 27 | await page.assertValue('[name="body"]', post.body) 28 | }) 29 | 30 | test('we can update a post', async ({ browser, assert }) => { 31 | // Given we have a post 32 | const post = await Factory.model('App/Models/Post').create() 33 | 34 | // And we have a user 35 | const user = await Factory.model('App/Models/User').create() 36 | 37 | // And we are logged on the post update form page 38 | const page = await browser.visit(`/posts/${post.id}/edit`, (request) => { 39 | request.loginVia(user) 40 | }) 41 | 42 | // When we fill and send the form 43 | await page 44 | .clear('[name="title"]') 45 | .clear('[name="body"]') 46 | .type('[name="title"]', 'Post Edited') 47 | .type('[name="body"]', 'New Body') 48 | .submitForm('form') 49 | .waitForNavigation() 50 | 51 | // We expect to be again on the homepage 52 | await page 53 | .assertPath('/') 54 | 55 | // and to see the title of our post 56 | await page.assertHas('Post Edited') 57 | 58 | // and to not see the title of our old post 59 | assert.notInclude(await page.getText(), post.title) 60 | }).timeout(0) 61 | -------------------------------------------------------------------------------- /test/functional/write-post.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Factory = use('Factory') 4 | const { test, trait } = use('Test/Suite')('Write Post') 5 | 6 | trait('Auth/Client') 7 | trait('Test/Browser') 8 | trait('Session/Client') 9 | trait('DatabaseTransactions') 10 | 11 | test('we can write a post', async ({ browser }) => { 12 | // Given we have a user 13 | const user = await Factory.model('App/Models/User').create() 14 | 15 | // And a generated post 16 | const post = await Factory.model('App/Models/Post').make() 17 | 18 | // And we are logged on the post form page 19 | const page = await browser.visit('/posts/create', (request) => { 20 | request.loginVia(user) 21 | }) 22 | 23 | // When we fill and send the form 24 | await page 25 | .type('[name="title"]', post.title) 26 | .type('[name="body"]', post.body) 27 | .submitForm('form') 28 | .waitForNavigation() 29 | 30 | // We expect to be on the homepage 31 | await page.assertPath('/') 32 | 33 | // and to see the title of our post 34 | await page.assertHas(post.title) 35 | }) 36 | 37 | test('a post should have a title', async ({ browser }) => { 38 | // Given we have a user 39 | const user = await Factory.model('App/Models/User').create() 40 | 41 | // And a generated post 42 | const post = await Factory.model('App/Models/Post').make() 43 | 44 | // And we are logged on the post form page 45 | const page = await browser.visit('/posts/create', (request) => { 46 | request.loginVia(user) 47 | }) 48 | 49 | // When we fill and send the form 50 | await page 51 | .type('[name="body"]', post.body) 52 | .submitForm('form') 53 | .waitForNavigation() 54 | 55 | // We expect to be again on the post form page 56 | await page.assertPath('/posts/create') 57 | 58 | // And to see the body filled 59 | await page.assertValue('[name="body"]', post.body) 60 | 61 | // And we expect to see a form error 62 | await page.assertExists('small.form-text') 63 | }) 64 | 65 | test('a post should have a body', async ({ browser }) => { 66 | // Given we have a user 67 | const user = await Factory.model('App/Models/User').create() 68 | 69 | // And a generated post 70 | const post = await Factory.model('App/Models/Post').make() 71 | 72 | // And we are logged on the post form page 73 | const page = await browser.visit('/posts/create', (request) => { 74 | request.loginVia(user) 75 | }) 76 | 77 | // When we fill and send the form 78 | await page 79 | .type('[name="title"]', post.title) 80 | .submitForm('form') 81 | .waitForNavigation() 82 | 83 | // We expect to be again on the post form page 84 | await page.assertPath('/posts/create') 85 | 86 | // And to see the title filled 87 | await page.assertValue('[name="title"]', post.title) 88 | 89 | // And we expect to see a form error 90 | await page.assertExists('small.form-text') 91 | }) 92 | -------------------------------------------------------------------------------- /vowfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Vow file 6 | |-------------------------------------------------------------------------- 7 | | 8 | | The vow file is loaded before running your tests. This is the best place 9 | | to hook operations `before` and `after` running the tests. 10 | | 11 | */ 12 | 13 | const ace = require('@adonisjs/ace') 14 | 15 | module.exports = (cli, runner) => { 16 | runner.before(async () => { 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Start the server 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Starts the http server before running the tests. You can comment this 23 | | line, if http server is not required 24 | | 25 | */ 26 | use('Adonis/Src/Server').listen(process.env.HOST, process.env.PORT) 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Run migrations 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Migrate the database before starting the tests. 34 | | 35 | */ 36 | await ace.call('migration:run') 37 | }) 38 | 39 | runner.after(async () => { 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown server 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Shutdown the HTTP server when all tests have been executed. 46 | | 47 | */ 48 | use('Adonis/Src/Server').getInstance().close() 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Rollback migrations 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Once all tests have been completed, we should reset the database to it's 56 | | original state 57 | | 58 | */ 59 | await ace.call('migration:reset') 60 | }) 61 | } 62 | --------------------------------------------------------------------------------