diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/auth.js | 27 | ||||
-rw-r--r-- | src/db.js | 24 | ||||
-rw-r--r-- | src/geddit.js | 8 | ||||
-rw-r--r-- | src/index.js | 44 | ||||
-rw-r--r-- | src/mixins/head.pug | 4 | ||||
-rw-r--r-- | src/mixins/header.pug | 10 | ||||
-rw-r--r-- | src/public/styles.css | 61 | ||||
-rw-r--r-- | src/routes/index.js | 234 | ||||
-rw-r--r-- | src/views/comments.pug | 10 | ||||
-rw-r--r-- | src/views/index.pug | 56 | ||||
-rw-r--r-- | src/views/login.pug | 26 | ||||
-rw-r--r-- | src/views/register.pug | 28 | ||||
-rw-r--r-- | src/views/single_comment_thread.pug | 2 | ||||
-rw-r--r-- | src/views/subs.pug | 10 |
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 @@ | |||
1 | const jwt = require("jsonwebtoken"); | ||
2 | const { JWT_KEY } = require("./"); | ||
3 | |||
4 | function 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 | |||
27 | module.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 @@ | |||
1 | const { Database } = require("bun:sqlite"); | ||
2 | const db = new Database("readit.db", { | ||
3 | strict: true, | ||
4 | }); | ||
5 | |||
6 | db.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 | |||
14 | db.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 | |||
24 | module.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 @@ | |||
1 | const express = require("express"); | 1 | const express = require("express"); |
2 | const rateLimit = require("express-rate-limit"); | ||
2 | const path = require("node:path"); | 3 | const path = require("node:path"); |
3 | const geddit = require("./geddit.js"); | 4 | const geddit = require("./geddit.js"); |
4 | const { Database } = require("bun:sqlite"); | 5 | const cookieParser = require("cookie-parser"); |
5 | |||
6 | const db = new Database("readit.db"); | ||
7 | |||
8 | const 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 | |||
16 | createUsers.run(); | ||
17 | |||
18 | const 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 | |||
28 | createSubs.run(); | ||
29 | |||
30 | module.exports = { db }; | ||
31 | |||
32 | const app = express(); | 6 | const app = express(); |
7 | const hasher = new Bun.CryptoHasher("sha256", "secret-key"); | ||
8 | const JWT_KEY = hasher.update(Math.random().toString()).digest("hex"); | ||
9 | |||
10 | module.exports = { JWT_KEY }; | ||
33 | 11 | ||
34 | app.set("views", path.join(__dirname, "views")); | 12 | app.set("views", path.join(__dirname, "views")); |
35 | app.set("view engine", "pug"); | 13 | app.set("view engine", "pug"); |
@@ -38,6 +16,16 @@ const routes = require("./routes/index"); | |||
38 | app.use(express.json()); | 16 | app.use(express.json()); |
39 | app.use(express.urlencoded({ extended: true })); | 17 | app.use(express.urlencoded({ extended: true })); |
40 | app.use(express.static(path.join(__dirname, "public"))); | 18 | app.use(express.static(path.join(__dirname, "public"))); |
19 | app.use(cookieParser()); | ||
20 | app.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 | ); | ||
41 | app.use("/", routes); | 29 | app.use("/", routes); |
42 | 30 | ||
43 | const port = process.env.READIT_PORT; | 31 | const 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 @@ | |||
1 | mixin head() | 1 | 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 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 @@ | |||
1 | mixin header() | 1 | mixin 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 | |||
190 | form { | ||
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 { | |||
320 | blockquote { | 330 | blockquote { |
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 | ||
442 | form { | ||
443 | display: flex; | ||
444 | flex-direction: column; | ||
445 | align-items: center; | ||
446 | } | ||
447 | |||
448 | form label { | 452 | form 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 | ||
454 | form input[type="submit"] { | 459 | form 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"); | |||
2 | const he = require("he"); | 2 | const he = require("he"); |
3 | const { hash, compare } = require("bun"); | 3 | const { hash, compare } = require("bun"); |
4 | const jwt = require("jsonwebtoken"); | 4 | const jwt = require("jsonwebtoken"); |
5 | const router = express.Router(); | ||
6 | const secretKey = "your_secret_key"; // Replace with your actual secret key | ||
7 | const geddit = require("../geddit.js"); | 5 | const geddit = require("../geddit.js"); |
8 | const { db } = require("../index"); | 6 | const { JWT_KEY } = require("../"); |
7 | const { db } = require("../db"); | ||
8 | const { authenticateToken } = require("../auth"); | ||
9 | |||
10 | const router = express.Router(); | ||
9 | const G = new geddit.Geddit(); | 11 | const G = new geddit.Geddit(); |
10 | 12 | ||
11 | // GET / | 13 | // GET / |
12 | router.get("/", async (req, res) => { | 14 | router.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 |
17 | router.get("/r/:subreddit", async (req, res) => { | 19 | router.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 |
34 | router.get("/comments/:id", async (req, res) => { | 53 | router.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 |
46 | router.get("/comments/:parent_id/comment/:child_id", async (req, res) => { | 68 | router.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, | |
59 | router.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 |
64 | router.get("/subs", async (req, res) => { | 90 | router.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 |
69 | router.get("/media/*", async (req, res) => { | 98 | router.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 | ||
82 | router.post("/register", async (req, res) => { | 111 | router.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 | ||
159 | router.get("/login", async (req, res) => { | ||
160 | res.render("login", req.query); | ||
161 | }); | ||
162 | |||
104 | // POST /login | 163 | // POST /login |
105 | router.post("/login", async (req, res) => { | 164 | router.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 | ||
187 | router.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 |
118 | router.post("/subscribe", async (req, res) => { | 196 | router.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 | ||
144 | router.post("/unsubscribe", async (req, res) => { | 214 | router.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 | |||
3 | include ../mixins/head | 3 | include ../mixins/head |
4 | include ../utils | 4 | include ../utils |
5 | 5 | ||
6 | - var post = data.post | ||
7 | - var comments = data.comments | ||
6 | doctype html | 8 | doctype html |
7 | html | 9 | html |
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 @@ | |||
1 | include ../mixins/head | ||
2 | |||
3 | doctype html | ||
4 | html | ||
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 @@ | |||
1 | include ../mixins/head | ||
2 | |||
3 | doctype html | ||
4 | html | ||
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 | ||
6 | doctype html | 6 | doctype html |
7 | html | 7 | html |
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 | ||
5 | doctype html | 5 | doctype html |
6 | html | 6 | html |
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 | ||