|
| 1 | + |
| 2 | + |
| 3 | +> Opinionated user management service for AdonisJs |
| 4 | +
|
| 5 | +Since AdonisJs is all about removing redundant code from your code base. This add-on is another attempt for same. |
| 6 | + |
| 7 | + |
| 8 | +## What is Persona? |
| 9 | + |
| 10 | +Persona is a simple functional service to let you **create**, **verify** and **update** user profiles. |
| 11 | + |
| 12 | +Persona is not for everyone, if your login system is too complex and rely on many factors, then Persona is not for you. **However, persona works great for majority of use cases**. |
| 13 | + |
| 14 | +## What is does? |
| 15 | +1. It helps you in registering new users. |
| 16 | +2. Generate email verification token. |
| 17 | +3. Validate credentials on login. |
| 18 | +4. On email change, set the user account to `pending` state and re-generate the email verification token. |
| 19 | +5. Allow password change. |
| 20 | +6. Allow forget password. |
| 21 | + |
| 22 | +## What is doesn't? |
| 23 | + |
| 24 | +1. Do not generate any routes, controllers or views for you. |
| 25 | +2. Do not send emails. However emit events that you can catch and send emails. |
| 26 | +3. Doesn't set any sessions or generate JWT tokens |
| 27 | + |
| 28 | + |
| 29 | +## Setup |
| 30 | +Run the following command to grab the add-on from npm. |
| 31 | + |
| 32 | +```bash |
| 33 | +adonis install @adonisjs/persona |
| 34 | + |
| 35 | +# for yarn |
| 36 | +adonis install @adonisjs/persona --yarn |
| 37 | +``` |
| 38 | + |
| 39 | +And then register the provider inside the providers array. |
| 40 | + |
| 41 | +```js |
| 42 | +const providers = [ |
| 43 | + '@adonisjs/persona/providers/PersonaProvider' |
| 44 | +] |
| 45 | +``` |
| 46 | + |
| 47 | +And then you can access it as follows |
| 48 | + |
| 49 | +```js |
| 50 | +const Persona = use('Persona') |
| 51 | +``` |
| 52 | + |
| 53 | +## Config |
| 54 | + |
| 55 | +The config file is saved as `config/persona.js`. |
| 56 | + |
| 57 | +| Key | Value | Description | |
| 58 | +|-----|--------|------------| |
| 59 | +| uids | ['email'] | An array of database columns, that will be used as `uids`. If your system allows, `username` and `emails` both, then simply add them to this array. |
| 60 | +| email | email | The field to be used as email. Everytime user changes the value of this field, their account will be set to `pending` state. |
| 61 | +| password | password | The field to be used as password. |
| 62 | +| model | App/Models/User | The user model to be used. |
| 63 | +| newAccountState | pending | What is the account state of the user, when they first signup. |
| 64 | +| verifiedAccountState | active | The account state of the user when they verify their email address |
| 65 | +| dateFormat | YYYY-MM-DD HH:mm:ss | Your database date format, required for finding if the token has been expired or not. |
| 66 | +| validationMessages | function | A function that returns an object of messages to be used for validation. It is same the validator custom messages. |
| 67 | + |
| 68 | +## Constraints |
| 69 | + |
| 70 | +There are some intentional constraints in place. |
| 71 | + |
| 72 | +1. Only works with `Lucid` models. |
| 73 | +2. The `App/Models/User` must have a relationship setup with `App/Models/Token` and vice-versa. |
| 74 | + |
| 75 | + ```js |
| 76 | + class User extends Model { |
| 77 | + tokens () { |
| 78 | + return this.hasMany('App/Models/Token') |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + class Token extends Model { |
| 83 | + user () { |
| 84 | + return this.belongsTo('App/Models/User') |
| 85 | + } |
| 86 | + } |
| 87 | + ``` |
| 88 | + |
| 89 | + 3. User table must have a column called `account_status`. |
| 90 | + |
| 91 | +## API |
| 92 | + |
| 93 | +Let's go through the API of persona. |
| 94 | + |
| 95 | +#### register(payload, [callback]) |
| 96 | + |
| 97 | +> The optional `callback` is invoked with the original payload, just before the user is saved to the database. So this is your chance to attach any other properties to the payload. |
| 98 | +
|
| 99 | +The register method takes the user input data and perform following actions on it. |
| 100 | + |
| 101 | +1. Validate that all `uids` are unique. |
| 102 | +2. Email is unique and is a valid email address. |
| 103 | +3. Password is confirmed. |
| 104 | +4. Creates user account with the `account_status = pending`. |
| 105 | +5. Generate and save email verification token inside the `tokens` table. |
| 106 | +5. Emits `user::created` event. You can listen this event to send an email to the user. |
| 107 | + |
| 108 | +```js |
| 109 | +const Persona = use('Persona') |
| 110 | + |
| 111 | +async register ({ request, auth, response }) { |
| 112 | + const payload = request.only(['email', 'password', 'password_confirmation']) |
| 113 | + |
| 114 | + const user = await Persona.register(payload) |
| 115 | + |
| 116 | + // optional |
| 117 | + await auth.login(user) |
| 118 | + response.redirect('/dashboard') |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +#### verify(payload, [callback]) |
| 123 | + |
| 124 | +> The optional `callback` is invoked with the user instance, just before the password verification. So this is your chance to check for `userRole` or any other property you want. |
| 125 | +
|
| 126 | +Verify the user credentials. The value of `uid` will be checked against all the `uids`. |
| 127 | + |
| 128 | +```js |
| 129 | +async login ({ request, auth, response }) { |
| 130 | + const payload = request.only(['uid', 'password']) |
| 131 | + const user = await Persona.verify(payload) |
| 132 | + |
| 133 | + await auth.login(user) |
| 134 | + response.redirect('/dashboard') |
| 135 | +}) |
| 136 | +``` |
| 137 | + |
| 138 | +#### verifyEmail(token) |
| 139 | + |
| 140 | +Verify user email using the token. Ideally it will be after someone clicks a URL from their email address. |
| 141 | + |
| 142 | +1. It will remove the token from the tokens table. |
| 143 | +2. Set user `account_status = active`. |
| 144 | + |
| 145 | +```js |
| 146 | +async verifyEmail ({ params, session, response }) { |
| 147 | + const user = await Persona.verifyEmail(params.token) |
| 148 | + |
| 149 | + session.flash({ message: 'Email verified' }) |
| 150 | + response.redirect('back') |
| 151 | +}) |
| 152 | +``` |
| 153 | + |
| 154 | +#### updateProfile(user, payload) |
| 155 | + |
| 156 | +Updates the user columns inside the database. However, if email is changed, then it will perform following steps. |
| 157 | + |
| 158 | +> Note this method will throw exception if user is trying to change the password. |
| 159 | +
|
| 160 | +1. Set user `account_status = pending`. |
| 161 | +2. Generate email verification token. |
| 162 | +3. Fire `email::changed` event. |
| 163 | + |
| 164 | +```js |
| 165 | +async update ({ request, auth }) { |
| 166 | + const payload = request.only(['firstname', 'email']) |
| 167 | + const user = auth.user |
| 168 | + await Persona.updateProfile(user, payload) |
| 169 | +}) |
| 170 | +``` |
| 171 | + |
| 172 | +#### updatePassword(user, payload) |
| 173 | + |
| 174 | +Updates the user password by performing following steps. |
| 175 | + |
| 176 | +1. Ensure `old_password` matches the user password. |
| 177 | +2. New password is confirmed. |
| 178 | +3. Updates the user password |
| 179 | +4. Fires `password::changed` event. You can use this event to send an email about password change. |
| 180 | + |
| 181 | +```js |
| 182 | +async updatePassword ({ request, auth }) { |
| 183 | + const payload = request.only(['old_password', 'password', 'password_confirmation']) |
| 184 | + const user = auth.user |
| 185 | + await Persona.updatePassword(user, payload) |
| 186 | +}) |
| 187 | +``` |
| 188 | + |
| 189 | +#### forgotPassword(uid) |
| 190 | + |
| 191 | +Take a forgot password request from the user by passing their `uid`. Uid will be matched for all the `uids` inside the config file. |
| 192 | + |
| 193 | +1. Find a user with the matching uid. |
| 194 | +2. Generate password change token. |
| 195 | +3. Emit `forgot::password` event. You can use this event to send the email with the token to reset the password. |
| 196 | + |
| 197 | +```js |
| 198 | +forgotPassword ({ request }) { |
| 199 | + await Persona.forgotPassword(request.input('uid')) |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +#### updatePasswordByToken(token, payload) |
| 204 | + |
| 205 | +Update the user password by using a token. This method will perform following checks. |
| 206 | + |
| 207 | +1. Make sure token is valid and not expired. |
| 208 | +2. Ensure password is confirmed. |
| 209 | +3. Update user password. |
| 210 | + |
| 211 | +```js |
| 212 | +updatePasswordByToken ({ request, params }) { |
| 213 | + const token = params.token |
| 214 | + const payload = request.only(['password', 'password_confirmation']) |
| 215 | + |
| 216 | + const user = await Persona.updatePasswordByToken(payload) |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +## Custom messages |
| 221 | +You can define a function inside `config/persona.js` file, which returns an object of messages to be used as validation messages. The syntax is same as the `Validator` custom messages. |
| 222 | + |
| 223 | +```js |
| 224 | +{ |
| 225 | + validationMessages (action) => { |
| 226 | + return { |
| 227 | + 'email.required': 'Email is required', |
| 228 | + 'password.mis_match': 'Invalid password' |
| 229 | + } |
| 230 | + } |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | +The `validationMessages` method gets an `action` parameter. You can use it to customize the messages for different actions. Following is the list of actions. |
| 235 | + |
| 236 | +1. register |
| 237 | +2. login |
| 238 | +3. emailUpdate |
| 239 | +4. passwordUpdate |
| 240 | + |
| 241 | +## Events emitted |
| 242 | + |
| 243 | +Below is the list of events emitted at different occasion. |
| 244 | + |
| 245 | +| Event | Payload | Description | |
| 246 | +|--------|--------|-------------| |
| 247 | +| user::created | `{ user, token }` | Emitted when a new user is created | |
| 248 | +| email::changed | `{ user, oldEmail, token }` | Emitted when user changes their email address |
| 249 | +| password::changed | `{ user }` | When user change their password by providing the old password | |
| 250 | +| forgot::password | `{ user, token }` | Emitted when user asks for a token to change their password. |
| 251 | +| password::recovered | `{ user }` | Emitted when user password is changed using the token | |
| 252 | + |
| 253 | +## Exceptions raised |
| 254 | + |
| 255 | +The entire API is driven by exceptions, which means you will hardly have to write `if/else` statements. |
| 256 | + |
| 257 | +This is great, since Adonis allows managing response by catching exceptions globally. |
| 258 | + |
| 259 | +#### ValidationException |
| 260 | +The validation exception is raised when validation fails. If you are already handling `Validator` exceptions, then you won't have to do anything special. |
| 261 | + |
| 262 | +#### InvalidTokenException |
| 263 | +Raised when the token user is using to verify their email, or reset password is invalid. |
0 commit comments