diff --git a/package.json b/package.json index 417d453..76ee416 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "scripts": { "dev": "vite", "build": "vite build", - "start": "http-server dist" + "start": "node server.js" }, "dependencies": { + "client-sessions": "^0.8.0", + "express": "^4.18.2", "vue": "^3.0.5" }, "devDependencies": { diff --git a/server.js b/server.js new file mode 100644 index 0000000..4c57f6b --- /dev/null +++ b/server.js @@ -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}`); diff --git a/src/api/404.html b/src/api/404.html new file mode 100644 index 0000000..dc5a140 --- /dev/null +++ b/src/api/404.html @@ -0,0 +1,11 @@ + + + + + 404 Not Found + + +

404 Not Found

+

The page you requested could not be found.

+ + diff --git a/src/api/404.js b/src/api/404.js new file mode 100644 index 0000000..0e76e54 --- /dev/null +++ b/src/api/404.js @@ -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) { + res.status(404).sendFile(notFoundPath); +} diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..c7b8385 --- /dev/null +++ b/src/api/index.js @@ -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) => { + req.session.reset(); + res.json({ ok: true }); +}); + +export function requireLogin(req, res, next) { + if (!req.session?.user) { + return res.status(401).json({ error: "unauthorized" }); + } + next(); +} diff --git a/src/api/login.html b/src/api/login.html new file mode 100644 index 0000000..09a4645 --- /dev/null +++ b/src/api/login.html @@ -0,0 +1,29 @@ + + + + + Log In + + +

Log In

+
+ + +
+ + + diff --git a/src/api/logout.html b/src/api/logout.html new file mode 100644 index 0000000..f4017c4 --- /dev/null +++ b/src/api/logout.html @@ -0,0 +1,22 @@ + + + + + Log Out + + +

Log Out

+
+ +
+ + +