I'm going to add user signup support for the system. The main reason will be to separate users' data. Currently, everybody can see the stats for everybody else's emails, which is less than ideal.
I decided to use Auth0
for this, one reason is that I have already studied about this a little and weighed the features of a few similar services, and I think Auth0
is going to be a good solution for my use case. The other reason is that it gives me a ready-made login/signup page, so that's one less thing to worry about.
Setting up Auth0
Setting up Auth0
was very easy, in fact, the getting started process basically gives you the code that works for your app based on the type of your app (which is incredible).
However, I tried to enable the Passwordless login features, and despite my efforts, I couldn't get any email with a magic link. So finally decided to skip that feature for now and use the default Universal login
the template which asks for user/pass.
There is a warning on the page for social login (Google is enabled by default), and I need to create a Client ID
to make it go away. But I think for now I can skip it.
Express OpenID middleware
The following code is basically copy-pasted from the getting started page of Auth0:
const { auth, requiresAuth } = require('express-openid-connect');
const config = {
authRequired: false,
auth0Logout: true,
secret: process.env.AUTH0_SECRET,
baseURL: process.env.AUTH0_BASEURL,
clientID: '[client ID]',
issuerBaseURL: 'https://litcodes.us.auth0.com'
};
Development vs. production
I didn't want to accidentally leak any secrets, so I decided not to include the Auth0
secret even in the .env
file.
This lets me configure production and development environments differently without changing the env file much (the database.env
the file is actually committed to git right now).
Integrating Auth0 into the app
This part really surprised me, it was so easy to implement using the express-openid-connect
and the requiresAuth
function. I could make any existing call authenticated using that middleware.
app.get('/stats', requiresAuth(), async (req, res) => {
console.log(JSON.stringify(req.oidc.user));
The output looks like this:
{
nickname: 'Amin',
name: '[my full name]',
picture: '[A full path to my profile picture]',
updated_at: '2021-01-26T16:22:24.707Z',
email: '[my email address]',
email_verified: true,
sub: 'auth0|[some hash]'
}
In this app, we will be used email
to uniquely identify users.
The great thing about the express-openid-connect
middleware is that I didn't have to implement any logic for login
basically if users try to access any unauthenticated page, they will automatically be redirected to the login page.
That's it, now I can change the database to accommodate users and map the email receipts with the user IDs. Fortunately, by the time of writing the code, there were zero receipts in the database, therefore I didn't have to worry about migrating data.
Database changes
I created a new table called users
and made some changes to the receipts
and receipt_records
to make it work for users. It took me a few try-and-errors, but finally, I was satisfied with the following schema:
CREATE TABLE users(
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE receipts(
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) NOT NULL,
name TEXT NOT NULL,
UNIQUE (id, name)
);
CREATE TABLE receipt_records(
receipt_id INTEGER REFERENCES receipts(id) NOT NULL,
record TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT current_timestamp
);
Previously the receipt name
the field was set to be the PRIMARY KEY
, I decided to create IDs for receipts
and users
instead, this made things cleaner a bit.
Then I had to go through all of the functions dealing with DB queries and make sure they make use of the user_id
.
Making use of the user ID in the image path
To make public links to images unique, I decided to prefix the image URLs with the actual user ID in the database, this way I could map the unauthenticated access to the image files to the ID in the database.
So now the image URLs are https://seen.lit.codes/[uid]/[email name].png
Adding users to the DB
DB joins
Because of the decision to map everything using user ID, I had to make slightly complicated joins like the following:
SELECT receipts.name as receipt, receipt_records.*
FROM receipt_records
LEFT JOIN receipts ON receipt_id = id
WHERE user_id = ${user_id} AND receipts.name = ${receipt}
Bugfixes
There was a lot of wrestling involved with pug
which I use as the templating language for the pages. I decided to use native html
tags as much as possible to avoid those issues. Later on, I might decide to change to something else, possibly ReactJS?
When deployed to production for the first time, the script failed with the following error:
seen_1 | /opt/index.js:58
seen_1 | })[0].id;
seen_1 | ^
seen_1 |
seen_1 | TypeError: Cannot read property 'id' of undefined
It was coming from the following code:
return await db.any('INSERT INTO users(email) VALUES(${email}) ON CONFLICT DO NOTHING RETURNING id', {
email,
})[0].id
Then I realized what the problem is, I was trying to get the first index of the Promise
object returned by db.any
instead of the result of await
, so I had to wrap the await db.any(...)
in parenthesis to make that work.
Rendering user info
Lastly, I decided to create a header with the profile pictures and a logout button and add it to all of the existing pages, thankfully it was relatively easy to use the include
directive of the pug
.
I had to send the user info (`req.oidc.user`) to the render function:
res.status(200).render('create', { user: req.oidc.user, receipt, path: `/${user_id}` });
And then render it:
<header>
<div style="float: right">
<img style="border-radius: 50%; display: block; width: 50px" src="#{user.picture}"/>
<a href="/logout">Logout!</a>
</div>
</header>