aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/auth.js35
-rw-r--r--src/db.js35
-rw-r--r--src/mixins/head.pug2
-rw-r--r--src/mixins/header.pug2
-rw-r--r--src/mixins/sub.pug32
-rw-r--r--src/public/styles.css22
-rw-r--r--src/routes/index.js60
-rw-r--r--src/views/dashboard.pug44
-rw-r--r--src/views/index.pug3
-rw-r--r--src/views/subs.pug20
10 files changed, 195 insertions, 60 deletions
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 @@
1const jwt = require("jsonwebtoken"); 1const jwt = require("jsonwebtoken");
2const { db } = require("./db");
2const { JWT_KEY } = require("./"); 3const { JWT_KEY } = require("./");
3 4
4function authenticateToken(req, res, next) { 5function authenticateToken(req, res, next) {
@@ -24,4 +25,36 @@ function authenticateToken(req, res, next) {
24 } 25 }
25} 26}
26 27
27module.exports = { authenticateToken }; 28function authenticateAdmin(req, res, next) {
29 if (!req.cookies || !req.cookies.auth_token) {
30 return res.redirect("/login");
31 }
32
33 const token = req.cookies.auth_token;
34
35 // If no token, deny access
36 if (!token) {
37 return res.redirect(
38 `/login?redirect=${encodeURIComponent(req.originalUrl)}`,
39 );
40 }
41
42 try {
43 const user = jwt.verify(token, JWT_KEY);
44 req.user = user;
45 const isAdmin = db
46 .query("SELECT isAdmin FROM users WHERE id = $id and isAdmin = 1")
47 .get({
48 id: req.user.id,
49 });
50 if (isAdmin) {
51 next();
52 } else {
53 res.status(400).send("only admins can invite");
54 }
55 } catch (error) {
56 res.send(`failed to authenticate as admin: ${error}`);
57 }
58}
59
60module.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", {
3 strict: true, 3 strict: true,
4}); 4});
5 5
6function runMigration(name, migrationFn) {
7 const exists = db
8 .query("SELECT * FROM migrations WHERE name = $name")
9 .get({ name });
10
11 if (!exists) {
12 migrationFn();
13 db.query("INSERT INTO migrations (name) VALUES ($name)").run({ name });
14 }
15}
16
17// users table
6db.query(` 18db.query(`
7 CREATE TABLE IF NOT EXISTS users ( 19 CREATE TABLE IF NOT EXISTS users (
8 id INTEGER PRIMARY KEY AUTOINCREMENT, 20 id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -11,6 +23,7 @@ db.query(`
11 ) 23 )
12`).run(); 24`).run();
13 25
26// subs table
14db.query(` 27db.query(`
15 CREATE TABLE IF NOT EXISTS subscriptions ( 28 CREATE TABLE IF NOT EXISTS subscriptions (
16 id INTEGER PRIMARY KEY AUTOINCREMENT, 29 id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -21,4 +34,26 @@ db.query(`
21 ) 34 )
22`).run(); 35`).run();
23 36
37// migrations table
38db.query(`
39 CREATE TABLE IF NOT EXISTS migrations (
40 id INTEGER PRIMARY KEY AUTOINCREMENT,
41 name TEXT UNIQUE
42 )
43`).run();
44
45runMigration("add-isAdmin-column", () => {
46 db.query(`
47 ALTER TABLE users
48 ADD COLUMN isAdmin INTEGER DEFAULT 0
49 `).run();
50
51 // first user is admin
52 db.query(`
53 UPDATE users
54 SET isAdmin = 1
55 WHERE id = (SELECT MIN(id) FROM users)
56 `).run();
57});
58
24module.exports = { db }; 59module.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)
2 head 2 head
3 meta(name="viewport" content="width=device-width, initial-scale=1.0") 3 meta(name="viewport" content="width=device-width, initial-scale=1.0")
4 meta(charset='UTF-8') 4 meta(charset='UTF-8')
5 title #{`readit ${title}`} 5 title #{`${title} ยท readit `}
6 link(rel="stylesheet", href="/styles.css") 6 link(rel="stylesheet", href="/styles.css")
7 link(rel="preconnect" href="https://rsms.me/") 7 link(rel="preconnect" href="https://rsms.me/")
8 link(rel="stylesheet" href="https://rsms.me/inter/inter.css") 8 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)
10 a(href=`/subs`) subscriptions 10 a(href=`/subs`) subscriptions
11 if user 11 if user
12 div.header-item 12 div.header-item
13 | #{user.username} 13 a(href='/dashboard') #{user.username}
14 |  14 | 
15 a(href='/logout') (logout) 15 a(href='/logout') (logout)
16 else 16 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 @@
1mixin subMgmt()
2 script.
3 function getSubs() {
4 var store = localStorage.getItem('subs');
5 if (store) {
6 return store.split(',').map((n)=>n.replace(/\/?r\//,''));
7 } else {
8 return [];
9 }
10 }
11
12 function subscribe(newsub) {
13 var subs = getSubs();
14 if (!subs.includes(newsub)) {
15 localStorage.setItem('subs',[...subs,newsub]);
16 updateButton(newsub);
17 }
18 }
19
20 function unsubscribe(sub) {
21 var subs = getSubs();
22 if (subs.includes(sub)) {
23 localStorage.setItem('subs',subs.filter((s)=>s!=sub));
24 updateButton(sub);
25 }
26 }
27
28 function issub(sub) {
29 return getSubs().includes(sub);
30 }
31
32
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 {
517 color: var(--text-color-muted); 517 color: var(--text-color-muted);
518} 518}
519 519
520.register-error-message { 520.register-error-message,
521.dashboard-error-message {
521 margin-bottom: 1rem; 522 margin-bottom: 1rem;
522 flex-flow: row wrap; 523 flex-flow: row wrap;
523 color: var(--error-text-color); 524 color: var(--error-text-color);
524} 525}
526
527.invite-table {
528 width: 100%;
529 padding: 10px 0;
530}
531
532.invite-table th,
533.invite-table td
534{
535 padding: 5px 0;
536}
537
538.invite-table-header {
539 text-align: left;
540}
541
542.invite-link {
543 font-family: monospace;
544}
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");
5const geddit = require("../geddit.js"); 5const geddit = require("../geddit.js");
6const { JWT_KEY } = require("../"); 6const { JWT_KEY } = require("../");
7const { db } = require("../db"); 7const { db } = require("../db");
8const { authenticateToken } = require("../auth"); 8const { authenticateToken, authenticateAdmin } = require("../auth");
9const { validateInviteToken } = require("../invite"); 9const { validateInviteToken } = require("../invite");
10 10
11const router = express.Router(); 11const router = express.Router();
@@ -103,6 +103,64 @@ router.get("/subs", authenticateToken, async (req, res) => {
103 res.render("subs", { subs, user: req.user }); 103 res.render("subs", { subs, user: req.user });
104}); 104});
105 105
106// GET /dashboard
107router.get("/dashboard", authenticateToken, async (req, res) => {
108 let invites = null;
109 const isAdmin = db
110 .query("SELECT isAdmin FROM users WHERE id = $id and isAdmin = 1")
111 .get({
112 id: req.user.id,
113 });
114 if (isAdmin) {
115 invites = db
116 .query("SELECT * FROM invites")
117 .all()
118 .map((inv) => ({
119 ...inv,
120 createdAt: Date.parse(inv.createdAt),
121 usedAt: Date.parse(inv.usedAt),
122 }));
123 }
124 res.render("dashboard", { invites, isAdmin, user: req.user });
125});
126
127router.get("/create-invite", authenticateAdmin, async (req, res) => {
128 function generateInviteToken() {
129 const hasher = new Bun.CryptoHasher("sha256", "super-secret-invite-key");
130 return hasher.update(Math.random().toString()).digest("hex").slice(0, 10);
131 }
132
133 function createInvite() {
134 const token = generateInviteToken();
135 db.run("INSERT INTO invites (token) VALUES ($token)", { token });
136 }
137
138 try {
139 db.run(`
140 CREATE TABLE IF NOT EXISTS invites (
141 id INTEGER PRIMARY KEY AUTOINCREMENT,
142 token TEXT NOT NULL,
143 createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
144 usedAt TIMESTAMP
145 )
146 `);
147
148 createInvite();
149 return res.redirect("/dashboard");
150 } catch (err) {
151 return res.send("failed to create invite");
152 }
153});
154
155router.get("/delete-invite/:id", authenticateToken, async (req, res) => {
156 try {
157 db.run("DELETE FROM invites WHERE id = $id", { id: req.params.id });
158 return res.redirect("/dashboard");
159 } catch (err) {
160 return res.send("failed to delete invite");
161 }
162});
163
106// GET /media 164// GET /media
107router.get("/media/*", authenticateToken, async (req, res) => { 165router.get("/media/*", authenticateToken, async (req, res) => {
108 const url = req.params[0]; 166 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 @@
1include ../mixins/header
2include ../mixins/head
3include ../utils
4
5doctype html
6html
7 +head("dashboard")
8 body
9 main#content
10 +header(user)
11 div.hero
12 h1 dashboard
13
14 if message
15 div.dashboard-error-message
16 | #{message}
17
18 if isAdmin
19 h2 invites
20
21 if invites
22 table.invite-table
23 tr
24 th.invite-table-header link
25 th.invite-table-header created
26 th.invite-table-header claimed
27 th.invite-table-header delete
28 each invite in invites
29 tr
30 td.invite-link
31 a(href=`/register?token=${invite.token}`) #{invite.token}
32 td #{timeDifference(Date.now(), invite.createdAt)} ago
33 if invite.usedAt
34 td #{timeDifference(Date.now(), invite.usedAt)} ago
35 else
36 td unclaimed
37 td
38 a(href=`/delete-invite/${invite.id}`) delete
39
40 a(href="/create-invite") create invite
41
42 else
43 p you aren't an admin and therefore there is nothing to see here yet
44
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 @@
1include ../mixins/post 1include ../mixins/post
2include ../mixins/sub
3include ../mixins/header 2include ../mixins/header
4include ../mixins/head 3include ../mixins/head
5include ../utils 4include ../utils
6- var subs = []
7doctype html 5doctype html
8html 6html
9 +head("home") 7 +head("home")
10 +subMgmt()
11 script(defer). 8 script(defer).
12 async function subscribe(sub) { 9 async function subscribe(sub) {
13 await doThing(sub, 'subscribe'); 10 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 @@
1include ../mixins/sub
2include ../mixins/header 1include ../mixins/header
3include ../mixins/head 2include ../mixins/head
4 3
5doctype html 4doctype html
6html 5html
7 +head("subscriptions") 6 +head("subscriptions")
8 +subMgmt()
9 script.
10 function newSubItem(sub) {
11 const p = document.createElement("p");
12 const a = document.createElement("a");
13 a.href = `/r/${sub}`;
14 a.innerText = `r/${sub}`;
15 p.appendChild(a);
16 return p;
17 }
18
19 function buildSubList() {
20 var subList = document.getElementById('subList');
21 getSubs().forEach((sub)=>{
22 subList.appendChild(newSubItem(sub));
23 });
24 }
25
26 document.addEventListener('DOMContentLoaded', buildSubList);
27 body 7 body
28 main#content 8 main#content
29 +header(user) 9 +header(user)