sourceapproutesauth.js

import { randomBytes } from "crypto";

import Database from "better-sqlite3";
import express from "express";
import passport from "passport";
import LocalStrategy from "passport-local";
import RememberMeStrategy from "../passport-remember-me.js";

import grimm from "../grimm.js";
import { authHash } from "../../shush/shush.js";

const router = express.Router();
const db = new Database("./db/site.db");
db.pragma("journal_mode = DELETE");
const rememberDb = new Database("./db/remember.db");

const doubleTranslate = (string, ...args) =>
	`<span lang="en-GB">${grimm.translate(
		"en",
		string,
		...args
	)}</span> · <i lang="nl-NL">${grimm.translate("nl", string, ...args)}</i>`;

const formPage = (slug, title) => (req, res, next) => {
	const query = Object.assign(req.query, req.body);

	const toSendOver = {
		lang: query.lang || "en",
		grimm: grimm,
		title: grimm.translate(query.lang || "en", title),
		tr: query.lang ? grimm.translator(query.lang) : doubleTranslate,
		user: req?.session?.passport?.user ?? null
	};

	return req.app.render(`../admin/auth/${slug}`, toSendOver, (err, html) => {
		if (err) {
			return next(err);
		}
		if (req.headers["hx-request"]) {
			return res.send(html);
		} else {
			return res.render(
				`../admin/auth/authform`,
				Object.assign(toSendOver, { form: html })
			);
		}
	});
};

passport.use(
	new LocalStrategy((username, password, done) => {
		const row = db
			.prepare("Select * From users Where username = ?")
			.get(username.trim());

		if (!row) {
			return done(null, false, { message: "auth.message.noLogin" });
		} else {
			const passwordMatches =
				row.hash === authHash(username, password, row.salt);
			delete row.hash;
			delete row.salt;

			if (passwordMatches) {
				return done(null, false, { message: "auth.message.noLogin" });
			} else {
				return done(null, row);
			}
		}
	})
);

passport.use(
	new RememberMeStrategy(
		(token, done) => {
			try {
				const userId = rememberDb
					.prepare(`Select userId From remember Where token = ?`)
					.get(token)?.userId;

				const user = db
					.prepare(
						"Select userID, username, permissions From users Where userID = ?"
					)
					.get(userId);

				rememberDb
					.prepare(`Delete From remember Where token = ?`)
					.run(token);

				return done(null, user || false);
			} catch (err) {
				return done(err);
			}
		},
		(user, done) => {
			const token = randomBytes(64).toString("hex");

			try {
				rememberDb
					.prepare(
						`Insert Into remember (token, userId) Values (@token, @userId)`
					)
					.run({ token: token, userId: user.userID });

				return done(null, token);
			} catch (err) {
				return done(err);
			}
		}
	)
);

passport.serializeUser((user, done) => {
	process.nextTick(() => {
		done(null, user);
	});
});

passport.deserializeUser((user, done) => {
	process.nextTick(() => {
		done(null, user);
	});
});

router.get("/x/signup", formPage("signup", "auth.signUp"));

router.get("/x/login", (req, res, next) => {
	if (req.isAuthenticated()) {
		console.log(req?.session?.passport?.user);
		return formPage("logout", "auth.logOut")(req, res, next);
	}
	return formPage("login", "auth.logIn")(req, res, next);
});

router.post("/x/post-signup", (req, res, next) => {
	const query = Object.assign(req.query, req.body);

	const tr = query.lang ? grimm.translator(query.lang) : doubleTranslate;

	if (!(query.username.trim() && query.password && query.key)) {
		return res.status(400).send(tr("auth.message.missingFields"));
	}

	const rowKey = db
		.prepare(`SELECT * FROM signup_keys WHERE key = ?`)
		.get(query.key);

	if (
		!rowKey ||
		rowKey.spent ||
		new Date(rowKey.created) <
			new Date().getTime() - 7 * 24 * 60 * 60 * 1000
	) {
		res.status(400).send(tr("auth.message.invalidSignupKey"));
		return;
	}

	const salt = randomBytes(32).toString("hex");

	const userData = {
		username: query.username.trim(),
		salt: salt,
		hash: authHash(query.username, query.password, query.salt),
		permissions: rowKey.permissions
	};

	db.transaction(() => {
		db.prepare(`UPDATE signup_keys SET spent = 1 WHERE key = ?`).run(
			query.key
		);
		db.prepare(
			`INSERT INTO users (username, salt, hash, permissions) VALUES (@username, @salt, @hash, @permissions)`
		).run(userData);
	})();

	console.log(
		`Registered user ${userData.username} with ${userData.permissions} rights`
	);

	return res.status(200).send(tr("auth.message.signedUp"));
});

router.post(
	"/x/post-login",
	(req, res, next) => {
		const query = Object.assign(req.query, req.body);

		const tr = query.lang ? grimm.translator(query.lang) : doubleTranslate;

		passport.authenticate("local", (err, user, info) => {
			if (err) {
				return next(err);
			}
			if (!user) {
				return res.status(400).send(tr(info.message));
			}
			return req.login(user, err => (err ? next(err) : next()));
		})(req, res, next);
	},
	(req, res, next) => {
		if (!req.body.rememberMe) return next();
		const token = randomBytes(64).toString("hex");

		rememberDb
			.prepare(
				`Insert Into remember (token, userId) Values (@token, @userId)`
			)
			.run({ token: token, userId: req.user.userID });

		res.cookie("remember_me", token, {
			path: "/",
			httpOnly: true,
			maxAge: 365 * 24 * 60 * 60 * 1000
		});

		return next();
	},
	(req, res) => res.redirect("back")
);

router.post("/x/post-logout", (req, res, next) => {
	try {
		req.logout(err => {
			if (err) {
				console.log(err);
				return next(err);
			}
			return res.redirect("back");
		});
	} catch (catchErr) {
		req.session.destroy(err => next(err));
		return res.redirect("back");
	}
});

router.get("*", (req, res, next) => next());

export default router;