npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@apparts/login-server

v3.0.1

Published

Login and Signup for Web, Apps, Server

Downloads

16

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(/* , */);

        // 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]]).

*** Defining password policies

To require certain passwords, you can overwrite the =setPw= function of the user model. The new function should validate the password requirements and in case of success call =await super.setPw(password)= to set the password. In case of rejection a =PasswordNotValidError= should be thrown.

#+BEGIN_SRC js const { PasswordNotValidError } = require("@apparts/login-server");

//...

async setPw(password) { if (password.length < 10) { throw new PasswordNotValidError("Password must be 10+ characters"); } await super.setPw(password); return this; } #+END_SRC

*** Login: Exponential Backoff

To archive exponential backoff you need to create a database table =logins= and use the =createUseLogins= function to generate the model.

Then you can overwrite the =checkAuthPw= function of the user model:

#+BEGIN_SRC js const { createUseLogins } = require("@apparts/login-server"); const { useLogin } = createUseLogins();

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

//...

checkAuthPw(password) { return checkAuthPwExponential( this._dbs, useLogin, this.content.id, password, (password) => super.checkAuthPw(password) ); } #+END_SRC

*** 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]]