@apparts/login-server

Login and Signup for Web, Apps, Server

Usage no npm install needed!

<script type="module">
  import appartsLoginServer from 'https://cdn.skypack.dev/@apparts/login-server';
</script>

README

+TITLE: Login and Signup

+DATE: [2019-02-06 Wed]

+AUTHOR: Philipp Uhl

  • Backend

** Usage

  1. npm i --save @apparts/login-server

  2. Add the package to your codebase:

    +BEGIN_SRC js

    const { addRoutes, createUseUser } = require("@apparts/login-server"); const express = require("express");

    const { useUser } = createUseUser();

    const app = express(); // ...

    const mail = { async sendMail(to, body, title){ // ... } }; addRoutes(app, useUser, mail); // ...

    +END_SRC

    • If you need to specify the route version by yourself:

      +BEGIN_SRC js

      const newApiVersion = 2; addRoutes(app, useUser, mail, newApiVersion); // routes will start with /v/2/...

      const oldApiVersion = 1; addRoutes.upgrade(app, oldApiVersion, (_, res) => { res.status(410); res.send("Your app is too old. Please upgrade your app to the newest version."); }); // all /v/1/... routes will send 410

      +END_SRC

    • If you need to specify the route names (and methods) by yourself:

      +BEGIN_SRC js

      const { routes } = require("@apparts/login-server");

      const { addUser, getUser, getToken, getAPIToken, deleteUser, updateUser, resetPassword, } = routes(useUser, mail);

      // E.g.: app.post("/v/1/user", addUser); app.get("/v/1/user/login", getToken); app.get("/v/1/user/apiToken", getAPIToken); app.get("/v/1/user", getUser); app.delete("/v/1/user", deleteUser); app.put("/v/1/user", updateUser); app.post("/v/1/user/:email/reset", resetPassword);

      +END_SRC

    • If you need to configure the User-model:

      • The table name (default users):

        +BEGIN_SRC js

        const { createUseUser } = require("@apparts/login-server"); const { useUser } = createUseUser(undefined, "myTableName");

        +END_SRC

      • The types of the user (see [[https://github.com/phuhl/apparts-types#usage][@apparts/types]]):

        +BEGIN_SRC js

        const { createUseUser } = require("@apparts/login-server"); const { useUser } = createUseUser({ customElement: { type: "string", public: true }});

        +END_SRC

      • Overwriting functions of the user: See [[Overwriting functions of the User-model]]

      +BEGIN_SRC js

      const { makeModel } = require("@apparts/model");
      const { createUseUser } = require("@apparts/login-server");
      const { Users, User, NoUser } = createUseUser(/* <custom type>, <custom table name>*/);
      
      // e.g. for User
      class SpecialUser extends User {
        /* overwrite stuff here */
      }
      
      module.exports = makeModel("SpecialUser", [Users, SpecialUser, NoUser]);
      

      +END_SRC

** Configuration

In =db-config=

  • bigIntAsNumber must be true

In =types-config=

  • idType must be "int"

The configuration options for this component have to be stored accessible by the =apparts-config=-module through the name =login-config=. They are:

  • pwHashRounds {int} :: Amount of hash-rounds for password storing, passend on to =bcrypt=
  • tokenLength {int} :: Length of login-token
  • welcomeMail {object} :: The object should have keys title and body. Within both, all occurrences of the string "##NAME##" will be replaced with the users name. In the body, all occurences of the string "##URL##" will be replaced with a URL for setting the password and thus confirming the email address.
  • resetPWMail {object} :: The object should have keys title and body. Within both, all occurrences of the string "##NAME##" will be replaced with the users name. In the body, all occurences of the string "##URL##" will be replaced with a URL for resetting the password.
  • resetUrl {string} :: The URL, that will be send emails for setting and resetting passwords. Appended query parameters:
    • token {string} :: An auth token
    • ?welcome {bool} :: Set to true, if this is not from a password reset mail, but from a welcome mail.
  • extraTypes {?object} :: An optional object that contains type definitions as expected by the preperator of [[https://github.com/phuhl/apparts-types#usage][@apparts/types]]. Will be injected into the body-type definitions of POST /v/1/user and can be used to validate extra parameters on user creation. These extra parameters will be passed to the setExtra function (see [[Overwriting the User-model]]).

*** Customizing the User-model

*** Overwriting functions of the User-model

The user model can be overwritten to provide extra functionality. For more information on how to overwrite functions of the user model, see the documentation of [[https://github.com/phuhl/apparts-model#usage][@apparts/model]]. All of these functions are only called on the OneModel of the user, thus only the User has to be extended, not the Users or NoUser classes. The functions, explicitly intended for overwriting:

  • getWelcomeMail() {object} :: Returns the content of a welcome email that is send after registration. The function returns an object of the form { title: {string}, body: {string}}. The function can access this.content. It's content should contain a link with the reset token. Default implementation:

    +BEGIN_SRC js

    getWelcomeMail() { return { title: welcomeMail.title, body: welcomeMail.body .replace( /##URL##/g, resetUrl + ?token=${encodeURIComponent( this.content.tokenforreset )}&email=${encodeURIComponent(this.content.email)}&welcome=true ), }; }

    +END_SRC

  • getResetPWMail() {object} :: Returns the content of a reset password email. The function returns an object of the form { title: {string}, body: {string}}. The function can access this.content. It's content should contain a link with the reset token. Default implementation:

    +BEGIN_SRC js

    getResetPWMail() { return { title: resetMail.title, body: resetMail.body.replace( /##URL##/g, resetUrl + ?token=${encodeURIComponent( this.content.tokenforreset )}&email=${encodeURIComponent(this.content.email)} ), }; }

    +END_SRC

  • async setExtra(extraParams) {void} :: This function is called on user creation. It receives as parameter all the body parameters (except for email) that where present on the call of POST /v/1/user. It can set the values into this.content. The content will be saved afterwards automatically. To validate the types of the values, you also can configure extraTypes (see [[Configuration]]).
  • async getExtraAPITokenContent() {?object} :: This function can be used to inject extra information into the APIToken. Useful for providing a JWT that contains all necessary information for the API and thus reducing the amount of database calls.
  • async deleteMe() {void} :: This function can be overwritten to perform the necessary actions on deletion. Call the super function when overwriting!

** Provided REST-API

*** Create a user: POST =/v/1/user/=

  • Body Parameters
    • email {email} :: Email
  • Returns
    • 200, "ok"
    • 413, "User exists"

After successfully calling this API, an email will be send to email, containing a link for verifying the email. This link contains a token that can be used for the reset password API and thus can be used to set the password.

*** Get user info: GET =/v/1/user=

Returns the user info. All values that are set to public (see [[https://github.com/phuhl/apparts-model#usage][@apparts/model]]) in the extraTypes (see [[Configuration]]) are also returned.

  • Headers
    • =Authorization= with =Basic base64(username:token)=
  • Returns
    • 200, { id: {id}, email: {string}, [...public extra] }
    • 400, "Authorization wrong"
    • 401, "Unauthorized"
    • 401, "User not found""

*** Login: GET =/v/1/user/login=

  • Headers
    • =Authorization= with =Basic base64(username:password)=
  • Returns
    • 200, : { : type: "object", : values: { : id: { type: "id" }, : loginToken: { type: "base64" }, : apiToken: { type: "string" }, : }, : }
    • 400, "Authorization wrong"
    • 401, "Unauthorized"
    • 401, "User not found""

*** Refresh API Token: GET =/v/1/user/apiToken=

  • Headers
    • =Authorization= with =Bearer loginToken=
  • Returns
    • 200, : { : type: "string" : }
    • 400, "Authorization wrong"
    • 401, "Unauthorized"
    • 401, "User not found""

*** Update user: PUT =/v/1/user=

Update the user. All extra info must be updated over custom written APIs. Checking the password for a special password policy must be done by overwriting the async setPw(password) function. An example for checking for a minimum password length:

+BEGIN_SRC js

async setPw(password) { if (password.length <= 8) { throw new HttpError(400, "Password too short"); }

return await super.setPw(password);

}

+END_SRC

TODO: update email with verification email.

  • Body Parameters
    • password {password} :: Optional, the new password
  • Headers
    • =Authorization= with =Basic base64(username:token)=.
      • Token can either be the loginToken or a tokenforreset
  • Returns
    • 200, : { : type: "object", : values: { : id: { type: "id" }, : loginToken: { type: "base64" }, : apiToken: { type: "string" }, : }, : }
    • 400, "Authorization wrong"
    • 400, "Nothing to update"
    • 400, "Password required"
    • 401, "Unauthorized"
    • 401, "User not found""

*** Request password reset: POST =/v/1/user/:email/reset=

  • Path Parameters
    • email {email} :: Email of the user to be changed
  • Returns
    • 200, "ok"
    • 404, "User not found"

*** Delete a user: DELETE =/v/1/user=

This function does not delete the user. It only disables access to the login server API in any way. To the outside it should not be visible, if the user is disabled or non-existing. To delete a user, overwrite the async deleteMe() {void} (see [[Overwriting the User-model]]) function of the User object. The reason for this is, that the use of foreign keys in databases might be disturbed by deleting the entity from the database.

  • Headers
    • =Authorization= with =Basic base64(username:password)=
  • Returns
    • 200, "ok"
    • 400, "Authorization wrong"
    • 401, "Unauthorized"
    • 401, "User not found"
  • Flows

** Signup

+BEGIN_SRC plantuml :file signup.png :exports results

skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false actor User

group Signup User -> Loginservice : POST /v/1/user activate Loginservice Loginservice -> Mailserver : Send mail with token activate Mailserver User <-- Loginservice : "ok" deactivate Loginservice User <-- Mailserver : Mail with token deactivate Mailserver

User -> Loginservice : PUT /v/1/user [token] activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice end

+END_SRC

+RESULTS:

[[file:signup.png]] ** Login and API-flow

+BEGIN_SRC plantuml :exports results :file login.png

actor User skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false

group Login User -> Loginservice : GET /v1/user/login [PW] activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice end

group API request User -> API : api request [JWT] activate API API --> User : response deactivate API note right The API does not need to contact the Loginservice, as all required data is in the JWT end note end

group Refresh token

... JWT expire time reached ...

User -> API : api request [stale JWT] Activate API User <--x API : 401 deactivate API

User -> Loginservice : GET /v1/user/apiToken [loginToken] activate Loginservice User <-- Loginservice : JWT deactivate Loginservice

User -> API : api request with [JWT] activate API API --> User : response deactivate API end

+END_SRC

+RESULTS:

[[file:login.png]]

** Password reset

+BEGIN_SRC plantuml :file resetpw.png :exports results

actor User skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false

User -> Loginservice : GET /v1/user/login [wrong PW] activate Loginservice User <--x Loginservice : 401 deactivate Loginservice

User -> Loginservice : POST /v/1/user/:email/reset activate Loginservice Loginservice -> Mailserver : Send mail with token activate Mailserver User <-- Loginservice : "ok" deactivate Loginservice User <-- Mailserver : Mail with token deactivate Mailserver

User -> Loginservice : PUT /v/1/user [token] activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice

+END_SRC

+RESULTS:

[[file:resetpw.png]]