summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/app.js31
-rw-r--r--app/auth.js41
-rw-r--r--app/config.js52
-rw-r--r--app/lib/otp.js8
-rw-r--r--app/lib/pipe.js6
-rw-r--r--app/lib/router.js10
-rw-r--r--app/lib/socket.js6
-rw-r--r--app/lib/static.js18
-rw-r--r--app/note-store.js34
-rw-r--r--app/package.json1
-rw-r--r--app/utils.js32
11 files changed, 102 insertions, 137 deletions
diff --git a/app/app.js b/app/app.js
index 23bf0ab..b4af44b 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,18 +1,18 @@
-"use strict";
+import fs from "node:fs";
+import http from "node:http";
+import crypto from "node:crypto";
+import {sj, cors} from "./utils.js";
-const fs = require("fs");
-const http = require("http");
-const crypto = require("crypto");
-const {sj, cors} = require("./utils");
+import Router from "./lib/router.js";
+import Static from "./lib/static.js";
+//import Socket from "./lib/Socket.js";
+//import pipe from "./lib/pipe.js";
+//import otp from "./lib/otp.js";
-const Router = require("./lib/router");
-const Static = require("./lib/static");
-//const Socket = require("./lib/socket");
-//const pipe = require("./lib/pipe");
-//const otp = require("./lib/otp");
+import {HOST, PORT} from "./config.js";
-const config = require("./config");
-const {HOST, PORT} = config;
+import * as auth from "./auth.js";
+import * as noteStore from "./note-store.js";
if (!fs.existsSync("./logs")) fs.mkdirSync("./logs");
if (!fs.existsSync("./users")) fs.mkdirSync("./users");
@@ -23,13 +23,10 @@ const stat = new Static("./public");
//const wss = new Socket(server);
const app = new Router();
-const auth = require("./auth")(config);
-const authed = auth.authed;
auth.attach(app);
+noteStore.attach(app);
-const noteStore = require("./note-store");
-noteStore.attach(app, auth);
-
+const authed = auth.authed;
app.get("/user", authed((req, res) => {
console.log(Date.now()+" Getting user data for "+req.uid);
sj(res, {uid: req.uid, username: auth.getUsername(req.uid)});
diff --git a/app/auth.js b/app/auth.js
index cd771ca..7859a24 100644
--- a/app/auth.js
+++ b/app/auth.js
@@ -1,23 +1,18 @@
-"use strict";
+import {readFile as rf, writeFile as wf} from "node:fs/promises";
+import {readFileSync, rename} from "node:fs";
+import {randomBytes} from "node:crypto";
+import argon2 from "argon2";
-const fs = require("fs");
-const util = require("util");
-const crypto = require("crypto");
-const argon2 = require("argon2");
-const {sj, parseCookies, res204, err400, err401, err500} = require("./utils");
+import {sj, parseCookies, res204, err400, err401, err500} from "./utils.js";
-const rf = util.promisify(fs.readFile);
-const wf = util.promisify(fs.writeFile);
-const randHex = len => crypto.randomBytes(len).toString("hex");
-
-module.exports = config => {
+import {DOMAIN, SECURE, auth as authConfig} from "./config.js";
const {
TOKEN_RENEW_AGE, TOKEN_MAX_AGE, TOKEN_LENGTH, SESSION_ID_LENGTH,
USERNAME_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
- RATE_LIMIT_MAX_LEVEL, RATE_LIMIT_MAX_WAITING,
- fingerprintIP} = config.auth;
-const {DOMAIN, SECURE} = config;
-const exports = {};
+ RATE_LIMIT_MAX_LEVEL, RATE_LIMIT_MAX_WAITING, fingerprintIP,
+} = authConfig;
+
+const randHex = len => randomBytes(len).toString("hex");
// Run once immediately, then every interval until not called
function debounce(fn, interval=100) {
@@ -42,10 +37,10 @@ function debounce(fn, interval=100) {
function loadJSONSync(path) {
const obj = {};
try {
- Object.assign(obj, JSON.parse(fs.readFileSync(path)));
+ Object.assign(obj, JSON.parse(readFileSync(path)));
} catch(e) {
console.log(Date.now()+` Error loading ${path}, creating fallback empty set`);
- fs.rename(path, path+".bad."+Date.now(), err => {});
+ rename(path, path+".bad."+Date.now(), err => {});
}
return obj;
}
@@ -64,9 +59,9 @@ const userIDsFile = "private/userIDs.json";
const userNames = loadJSONSync(userIDsFile), userIDs = {};
for (const [uid, username] of Object.entries(userNames)) userIDs[username.toLowerCase()] = uid;
const writeUIDs = debounce(() => wf(userIDsFile, JSON.stringify(userNames)));
-const getUID = exports.getUID = username =>
+export const getUID = username =>
userIDs.hasOwnProperty(username.toLowerCase()) && userIDs[username.toLowerCase()];
-const getUsername = exports.getUsername = uid =>
+export const getUsername = uid =>
userNames.hasOwnProperty(uid) && userNames[uid];
const createUID = username => { let uid;
do { uid = randHex(5); } while (getUsername(uid));
@@ -385,7 +380,7 @@ const rateLimit = fn => (req, res, ...rest) => {
}, rateLimitTime(rl.level));
}
-function authed(fn) { return rateLimit((req, res, ...rest) => {
+export function authed(fn) { return rateLimit((req, res, ...rest) => {
const token = parseCookies(req)?.token;
if (!token) return err401(res);
@@ -421,9 +416,9 @@ function authed(fn) { return rateLimit((req, res, ...rest) => {
req.uid = uid;
req.sessionID = tokenData.sessionID;
return fn(req, res, ...rest);
-}); } exports.authed = authed;
+}); }
-exports.attach = (app) => { // TODO make endpoints RESTier?
+export const attach = (app) => { // TODO make endpoints RESTier?
app.jpost("/login", rateLimit(login)); // {username, password[, keepSession]} -> {success[, msg][, mustChangePassword]}
app.post("/logout", rateLimit(logout));
app.jpost("/change-password", rateLimit(changePassword)); // {password, newPassword[, username[, keepSession]]} -> {success[, msg]}
@@ -444,5 +439,3 @@ exports.attach = (app) => { // TODO make endpoints RESTier?
// - https://github.com/zxcvbn-ts/zxcvbn
// - non-login-event user notification system (warnings, must change password...)
// - encrypt `private/` on disk using TPM?
-
-return exports; };
diff --git a/app/config.js b/app/config.js
index b7212e3..f137cc4 100644
--- a/app/config.js
+++ b/app/config.js
@@ -1,34 +1,24 @@
-"use strict";
+import {clamp, deepFreeze} from "./utils.js";
-function deepFreeze(obj) {
- for (const name of Reflect.ownKeys(obj)) {
- const value = obj[name];
- if ((value && typeof value === "object") ||
- typeof value === "function") deepFreeze(value);
- } return Object.freeze(obj);
-}
+export const PORT = clamp(+process.env.PORT||8080, 1, 65535);
+export const HOST = process.env.HOST||"0.0.0.0";
+export const DOMAIN = "localhost";
+export const SECURE = false; // i.e. served over https
-Math.clamp ||= ((x,l,h) => Math.max(l,Math.min(x,h)));
-
-module.exports = deepFreeze({
- PORT: Math.clamp(+process.env.PORT||8080, 1, 65535),
- HOST: process.env.HOST||"0.0.0.0",
- DOMAIN: "localhost",
- SECURE: false, // i.e. served over https
-
- auth: {
- TOKEN_RENEW_AGE: 15*60, // 15 mins
- TOKEN_MAX_AGE: 30*86400, // 30 days
- TOKEN_LENGTH: 24, // bytes
- SESSION_ID_LENGTH: 12,
- USERNAME_MAX_LENGTH: 128,
- PASSWORD_MIN_LENGTH: 8,
- PASSWORD_MAX_LENGTH: 512, // Avoid DDOS
- RATE_LIMIT_MAX_LEVEL: 16,
- RATE_LIMIT_MAX_WAITING: 512,
- fingerprintIP: req =>
- //req.headers["cf-connecting-ip"] ||
- //req.headers["x-forwarded-for"] ||
- req.socket.remoteAddress,
- },
+export const auth = deepFreeze({
+ TOKEN_RENEW_AGE: 15*60, // 15 mins
+ TOKEN_MAX_AGE: 30*86400, // 30 days
+ TOKEN_LENGTH: 24, // bytes
+ SESSION_ID_LENGTH: 12,
+ USERNAME_MAX_LENGTH: 128,
+ PASSWORD_MIN_LENGTH: 8,
+ PASSWORD_MAX_LENGTH: 512, // Avoid DDOS
+ RATE_LIMIT_MAX_LEVEL: 16,
+ RATE_LIMIT_MAX_WAITING: 512,
+ fingerprintIP: req =>
+ //req.headers["cf-connecting-ip"] ||
+ //req.headers["x-forwarded-for"] ||
+ req.socket.remoteAddress,
});
+
+export const NOTE_DIR = "./users";
diff --git a/app/lib/otp.js b/app/lib/otp.js
index 751edf4..14763a8 100644
--- a/app/lib/otp.js
+++ b/app/lib/otp.js
@@ -1,6 +1,4 @@
-"use strict";
-
-const { createHmac } = require("crypto");
+import { createHmac } from "node:crypto";
const getHMAC = (k,c) => { const h = createHmac("sha1", k);
h.update(c, "hex"); return h.digest("hex"); }
@@ -12,7 +10,7 @@ const b32u = s => Uint8Array.from(s.split("").reduce((a,c) =>
a+b32a.indexOf(c.toUpperCase()).toString(2).padStart(5,0), "")
.match(/.{8}/g).map(b => parseInt(b,2)));
-module.exports = function totp(secret, { expiry=30,
+export default function totp(secret, { expiry=30,
now=Math.round(new Date().getTime()/1000), length=6 }={}) {
const time = d2h(now/expiry).padStart(16,0);
const hmac = getHMAC(b32u(secret), time);
@@ -21,7 +19,7 @@ module.exports = function totp(secret, { expiry=30,
return otp.padStart(length,0).substr(-length);
}
-module.exports.check = function check(token, secret, { expiry=30,
+export function check(token, secret, { expiry=30,
now=Math.round(new Date().getTime()/1000), length=6, window=0 }={}) {
const i = Array(window*2+1).fill().map((e,i) => now+(expiry*(i-window)))
.findIndex(n => token === this(secret, {now:n, expiry, length}));
diff --git a/app/lib/pipe.js b/app/lib/pipe.js
index e329b2b..db58e5f 100644
--- a/app/lib/pipe.js
+++ b/app/lib/pipe.js
@@ -1,8 +1,6 @@
-"use strict";
+import { spawn } from "node:child_process";
-const { spawn } = require("child_process");
-
-module.exports = function pipe({ command, flags, stdin="", buffer } = {}) {
+export default function pipe({ command, flags, stdin="", buffer } = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, flags);
let stdout = (buffer?[]:"");
diff --git a/app/lib/router.js b/app/lib/router.js
index fd24693..186e6ad 100644
--- a/app/lib/router.js
+++ b/app/lib/router.js
@@ -1,15 +1,13 @@
-"use strict"; // https://github.com/mixu/minimal
-
-const url = require("url");
+import {parse as parseURL} from "node:url";
const degroup = path => Object.assign(path, path.groups);
-class Router {
+export default class Router {
constructor() {
this.routes = [];
}
route(req, res) {
- const pathname = url.parse(req.url).pathname;
+ const pathname = parseURL(req.url).pathname;
return this.routes.some(route => {
const isMatch = route.method === req.method && route.re.test(pathname);
if (isMatch) route.cb(req, res, degroup(route.re.exec(pathname)));
@@ -49,5 +47,3 @@ class Router {
Router.prototype[method] = function(re, cb) {
this.routes.push({method: method.toUpperCase(), cb,
re: (re instanceof RegExp)? re : new RegExp(`^${re}$`)})});
-
-module.exports = Router;
diff --git a/app/lib/socket.js b/app/lib/socket.js
index 88fdd49..9222d00 100644
--- a/app/lib/socket.js
+++ b/app/lib/socket.js
@@ -1,6 +1,4 @@
-"use strict";
-
-const WebSocket = require("ws");
+import WebSocket from "ws";
class Client {
constructor(ws) {
@@ -27,7 +25,7 @@ class Client {
}
}
-module.exports = class Socket {
+export default class Socket {
constructor(server) {
this.wss = new WebSocket.Server({server});
this.handlers = {};
diff --git a/app/lib/static.js b/app/lib/static.js
index ef48ae6..26fc231 100644
--- a/app/lib/static.js
+++ b/app/lib/static.js
@@ -1,13 +1,13 @@
-"use strict";
-
-const Path = require("path");
-const url = require("url");
-const fs = require("fs");
+import {normalize, extname} from "node:path";
+import {parse as parseURL} from "node:url";
+import fs from "node:fs";
const mimeTypes = {
".html": "text/html",
".css": "text/css",
".js": "text/javascript",
+ ".cjs": "text/javascript",
+ ".mjs": "text/javascript",
".json": "application/json",
".wasm": "application/wasm",
".pdf": "application/pdf",
@@ -32,7 +32,7 @@ const mimeTypes = {
".glb": "model/gltf-binary",
};
-module.exports = class Static {
+export default class Static {
constructor(root) {
this.root = `${root||"."}`;
this.e404 = fs.readFileSync(`${this.root}/404.html`);
@@ -46,8 +46,8 @@ module.exports = class Static {
return;
}
- const pathname = url.parse(req.url).pathname;
- const sane = Path.normalize(pathname).replace(/^(\.\.\/)+/, "");
+ const pathname = parseURL(req.url).pathname;
+ const sane = normalize(pathname).replace(/^(\.\.\/)+/, "");
let path = `${this.root}${sane}`; //Path.join(__dirname, sane);
fs.stat(path, (err, stats) => {
@@ -60,7 +60,7 @@ module.exports = class Static {
}}
if (stats.isDirectory()) path += "/index.html";
- const ext = `${Path.extname(path)}`.toLowerCase();
+ const ext = `${extname(path)}`.toLowerCase();
const stream = fs.createReadStream(path);
stream.on("error", e => { console.log(e);
diff --git a/app/note-store.js b/app/note-store.js
index c816efd..eeebf28 100644
--- a/app/note-store.js
+++ b/app/note-store.js
@@ -1,17 +1,8 @@
-"use strict";
+import {readdir as rd, readFile as rf, writeFile as wf} from "node:fs/promises";
+import {sj, res204, err400, err500} from "./utils.js";
+import {authed} from "./auth.js";
-const fs = require("fs");
-const util = require("util");
-const {sj, res204, err400, err500} = require("./utils");
-
-const rd = util.promisify(fs.readdir);
-const rf = util.promisify(fs.readFile);
-const wf = util.promisify(fs.writeFile);
-
-const {R_OK,W_OK} = fs.constants;
-const exists = s => new Promise(r => fs.access(s, R_OK|W_OK, e => r(!e)));
-
-const NOTE_DIR = "./users";
+import {NOTE_DIR} from "./config.js";
function genNoteID() {
const now = new Date().toISOString();
@@ -19,15 +10,9 @@ function genNoteID() {
}
async function newNote(req, res) {
- let noteID, noteFile;
-
- do {
- noteID = genNoteID();
- noteFile = `${NOTE_DIR}/${req.uid}/${noteID}.md`;
- } while (await exists(noteFile)) // TODO increment
-
+ const noteID = genNoteID();
+ const noteFile = `${NOTE_DIR}/${req.uid}/${noteID}.md`;
console.log(Date.now()+` Creating note ${req.uid}:${noteID}`);
-
await wf(noteFile, "");
sj(res, {id:noteID, content:""});
@@ -36,7 +21,7 @@ async function newNote(req, res) {
async function getNote(req, res, match) {
console.log(Date.now()+` Getting note ${req.uid}:${match.noteID}`);
const noteFile = `${NOTE_DIR}/${req.uid}/${match.noteID}.md`;
- const content = await rf(noteFile, "UTF-8"); // TODO exists
+ const content = await rf(noteFile, "UTF-8");
sj(res, {id:match.noteID, content});
}
@@ -56,9 +41,10 @@ async function listNotes(req, res) {
}
// TODO list in order of recent updates w/ timestamps
-// TODO pagination?
+// TODO pagination? Bulk get?
+// TODO handle fs errors
-module.exports.attach = (app, {authed}) => {
+export const attach = app => {
app.get("/list", authed(listNotes));
app.post("/new", authed(newNote));
app.get("/(?<noteID>[0-9]{16})", authed(getNote));
diff --git a/app/package.json b/app/package.json
index 0a4ce2d..a83c944 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,5 +1,6 @@
{
"name": "notes-web",
+ "type": "module",
"version": "0.0.1",
"description": "Zettelkasten-inspired note taking web app",
"author": "Alexis Hovorka <[email protected]>",
diff --git a/app/utils.js b/app/utils.js
index 2ff2b9c..03e2233 100644
--- a/app/utils.js
+++ b/app/utils.js
@@ -1,23 +1,31 @@
-"use strict";
-
-const ct = module.exports.ct = (res, mime, code, head) =>
+export const ct = (res, mime, code, head) =>
res.writeHead(code||200, Object.apply({"Content-Type": mime||"application/json"}, head));
-module.exports.sj = (res, data, {code, head}={}) => { ct(res, null, code, head); res.end(JSON.stringify(data)); }
+export const sj = (res, data, {code, head}={}) => { ct(res, null, code, head); res.end(JSON.stringify(data)); }
-module.exports.res204 = (res) => { res.statusCode = 204; res.end(); }
-module.exports.err400 = (res, msg) => { ct(res, "text/plain", 400); res.end(""+(msg||"400 Bad Request")); }
-module.exports.err401 = (res, msg) => { ct(res, "text/plain", 401); res.end(""+(msg||"401 Unauthorized")); }
-module.exports.err403 = (res, msg) => { ct(res, "text/plain", 403); res.end(""+(msg||"403 Forbidden")); }
-module.exports.err404 = (res, msg) => { ct(res, "text/plain", 404); res.end(""+(msg||"404 Not Found")); }
-module.exports.err500 = (res, msg) => { ct(res, "text/plain", 500); res.end(""+(msg||"500 Internal Server Error"));
+export const res204 = (res) => { res.statusCode = 204; res.end(); }
+export const err400 = (res, msg) => { ct(res, "text/plain", 400); res.end(""+(msg||"400 Bad Request")); }
+export const err401 = (res, msg) => { ct(res, "text/plain", 401); res.end(""+(msg||"401 Unauthorized")); }
+export const err403 = (res, msg) => { ct(res, "text/plain", 403); res.end(""+(msg||"403 Forbidden")); }
+export const err404 = (res, msg) => { ct(res, "text/plain", 404); res.end(""+(msg||"404 Not Found")); }
+export const err500 = (res, msg) => { ct(res, "text/plain", 500); res.end(""+(msg||"500 Internal Server Error"));
console.log(Date.now+" [ERROR] 500"); }
-module.exports.cors = fn => (req, res, ...rest) => {
+export const cors = fn => (req, res, ...rest) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
fn(req, res, ...rest); };
-module.exports.parseCookies = req =>
+export const parseCookies = req =>
req.headers.cookie?.split(";")
.map(c => c.split("=").map(s => decodeURIComponent(s.trim())))
.reduce((a,c) => (a[c[0]]=c[1],a), {});
+
+export const clamp = (x,l,h) => Math.max(l,Math.min(x,h));
+
+export function deepFreeze(obj) {
+ for (const name of Reflect.ownKeys(obj)) {
+ const value = obj[name];
+ if ((value && typeof value === "object") ||
+ typeof value === "function") deepFreeze(value);
+ } return Object.freeze(obj);
+}