-
Notifications
You must be signed in to change notification settings - Fork 8
Add production Express server (dist/ + dev/prod parity) #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
084d7c7
23b2a3c
7d5c3f8
0cd7abc
d55c8f1
e4ecf56
fe4b09d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not following the pattern: https://chriscalo.github.io/dev-skills/skills/nodejs/express/server-startup Please fix. Use the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { createServer } from "node:http"; | ||
| import { execSync } from "node:child_process"; | ||
| import express from "express"; | ||
| import { app } from "./src/api/index.js"; | ||
| import notFound from "./src/api/404.js"; | ||
|
|
||
| const DEVELOPMENT = process.env.NODE_ENV !== "production"; | ||
|
|
||
| function delay(ms) { | ||
| return new Promise(resolve => setTimeout(resolve, ms)); | ||
| } | ||
|
|
||
| function clearPort(port) { | ||
| try { | ||
| execSync(`fuser -k ${port}/tcp`, { stdio: "ignore" }); | ||
| } catch {} | ||
| } | ||
|
|
||
| app.use(express.static("dist")); | ||
| app.use(notFound); | ||
|
|
||
| export async function listen(app, port) { | ||
| const listener = createServer(app); | ||
| return startServer(); | ||
|
|
||
| async function startServer() { | ||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| listener.listen(port); | ||
| listener.once("listening", resolve); | ||
| listener.once("error", reject); | ||
| }); | ||
|
|
||
| const url = `http://localhost:${port}`; | ||
| return { url, port, listener }; | ||
| } catch (error) { | ||
| if (DEVELOPMENT && error.code === "EADDRINUSE") { | ||
| console.warn(`Port ${port} in use, retrying...`); | ||
| await clearPort(port); | ||
| await delay(1000); | ||
| return startServer(); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const port = Number(process.env.PORT) || 3000; | ||
| const { url } = await listen(app, port); | ||
| console.log(`Listening on ${url}`); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!DOCTYPE html> | ||
|
chriscalo marked this conversation as resolved.
|
||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>404 Not Found</title> | ||
| </head> | ||
| <body> | ||
| <h1>404 Not Found</h1> | ||
| <p>The page you requested could not be found.</p> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { fileURLToPath } from "url"; | ||
| import { dirname, join } from "path"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const notFoundPath = join(__dirname, "404.html"); | ||
|
|
||
| export default function notFound(req, res) { | ||
|
chriscalo marked this conversation as resolved.
|
||
| res.status(404).sendFile(notFoundPath); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import express from "express"; | ||
| import sessions from "client-sessions"; | ||
| import { fileURLToPath } from "url"; | ||
| import { dirname, join } from "path"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| const ONE_DAY_MS = 24 * 60 * 60 * 1000; | ||
| const FIVE_MINUTES_MS = 5 * 60 * 1000; | ||
|
|
||
| export const app = express(); | ||
|
|
||
| app.use(sessions({ | ||
| cookieName: "session", | ||
| secret: process.env.SESSION_SECRET, | ||
| duration: ONE_DAY_MS, | ||
| activeDuration: FIVE_MINUTES_MS, | ||
| cookie: { httpOnly: true, secure: true, sameSite: "lax" }, | ||
| })); | ||
|
|
||
| app.get("/login", (req, res) => { | ||
| res.sendFile(join(__dirname, "login.html")); | ||
| }); | ||
|
|
||
| app.post("/api/login", express.urlencoded({ extended: false }), (req, res) => { | ||
| req.session.user = { name: req.body.name }; | ||
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
| app.get("/logout", (req, res) => { | ||
| res.sendFile(join(__dirname, "logout.html")); | ||
| }); | ||
|
|
||
| app.post("/api/logout", (req, res) => { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There needs to be a UI for logging out |
||
| req.session.reset(); | ||
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
| export function requireLogin(req, res, next) { | ||
|
chriscalo marked this conversation as resolved.
|
||
| if (!req.session?.user) { | ||
| return res.status(401).json({ error: "unauthorized" }); | ||
| } | ||
| next(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Log In</title> | ||
| </head> | ||
| <body> | ||
| <h1>Log In</h1> | ||
| <form> | ||
| <label> | ||
| Name | ||
| <input type="text" name="name" required /> | ||
| </label> | ||
| <button type="submit">Log In</button> | ||
| </form> | ||
| <script> | ||
| document.querySelector("form").addEventListener("submit", async (e) => { | ||
| e.preventDefault(); | ||
| const res = await fetch("/api/login", { | ||
| method: "POST", | ||
| body: new URLSearchParams(new FormData(e.target)), | ||
| }); | ||
| if (res.ok) { | ||
| window.location.href = "/"; | ||
| } | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Log Out</title> | ||
| </head> | ||
| <body> | ||
| <h1>Log Out</h1> | ||
| <form> | ||
| <button type="submit">Log Out</button> | ||
| </form> | ||
| <script> | ||
| document.querySelector("form").addEventListener("submit", async (e) => { | ||
| e.preventDefault(); | ||
| const res = await fetch("/api/logout", { method: "POST" }); | ||
| if (res.ok) { | ||
| window.location.href = "/login"; | ||
| } | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
Uh oh!
There was an error while loading. Please reload this page.