Start coding open-source

This message aims to introduce you to an open-source Quotes website: https://quotes.programmersdiary.com/.

Motivation

  1. Share the fun of creating open-source software → attract more people to develop it.

  2. Combat paid software → free software is accessible to a larger audience.

  3. Repay good will → I use open-source, it is only fair I create it in return.

  4. Build my portfolio → Coding open-source shows to others what I can do.

Purpose

Let all people who can access the internet enjoy the quotes.

Had a bad day: read some quotes.

Had a good day: have more inspiration to continue.

Quotes are made by a wide variety of people, so users can have many different views on which they can agree. And maybe learn some past values that might have been forgotten.

Currently, users can view/save/comment on quotes.

Software structure

The Quotes software has three main parts: frontend, backend and resources.

Frontend

Git repo: https://github.com/EvalVis/QuotesFE.

Setup details are covered in README.md.

Frontend is coded with React.

Tailwind CSS is used for styling.

Backend

Git repo: https://github.com/EvalVis/QuotesBE.

Setup details are covered in README.md.

Coded in Typescript.

Resources

To persist data, MongoDB Atlas is used: https://www.mongodb.com/products/platform/atlas-database. It's an online MongoDB. It mainly contains quotes.

The quotes are from https://huggingface.co/datasets/Abirate/english_quotes.

Technical insights

This section covers lessons learned when developing Quotes websites.

Do not execute operations on a server that can be executed on a client

You will have fewer server instances than client instances. Everyone connecting to https://quotes.programmersdiary.com/ uses a browser as a client. A cloud software owner does not pay a cent when its client does some operations locally. However, you will pay if you host a service and need to scale. Try minimizing server-side operations. Let the client do as many operations as possible.

Example:

Let’s consider this user story:

When user clicks and quote comments we want to see them from most recent to oldest. More over, if user is logged in, users comments should appear on top.

We have backend code that retrieves quote comment:

app.get('/api/quotes/comments/:quoteId', optionalJwtCheck, async (req: express.Request, res: express.Response): Promise<void> => {
try {
    const sub = req.auth?.sub;
    const { quoteId } = req.params;

    const quote = await quotesCollection.findOne(
    { _id: ObjectId.createFromHexString(quoteId) },
    { projection: { comments: 1 } }
    );

    if (!quote) {
        res.status(404).send();
        return;
    }

    if (!quote.comments) {
        res.json([]);
        return;
    }

    const comments = quote.comments.map((comment: any) => {
        return {
            text: comment.text,
            username: comment.username,
            isOwner: sub !== null && comment.sub === sub,
            createdAt: comment.createdAt
        };
    });

    res.json(comments);
} catch (error) {
    res.status(500).send();
}
});

As you can see, the backend does not sort the comments. It only does what the client can’t do: we can’t allow clients to access the database directly, otherwise someone could manipulate the data.

So we extract a specific quote’s comments and tag each comment with

isOwner: sub !== null && comment.sub === sub

it will tell the client if it’s user made the specific comment.

[Code reference: https://github.com/EvalVis/QuotesBE/blob/main/src/api.ts#L410, Swagger docs: https://quotesapi.fly.dev/api-docs.]

In frontend we finalize the logic:

const response = await fetch(`${import.meta.env.VITE_BE_URL}/api/quotes/comments/${quote._id}`, {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

const commentData = await response.json();

let sortedComments = [...commentData].sort((a, b) => 
  new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);

sortedComments = [
  ...sortedComments.filter(c => c.isOwner),
  ...sortedComments.filter(c => !c.isOwner)
];

The backend has returned as a date when each comment was created. So in client we can sort by date. Also, since we know if current user is the owner of a comment, we can sort further.

[Code reference: https://github.com/EvalVis/QuotesFE/blob/main/src/components/Quote.tsx#L72.]

With this logic, the user story is finished. We did the least amount of work with the backend, transferring all remaining logic to the client.

Do not use email as primary key for user

We have been testing this software with my cousin and we found a huge security flaw.

This app uses Auth0. When you setup an application in Auth0, by default anyone can sign up without email verification is not required.

Intrusion scenario

  1. We use email as primary key for user related data.

  2. A user logs in with Google using tester@gmail.com as email. He views quotes, saves them and comments on them.

  3. Another user creates an Auth0 account using the same tester@gmail.com. The user can do this since Auth0 account creation is used instead of Google login. The intruder does not need to verify the email and is instantly logged into the Quotes software.

  4. Since we use email as PK, the intruder can see the victim’s saved quotes, delete them and comment in his name.

  5. The intruder has overtaken the victim’s account and we are to blame.

Prevention measures

Let’s consider two ways to counter this:

  1. Do not allow logging in while your email is unverified.

  2. Use User ID as true user identification.

You can block unverified users from logging in by going to Auth0 website → Actions → Triggers → Post login. Then you can either create a custom action that denies login to unverified users or use a pre-built one. However, you would need to explain clearly why the login was unsuccessful. The pre-built action seems to simply redirect back without logging in. If you have no clear indication that login was unsuccessful due to unverified email you will lose lots of users. Even if you have one you will still lose users who are two lazy to verify the email.

And consider this: what if you implement e.g. Facebook login. User logs in, saves quotes, comments. And when the user changes email. What then? He can no longer log in. All the saved quotes are lost.

So instead use User ID as primary key. In scenario where intruder logs in with same email he would still have different User ID and account take over would fail.

Where to get quotes?

Search for free datasets like

https://huggingface.co/datasets/Abirate/english_quotes,

https://huggingface.co/datasets/asuender/motivational-quotes,

https://www.kaggle.com/datasets/manann/quotes-500k,

https://www.kaggle.com/datasets/akmittal/quotes-dataset.

Improvise by searching for more specific quotes like movie quotes or programming/physics quotes.

Contribution

Since this is an open-source project, it would be excellent if a reader contributed (just a simple line change, maybe? :)).

If you are interested, please visit both https://github.com/EvalVis/QuotesFE?tab=readme-ov-file#contributing and https://github.com/EvalVis/QuotesFE?tab=readme-ov-file#contributing, grab a keyboard and start developing open-source.

Realm of possibilities

Consider coding these user stories:

  • As a humorist, I want to filter humor-related quotes.

  • As a user with little time, I want to see the most popular quotes.

  • As William Shakespeare admirer, I want to see his quotes.

  • As an aesthetic, I want to see better graphics in the website.

  • As a quote enthusiast, I want to see a wider variety of quotes. [Currently, there are around 2000-3000 quotes; however, the quotes format is described at https://github.com/EvalVis/QuotesBE?tab=readme-ov-file#quotes-format. If you want to contribute by adding quotes, please create a file containing a list of quotes in the specified format and raise a Pull request.]

Here are the issues, feel free to contribute:

https://github.com/EvalVis/QuotesFE/issues and https://github.com/EvalVis/QuotesBE/issues.

Have another idea for a user story or want to implement another feature?

Go ahead: code and raise a Pull request.

End note

Thanks for reading.

I hope you appreciate open-source. This message is for you to start coding open-source.

Show others what you can do.

0
Subscribe to my newsletter

Read articles from Evaldas Visockas directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Evaldas Visockas
Evaldas Visockas