fragments-user

have a token-auth protected API running within minutes with fragments

Usage no npm install needed!

<script type="module">
  import fragmentsUser from 'https://cdn.skypack.dev/fragments-user';
</script>

README

fragments-user

the documentation in this readme is work in progress and currently unfinished !

fragment-user is currently in alpha state and subject to breaking changes without notice.

ALPHA NPM Package Build Status codecov.io Dependencies Downloads per Month

have a token-auth protected API running within minutes with fragments

fragments is an up and coming Node.js library that structures web applications with (request time) dependency injection.

fragments-user is a collection of factories for fragments that provide small to large building blocks for the rapid and fun development of maintainable, testable and elegant postgres-backed rest APIs with token-based-auth (JWT) and rights management. it comes preloaded with authentication and user management API endpoints.

fragments-user also serves as an example-application for fragments.
the tests for that example-application serve as additional integration tests for fragments.

install

npm install fragments-user

in your fragments app file make sure you are using fragments-postgres and fragments-user:

#!/usr/bin/env node

var hinoki = require('hinoki');
var fragments = require('fragments');
var fragmentsPostgres = require('fragments-postgres');
var app = hinoki.source(__dirname + '/src/factories');

var source = hinoki.source([
  app,
  fragmentsPostgres,
  fragments.source,
  fragments.umgebung,
]);

source = hinoki.decorateSourceToAlsoLookupWithPrefix(source, 'fragments_');

module.exports = fragments(source);

if (require.main === module) {
  module.exports.runCommand();
}

configuration environment variables

make sure that the following environment variables are set to your own values:

export PORT=8080
export BASE_URL="http://localhost:$PORT"

export MIGRATION_PATH="migrations"

export POSTGRES_DATABASE='my_database'
export DATABASE_URL="postgres://localhost:5432/$POSTGRES_DATABASE"
export POSTGRES_POOL_SIZE=40

export JWT_ENCRYPTION_PASSWORD='replace this with your super secret jwt encryption password'
export JWT_SIGNING_SECRET='replace this with your super secret jwt signing secret'

commands

call ./app to see a list of all available commands.

fragments-user itself adds the following commands:

rights {user-id} - list the rights of user with `user-id`
rights:delete {user-id} {right} - revoke `right` from user with `id`
rights:insert {user-id} {right} - grant `right` to user with `id`
users [optional-user-id] - show all users or just the user with `optional-user-id` (if given)
users:delete {user-id} - delete user with `user-id`
users:insert {name} {email} {password} - insert user
fake:users {count} - insert `count` fake users

add migrations/20150327204310-users.sql to the migrations folder of your app.

reset your database if necessary:

./app pg:drop-create

migrate:

./app pg:migrate

insert a user:

./app users:insert casca casca@example.com opensesame

confirm that the user is inserted by listing all users:

./app users

start the server:

./app serve

you can find the server callback/middleware in src/factories/server.coffee. it only contains a user API. if you need more than that - and you probably do - just copy the server factory over to your application and extend it.

customization

fragments-user provides sensible defaults. customizing them is dead-simple.

you can overwrite every part which vastly changes the behaviour

user API documentation

the http command used in the following is https://github.com/jkbrzt/httpie

the code and test for each API action are linked. refer to them for additional documentation (especially for edge cases). use the code as inspiration to build your own API actions. most actions are only a few lines of code.

signup ! (tests)

signup

http POST localhost:8080/api/signup username=casca email=casca@example.com password=opensesame

if you don't want users to be able to sign up just omit apiSignupPost from your middleware.

login ! (tests)

login to get an access token in the response:

http POST localhost:8080/api/login username=casca password=opensesame

if you don't want the API to be at /api just add the factory urlApi in your own app which overwrites urlApi in src/factories/url.coffee. overwrite other fragments as needed.

get current user (tests)

see the user that is logged in with a specific token:

http GET localhost:8080/api/me 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

(don’t forget to replace the token with the one you got in the response to the login request)

you should get a forbidden response as the user doesn't have the right to access the cockpit yet.

let’s give user the right to access cockpit:

./app rights:insert 1 canAccessCockpit

(don’t forget to replace the id with the one you got in the response to the user insert)

see the user logged in with a specific token:

http GET localhost:8080/api/me 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

you should now get the user record in the response.

update current user ! (tests)

http PATCH localhost:8080/api/me name=griffith 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

filter all users (tests)

to read all users the logged in user needs the right canReadUsers.
let's grant that right:

./app rights:insert 1 canReadUsers

insert some fake users so we have some records to filter:

./app fake:users 100

all users:

http GET localhost:8080/api/users 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

with limit and offset:

http GET 'localhost:8080/api/users?limit=5&offset=20' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

ordered by username:

http GET 'localhost:8080/api/users?order=name&asc=true' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where email:

http GET 'localhost:8080/api/users?where[email]=Evans_Jacobi@yahoo.com' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where email contains:

http GET 'localhost:8080/api/users?where[email][contains]=yahoo' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where email ends:

http GET 'localhost:8080/api/users?where[email][ends]=gmail.com' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where name:

http GET 'localhost:8080/api/users?where[name]=Rashawn.Haag' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where name contains:

http GET 'localhost:8080/api/users?where[name][contains]=nn' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

where name begins:

http GET 'localhost:8080/api/users?where[name][begins]=an' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

id below:

http GET 'localhost:8080/api/users?where[id][lt]=10' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

id between 10 and 20 (inclusive):

http GET 'localhost:8080/api/users?where[id][gte]=10&where[id][lte]=20' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

created today:

http GET 'localhost:8080/api/users?where[created_at]=today' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

... you get the idea. if something feels like it should work but doesn't: file an issue !

create user ! (tests)

to create users the logged in user needs the right canCreateUsers.
let's grant that right:

./app rights:insert 1 canCreateUsers

now let's create a user:

http POST 'localhost:8080/api/users name=ubik email=ubik@example.com password=opensesame rights='' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

get user where id (tests)

http GET 'localhost:8080/api/users/55' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

update user ! (tests)

to update users the logged in user needs the right canUpdateUsers.
let's grant that right:

./app rights:insert 1 canUpdateUsers

now let's update a user:

http PATCH 'localhost:8080/api/users/1 name=ubik email=ubik@example.com password=opensesame rights='' 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

delete user ! (tests)

to delete users the logged in user needs the right canDeleteUsers.
let's grant that right:

./app rights:insert 1 canDeleteUsers

now let's delete a user:

http DELETE 'localhost:8080/api/users/3 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.ZTdiMjJhZDk4OWY4Y2M5ZGQ1ZjcxM2Q3MDIxZjc2NTk.Tl-xvkKK9YP9Oz9o-BvuN2R3qi8VGwFpRzSh5cik-78'

access control

just overwrite them

some properties of the currently implemented rights management.

a user can't update his own rights. it's not even allowed.

a user with the right to create users can potentially create a user that has more rights.

a user with the right to updates users can change his and other users rights.

the rights canCreateUsers and canUpdateUsers can be escalated into all rights. they are to be considered superuser rights.

license: MIT