These resources are helpful extra reading that may clarify or enhance concepts you're learning in the main curriculum.
A graphical interface for viewing and editing your database
W3Schools have lots of clear simple example for different types of SQL queries
Amazing summary of the history of databases, what they are, how they work and the different types
Summary of one of the most common ways web apps get hacked
An example of how you might structure a project using Express & SQlite
5 Critical Differences
Web development has evolved dramatically over the years, and one of the pivotal aspects that have grown with it is authentication. Authentication plays a significant role in the modern web, ensuring that users are who they claim to be.
Most websites need a way to verify who is making requests. Generally this involves providing some secret only the user could know: often called a password.
This is fine as a one-off, but websites need to keep users logged in. You could ask the user to provide their password on every request. This would be a terrible user experience though.
A challenge arises because HTTP, the protocol that underlies the web, is inherently stateless. This means that each HTTP request knows nothing about previous or future requests. With this limitation, there was a need for a mechanism to maintain continuity and remember users across requests. Enter "cookies".
Invented in 1994, cookies were designed to preserve state between HTTP requests. This could range from keeping track of items in a shopping cart to monitoring user behavior.
Fundamentally, cookies are HTTP headers. When a server sends a response that contains a set-cookie
header, browsers are designed to store this information. Consequently, for all future requests to the same domain, the browser will automatically include this stored information in the form of a cookie
header.
For instance, consider a server response:
If the user visits another page (sending a request), the browser automatically adds a cookie
header:
This allows the server to save info from one request and retrieve it on later requests from the same browser.
Cookies come equipped with attributes that dictate their behaviour:
Expiry: By default, a cookie is termed a "session" cookie and lasts until the user closes the browser. However, its lifespan can be extended using the Max-Age
attribute. For example, set-cookie: test=123; Max-Age=60
ensures the cookie persists for 60 seconds.
Security: Given that cookies can carry sensitive information, especially related to authentication, their protection is paramount.
The **HttpOnly**
attribute prevents JavaScript from accessing the value. This is important to avoid malicious scripts stealing cookie data (XSS attacks):
The **Same-Site**
option prevents cookies from being sent on requests from other domains. This is important to avoid other sites impersonating your users (CSRF attacks):
The **Secure**
option stops the cookie being set on un-encrypted connections. This is important to stop hackers intercepting requests and stealing cookies (MITM attacks):
So how do we actually track whether a user is logged in?
The simplest way is to put all the info you need into cookies:
The server can read this JSON string and parse it into an object to find out info about which user is logged in.
This is called "stateless" as no state is stored on the server. Assuming the server can trust the cookie it can trust this user previously logged in.
But can you trust cookies? Cookies are just HTTP headers, so they're easy to fake:
You can even edit them using dev tools
We can use cryptography to create a value that we can verify later.
Cryptography is the practice and study of techniques for secure communication.
A "hash function" takes a value and a secret, and returns a "hash":
Hashing is a one-way process. There's no way to get the value back.
Hashing the same value with the same secret will always produce the same result:
The only way to recreate this value is to know the secret.
We can use this to "sign" our cookies:
When the server receives the cookie it can re-hash the value ("hello") using the secret only it knows. If the hash matches the one in the cookie the server knows it hasn't been tampered with.
You won't have to implement this yourself. It's safer to rely on battle-tested libraries for security features.
Cookies have a 4kb size limit. You'll often want to store more user info than that.
The server cannot control who is logged in. If a user presents a non-expired cookie they are authenticated.
This is the opposite of stateless auth. Store a single "session ID" in the cookie:
The server reads the cookie, then uses it to look up the saved session data. This would usually be stored in a database.
Since the session ID is basically a password it must be secure.
Session IDs should be long random strings. This prevents people from guessing them.
They should also still be signed with a hash, so you can trust them.
It's important to treat security very seriously. The web has been plagued by serious breaches.
As web developers you have a responsibility to safeguard your users' private data.
The average user re-uses the same password for most sites. If you half-ass your site's security and get hacked that has wider repercussions.
Don't roll your own crypto.
The golden rule of security. If in doubt, look it up. Find out what is popular and recommended by experts.
Building a good secure user-experience takes time and effort. Here are some features we haven't covered:
Email verification
Password resets
Preventing automated mass account creation
Banning abusive accounts
If you don't have the time or ability to do it properly (e.g. you're hired for a 2 week freelance project) use a trusted 3rd party service.
Your project this week is to build a web app that stores data in a SQLite database.
Before you start writing features you need to design the schema for your data. Think about what different things your app needs to store, how they relate to each other, and how you can avoid duplicating information. Record your schema in your README.md
using Markdown tables. Consider embedding a diagram to help visualise the relationships.
What kinds of data relationships are there?
What's a foreign key? How can they help us design schemas with relational data?
As a user, I want to: submit information to your site for anyone to see
As a user, I want to: come back to your site later and see what I posted is still there
Since this project is open-ended you'll need to write your own more specific user stories once you know what you want to build.
Founders & Coders book sharing system
Food / coffee recommendations around Founders & Coders
Founders & Coders events calendar
A simple mental model for app deployment is that we’re installing and running our app on a computer in the cloud. Yet this is an oversimplification and an important difference is that the PaaS services we use for deployment (ie Fly) have an ephemeral file system.
An ephemeral file system is a type of file system that is not permanently stored on a device or disk. It exists only for a short period of time, and any files or data stored in it are not persisted after the session ends or the file system is deleted. This type of file system is usually used in cloud computing environment as it allows for greater flexibility and efficiency compared to using permanent storage.
As Fly uses an ephemeral file system and SQLite works by storing the database as a file on the system we need to make special provisions for deployment. If we didn’t, then our database would disappear every time the file system is restarted.
To solve this we’re going to use Fly’s Volume’s feature.
Install the Fly CLI and deploy your app if you haven’t already.
Create a volume by running the following command
flyctl volumes create data --region lhr --size 1
This will create a 1GB volume for the app whose fly.toml
is in the current directory.
You can think of a volume as being like an external hard drive that we get to use with our app.
You can use the volume you just created in your app by adding it to the fly.toml
:
This will tell us the directory under which this volume can be used, i.e. the volume data
that we created in the last step will be available on the path /data
when our app runs on Fly.io
's servers.
Now we have to deploy again so that the app knows about our updated fly.toml
file
flyctl deploy
The last step is to set the environment variable which we use in the our app (i.e. in database/db.js
). On fly, this needs to point to the directory we referenced in our fly.toml
file (ie data/
)
flyctl secrets set DB_FILE=/data/db.sqlite
We might also want to seed our database with initial data in our deployment environment. When we do this locally we usually run a command on the terminal which calls a script to insert the data, e.g. node src/database/seed.js
.
We’re going to do something similar to seed the remote data, but instead of running a command on our local terminal, we’re going to connect to the virtual machine that Fly has spun up for our app and run a command on that virtual machine’s terminal.
Before we connecting to the virtual machine, we'll need to add the volta
key to our package.json
. Volta is the tool that Fly uses to manage node versions.
Let's add the following to our package.json
(making sure that the node version you specify is the same one as in the generated Dockerfile
)
Then run flyctl deploy
to get these changes onto our deployed app.
To connect to the remote virtual machine we run the following command. SSH stands for Secure Shell and is a way to securely connect to another computer over a network.
flyctl ssh console
Once you’ve connected to the terminal you can find your files in the app/
directory and from there you can run your script.
cd app
node src/database/seed.js
Your database should now be seeded and you can check this by going to the URL for your deployed app and checking the data is there.