We need two tables in our applications: one for storing users and their passwords, and another one for storing the sessions.
Let's start with the table for users. We will call it person
:
create table person (
id serial primary key,
name text not null,
email text not null unique,
password text not null
);
person
contains the email
and password
fields that will be used when logging. Optionally we defined the name
field. email
must be unique
and password
will be storing a hashed version of the user password.
Next, we need the session
table to store user sessions:
CREATE TABLE session (
id serial primary key,
token text,
person_id integer references person(id),
created_at timestamptz not null default now()
);
The person_id
is the foreign key that references records in the person
table. token
will hold the current user session that will be hashed for additional security.
Let's create a controller for registering users. We will use a Kretes functionality to implicitly create REST routes based on the directory structure.
Inside features/Person/Controller
put the create.ts
file with the following content:
import { auth } from 'kretes';
const { register } = auth;
export register({ fields: ['email', 'name'] });
This action will create users in the person
table. Kretes provide a convenient template for this functionality as the register
function from the auth
module. We only need to define which fields should be extracted from the request params during the user creation. In our case it's email
and name
as the password
field being mandatory is implicitly extracted as well.
Similarly, in features/Session/Controller
put the create.ts
file with the following content:
import { auth } from 'kretes';
const { login } = auth;
export login({ finder })
This time we need to provide the finder
function that is mandatory. This function describes how to find a particular user in the database. In our case it will be relatively simple:
import { auth, database as db } from 'kretes';
const { login } = auth;
const finder = async ({ email }) => await db.from('person').where({ email });
export login({ finder })
Let's create a simple HTML page that will be protected using our authentication mechanism. In features/Base/Controller/
create browse.ts
:
import { Handler, response } from 'kretes';
const { OK } = response;
export const browse: Handler = _ => {
const records = ['Widget 1', 'Widget 2', 'Widget 3'];
return OK(records);
}
When visiting the /base
the action above will return a list of three widgets as application/json
. Let's force authentication for this endpoint using another built-in function authenticate
.
import { Handler, auth, response } from 'kretes';
const { OK } = response;
const { authenticate } = auth;
const browse: Handler = _ => {
const records = ['Widget 1', 'Widget 2', 'Widget 3'];
return OK(records);
}
export [authenticate, browse];
The array syntax designates the function composition, i.e. [authenticate, browse]
is equivalent to authenticate(browse)
. In other words, we compose functions one on another from left to right.
Finally, we can test the whole process. Let's start by registering a user:
http POST :5544/user email=user@domain.com password=test1234 name=User
We can now log in using the password provided during the registration:
http POST :5544/session email=user@domain.com password=test1234
As a response we will receive a token
than can be used to pass along with requests to authenticate them. This token can be passed as query params or in the request body. Let's use the query params approach to access the protected route /base
.
http :5544/base?token=<PUT YOUR TOKEN HERE>
If the token is correct you should see the list of widgets, otherwise you will get 401 Unauthorized
.
Found a mistake?Found a mistake? Would you like to suggest an improvement?