README
+TITLE: Login and Signup
+DATE: [2019-02-06 Wed]
+AUTHOR: Philipp Uhl
- Backend
** Usage
npm i --save @apparts/login-serverAdd 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
- The table name (default
** Configuration
In =db-config=
bigIntAsNumbermust betrue
In =types-config=
idTypemust 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-tokenwelcomeMail {object}:: The object should have keystitleandbody. 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 keystitleandbody. 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 thebody-type definitions ofPOST /v/1/userand can be used to validate extra parameters on user creation. These extra parameters will be passed to thesetExtrafunction (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 accessthis.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 accessthis.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 foremail) that where present on the call ofPOST /v/1/user. It can set the values intothis.content. The content will be saved afterwards automatically. To validate the types of the values, you also can configureextraTypes(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"
- 200,
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""
- 200,
*** 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
loginTokenor atokenforreset
- Token can either be the
- =Authorization= with =Basic base64(username:token)=.
- 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"
- 200,
*** 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"
- 200,
- 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]]