From 42409ecb39b5bb7bb98a8f3c0515876dcba40326 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sat, 16 Nov 2024 22:08:05 +0000 Subject: add dashboard view, invites --- src/auth.js | 35 ++++++++++++++++++++++++++++- src/db.js | 35 +++++++++++++++++++++++++++++ src/mixins/head.pug | 2 +- src/mixins/header.pug | 2 +- src/mixins/sub.pug | 32 -------------------------- src/public/styles.css | 22 +++++++++++++++++- src/routes/index.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++++- src/views/dashboard.pug | 44 ++++++++++++++++++++++++++++++++++++ src/views/index.pug | 3 --- src/views/subs.pug | 20 ----------------- 10 files changed, 195 insertions(+), 60 deletions(-) delete mode 100644 src/mixins/sub.pug create mode 100644 src/views/dashboard.pug (limited to 'src') diff --git a/src/auth.js b/src/auth.js index f907e6c..78e3dea 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,4 +1,5 @@ const jwt = require("jsonwebtoken"); +const { db } = require("./db"); const { JWT_KEY } = require("./"); function authenticateToken(req, res, next) { @@ -24,4 +25,36 @@ function authenticateToken(req, res, next) { } } -module.exports = { authenticateToken }; +function authenticateAdmin(req, res, next) { + if (!req.cookies || !req.cookies.auth_token) { + return res.redirect("/login"); + } + + const token = req.cookies.auth_token; + + // If no token, deny access + if (!token) { + return res.redirect( + `/login?redirect=${encodeURIComponent(req.originalUrl)}`, + ); + } + + try { + const user = jwt.verify(token, JWT_KEY); + req.user = user; + const isAdmin = db + .query("SELECT isAdmin FROM users WHERE id = $id and isAdmin = 1") + .get({ + id: req.user.id, + }); + if (isAdmin) { + next(); + } else { + res.status(400).send("only admins can invite"); + } + } catch (error) { + res.send(`failed to authenticate as admin: ${error}`); + } +} + +module.exports = { authenticateToken, authenticateAdmin }; diff --git a/src/db.js b/src/db.js index 24bba3d..747168a 100644 --- a/src/db.js +++ b/src/db.js @@ -3,6 +3,18 @@ const db = new Database("readit.db", { strict: true, }); +function runMigration(name, migrationFn) { + const exists = db + .query("SELECT * FROM migrations WHERE name = $name") + .get({ name }); + + if (!exists) { + migrationFn(); + db.query("INSERT INTO migrations (name) VALUES ($name)").run({ name }); + } +} + +// users table db.query(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -11,6 +23,7 @@ db.query(` ) `).run(); +// subs table db.query(` CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -21,4 +34,26 @@ db.query(` ) `).run(); +// migrations table +db.query(` + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE + ) +`).run(); + +runMigration("add-isAdmin-column", () => { + db.query(` + ALTER TABLE users + ADD COLUMN isAdmin INTEGER DEFAULT 0 + `).run(); + + // first user is admin + db.query(` + UPDATE users + SET isAdmin = 1 + WHERE id = (SELECT MIN(id) FROM users) + `).run(); +}); + module.exports = { db }; diff --git a/src/mixins/head.pug b/src/mixins/head.pug index b95f661..f96e91c 100644 --- a/src/mixins/head.pug +++ b/src/mixins/head.pug @@ -2,7 +2,7 @@ mixin head(title) head meta(name="viewport" content="width=device-width, initial-scale=1.0") meta(charset='UTF-8') - title #{`readit ${title}`} + title #{`${title} ยท readit `} link(rel="stylesheet", href="/styles.css") link(rel="preconnect" href="https://rsms.me/") link(rel="stylesheet" href="https://rsms.me/inter/inter.css") diff --git a/src/mixins/header.pug b/src/mixins/header.pug index 02a8667..4bec1f8 100644 --- a/src/mixins/header.pug +++ b/src/mixins/header.pug @@ -10,7 +10,7 @@ mixin header(user) a(href=`/subs`) subscriptions if user div.header-item - | #{user.username} + a(href='/dashboard') #{user.username} |  a(href='/logout') (logout) else diff --git a/src/mixins/sub.pug b/src/mixins/sub.pug deleted file mode 100644 index a40aa68..0000000 --- a/src/mixins/sub.pug +++ /dev/null @@ -1,32 +0,0 @@ -mixin subMgmt() - script. - function getSubs() { - var store = localStorage.getItem('subs'); - if (store) { - return store.split(',').map((n)=>n.replace(/\/?r\//,'')); - } else { - return []; - } - } - - function subscribe(newsub) { - var subs = getSubs(); - if (!subs.includes(newsub)) { - localStorage.setItem('subs',[...subs,newsub]); - updateButton(newsub); - } - } - - function unsubscribe(sub) { - var subs = getSubs(); - if (subs.includes(sub)) { - localStorage.setItem('subs',subs.filter((s)=>s!=sub)); - updateButton(sub); - } - } - - function issub(sub) { - return getSubs().includes(sub); - } - - diff --git a/src/public/styles.css b/src/public/styles.css index 6c6e705..ba2940d 100644 --- a/src/public/styles.css +++ b/src/public/styles.css @@ -517,8 +517,28 @@ form input[type="submit"]:hover { color: var(--text-color-muted); } -.register-error-message { +.register-error-message, +.dashboard-error-message { margin-bottom: 1rem; flex-flow: row wrap; color: var(--error-text-color); } + +.invite-table { + width: 100%; + padding: 10px 0; +} + +.invite-table th, +.invite-table td +{ + padding: 5px 0; +} + +.invite-table-header { + text-align: left; +} + +.invite-link { + font-family: monospace; +} diff --git a/src/routes/index.js b/src/routes/index.js index 6efeb79..e7ca573 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -5,7 +5,7 @@ const jwt = require("jsonwebtoken"); const geddit = require("../geddit.js"); const { JWT_KEY } = require("../"); const { db } = require("../db"); -const { authenticateToken } = require("../auth"); +const { authenticateToken, authenticateAdmin } = require("../auth"); const { validateInviteToken } = require("../invite"); const router = express.Router(); @@ -103,6 +103,64 @@ router.get("/subs", authenticateToken, async (req, res) => { res.render("subs", { subs, user: req.user }); }); +// GET /dashboard +router.get("/dashboard", authenticateToken, async (req, res) => { + let invites = null; + const isAdmin = db + .query("SELECT isAdmin FROM users WHERE id = $id and isAdmin = 1") + .get({ + id: req.user.id, + }); + if (isAdmin) { + invites = db + .query("SELECT * FROM invites") + .all() + .map((inv) => ({ + ...inv, + createdAt: Date.parse(inv.createdAt), + usedAt: Date.parse(inv.usedAt), + })); + } + res.render("dashboard", { invites, isAdmin, user: req.user }); +}); + +router.get("/create-invite", authenticateAdmin, async (req, res) => { + function generateInviteToken() { + const hasher = new Bun.CryptoHasher("sha256", "super-secret-invite-key"); + return hasher.update(Math.random().toString()).digest("hex").slice(0, 10); + } + + function createInvite() { + const token = generateInviteToken(); + db.run("INSERT INTO invites (token) VALUES ($token)", { token }); + } + + try { + db.run(` + CREATE TABLE IF NOT EXISTS invites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + usedAt TIMESTAMP + ) + `); + + createInvite(); + return res.redirect("/dashboard"); + } catch (err) { + return res.send("failed to create invite"); + } +}); + +router.get("/delete-invite/:id", authenticateToken, async (req, res) => { + try { + db.run("DELETE FROM invites WHERE id = $id", { id: req.params.id }); + return res.redirect("/dashboard"); + } catch (err) { + return res.send("failed to delete invite"); + } +}); + // GET /media router.get("/media/*", authenticateToken, async (req, res) => { const url = req.params[0]; diff --git a/src/views/dashboard.pug b/src/views/dashboard.pug new file mode 100644 index 0000000..be6c6f3 --- /dev/null +++ b/src/views/dashboard.pug @@ -0,0 +1,44 @@ +include ../mixins/header +include ../mixins/head +include ../utils + +doctype html +html + +head("dashboard") + body + main#content + +header(user) + div.hero + h1 dashboard + + if message + div.dashboard-error-message + | #{message} + + if isAdmin + h2 invites + + if invites + table.invite-table + tr + th.invite-table-header link + th.invite-table-header created + th.invite-table-header claimed + th.invite-table-header delete + each invite in invites + tr + td.invite-link + a(href=`/register?token=${invite.token}`) #{invite.token} + td #{timeDifference(Date.now(), invite.createdAt)} ago + if invite.usedAt + td #{timeDifference(Date.now(), invite.usedAt)} ago + else + td unclaimed + td + a(href=`/delete-invite/${invite.id}`) delete + + a(href="/create-invite") create invite + + else + p you aren't an admin and therefore there is nothing to see here yet + diff --git a/src/views/index.pug b/src/views/index.pug index ace8922..140cd57 100644 --- a/src/views/index.pug +++ b/src/views/index.pug @@ -1,13 +1,10 @@ include ../mixins/post -include ../mixins/sub include ../mixins/header include ../mixins/head include ../utils -- var subs = [] doctype html html +head("home") - +subMgmt() script(defer). async function subscribe(sub) { await doThing(sub, 'subscribe'); diff --git a/src/views/subs.pug b/src/views/subs.pug index 86de604..41f29ce 100644 --- a/src/views/subs.pug +++ b/src/views/subs.pug @@ -1,29 +1,9 @@ -include ../mixins/sub include ../mixins/header include ../mixins/head doctype html html +head("subscriptions") - +subMgmt() - script. - function newSubItem(sub) { - const p = document.createElement("p"); - const a = document.createElement("a"); - a.href = `/r/${sub}`; - a.innerText = `r/${sub}`; - p.appendChild(a); - return p; - } - - function buildSubList() { - var subList = document.getElementById('subList'); - getSubs().forEach((sub)=>{ - subList.appendChild(newSubItem(sub)); - }); - } - - document.addEventListener('DOMContentLoaded', buildSubList); body main#content +header(user) -- cgit v1.2.3