+34
-1
src/auth.js
+34
-1
src/auth.js
···
1
1
const jwt = require("jsonwebtoken");
2
+
const { db } = require("./db");
2
3
const { JWT_KEY } = require("./");
3
4
4
5
function authenticateToken(req, res, next) {
···
24
25
}
25
26
}
26
27
27
-
module.exports = { authenticateToken };
28
+
function 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
+
60
+
module.exports = { authenticateToken, authenticateAdmin };
+35
src/db.js
+35
src/db.js
···
3
3
strict: true,
4
4
});
5
5
6
+
function 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
6
18
db.query(`
7
19
CREATE TABLE IF NOT EXISTS users (
8
20
id INTEGER PRIMARY KEY AUTOINCREMENT,
···
11
23
)
12
24
`).run();
13
25
26
+
// subs table
14
27
db.query(`
15
28
CREATE TABLE IF NOT EXISTS subscriptions (
16
29
id INTEGER PRIMARY KEY AUTOINCREMENT,
···
20
33
UNIQUE(user_id, subreddit)
21
34
)
22
35
`).run();
36
+
37
+
// migrations table
38
+
db.query(`
39
+
CREATE TABLE IF NOT EXISTS migrations (
40
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+
name TEXT UNIQUE
42
+
)
43
+
`).run();
44
+
45
+
runMigration("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
+
});
23
58
24
59
module.exports = { db };
+1
-1
src/mixins/head.pug
+1
-1
src/mixins/head.pug
···
2
2
head
3
3
meta(name="viewport" content="width=device-width, initial-scale=1.0")
4
4
meta(charset='UTF-8')
5
-
title #{`readit ${title}`}
5
+
title #{`${title} · readit `}
6
6
link(rel="stylesheet", href="/styles.css")
7
7
link(rel="preconnect" href="https://rsms.me/")
8
8
link(rel="stylesheet" href="https://rsms.me/inter/inter.css")
+1
-1
src/mixins/header.pug
+1
-1
src/mixins/header.pug
-32
src/mixins/sub.pug
-32
src/mixins/sub.pug
···
1
-
mixin 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
-
+21
-1
src/public/styles.css
+21
-1
src/public/styles.css
···
517
517
color: var(--text-color-muted);
518
518
}
519
519
520
-
.register-error-message {
520
+
.register-error-message,
521
+
.dashboard-error-message {
521
522
margin-bottom: 1rem;
522
523
flex-flow: row wrap;
523
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
+
}
+59
-1
src/routes/index.js
+59
-1
src/routes/index.js
···
5
5
const geddit = require("../geddit.js");
6
6
const { JWT_KEY } = require("../");
7
7
const { db } = require("../db");
8
-
const { authenticateToken } = require("../auth");
8
+
const { authenticateToken, authenticateAdmin } = require("../auth");
9
9
const { validateInviteToken } = require("../invite");
10
10
11
11
const router = express.Router();
···
101
101
.query("SELECT * FROM subscriptions WHERE user_id = $id")
102
102
.all({ id: req.user.id });
103
103
res.render("subs", { subs, user: req.user });
104
+
});
105
+
106
+
// GET /dashboard
107
+
router.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
+
127
+
router.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
+
155
+
router.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
+
}
104
162
});
105
163
106
164
// GET /media
+44
src/views/dashboard.pug
+44
src/views/dashboard.pug
···
1
+
include ../mixins/header
2
+
include ../mixins/head
3
+
include ../utils
4
+
5
+
doctype html
6
+
html
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
+
-3
src/views/index.pug
-3
src/views/index.pug
···
1
1
include ../mixins/post
2
-
include ../mixins/sub
3
2
include ../mixins/header
4
3
include ../mixins/head
5
4
include ../utils
6
-
- var subs = []
7
5
doctype html
8
6
html
9
7
+head("home")
10
-
+subMgmt()
11
8
script(defer).
12
9
async function subscribe(sub) {
13
10
await doThing(sub, 'subscribe');
-20
src/views/subs.pug
-20
src/views/subs.pug
···
1
-
include ../mixins/sub
2
1
include ../mixins/header
3
2
include ../mixins/head
4
3
5
4
doctype html
6
5
html
7
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
7
body
28
8
main#content
29
9
+header(user)