Skip to content
Snippets Groups Projects
Commit 8c48efce authored by Galen Long's avatar Galen Long
Browse files

whatever

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 593 additions and 0 deletions
**/node_modules
**/.DS_Store
/.git
/node_modules
.dockerignore
.env
Dockerfile
fly.toml
**/.env
**/node_modules
**/.DS_Store
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.9.0
FROM node:${NODE_VERSION}-slim as base
LABEL fly_launch_runtime="Node.js"
# Node.js app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y build-essential pkg-config python-is-python3
# Install node modules
COPY --link package-lock.json package.json ./
RUN npm ci
# Copy application code
COPY --link . .
# Final stage for app image
FROM base
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD [ "npm", "run", "start" ]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Demo</title>
<style>
input, button {
display: block;
margin: 1rem 0;
}
</style>
</head>
<body>
<p>Enter some data:</p>
<input type="text" id="datum">
<button id="submit">Submit</button>
<p>All data:</p>
<div id="data"></div>
<script>
let input = document.getElementById("datum");
let button = document.getElementById("submit");
let div = document.getElementById("data");
function populate() {
div.textContent = "";
fetch("/data").then(response => response.json()).then(body => {
let data = body.data;
if (data.length === 0) {
div.textContent = "No data yet"
} else {
for (let obj of data) {
let datumDiv = document.createElement("div")
datumDiv.textContent = obj.datum;
div.append(datumDiv);
}
}
});
}
button.addEventListener("click", () => {
fetch("/datum", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({datum: input.value})
}).then(response => {
return response.json();
}).then(body => {
populate();
}).catch(error => {
console.log(error);
})
});
populate();
</script>
</body>
</html>
let express = require("express");
let { Pool } = require("pg");
// make this script's dir the cwd
// b/c npm run start doesn't cd into src/ to run this
// and if we aren't in its cwd, all relative paths will break
process.chdir(__dirname);
let port = 3000;
let host;
let databaseConfig;
// fly.io sets NODE_ENV to production automatically, otherwise it's unset when running locally
if (process.env.NODE_ENV == "production") {
host = "0.0.0.0";
databaseConfig = { connectionString: process.env.DATABASE_URL };
} else {
host = "localhost";
let { PGUSER, PGPASSWORD, PGDATABASE, PGHOST, PGPORT } = process.env;
databaseConfig = { PGUSER, PGPASSWORD, PGDATABASE, PGHOST, PGPORT };
}
let app = express();
app.use(express.json());
app.use(express.static("public"));
// uncomment these to debug
// console.log(JSON.stringify(process.env, null, 2));
// console.log(JSON.stringify(databaseConfig, null, 2));
let pool = new Pool(databaseConfig);
pool.connect().then(() => {
console.log("Connected to db");
});
app.post("/datum", (req, res) => {
let { datum } = req.body;
if (datum === undefined) {
return res.status(400).send({});
}
pool.query("INSERT INTO foo (datum) VALUES ($1)", [datum]).then(result => {
return res.send({});
}).catch(error => {
console.log(error);
return res.status(500).send({});
})
});
app.get("/data", (req, res) => {
pool.query("SELECT * FROM foo").then(result => {
return res.send({data: result.rows});
}).catch(error => {
console.log(error);
return res.status(500).send({data: []});
})
})
app.listen(port, host, () => {
console.log(`http://${host}:${port}`);
});
# rename this to .env to get code to run locally
PGUSER=YOURPOSTGRESUSER
PGPORT=5432
PGHOST=localhost
PGPASSWORD=YOURPASSWORD
PGDATABASE=YOURFLYWEBAPPNAME
# fly.toml app configuration file generated for wxvsrhsmnh on 2023-12-08T13:12:16-05:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "wxvsrhsmnh"
primary_region = "ewr"
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 1024
This diff is collapsed.
{
"scripts": {
"start": "node app/server.js",
"setup": "fly postgres connect -a FLYPOSTGRESAPPNAME < setup.sql",
"start:dev": "env-cmd node app/server.js",
"setup:dev": "env-cmd psql -d postgres -f setup.sql"
},
"dependencies": {
"env-cmd": "^10.1.0",
"express": "^4.18.2",
"pg": "^8.11.3"
},
"devDependencies": {
"@flydotio/dockerfile": "^0.4.11"
}
}
# Demo Fly Postgres
This demo shows you how to deploy a website to Fly with a Postgres database.
**Make sure to follow all instructions carefully.**
## Billing
fly.io's pay as you go plan will charge you based on usage, but as of 2024-08-07, they have a secret policy that if your monthly bill is < $5, they'll waive your bill. Most groups will probably qualify for this. If not, I'd expect your bill to be no more than $5, and definitely no more than $10, as long as you select "Development" and not "Production" when deploying. If this is an issue for you, let me know and we can work something out.
## Set up web app configuration, deploy Postgres app
- Follow steps 1 - 3 [here](https://fly.io/docs/speedrun/) if you don't already have a Fly account and to install the fly command line program
- `fly launch` will fail if you don't have a credit card on file; see [here](https://fly.io/docs/about/pricing/) and above for information on billing
- Then when running `fly launch`, when prompted "Do you want to tweak these settings before proceeding?", enter `y`, will take you to webpage
- Choose unique app name, can't match any other existing fly apps globally
- Note that your app name will also have to become your database name; see the creating database tables section below
- **Make your app name a valid variable name, as we'll be using it later on**. The default generated names have hyphens in them - don't use any hyphens
- Check that the port your server.js listens on matches what's on the Fly page, and if not, change either so they match
- Under database section, select Fly Postgres, give your database its own app name (I did my web app name + `-db`),
- **Make sure to pick the Development deployment option** ; default is production, which costs more
- Then confirm settings
- This command will generate credentials for your Postgres cluster &mdash; **save these somewhere private in case you need them later**, as after you close this terminal window you'll never be able to get them again
- If this worked, it'll create a Dockerfile and a fly.toml with your configuration settings for your web app; you won't need to run fly launch again
<!-- - Make sure listed port on previous web page is same as port in generated Dockerfile (`EXPOSE`) and fly.toml (`internal_port`); was done for me automatically -->
- It will also deploy a Fly app for your Postgres cluster and create a database in your Postgres cluster with the same name as your web app name
- It will also make an environment variable called `DATABASE_URL` available to your Fly web app, which we'll use to connect to the Postgres database
- Both of these apps will be visible on the Fly website on your dashboard
> You can also configure your web app and Postgres app separately by running `fly launch` to create your web app, `fly postgres create` to create a Postgres app, and `fly attach` to make the DATABASE_URL available to the web app; see [here](https://fly.io/docs/postgres/getting-started/create-pg-cluster/) for more details. I don't recommend doing this, as you have to do a little more work to get your web app and database app talking to each other.
> `fly launch` won't generate a separate fly.toml for the Postgres app; if you want to tweak the Postgres settings, you need to run `fly config save --app FLYPOSTGRESAPPNAME` (replacing with your DB's app name) to create a fly.toml for the Postgres Fly app (make sure you run this in a different directory as this may overwrite the fly.toml for your web app; not sure, haven't checked).
Also, **any fly commands you run for Postgres app specifically will need to provide its app name through the command line**, since otherwise fly will use the app name in the fly.toml which is your web app, e.g. `fly postgres connect -a APPNAME`.
## Code changes you'll need to make web app work and connect to Postgres app
Your local and deployed server will need different configurations to run:
- Fly expects server to listen to host `0.0.0.0`, not `localhost`
- Port can remain 3000 as long as you've configured Fly to deploy to that (see setup section above, should happen automatically when you run fly launch)
- Fly will automatically put your DB credentials in the environment variable `DATABASE_URL` (you can access environment variables in the global Node object `process.env`); node-postgres can take a [connectionString](https://node-postgres.com/features/connecting#connection-uri) parameter when creating the pool, so we can pass DATABASE_URL to that, e.g. `let pool = new Pool({ connectionString: process.env.DATABASE_URL });`
- This means that you don't need to pass Postgres credentials to Fly, so it doesn't need access to your `.env` to run Postgres
- If you want to pass API keys to Fly, note that Fly includes `.env` in the `.dockerignore` file by default, which means any files named .env will be excluded from your deployed app code base. You can remove the .env line from .dockerignore, or just have a `keys.json` file that you .gitignore (but don't .dockerignore) and read from it like we've done before. You can also set secret env vars through the command line interface; see [here](https://fly.io/docs/rails/the-basics/configuration/).
- To make your code work both locally and on Fly, you can do different things if `process.env.NODE_ENV === "production"`, because Fly sets NODE_ENV to production automatically, and otherwise it's the empty string by default; see server.js for details
### Using this repo
If you use this repo, make the following changes:
1) Copy env.sample to .env and replace all variables with your local Postgres credentials
2) Change .env's database name to match your Fly web app name
- Your .env is just for running locally, as Fly won't use this for Postgres, if your local database has the same name as your Fly database, it makes it easier to write a setup.sql that works for both; see below
3) Change setup.sql's database name references to match your Fly web app name (to make it consistent with your local .env)
4) In package.json, change FLYPOSTGRESAPPNAME to match your Fly Postgres app name
To run your code locally:
- `npm run setup:dev` to initialize your DB and tables - you only need to do this once (or whenever you change your schema/want to delete all data and start fresh)
- `npm run start:dev` to run your server
To set up your Fly database:
- `npm run setup`
Fly will use the `npm run start` command automatically to start your server when you deploy. You shouldn't run `npm run start` locally if you're using Postgres, because the start command doesn't inject the Postgres environment variables that server.js expects to connect to the local DB.
## Creating your database tables
- The Postgres Fly app's connection string in the DATABASE_URL assumes you're using a Fly database with the same name as your Fly web app
- It's easiest to change your local database's name to match your Fly web app name (_not_ your Fly Postgres app name) in your setup.sql, e.g. if you named your Fly app `foo` and your Fly Postgres app `foo-db`, you'll run `CREATE DATABASE foo`
- You'll need to change the database name in any files that reference it, like .env/env.json, setup.sql, and if package.json uses the database name in any script commands, there as well
- To run your setup.sql on the deployed Fly Postgres cluster, run `fly postgres connect -a FLYPOSTGRESAPPNAME < setup.sql` (or if you're using this repo, just run `npm run setup`), replacing FLYPOSTGRESAPPNAME with the name of your Fly Postgres app (I made mine the same as my webapp + `-db`); this will feed the SQL commands inside setup.sql to a SQL interpreter running on your Postgres instance, which will create the needed tables in your deployed database
- FYI, the DROP DATABASE command will probably say it failed with an error saying the database is currently in use, but the CREATE TABLE commands seem to work anyway, so don't worry about this
- You need to provide an explicit app name when running this command or else it'll use the one in your fly.toml, which is your web app's app name, not your Postgres app's app name
- You can also run `fly postgres connect -a FLYPOSTGRESAPPNAME` to open up a psql command line on your deployed DB and copy-paste the SQL commands in setup.sql manually if the above command doesn't work because you're not using a Unix shell; see docs [here](https://fly.io/docs/flyctl/postgres/)
## Deploy your web app
Your Postgres app is already deployed, but we still need to deploy your web app.
- Run `fly deploy` to actually deploy web app to public internet
- Rerun `fly deploy` every time you change code to re-deploy it
- Web app will be available at `https://FLYWEBAPPNAME.fly.dev/`
- Can see server logs at `https://fly.io/apps/FLYWEBAPPNAME/monitoring` or by running `fly logs` from the command line
- Can ssh into server with `fly ssh console`; note that this won't work if no instances of your web app are active, and Fly will automatically autoscale down to 0 instances if the site hasn't been used in a bit, so if this fails, make sure your deployment didn't crash, then visit your site's URL to start one of the machines and then it should work
- Can run `fly postgres connect -a FLYPOSTGRESAPPNAME` to connect to your Postgres instance with a psql shell
It seems like sometimes the Fly Postgres app is slow to accept connections, so any code that touches the database might fail at first when you visit your website. Try again after a few seconds if you get a network error to see if it's started.
DROP DATABASE IF EXISTS FLYWEBAPPNAME;
CREATE DATABASE FLYWEBAPPNAME;
\c FLYWEBAPPNAME
CREATE TABLE foo (
id SERIAL PRIMARY KEY,
datum TEXT
);
This diff is collapsed.
{
"dependencies": {
"cookie-parser": "^1.4.6",
"express": "^4.19.2"
}
}
# Demo front-end cookies
A simple example with a server that sets/deletes cookies when you visit different routes.
- Run `npm i`
- Run `npm run start`
- Visit http://localhost:3000/addcookie in your browser, the server will send a cookie, open your devtools Storage tab to view cookies for localhost to see it
- Refresh the page, your browser will send the cookie that was previously set automatically, so the server will see it
- Visit http://localhost:3000/removecookie and check your cookies again - the abc cookie should be deleted
let express = require("express");
let cookieParser = require("cookie-parser");
let hostname = "localhost";
let port = 3000;
let app = express();
app.use(cookieParser());
let cookieOptions = {
httpOnly: true, // JS can't access it
secure: true, // only sent over HTTPS connections
sameSite: "strict", // only sent to this domain
};
app.get("/addcookie", (req, res) => {
let { abc } = req.cookies;
console.log(req.cookies, abc);
if (abc === undefined) {
return res.cookie("abc", "def", cookieOptions).send("Cookie added");
} else {
return res.send(`You already have a cookie abc with value ${req.cookies.abc}`);
}
});
app.get("/removecookie", (req, res) => {
console.log(req.cookies);
return res.clearCookie("abc", cookieOptions).send("Cookie removed");
});
app.listen(port, hostname, () => {
console.log(`http://${hostname}:${port}`);
});
// http://localhost:3000/addcookie
// http://localhost:3000/removecookie
let express = require("express");
let { Pool } = require("pg");
let argon2 = require("argon2"); // or bcrypt, whatever
let cookieParser = require("cookie-parser");
let crypto = require("crypto");
let env = require("../env.json");
let hostname = "localhost";
let port = 3000;
let pool = new Pool(env);
let app = express();
app.use(express.json());
app.use(cookieParser());
// global object for storing tokens
// in a real app, we'd save them to a db so even if the server exits
// users will still be logged in when it restarts
let tokenStorage = {};
pool.connect().then(() => {
console.log("Connected to database");
});
/* returns a random 32 byte string */
function makeToken() {
return crypto.randomBytes(32).toString("hex");
}
// must use same cookie options when setting/deleting a given cookie with res.cookie and res.clearCookie
// or else the cookie won't actually delete
// remember that the token is essentially a password that must be kept secret
let cookieOptions = {
httpOnly: true, // client-side JS can't access this cookie; important to mitigate cross-site scripting attack damage
secure: true, // cookie will only be sent over HTTPS connections (and localhost); important so that traffic sniffers can't see it even if our user tried to use an HTTP version of our site, if we supported that
sameSite: "strict", // browser will only include this cookie on requests to this domain, not other domains; important to prevent cross-site request forgery attacks
};
function validateLogin(body) {
// TODO
return true;
}
app.post("/create", async (req, res) => {
let { body } = req;
// TODO validate body is correct shape and type
if (!validateLogin(body)) {
return res.sendStatus(400); // TODO
}
let { username, password } = body;
console.log(username, password);
// TODO check username doesn't already exist
// TODO validate username/password meet requirements
let hash;
try {
hash = await argon2.hash(password);
} catch (error) {
console.log("HASH FAILED", error);
return res.sendStatus(500); // TODO
}
console.log(hash); // TODO just for debugging
try {
await pool.query("INSERT INTO users (username, password) VALUES ($1, $2)", [
username,
hash,
]);
} catch (error) {
console.log("INSERT FAILED", error);
return res.sendStatus(500); // TODO
}
// TODO automatically log people in when they create account, because why not?
return res.status(200).send(); // TODO
});
app.post("/login", async (req, res) => {
let { body } = req;
// TODO validate body is correct shape and type
if (!validateLogin(body)) {
return res.sendStatus(400); // TODO
}
let { username, password } = body;
let result;
try {
result = await pool.query(
"SELECT password FROM users WHERE username = $1",
[username],
);
} catch (error) {
console.log("SELECT FAILED", error);
return res.sendStatus(500); // TODO
}
// username doesn't exist
if (result.rows.length === 0) {
return res.sendStatus(400); // TODO
}
let hash = result.rows[0].password;
console.log(username, password, hash);
let verifyResult;
try {
verifyResult = await argon2.verify(hash, password);
} catch (error) {
console.log("VERIFY FAILED", error);
return res.sendStatus(500); // TODO
}
// password didn't match
console.log(verifyResult);
if (!verifyResult) {
console.log("Credentials didn't match");
return res.sendStatus(400); // TODO
}
// generate login token, save in cookie
let token = makeToken();
console.log("Generated token", token);
tokenStorage[token] = username;
return res.cookie("token", token, cookieOptions).send(); // TODO
});
/* middleware; check if login token in token storage, if not, 403 response */
let authorize = (req, res, next) => {
let { token } = req.cookies;
console.log(token, tokenStorage);
if (token === undefined || !tokenStorage.hasOwnProperty(token)) {
return res.sendStatus(403); // TODO
}
next();
};
app.post("/logout", (req, res) => {
let { token } = req.cookies;
if (token === undefined) {
console.log("Already logged out");
return res.sendStatus(400); // TODO
}
if (!tokenStorage.hasOwnProperty(token)) {
console.log("Token doesn't exist");
return res.sendStatus(400); // TODO
}
console.log("Before", tokenStorage);
delete tokenStorage[token];
console.log("Deleted", tokenStorage);
return res.clearCookie("token", cookieOptions).send();
});
app.get("/public", (req, res) => {
return res.send("A public message\n");
});
// authorize middleware will be called before request handler
// authorize will only pass control to this request handler if the user passes authorization
app.get("/private", authorize, (req, res) => {
return res.send("A private message\n");
});
app.listen(port, hostname, () => {
console.log(`http://${hostname}:${port}`);
});
let argon2 = require("argon2");
let { Pool } = require("pg");
// TODO move dummy.js inside app
let env = require("./env.json");
let pool = new Pool(env);
let dummyUsers = [
["abc", "mycoolpassword"],
["admin", "root"],
["fiddlesticks", "bibblebap"],
];
// client/pool.end stuff taken from here
// https://stackoverflow.com/a/50630792/6157047
// so script actually exits
pool.connect().then(async (client) => {
for (let [username, password] of dummyUsers) {
let hash;
try {
hash = await argon2.hash(password);
} catch (error) {
console.log(`Error when hashing '${password}':`, error);
continue;
}
try {
await client.query(
"INSERT INTO users (username, password) VALUES ($1, $2)",
[username, hash],
);
} catch (error) {
console.log(`Error when INSERTING '${username}', '${hash}':`, error);
}
console.log(`Inserted ('${username}', '${password}') with hash '${hash}'`);
}
let result = await client.query("SELECT * FROM users");
console.log(result.rows);
await client.release();
});
// TODO why is this not a race condition
pool.end();
{
"user": "postgres",
"host": "localhost",
"database": "demo",
"password": "MYPASSWORD",
"port": 5432
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment