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