aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/auth.js27
-rw-r--r--src/db.js24
-rw-r--r--src/geddit.js8
-rw-r--r--src/index.js44
-rw-r--r--src/mixins/head.pug4
-rw-r--r--src/mixins/header.pug10
-rw-r--r--src/public/styles.css61
-rw-r--r--src/routes/index.js234
-rw-r--r--src/views/comments.pug10
-rw-r--r--src/views/index.pug56
-rw-r--r--src/views/login.pug26
-rw-r--r--src/views/register.pug28
-rw-r--r--src/views/single_comment_thread.pug2
-rw-r--r--src/views/subs.pug10
14 files changed, 366 insertions, 178 deletions
diff --git a/src/auth.js b/src/auth.js
new file mode 100644
index 0000000..f907e6c
--- /dev/null
+++ b/src/auth.js
@@ -0,0 +1,27 @@
1const jwt = require("jsonwebtoken");
2const { JWT_KEY } = require("./");
3
4function authenticateToken(req, res, next) {
5 if (!req.cookies || !req.cookies.auth_token) {
6 return res.redirect("/login");
7 }
8
9 const token = req.cookies.auth_token;
10
11 // If no token, deny access
12 if (!token) {
13 return res.redirect(
14 `/login?redirect=${encodeURIComponent(req.originalUrl)}`,
15 );
16 }
17
18 try {
19 const user = jwt.verify(token, JWT_KEY);
20 req.user = user;
21 next();
22 } catch (error) {
23 res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
24 }
25}
26
27module.exports = { authenticateToken };
diff --git a/src/db.js b/src/db.js
new file mode 100644
index 0000000..24bba3d
--- /dev/null
+++ b/src/db.js
@@ -0,0 +1,24 @@
1const { Database } = require("bun:sqlite");
2const db = new Database("readit.db", {
3 strict: true,
4});
5
6db.query(`
7 CREATE TABLE IF NOT EXISTS users (
8 id INTEGER PRIMARY KEY AUTOINCREMENT,
9 username TEXT UNIQUE,
10 password_hash TEXT
11 )
12`).run();
13
14db.query(`
15 CREATE TABLE IF NOT EXISTS subscriptions (
16 id INTEGER PRIMARY KEY AUTOINCREMENT,
17 user_id INTEGER,
18 subreddit TEXT,
19 FOREIGN KEY(user_id) REFERENCES users(id),
20 UNIQUE(user_id, subreddit)
21 )
22`).run();
23
24module.exports = { db };
diff --git a/src/geddit.js b/src/geddit.js
index 3231b5e..aee7703 100644
--- a/src/geddit.js
+++ b/src/geddit.js
@@ -18,11 +18,11 @@ class Geddit {
18 include_over_18: true, 18 include_over_18: true,
19 }; 19 };
20 20
21 subreddit = subreddit ? `/r/${subreddit}` : ""; 21 const subredditStr = subreddit ? `/r/${subreddit}` : "";
22 22
23 return await fetch( 23 return await fetch(
24 `${ 24 `${
25 this.host + subreddit 25 this.host + subredditStr
26 }/${sort}.json?${new URLSearchParams(Object.assign(params, options))}`, 26 }/${sort}.json?${new URLSearchParams(Object.assign(params, options))}`,
27 ) 27 )
28 .then((res) => res.json()) 28 .then((res) => res.json())
@@ -300,7 +300,7 @@ class Geddit {
300 300
301 async searchAll(query, subreddit = null, options = {}) { 301 async searchAll(query, subreddit = null, options = {}) {
302 options.q = query; 302 options.q = query;
303 subreddit = subreddit ? `/r/${subreddit}` : ""; 303 const subredditStr = subreddit ? `/r/${subreddit}` : "";
304 304
305 const params = { 305 const params = {
306 limit: 25, 306 limit: 25,
@@ -310,7 +310,7 @@ class Geddit {
310 310
311 return await fetch( 311 return await fetch(
312 `${ 312 `${
313 this.host + subreddit 313 this.host + subredditStr
314 }/search.json?${new URLSearchParams(Object.assign(params, options))}`, 314 }/search.json?${new URLSearchParams(Object.assign(params, options))}`,
315 ) 315 )
316 .then((res) => res.json()) 316 .then((res) => res.json())
diff --git a/src/index.js b/src/index.js
index 6885ee5..6296534 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,35 +1,13 @@
1const express = require("express"); 1const express = require("express");
2const rateLimit = require("express-rate-limit");
2const path = require("node:path"); 3const path = require("node:path");
3const geddit = require("./geddit.js"); 4const geddit = require("./geddit.js");
4const { Database } = require("bun:sqlite"); 5const cookieParser = require("cookie-parser");
5
6const db = new Database("readit.db");
7
8const createUsers = db.query(`
9 CREATE TABLE IF NOT EXISTS users (
10 id INTEGER PRIMARY KEY AUTOINCREMENT,
11 username TEXT UNIQUE,
12 password_hash TEXT
13 )
14`);
15
16createUsers.run();
17
18const createSubs = db.query(`
19 CREATE TABLE IF NOT EXISTS subscriptions (
20 id INTEGER PRIMARY KEY AUTOINCREMENT,
21 user_id INTEGER,
22 subreddit TEXT,
23 FOREIGN KEY(user_id) REFERENCES users(id),
24 UNIQUE(user_id, subreddit)
25 )
26`);
27
28createSubs.run();
29
30module.exports = { db };
31
32const app = express(); 6const app = express();
7const hasher = new Bun.CryptoHasher("sha256", "secret-key");
8const JWT_KEY = hasher.update(Math.random().toString()).digest("hex");
9
10module.exports = { JWT_KEY };
33 11
34app.set("views", path.join(__dirname, "views")); 12app.set("views", path.join(__dirname, "views"));
35app.set("view engine", "pug"); 13app.set("view engine", "pug");
@@ -38,6 +16,16 @@ const routes = require("./routes/index");
38app.use(express.json()); 16app.use(express.json());
39app.use(express.urlencoded({ extended: true })); 17app.use(express.urlencoded({ extended: true }));
40app.use(express.static(path.join(__dirname, "public"))); 18app.use(express.static(path.join(__dirname, "public")));
19app.use(cookieParser());
20app.use(
21 rateLimit({
22 windowMs: 15 * 60 * 1000,
23 max: 100,
24 message: "Too many requests from this IP, please try again later.",
25 standardHeaders: true,
26 legacyHeaders: false,
27 }),
28);
41app.use("/", routes); 29app.use("/", routes);
42 30
43const port = process.env.READIT_PORT; 31const port = process.env.READIT_PORT;
diff --git a/src/mixins/head.pug b/src/mixins/head.pug
index f7c1baf..b95f661 100644
--- a/src/mixins/head.pug
+++ b/src/mixins/head.pug
@@ -1,8 +1,8 @@
1mixin head() 1mixin 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 reddit 5 title #{`readit ${title}`}
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 f4b85e0..02a8667 100644
--- a/src/mixins/header.pug
+++ b/src/mixins/header.pug
@@ -1,4 +1,4 @@
1mixin header() 1mixin header(user)
2 div.header 2 div.header
3 div.header-item 3 div.header-item
4 a(href=`/`) home 4 a(href=`/`) home
@@ -8,4 +8,12 @@ mixin header()
8 a(href=`/r/popular`) popular 8 a(href=`/r/popular`) popular
9 div.header-item 9 div.header-item
10 a(href=`/subs`) subscriptions 10 a(href=`/subs`) subscriptions
11 if user
12 div.header-item
13 | #{user.username}
14 | 
15 a(href='/logout') (logout)
16 else
17 div.header-item
18 a(href=`/login`) login
11 19
diff --git a/src/public/styles.css b/src/public/styles.css
index b753bd8..0a0a2e2 100644
--- a/src/public/styles.css
+++ b/src/public/styles.css
@@ -8,6 +8,7 @@
8 --link-color: #29BC9B; 8 --link-color: #29BC9B;
9 --link-visited-color: #999; 9 --link-visited-color: #999;
10 --accent: var(--link-color); 10 --accent: var(--link-color);
11 --error-text-color: red;
11 12
12 font-family: Inter, sans-serif; 13 font-family: Inter, sans-serif;
13 font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'dlig' 1, 'ss01' 1, 'ss07' 1, 'ss08' 1; 14 font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'dlig' 1, 'ss01' 1, 'ss07' 1, 'ss08' 1;
@@ -24,6 +25,7 @@
24 --link-color: #79ffe1; 25 --link-color: #79ffe1;
25 --link-visited-color: #999; 26 --link-visited-color: #999;
26 --accent: var(--link-color); 27 --accent: var(--link-color);
28 --error-text-color: lightcoral;
27 } 29 }
28} 30}
29 31
@@ -156,8 +158,7 @@ summary::before {
156.info-item, .header-item, .footer-item { 158.info-item, .header-item, .footer-item {
157 margin-right: 14px; 159 margin-right: 14px;
158} 160}
159 161
160
161.media-preview img, 162.media-preview img,
162.media-preview video { 163.media-preview video {
163 object-fit: cover; 164 object-fit: cover;
@@ -185,6 +186,13 @@ summary::before {
185 max-width: 95%; 186 max-width: 95%;
186 padding: 5px; 187 padding: 5px;
187} 188}
189
190form {
191 display: flex;
192 flex-direction: column;
193 align-items: center;
194 width: 90%;
195}
188 196
189@media (min-width: 768px) { 197@media (min-width: 768px) {
190 .post, .comments-container, .hero, .header, .footer { 198 .post, .comments-container, .hero, .header, .footer {
@@ -203,6 +211,9 @@ summary::before {
203 .post-media { 211 .post-media {
204 max-width: 50%; 212 max-width: 50%;
205 } 213 }
214 form {
215 width: 40%;
216 }
206} 217}
207 218
208@media (min-width: 1080px) { 219@media (min-width: 1080px) {
@@ -226,6 +237,9 @@ summary::before {
226 .post-media { 237 .post-media {
227 max-width: 50%; 238 max-width: 50%;
228 } 239 }
240 form {
241 width: 20%;
242 }
229} 243}
230 244
231@media (min-width: 2560px) { 245@media (min-width: 2560px) {
@@ -235,10 +249,6 @@ summary::before {
235 } 249 }
236} 250}
237 251
238.comments-container, .self-text {
239 text-align: justify;
240}
241
242.comment, .more { 252.comment, .more {
243 width: 100%; 253 width: 100%;
244 border-left: 1px dashed var(--text-color-muted); 254 border-left: 1px dashed var(--text-color-muted);
@@ -320,7 +330,7 @@ hr {
320blockquote { 330blockquote {
321 margin: 0px; 331 margin: 0px;
322 padding-left: 10px; 332 padding-left: 10px;
323 border-left: 4px solid var(--blockquote-color); 333 border-left: 2px solid var(--blockquote-color);
324 color: var(--blockquote-color); 334 color: var(--blockquote-color);
325} 335}
326 336
@@ -400,8 +410,8 @@ a {
400} 410}
401 411
402.gallery-item { 412.gallery-item {
403 flex: 0 0 auto; 413 flex: 0 0 auto;
404 margin-right: 10px; 414 margin-right: 10px;
405} 415}
406 416
407.gallery img { 417.gallery img {
@@ -439,20 +449,15 @@ textarea {
439 color: var(--text-color); 449 color: var(--text-color);
440} 450}
441 451
442form {
443 display: flex;
444 flex-direction: column;
445 align-items: center;
446}
447
448form label { 452form label {
449 width: 100%; 453 width: 100%;
454 flex-basis: 100%;
450 margin: 5px 0; 455 margin: 5px 0;
451 color: var(--text-color); 456 color: var(--text-color);
452} 457}
453 458
454form input[type="submit"] { 459form input[type="submit"] {
455 width: auto; 460 width: 100%;
456 padding: 10px 20px; 461 padding: 10px 20px;
457 margin-top: 20px; 462 margin-top: 20px;
458 background-color: var(--link-color); 463 background-color: var(--link-color);
@@ -466,3 +471,27 @@ form input[type="submit"]:hover {
466 background-color: var(--link-color); 471 background-color: var(--link-color);
467 opacity: 0.8; 472 opacity: 0.8;
468} 473}
474
475.input-text {
476 width: 100%;
477}
478
479.submit-button {
480 margin: 24px 0;
481 width: 100%;
482 display: flex;
483 flex-direction: row;
484 justify-content: center;
485}
486
487.submit-button button {
488 width: 100%;
489 padding: 12px;
490 background-color: var(--accent);
491 color: var(--bg-color);
492}
493
494.register-error-message {
495 flex-flow: row wrap;
496 color: var(--error-text-color);
497}
diff --git a/src/routes/index.js b/src/routes/index.js
index d517ba2..7e68636 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -2,19 +2,21 @@ const express = require("express");
2const he = require("he"); 2const he = require("he");
3const { hash, compare } = require("bun"); 3const { hash, compare } = require("bun");
4const jwt = require("jsonwebtoken"); 4const jwt = require("jsonwebtoken");
5const router = express.Router();
6const secretKey = "your_secret_key"; // Replace with your actual secret key
7const geddit = require("../geddit.js"); 5const geddit = require("../geddit.js");
8const { db } = require("../index"); 6const { JWT_KEY } = require("../");
7const { db } = require("../db");
8const { authenticateToken } = require("../auth");
9
10const router = express.Router();
9const G = new geddit.Geddit(); 11const G = new geddit.Geddit();
10 12
11// GET / 13// GET /
12router.get("/", async (req, res) => { 14router.get("/", authenticateToken, async (req, res) => {
13 res.render("home"); 15 res.render("home");
14}); 16});
15 17
16// GET /r/:id 18// GET /r/:id
17router.get("/r/:subreddit", async (req, res) => { 19router.get("/r/:subreddit", authenticateToken, async (req, res) => {
18 const subreddit = req.params.subreddit; 20 const subreddit = req.params.subreddit;
19 const isMulti = subreddit.includes("+"); 21 const isMulti = subreddit.includes("+");
20 const query = req.query ? req.query : {}; 22 const query = req.query ? req.query : {};
@@ -22,16 +24,33 @@ router.get("/r/:subreddit", async (req, res) => {
22 query.sort = "hot"; 24 query.sort = "hot";
23 } 25 }
24 26
27 let isSubbed = false;
28 if (!isMulti) {
29 isSubbed =
30 db
31 .query(
32 "SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
33 )
34 .get({ id: req.user.id, subreddit }) !== null;
35 }
25 const postsReq = G.getSubmissions(query.sort, `${subreddit}`, query); 36 const postsReq = G.getSubmissions(query.sort, `${subreddit}`, query);
26 const aboutReq = G.getSubreddit(`${subreddit}`); 37 const aboutReq = G.getSubreddit(`${subreddit}`);
27 38
28 const [posts, about] = await Promise.all([postsReq, aboutReq]); 39 const [posts, about] = await Promise.all([postsReq, aboutReq]);
29 40
30 res.render("index", { subreddit, posts, about, query, isMulti }); 41 res.render("index", {
42 subreddit,
43 posts,
44 about,
45 query,
46 isMulti,
47 user: req.user,
48 isSubbed,
49 });
31}); 50});
32 51
33// GET /comments/:id 52// GET /comments/:id
34router.get("/comments/:id", async (req, res) => { 53router.get("/comments/:id", authenticateToken, async (req, res) => {
35 const id = req.params.id; 54 const id = req.params.id;
36 55
37 const params = { 56 const params = {
@@ -39,34 +58,44 @@ router.get("/comments/:id", async (req, res) => {
39 }; 58 };
40 response = await G.getSubmissionComments(id, params); 59 response = await G.getSubmissionComments(id, params);
41 60
42 res.render("comments", unescape_submission(response)); 61 res.render("comments", {
62 data: unescape_submission(response),
63 user: req.user,
64 });
43}); 65});
44 66
45// GET /comments/:parent_id/comment/:child_id 67// GET /comments/:parent_id/comment/:child_id
46router.get("/comments/:parent_id/comment/:child_id", async (req, res) => { 68router.get(
47 const parent_id = req.params.parent_id; 69 "/comments/:parent_id/comment/:child_id",
48 const child_id = req.params.child_id; 70 authenticateToken,
71 async (req, res) => {
72 const parent_id = req.params.parent_id;
73 const child_id = req.params.child_id;
49 74
50 const params = { 75 const params = {
51 limit: 50, 76 limit: 50,
52 }; 77 };
53 response = await G.getSingleCommentThread(parent_id, child_id, params); 78 response = await G.getSingleCommentThread(parent_id, child_id, params);
54 const comments = response.comments; 79 const comments = response.comments;
55 comments.forEach(unescape_comment); 80 comments.forEach(unescape_comment);
56 res.render("single_comment_thread", { comments, parent_id }); 81 res.render("single_comment_thread", {
57}); 82 comments,
58 83 parent_id,
59router.get("/login", async (req, res) => { 84 user: req.user,
60 res.render("login"); 85 });
61}); 86 },
87);
62 88
63// GET /subs 89// GET /subs
64router.get("/subs", async (req, res) => { 90router.get("/subs", authenticateToken, async (req, res) => {
65 res.render("subs"); 91 const subs = db
92 .query("SELECT * FROM subscriptions WHERE user_id = $id")
93 .all({ id: req.user.id });
94 res.render("subs", { subs, user: req.user });
66}); 95});
67 96
68// GET /media 97// GET /media
69router.get("/media/*", async (req, res) => { 98router.get("/media/*", authenticateToken, async (req, res) => {
70 const url = req.params[0]; 99 const url = req.params[0];
71 const ext = url.split(".").pop().toLowerCase(); 100 const ext = url.split(".").pop().toLowerCase();
72 const kind = ["jpg", "jpeg", "png", "gif", "webp"].includes(ext) 101 const kind = ["jpg", "jpeg", "png", "gif", "webp"].includes(ext)
@@ -81,89 +110,124 @@ router.get("/register", async (req, res) => {
81 110
82router.post("/register", async (req, res) => { 111router.post("/register", async (req, res) => {
83 const { username, password, confirm_password } = req.body; 112 const { username, password, confirm_password } = req.body;
84 console.log("Request body:", req.body); 113
85 if (!username || !password || !confirm_password) { 114 if (!username || !password || !confirm_password) {
86 return res.status(400).send("All fields are required"); 115 return res.status(400).send("All fields are required");
87 } 116 }
117
118 const user = db
119 .query("SELECT * FROM users WHERE username = $username")
120 .get({ username });
121 if (user) {
122 return res.render("register", {
123 message: `user by the name "${username}" exists, choose a different username`,
124 });
125 }
126
88 if (password !== confirm_password) { 127 if (password !== confirm_password) {
89 return res.status(400).send("Passwords do not match"); 128 return res.render("register", {
129 message: "passwords do not match, try again",
130 });
90 } 131 }
132
91 try { 133 try {
92 const hashedPassword = await hash(password); 134 const hashedPassword = await Bun.password.hash(password);
93 db.query("INSERT INTO users (username, password_hash) VALUES (?, ?)", [ 135 const insertedRecord = db
94 username, 136 .query(
95 hashedPassword, 137 "INSERT INTO users (username, password_hash) VALUES ($username, $hashedPassword)",
96 ]).run(); 138 )
97 res.status(201).redirect("/"); 139 .run({
140 username,
141 hashedPassword,
142 });
143 const id = insertedRecord.lastInsertRowid;
144 const token = jwt.sign({ username, id }, JWT_KEY, { expiresIn: "100h" });
145 res
146 .status(200)
147 .cookie("auth_token", token, {
148 httpOnly: true,
149 maxAge: 2 * 24 * 60 * 60 * 1000,
150 })
151 .redirect("/");
98 } catch (err) { 152 } catch (err) {
99 console.log(err); 153 return res.render("register", {
100 res.status(400).send("Error registering user"); 154 message: "error registering user, try again later",
155 });
101 } 156 }
102}); 157});
103 158
159router.get("/login", async (req, res) => {
160 res.render("login", req.query);
161});
162
104// POST /login 163// POST /login
105router.post("/login", async (req, res) => { 164router.post("/login", async (req, res) => {
106 const { username, password } = req.body; 165 const { username, password } = req.body;
107 const user = db 166 const user = db
108 .query("SELECT * FROM users WHERE username = ?", [username]) 167 .query("SELECT * FROM users WHERE username = $username")
109 .get(); 168 .get({ username });
110 if (user && await compare(password, user.password_hash)) { 169 if (user && (await Bun.password.verify(password, user.password_hash))) {
111 res.status(200).redirect("/"); 170 const token = jwt.sign({ username, id: user.id }, JWT_KEY, {
171 expiresIn: "1h",
172 });
173 res
174 .cookie("auth_token", token, {
175 httpOnly: true,
176 maxAge: 2 * 24 * 60 * 60 * 1000,
177 })
178 .redirect(req.query.redirect || "/");
112 } else { 179 } else {
113 res.status(401).send("Invalid credentials"); 180 res.render("login", {
181 message: "invalid credentials, try again",
182 });
114 } 183 }
115}); 184});
116 185
186// this would be post, but i cant stuff it in a link
187router.get("/logout", (req, res) => {
188 res.clearCookie("auth_token", {
189 httpOnly: true,
190 secure: true,
191 });
192 res.redirect("/login");
193});
194
117// POST /subscribe 195// POST /subscribe
118router.post("/subscribe", async (req, res) => { 196router.post("/subscribe", authenticateToken, async (req, res) => {
119 const { username, subreddit } = req.body; 197 const { subreddit } = req.body;
120 const user = db 198 const user = req.user;
121 .query("SELECT * FROM users WHERE username = ?", [username]) 199 const existingSubscription = db
122 .get(); 200 .query(
123 if (user) { 201 "SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
124 const existingSubscription = db 202 )
125 .query( 203 .get({ id: user.id, subreddit });
126 "SELECT * FROM subscriptions WHERE user_id = ? AND subreddit = ?", 204 if (existingSubscription) {
127 [user.id, subreddit], 205 res.status(400).send("Already subscribed to this subreddit");
128 )
129 .get();
130 if (existingSubscription) {
131 res.status(400).send("Already subscribed to this subreddit");
132 } else {
133 db.query("INSERT INTO subscriptions (user_id, subreddit) VALUES (?, ?)", [
134 user.id,
135 subreddit,
136 ]).run();
137 res.status(201).send("Subscribed successfully");
138 }
139 } else { 206 } else {
140 res.status(404).send("User not found"); 207 db.query(
208 "INSERT INTO subscriptions (user_id, subreddit) VALUES ($id, $subreddit)",
209 ).run({ id: user.id, subreddit });
210 res.status(201).send("Subscribed successfully");
141 } 211 }
142}); 212});
143 213
144router.post("/unsubscribe", async (req, res) => { 214router.post("/unsubscribe", authenticateToken, async (req, res) => {
145 const { username, subreddit } = req.body; 215 const { subreddit } = req.body;
146 const user = db 216 const user = req.user;
147 .query("SELECT * FROM users WHERE username = ?", [username]) 217 const existingSubscription = db
148 .get(); 218 .query(
149 if (user) { 219 "SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
150 const existingSubscription = db 220 )
151 .query( 221 .get({ id: user.id, subreddit });
152 "SELECT * FROM subscriptions WHERE user_id = ? AND subreddit = ?", 222 if (existingSubscription) {
153 [user.id, subreddit], 223 db.query(
154 ) 224 "DELETE FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
155 .get(); 225 ).run({ id: user.id, subreddit });
156 if (existingSubscription) { 226 console.log("done");
157 db.run("DELETE FROM subscriptions WHERE user_id = ? AND subreddit = ?", [ 227 res.status(200).send("Unsubscribed successfully");
158 user.id,
159 subreddit,
160 ]);
161 res.status(200).send("Unsubscribed successfully");
162 } else {
163 res.status(400).send("Subscription not found");
164 }
165 } else { 228 } else {
166 res.status(404).send("User not found"); 229 console.log("not");
230 res.status(400).send("Subscription not found");
167 } 231 }
168}); 232});
169 233
diff --git a/src/views/comments.pug b/src/views/comments.pug
index a7dd396..541a7bd 100644
--- a/src/views/comments.pug
+++ b/src/views/comments.pug
@@ -3,9 +3,11 @@ include ../mixins/header
3include ../mixins/head 3include ../mixins/head
4include ../utils 4include ../utils
5 5
6- var post = data.post
7- var comments = data.comments
6doctype html 8doctype html
7html 9html
8 +head() 10 +head(post.title)
9 script. 11 script.
10 function toggleDetails(details_id) { 12 function toggleDetails(details_id) {
11 var detailsElement = document.getElementById(details_id); 13 var detailsElement = document.getElementById(details_id);
@@ -16,7 +18,7 @@ html
16 18
17 body 19 body
18 main#content 20 main#content
19 +header() 21 +header(user)
20 div.hero 22 div.hero
21 h3.sub-title 23 h3.sub-title
22 a(href=`/r/${post.subreddit}`) ← r/#{post.subreddit} 24 a(href=`/r/${post.subreddit}`) ← r/#{post.subreddit}
@@ -40,10 +42,10 @@ html
40 each item in post.gallery_data.items 42 each item in post.gallery_data.items
41 - var url = `https://i.redd.it/${item.media_id}.jpg` 43 - var url = `https://i.redd.it/${item.media_id}.jpg`
42 div.gallery-item 44 div.gallery-item
43 a(href=`/media/${url}`)
44 img(src=url loading="lazy")
45 div.gallery-item-idx 45 div.gallery-item-idx
46 | #{`${++idx}/${total}`} 46 | #{`${++idx}/${total}`}
47 a(href=`/media/${url}`)
48 img(src=url loading="lazy")
47 else if post.post_hint == "image" && post.thumbnail && post.thumbnail != "self" && post.thumbnail != "default" 49 else if post.post_hint == "image" && post.thumbnail && post.thumbnail != "self" && post.thumbnail != "default"
48 img(src=post.url).post-media 50 img(src=post.url).post-media
49 else if post.post_hint == 'hosted:video' 51 else if post.post_hint == 'hosted:video'
diff --git a/src/views/index.pug b/src/views/index.pug
index 513491d..6eedfe3 100644
--- a/src/views/index.pug
+++ b/src/views/index.pug
@@ -9,55 +9,40 @@ html
9 +head("home") 9 +head("home")
10 +subMgmt() 10 +subMgmt()
11 script(defer). 11 script(defer).
12 async function updateButton(sub) {
13 var b = document.getElementById("button-container");
14 b.innerHTML = '';
15
16 const button = document.createElement("button");
17
18 if (issub(sub)) {
19 button.innerText = "unsubscribe";
20 button.onclick = async () => await unsubscribe(sub);
21 } else {
22 button.innerText = "subscribe";
23 button.onclick = async () => await subscribe(sub);
24 }
25 b.appendChild(button);
26 }
27
28 async function subscribe(sub) { 12 async function subscribe(sub) {
29 await postSubscription(sub, true); 13 await doThing(sub, 'subscribe');
30 updateButton(sub);
31 } 14 }
32 15
33 async function unsubscribe(sub) { 16 async function unsubscribe(sub) {
34 await postUnsubscription(sub); 17 await doThing(sub, 'unsubscribe');
35 updateButton(sub); 18 }
19
20 function getCookie(name) {
21 const value = `; ${document.cookie}`;
22 const parts = value.split(`; ${name}=`);
23 if (parts.length === 2) return parts.pop().split(";").shift();
36 } 24 }
37 25
38 async function postUnsubscription(sub) { 26 async function doThing(sub, thing) {
39 const response = await fetch('/unsubscribe', { 27 const jwtToken = getCookie("auth_token");
28 const response = await fetch(`/${thing}`, {
40 method: 'POST', 29 method: 'POST',
41 headers: { 30 headers: {
31 'Authorization': `Bearer ${jwtToken}`,
42 'Content-Type': 'application/json', 32 'Content-Type': 'application/json',
43 }, 33 },
44 body: JSON.stringify({ subreddit: sub }), 34 body: JSON.stringify({ subreddit: sub }),
45 }); 35 });
46 36
47 if (!response.ok) { 37 let thinger = document.getElementById('thinger');
48 console.error('Failed to update unsubscription'); 38 if (thing == 'subscribe') {
39 thinger.innerText = 'unsubscribe';
40 } else {
41 thinger.innerText = 'subscribe';
49 } 42 }
50 }
51 const response = await fetch('/subscribe', {
52 method: 'POST',
53 headers: {
54 'Content-Type': 'application/json',
55 },
56 body: JSON.stringify({ subreddit: sub, subscribe: subscribe }),
57 });
58 43
59 if (!response.ok) { 44 if (!response.ok) {
60 console.error('Failed to update subscription'); 45 console.error(`Failed to do ${thing}`);
61 } 46 }
62 } 47 }
63 48
@@ -68,7 +53,6 @@ html
68 } 53 }
69 } 54 }
70 55
71 document.addEventListener('DOMContentLoaded', () => updateButton("#{subreddit}"));
72 body 56 body
73 main#content 57 main#content
74 +header(user) 58 +header(user)
@@ -82,6 +66,10 @@ html
82 | r/#{subreddit} 66 | r/#{subreddit}
83 if !isMulti 67 if !isMulti
84 div#button-container 68 div#button-container
69 if isSubbed
70 button(onclick=`unsubscribe('${subreddit}')`)#thinger unsubscribe
71 else
72 button(onclick=`subscribe('${subreddit}')`)#thinger subscribe
85 if about 73 if about
86 p #{about.public_description} 74 p #{about.public_description}
87 details 75 details
diff --git a/src/views/login.pug b/src/views/login.pug
new file mode 100644
index 0000000..dd3c359
--- /dev/null
+++ b/src/views/login.pug
@@ -0,0 +1,26 @@
1include ../mixins/head
2
3doctype html
4html
5 +head("login")
6 body
7 main#content
8 h1 login
9 if message
10 div.register-error-message
11 | #{message}
12 - var url = redirect ? `/login?redirect=${redirect}` : '/login'
13 form(action=url method="post")
14 div.input-text
15 label(for="username") username
16 input(type="text" name="username" required)
17 div.input-text
18 label(for="password") password
19 input(type="password" name="password" required)
20 div.submit-button
21 button(type="submit") login
22 div
23 p
24 | don't have an account? 
25 a(href="/register") register
26
diff --git a/src/views/register.pug b/src/views/register.pug
new file mode 100644
index 0000000..22bca48
--- /dev/null
+++ b/src/views/register.pug
@@ -0,0 +1,28 @@
1include ../mixins/head
2
3doctype html
4html
5 +head("register")
6 body
7 main#content
8 h1 register
9 if message
10 div.register-error-message
11 | #{message}
12 form(action="/register" method="post")
13 div.input-text
14 label(for="username") username
15 input(type="text" name="username" required)
16 div.input-text
17 label(for="password") password
18 input(type="password" name="password" required)
19 div.input-text
20 label(for="confirm_password") confirm password
21 input(type="password" name="confirm_password" required)
22 div.submit-button
23 button(type="submit") register
24 div
25 p
26 | already have an account? 
27 a(href="/login") login
28
diff --git a/src/views/single_comment_thread.pug b/src/views/single_comment_thread.pug
index cd652e6..1f37e9f 100644
--- a/src/views/single_comment_thread.pug
+++ b/src/views/single_comment_thread.pug
@@ -5,7 +5,7 @@ include ../utils
5 5
6doctype html 6doctype html
7html 7html
8 +head() 8 +head("more comments")
9 body 9 body
10 main#content 10 main#content
11 +header() 11 +header()
diff --git a/src/views/subs.pug b/src/views/subs.pug
index f7e2b81..86de604 100644
--- a/src/views/subs.pug
+++ b/src/views/subs.pug
@@ -4,7 +4,7 @@ include ../mixins/head
4 4
5doctype html 5doctype html
6html 6html
7 +head() 7 +head("subscriptions")
8 +subMgmt() 8 +subMgmt()
9 script. 9 script.
10 function newSubItem(sub) { 10 function newSubItem(sub) {
@@ -26,7 +26,11 @@ html
26 document.addEventListener('DOMContentLoaded', buildSubList); 26 document.addEventListener('DOMContentLoaded', buildSubList);
27 body 27 body
28 main#content 28 main#content
29 +header() 29 +header(user)
30 div.hero 30 div.hero
31 h1 subscriptions 31 h1 subscriptions
32 div#subList 32 p
33 each s in subs
34 a(href=`/r/${s.subreddit}`)
35 | r/#{s.subreddit}
36 br