Git Product home page Git Product logo

persona's Introduction

Opinionated user management service for AdonisJs

Make sure @adonisjs/framework version is >= 5.0.6

AdonisJs is all about removing redundant code from your code base. This add-on tries to do the same.

What is Persona?

Persona is a simple, functional service to let you create, verify and update user profiles.

Persona is not for everyone; if your login system is too complex and relies on many factors, Persona is not for you. However, persona works great for most use cases.

What does it do?

  1. Helps you register new users.
  2. Generates email verification tokens.
  3. Validates credentials on login.
  4. On email change, sets the user account to a pending state and re-generates the email verification token.
  5. Allows changing passwords.
  6. Allows recovering forgotten passwords.

What does it NOT do?

  1. Does not generate any routes, controllers or views for you.
  2. Does not send emails. However, it emits events that you can use to send emails.
  3. Does not create sessions or generate JWT tokens.

Setup

Run the following command to grab the add-on from npm:

adonis install @adonisjs/persona

# for yarn
adonis install @adonisjs/persona --yarn

Follow up by registering the provider inside the providers array:

const providers = [
  '@adonisjs/persona/providers/PersonaProvider'
]

You may then access it as follows:

const Persona = use('Persona')

Config

The config file is saved as config/persona.js.

Key Value Description
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.
email email The field to be used as email. Every time a user changes the value of this field, their account will be set to the pending state.
password password The field to be used as password.
model App/Models/User The user model to be used.
newAccountState pending The default account state for new users.
verifiedAccountState active The account state for users after verifying their email address.
dateFormat YYYY-MM-DD HH:mm:ss Your database date format, required for determining if the token has been expired or not.
validationMessages function A function that returns an object of messages to be used for validation. The syntax is the same as Validator custom messages.

Constraints

There are some intentional constraints in place.

  1. Only works with Lucid models.

  2. The App/Models/User must have a relationship setup with App/Models/Token and vice-versa.

    class User extends Model {
      tokens () {
        return this.hasMany('App/Models/Token')
      }
    }
    
    class Token extends Model {
      user () {
        return this.belongsTo('App/Models/User')
      }
    }
  3. User table must have a string column called account_status.

API

Let's go through the API of persona.

register(payload, [callback])

The optional callback is invoked with the original payload just before the user is saved to the database. You can use it if you need to attach any other properties to the payload.

The register method takes the user input data and performs the following actions on it.

  1. Validates that all uids are unique.
  2. Checks that email is unique and is a valid email address.
  3. Makes sure the password is confirmed.
  4. Creates a new user account with the account_status = pending.
  5. Generates and saves an email verification token inside the tokens table.
  6. Emits a user::created event. You can listen for this event to send an email to the user.

Make sure to use querystring module to encode the token when sending via Email.

const Persona = use('Persona')

async register ({ request, auth, response }) {
  const payload = request.only(['email', 'password', 'password_confirmation'])

  const user = await Persona.register(payload)

  // optional
  await auth.login(user)
  response.redirect('/dashboard')
}

verify(payload, [callback])

The optional callback is invoked with the user instance just before the password verification. You can use it to check for userRole or any other property you want.

Verifies the user credentials. The value of uid will be checked against all the uids.

async login ({ request, auth, response }) {
  const payload = request.only(['uid', 'password'])
  const user = await Persona.verify(payload)

  await auth.login(user)
  response.redirect('/dashboard')
}

verifyEmail(token)

Verifies the user's email using the token. Ideally that should be after someone clicks a URL from their email address.

  1. Removes the token from the tokens table.
  2. Set user account_status = active.
async verifyEmail ({ params, session, response }) {
  const user = await Persona.verifyEmail(params.token)

  session.flash({ message: 'Email verified' })
  response.redirect('back')
}

updateProfile(user, payload)

Updates the user columns inside the database. However, if the email is changed, it performs the following steps:

Please note that this method will throw an exception if the user is trying to change the password.

  1. Sets the user's account_status = pending.
  2. Generates an email verification token.
  3. Fires the email::changed event.
async update ({ request, auth }) {
  const payload = request.only(['firstname', 'email'])
  const user = auth.user
  await Persona.updateProfile(user, payload)
}

updatePassword(user, payload)

Updates the user's password by performing the following steps:

Make sure to have the beforeSave hook in place for hashing the password. Otherwise the password will be saved as a plain string.

  1. Ensures old_password matches the user's password.
  2. Makes sure the new password is confirmed.
  3. Updates the user password.
  4. Fires the password::changed event. You can listen for this event to send an email about the password change.
async updatePassword ({ request, auth }) {
  const payload = request.only(['old_password', 'password', 'password_confirmation'])
  const user = auth.user
  await Persona.updatePassword(user, payload)
}

forgotPassword(uid)

Takes a forgot password request from the user by passing their uid. Uid will be matched against all the uids inside the config file.

  1. Finds a user with the matching uid.
  2. Generates a password change token.
  3. Emits the forgot::password event. You can listen for this event to send an email with the token to reset the password.
async forgotPassword ({ request }) {
  await Persona.forgotPassword(request.input('uid'))
}

updatePasswordByToken(token, payload)

Updates the user password using a token. This method performs the following checks:

  1. Makes sure the token is valid and not expired.
  2. Ensures the password is confirmed.
  3. Updates the user's password.
async updatePasswordByToken ({ request, params }) {
  const token = params.token
  const payload = request.only(['password', 'password_confirmation'])

  const user = await Persona.updatePasswordByToken(token, payload)
}

Custom messages

You can define a function inside the config/persona.js file, which returns an object of messages to be used as validation messages. The syntax is the same as Validator custom messages.

{
  validationMessages (action) => {
    return {
      'email.required': 'Email is required',
      'password.mis_match': 'Invalid password'
    }
  }
}

The validationMessages method gets an action parameter. You can use it to customize the messages for different actions. Following is the list of actions.

  1. register
  2. login
  3. emailUpdate
  4. passwordUpdate

Events emitted

Below is the list of events emitted at different occasions.

Event Payload Description
user::created { user, token } Emitted when a new user is created
email::changed { user, oldEmail, token } Emitted when a user changes their email address
password::changed { user } Emitted when a user changes their password by providing the old password
forgot::password { user, token } Emitted when a user asks for a token to change their password
password::recovered { user } Emitted when a user's password is changed using the token

Exceptions raised

The entire API is driven by exceptions, which means you will hardly have to write if/else statements.

This is great, since Adonis allows managing responses by catching exceptions globally.

ValidationException

Raised when validation fails. If you are already handling Validator exceptions, you won't have to do anything special.

InvalidTokenException

Raised when a supplied token, to verify an email or reset password with, is invalid.

Custom rules

At times, you may want to have custom set of rules when registering or login new users. You can override following methods for same.

The code can be added inside the hooks file or even in the registeration controller.

registerationRules

Persona.registerationRules = function () {
  return {
    email: 'required|email|unique:users,email',
    password: 'required|confirmed'
  }
}

updateEmailRules

Persona.updateEmailRules = function (userId) {
  return {
    email: `required|email|unique:users,email,id,${userId}`
  }
}

updatePasswordRules

Persona.updatePasswordRules = function (enforceOldPassword = true) {
  if (!enforceOldPassword) {
    return {
      password: 'required|confirmed'
    }
  }

  return {
    old_password: 'required',
    password: 'required|confirmed'
  }
}

loginRules

Persona.loginRules = function () {
  return {
    uid: 'required',
    password: 'required'
  }
}

persona's People

Contributors

draftproducts avatar grachov avatar rhwilr avatar romainlanz avatar thetutlage avatar xstoudi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

persona's Issues

Route not found

Hi,

i know that i must use querystring to encode the token... i have following view code:

@section('content')
  <p class="title"> Sehr geehrte(r) {{ user.firstName }} {{ user.lastName }},</p>
  <p>
    Sie haben für <span class="app-title">{{ app.title }}</span> ein neues Passwort angefordert!<br>Um diese Aktion bearbeiten zu können, müssen Sie diese nochmals bestätigen.
  </p>
  Bitte bestätigen Sie diese Aktion!
  <a href="{{ urlEncode(confirmURL) }}">&lt;hier klicken&gt;</a>
@endsection

and the mails a send from the events like this:

Event.on('forgot::password', async ({ user, token }) => {
  // collect the data for the template
  let mailParams = {
    app: appData,
    user: user,
    confirmURL: Env.get('PUBLIC_BASE_URL') + '/verifyToken/' + qs.escape(token)
  };

  try {
    // send the mail
    await Mail.send('emails.forgotpassword', mailParams, message => {
      message.from(Env.get('MAIL_FROM'));
      message.to(user.email, user.firstName + ' ' + user.lastName);
      message.subject('Passwort vergessen');

But sometimes, not often, the token in the mail contains backslash, so the router stopped there and did not find the route.

What´s wrong, what did I forget?

Adonis v5 support

I'm currently trying to upgrade a project from adonis v4 to v5 and it seems like Persona is not compatible yet. It would be great if Persona would be compatible, as I currently see no way of upgrading with the old Persona version.

Problem when not using email instead of uid

Hi there,

currently i play with persona an get some small problem

in the config i defined:

uids: ['email'],

When i try to login i always get an error like "Undefined binding(s) [...]"

i look in the code and found follwing:

  async verify (payload, callback) {
    await this.runValidation(payload, this.loginRules(), 'verify')
    const user = await this.getUserByUids(payload.uid)    /// REALLY payload.uid???

    const enteredPassword = this._getPassword(payload)
    const userPassword = this._getPassword(user)

    if (typeof (callback) === 'function') {
      await callback(user, enteredPassword)
    }

    await this.verifyPassword(enteredPassword, userPassword)

    return user
  }

i think the problem is the hardcoded payload.uid above?

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Managing Hached email

It would be great to have an option in the connection process that enables to use hached email. I work on a game where gamer should be anonymous. This part of the principle as the population we are targeting is very skeptical regarding marketing.
Therefor I need a connection system where the email is hached. If the length of the hached key is sufficient we have almost no probability of contention (2 different emails giving the same hached key). I did it in a previous project, we kept the hached email in the databse, the gamer give his email, we hache it and we compare the hached values. To recover his password he should give is email that we use for the recovering process.

2FA Support

add to the module the generation and support of 2fa codes

login error when user is not validated

Hi.
This are the errors I'm getting when using Persona with bad password, bad user or account not validated:

const payload = request.only(['email', 'password'])
const user = await Persona.verify(payload)

Response:

field: "uid"
message: "required validation failed on uid"
validation: "required"

I want to send the the following response when the user is not validated:
{'account_status':'Cuenta pendiente de validacion'}
but I can't find on the documentation how to do it:

Please your help.

B/R

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Can't update custom messages on verify action

Is an issue? If i try to login with email of a non existing user, the uid.exists key is not exists under verify, but it does under undefined key.

./src/Persona.js@449
./src/Persona.js@135

./config/persona.js

module.exports = {
  validationMessages (action) {
    switch (action) {
      case 'passwordUpdate':
        return {
          'old_password.mis_match': '>>>Doesnt work<<<'
        }
      case 'verify':
        return {
          'uid.exists': '>>>Doesnt work<<<',
          'password.mis_match': '>>>Doesnt works<<<'
        }
      case undefined:
        return {
          'uid.exists': '>>>It works<<<',
          'old_password.mis_match': '>>>It works<<<',
          'password.mis_match': '>>>It works<<<'
        }
      default:
        return {}
    }
}

./app/Controllers/Http/AuthController.js

class AuthController {
  async login ({ request, response, auth }) {
    Persona.loginRules = () => ({
      uid: 'required|email',
      password: 'required'
    })
    const payload = request.only(['uid', 'password'])
    try {
      const user = await Persona.verify(payload)
      await auth.login(user)
    } catch (error) {
      return response.send({ error })
    }
  }
}

node: v11.7.0, npm: 6.6.0, Adonis: 4.1.0

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Validation exception on failed login

This is not a bug of some sort, it more of an enhancement. Instead of throwing a validation exception on failed login, maybe throw a generic error of some sort then we can catch and handle the error however we like.

Also, displaying individual error for wrong email and password (can not locate user and password is invalid) on login seem less secured to me.

Doc: Persona.register method - how to encode email token for url

I had an issue where I was trying to encode the email token that is added to the token table on user registration.

The docs state that the token should be encoded using the querystring module but when I try to use the encode method from this module on the token it returns an empty string.

Make sure to use querystring module to encode the token when sending via Email.

start/events.js

const querystring = require("querystring");
const Env = use("Env");
const Event = use("Event");
const Mail = use("Mail");

Event.on("user::created", async (payload) => {
  const { user, token } = payload;

  const encodedToken = querystring.encode(token);
  console.log("Query string encode: ", encodedToken);

  // await Mail.send(
  //   "new.user",
  //   { user: user.toJSON(), token: encodeURIComponent(token) },
  //   (message) => {
  //     message
  //       .to(payload.user.email)
  //       .from(`<${Env.get("MAIL_USERNAME")}>`)
  //       .subject("Thanks for registering!");
  //   }
  // );
});

I did work around this by using uriEncodeComponent on the token and then constructing the email link url in the view.

start/events.js

const querystring = require("querystring");
const Env = use("Env");
const Event = use("Event");
const Mail = use("Mail");

Event.on("user::created", async (payload) => {
  const { user, token } = payload;

  const uriEncodedToken = encodeURIComponent(token);
  console.log("URI Encoded Token: ", uriEncodedToken);

  await Mail.send(
    "new.user",
    { user: user.toJSON(), token: encodeURIComponent(token) },
    (message) => {
      message
        .to(payload.user.email)
        .from(`<${Env.get("MAIL_USERNAME")}>`)
        .subject("Thanks for registering!");
    }
  );

});

new/user.edge

...
  <a href="http://localhost:3000/users/verify-email?token={{token}}">Verify your email</a>
...

I know Persona isn't concerned with sending emails itself but since the querystring mention seems to not be working (or I'm not using it as intended), I'm wondering if the docs could be clarified on how to get this working.

Here's my demo repo

adonis version.

Hi.
Adonisjs stable version is currently on version 4.1 and Persona is asking for 5.0.6. How can I use it on Adonis 4.1?

Adding to payload in events that are fired

My use case specifically is that I am using 'api-only' for Adonis, and that the emails being sent might have different end points for verifying the email depending on which front-end is making the request. I'd like to add the endpoint in the post method from the client to the server, and was hoping if there was a way to add some of this data through to the Persona methods that themselves could pass along to the events fired.

For instance, if forgotPassword is firing the 'forgot::password' event there could possibly be a parameter for eventsPayload or something? I'm aware that I could just use the Event.on() in the Controller, but was hoping to keep these things in the normal Adonis way.

If this cannot be done, or shouldn't be done out of principal, it's all cool.

confirmed validation failed on password on create user

I'm getting the following error after setting up Persona, when I try to register a new user:

{ ValidationException: E_VALIDATION_FAILED: Validation failed
    at Function.validationFailed (<node_modules path>\@adonisjs\validator\src\Exceptions\index.js:21:19)
    at Persona.runValidation (<node_modules path>\@adonisjs\persona\src\Persona.js:392:48)
    at <anonymous>
  messages:
   [ { message: 'confirmed validation failed on password',
       field: 'password',
       validation: 'confirmed' } ] }

Here's my app/Controllers/Http/AuthController.js

const Encryption = use('Encryption')
const Token = use('App/Models/Token')
const Persona = use('Persona')

class AuthController {
  async register({ request, response }) {
    const data = request.only(['email', 'password', 'password_confirmation'])
    
    try {
      const user = await Persona.register(data)
      return response.send({ message: 'User has been created' })
    } catch (err) {
      console.error(err)
      response.status(401).send({ error: 'Couldn\'t create user. Please try again' })
    }
  }

  <other functions>
}

Here's the data I'm sending:

{
  "password": "password",
  "email": "[email protected]"
}

There's nothing in README.md about the password_confirmation part - should I be sending something here?

Generate token that is valid longer than 24h

When I create a new user in my application I use Persona.generateToken(user, 'password') and send the token to the user via mail. The user can then set their initial password via the password reset route.
But I want the token to be valid for longer than 24h.

https://github.com/adonisjs/adonis-persona/blob/b28c5231c4355f8a11a2e357e90807ffd77ab70b/src/Persona.js#L156-L161

The constraint is static and added when checking the token, but also used in generateToken to check if a token already exists. I wanted to submit a PR that adds a validUntil property, but I would have to pass it in multiple places (when generating and when checking).

I'm wondering if it is the right approach to use the updated_at field to check if a token is valid of if we should add a valid_until field? This way we can define the duration a token is valid when generating it and use different durations for different tokens.

Is this project active?

Hi - just wondering if this project is still actively maintained and if it's the default Adonis go-to solution for user management?

Thanks in advance.

Break after Persona.verify(payload)

If I enter the wrong credentials the application breaks in the following.
Captura de tela de 2019-09-17 19-45-02

Wasn't it just returning the error for not being able to log in?

  async login ({ request, response, auth }) {
    const payload = request.only(['uid', 'password'])

    const user = await Persona.verify(payload)

    return await auth.generate(user)
  }

Sugestion uuid

Using complex token, does not allow me to make use of in the url.

Example: updatepassword/9ab5e3150e712f27d259c6965003581fWuV6rvLY+b7CGb/JaJ3gRPIefVFvdI5tjSI+IoUXsRU=

ps: this hash has /

result:
"message": "Route not found POST /requestforgotpassword/9ab5e3150e712f27d259c6965003581fWuV6rvLY+b7CGb/JaJ3gRPIefVFvdI5tjSI+IoUXsRU=",

code:

Route
  .post('/updatepassword/:token', 'User/TokenController.changePassword')
  .validator('User/forgotpassword')

https://www.npmjs.com/package/uuid resolve this problem

I brand new library persona but response always welcome.edge

const Persona = use('Persona')

async register ({ request, auth, response }) {
  const payload = request.only(['email', 'password', 'password_confirmation'])

  const user = await Persona.register(payload)

response.send('hello');
}

response request is

AdonisJs simplicity will make you feel confident about your code

Don't know where to start? Read the documentation.

whats wrong ?

Cannot login after using Persona.verifyEmail

Edit :

I was using the wrong "Hooks" With the Lucid Model. I was using afterSave which is trigger After a new record has been created or updated.

I close the issue.

Hello,
...

tokens still remains after email verification.

In documentation it says verifyEmail removes the token from the tokens table but it isn't.

After calling await Persona.verifyEmail(token) "account_status" changes to "active" but token still remains in the tokens table.

Return forgot password token

I have this project that registers new users without a password but has email verification. Once verified, it triggers Persona's forgotPassword so the user could set his account's password. Then it will redirect to a new password form, passing the token. But using forgotPassword method won't return any value, so I just run a relationship query to grab the token.

It would be awesome if forgotPassword could somehow return the token and the user values.

Cheers =)

getToken constraints failing - is_revoked is never updated on forgotPassword()

After using forgotPassword(), it created the record in tokens table, but it doesn't add anything to "is_revoked" field. I've tried changing the datatype for "is_revoked" to TINYINT, BOOLEAN, BOOL, INT and VARCHAR and no luck.

The only way updatePasswordByToken() works is if I set "is_revoked" to "0". I'm guessing this is a issue on my end, but was wondering if anyone experienced anything like this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.