diff options
-rw-r--r-- | flake.nix | 28 | ||||
-rw-r--r-- | scripts/gen-invite.js | 34 | ||||
-rw-r--r-- | src/db.js | 17 | ||||
-rw-r--r-- | src/invite.js | 8 | ||||
-rw-r--r-- | src/mixins/post.pug | 61 | ||||
-rw-r--r-- | src/mixins/postUtils.pug | 39 | ||||
-rw-r--r-- | src/routes/index.js | 24 | ||||
-rw-r--r-- | src/views/comments.pug | 28 |
8 files changed, 103 insertions, 136 deletions
@@ -42,32 +42,6 @@ | |||
42 | outputHashAlgo = "sha256"; | 42 | outputHashAlgo = "sha256"; |
43 | outputHashMode = "recursive"; | 43 | outputHashMode = "recursive"; |
44 | }; | 44 | }; |
45 | readit-gen-invite = with final; | ||
46 | stdenv.mkDerivation { | ||
47 | pname = "readit-gen-invite"; | ||
48 | version = "0.0.1"; | ||
49 | src = ./scripts; | ||
50 | nativeBuildInputs = [makeBinaryWrapper]; | ||
51 | buildInputs = [bun]; | ||
52 | |||
53 | buildPhase = '' | ||
54 | runHook preBuild | ||
55 | runHook postBuild | ||
56 | ''; | ||
57 | |||
58 | dontFixup = true; | ||
59 | |||
60 | installPhase = '' | ||
61 | runHook preInstall | ||
62 | |||
63 | mkdir -p $out/bin | ||
64 | cp -R ./* $out | ||
65 | |||
66 | makeBinaryWrapper ${bun}/bin/bun $out/bin/$pname \ | ||
67 | --prefix PATH : ${lib.makeBinPath [bun]} \ | ||
68 | --add-flags "run --prefer-offline --no-install $out/gen-invite.js" | ||
69 | ''; | ||
70 | }; | ||
71 | readit = with final; | 45 | readit = with final; |
72 | stdenv.mkDerivation { | 46 | stdenv.mkDerivation { |
73 | pname = "readit"; | 47 | pname = "readit"; |
@@ -110,7 +84,7 @@ | |||
110 | }); | 84 | }); |
111 | 85 | ||
112 | packages = forAllSystems (system: { | 86 | packages = forAllSystems (system: { |
113 | inherit (nixpkgsFor."${system}") readit readit-gen-invite node_modules; | 87 | inherit (nixpkgsFor."${system}") readit node_modules; |
114 | }); | 88 | }); |
115 | 89 | ||
116 | defaultPackage = forAllSystems (system: nixpkgsFor."${system}".readit); | 90 | defaultPackage = forAllSystems (system: nixpkgsFor."${system}".readit); |
diff --git a/scripts/gen-invite.js b/scripts/gen-invite.js deleted file mode 100644 index 4c0cbee..0000000 --- a/scripts/gen-invite.js +++ /dev/null | |||
@@ -1,34 +0,0 @@ | |||
1 | import { Database } from "bun:sqlite"; | ||
2 | |||
3 | const command = process.argv[2]; | ||
4 | |||
5 | const dbPath = process.argv[3] ? process.argv[3] : "readit.db"; | ||
6 | const db = new Database(dbPath, { | ||
7 | strict: true, | ||
8 | }); | ||
9 | |||
10 | db.run(` | ||
11 | CREATE TABLE IF NOT EXISTS invites ( | ||
12 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
13 | token TEXT NOT NULL, | ||
14 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
15 | usedAt TIMESTAMP | ||
16 | ) | ||
17 | `); | ||
18 | |||
19 | if (command === "create") { | ||
20 | createInvite(); | ||
21 | } else { | ||
22 | console.log("requires an arg"); | ||
23 | } | ||
24 | |||
25 | function generateInviteToken() { | ||
26 | const hasher = new Bun.CryptoHasher("sha256", "super-secret-invite-key"); | ||
27 | return hasher.update(Math.random().toString()).digest("hex").slice(0, 10); | ||
28 | } | ||
29 | |||
30 | function createInvite() { | ||
31 | const token = generateInviteToken(); | ||
32 | db.run("INSERT INTO invites (token) VALUES ($token)", { token }); | ||
33 | console.log(`Invite token created: ${token}`); | ||
34 | } | ||
@@ -15,16 +15,16 @@ function runMigration(name, migrationFn) { | |||
15 | } | 15 | } |
16 | 16 | ||
17 | // users table | 17 | // users table |
18 | db.query(` | 18 | db.run(` |
19 | CREATE TABLE IF NOT EXISTS users ( | 19 | CREATE TABLE IF NOT EXISTS users ( |
20 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 20 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
21 | username TEXT UNIQUE, | 21 | username TEXT UNIQUE, |
22 | password_hash TEXT | 22 | password_hash TEXT |
23 | ) | 23 | ) |
24 | `).run(); | 24 | `); |
25 | 25 | ||
26 | // subs table | 26 | // subs table |
27 | db.query(` | 27 | db.run(` |
28 | CREATE TABLE IF NOT EXISTS subscriptions ( | 28 | CREATE TABLE IF NOT EXISTS subscriptions ( |
29 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 29 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
30 | user_id INTEGER, | 30 | user_id INTEGER, |
@@ -32,7 +32,16 @@ db.query(` | |||
32 | FOREIGN KEY(user_id) REFERENCES users(id), | 32 | FOREIGN KEY(user_id) REFERENCES users(id), |
33 | UNIQUE(user_id, subreddit) | 33 | UNIQUE(user_id, subreddit) |
34 | ) | 34 | ) |
35 | `).run(); | 35 | `); |
36 | |||
37 | db.run(` | ||
38 | CREATE TABLE IF NOT EXISTS invites ( | ||
39 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
40 | token TEXT NOT NULL, | ||
41 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
42 | usedAt TIMESTAMP | ||
43 | ) | ||
44 | `); | ||
36 | 45 | ||
37 | // migrations table | 46 | // migrations table |
38 | db.query(` | 47 | db.query(` |
diff --git a/src/invite.js b/src/invite.js index 7e357ac..f0bc9b9 100644 --- a/src/invite.js +++ b/src/invite.js | |||
@@ -1,6 +1,14 @@ | |||
1 | const { db } = require("./db"); | 1 | const { db } = require("./db"); |
2 | 2 | ||
3 | const validateInviteToken = async (req, res, next) => { | 3 | const validateInviteToken = async (req, res, next) => { |
4 | const isFirstUser = db.query("SELECT 1 FROM users LIMIT 1").get() === null; | ||
5 | |||
6 | if (isFirstUser) { | ||
7 | req.isFirstUser = true; | ||
8 | next(); | ||
9 | return; | ||
10 | } | ||
11 | |||
4 | const token = req.query.token; | 12 | const token = req.query.token; |
5 | 13 | ||
6 | if (!token) { | 14 | if (!token) { |
diff --git a/src/mixins/post.pug b/src/mixins/post.pug index 172da32..487f366 100644 --- a/src/mixins/post.pug +++ b/src/mixins/post.pug | |||
@@ -1,4 +1,5 @@ | |||
1 | include ../utils | 1 | include ../utils |
2 | include postUtils | ||
2 | mixin post(p) | 3 | mixin post(p) |
3 | article.post | 4 | article.post |
4 | div.post-container | 5 | div.post-container |
@@ -20,62 +21,42 @@ mixin post(p) | |||
20 | | · | 21 | | · |
21 | a(href=`/comments/${p.id}`) #{fmtnum (p.num_comments)} ↩ | 22 | a(href=`/comments/${p.id}`) #{fmtnum (p.num_comments)} ↩ |
22 | div.media-preview | 23 | div.media-preview |
23 | if p.is_gallery && p.is_gallery == true | 24 | if isPostGallery(p) |
24 | if p.gallery_data | 25 | - var item = postGalleryItems(p)[0] |
25 | if p.gallery_data.items | 26 | img(src=item.url onclick=`toggleDetails('${p.id}')`) |
26 | - var item = p.gallery_data.items[0] | 27 | else if isPostImage(p) |
27 | - var url = `https://i.redd.it/${item.media_id}.jpg` | ||
28 | img(src=url onclick=`toggleDetails('${p.id}')`) | ||
29 | else if p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default" | ||
30 | img(src=p.thumbnail onclick=`toggleDetails('${p.id}')`) | 28 | img(src=p.thumbnail onclick=`toggleDetails('${p.id}')`) |
31 | else if p.post_hint == "hosted:video" | 29 | else if isPostVideo(p) |
32 | - var url = p.secure_media.reddit_video.scrubber_media_url | 30 | - var url = p.secure_media.reddit_video.scrubber_media_url |
33 | video(src=url data-dashjs-player width='100px' height='100px' onclick=`toggleDetails('${p.id}')`) | 31 | video(src=url data-dashjs-player width='100px' height='100px' onclick=`toggleDetails('${p.id}')`) |
34 | else if p.post_hint == "link" | 32 | else if isPostLink(p) |
35 | a(href=p.url) | 33 | a(href=p.url) |
36 | | ↗ | 34 | | ↗ |
37 | 35 | ||
38 | if p.is_gallery && p.is_gallery == true | 36 | if isPostGallery(p) |
39 | if p.gallery_data | 37 | details(id=`${p.id}`) |
40 | if p.gallery_data.items | 38 | summary.expand-post expand gallery |
41 | details(id=`${p.id}`) | 39 | div.gallery |
42 | summary.expand-post expand gallery | 40 | each item in postGalleryItems(p) |
43 | div.gallery | 41 | div.gallery-item |
44 | - var total = p.gallery_data.items.length | 42 | div.gallery-item-idx |
45 | - var idx = 0 | 43 | | #{`${item.idx}/${item.total}`} |
46 | - var metadata = p.media_metadata | 44 | a(href=`/media/${item.url}`) |
47 | - | 45 | img(src=item.url loading="lazy") |
48 | var img_ext = (id) => { | 46 | button(onclick=`toggleDetails('${p.id}')`) close |
49 | if (metadata[id].status == 'valid') { | 47 | else if isPostImage(p) |
50 | return stripPrefix(metadata[id].m, "image/"); | ||
51 | } else { | ||
52 | // dosent matter | ||
53 | return 'jpg'; | ||
54 | } | ||
55 | } | ||
56 | each item in p.gallery_data.items | ||
57 | - var id = item.media_id | ||
58 | - var ext = img_ext(item.media_id) | ||
59 | - var url = `https://i.redd.it/${id}.${ext}` | ||
60 | div.gallery-item | ||
61 | a(href=`/media/${url}`) | ||
62 | img(src=url loading="lazy") | ||
63 | div.gallery-item-idx | ||
64 | | #{`${++idx}/${total}`} | ||
65 | button(onclick=`toggleDetails('${p.id}')`) close | ||
66 | if p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default" | ||
67 | details(id=`${p.id}`) | 48 | details(id=`${p.id}`) |
68 | summary.expand-post expand image | 49 | summary.expand-post expand image |
69 | a(href=`/media/${p.url}`) | 50 | a(href=`/media/${p.url}`) |
70 | img(src=p.url loading="lazy").post-media | 51 | img(src=p.url loading="lazy").post-media |
71 | button(onclick=`toggleDetails('${p.id}')`) close | 52 | button(onclick=`toggleDetails('${p.id}')`) close |
72 | else if p.post_hint == "hosted:video" | 53 | else if isPostVideo(p) |
73 | details(id=`${p.id}`) | 54 | details(id=`${p.id}`) |
74 | summary.expand-post expand video | 55 | summary.expand-post expand video |
75 | - var url = p.secure_media.reddit_video.dash_url | 56 | - var url = p.secure_media.reddit_video.dash_url |
76 | video(src=url controls data-dashjs-player loading="lazy").post-media | 57 | video(src=url controls data-dashjs-player loading="lazy").post-media |
77 | button(onclick=`toggleDetails('${p.id}')`) close | 58 | button(onclick=`toggleDetails('${p.id}')`) close |
78 | else if p.post_hint == "link" | 59 | else if isPostLink(p) |
79 | details(id=`${p.id}`) | 60 | details(id=`${p.id}`) |
80 | summary.expand-post expand link | 61 | summary.expand-post expand link |
81 | a(href=`${p.url}`) | 62 | a(href=`${p.url}`) |
diff --git a/src/mixins/postUtils.pug b/src/mixins/postUtils.pug new file mode 100644 index 0000000..4f480b6 --- /dev/null +++ b/src/mixins/postUtils.pug | |||
@@ -0,0 +1,39 @@ | |||
1 | - | ||
2 | function isPostGallery(p) { | ||
3 | return (p.is_gallery && p.is_gallery == true); | ||
4 | } | ||
5 | - | ||
6 | function isPostImage(p) { | ||
7 | return (p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default"); | ||
8 | } | ||
9 | - | ||
10 | function isPostVideo(p) { | ||
11 | return (p.post_hint == "hosted:video"); | ||
12 | } | ||
13 | - | ||
14 | function isPostLink(p) { | ||
15 | return (p.post_hint == "link"); | ||
16 | } | ||
17 | - | ||
18 | function imgExt(p, id) { | ||
19 | var metadata = p.media_metadata; | ||
20 | if (metadata[id].status == 'valid') { | ||
21 | return stripPrefix(metadata[id].m, "image/"); | ||
22 | } else { | ||
23 | // dosent matter | ||
24 | return 'jpg'; | ||
25 | } | ||
26 | } | ||
27 | - | ||
28 | function postGalleryItems(p) { | ||
29 | if (p.gallery_data && p.gallery_data.items) { | ||
30 | return p.gallery_data.items.map((item, idx) => ({ | ||
31 | id: item.media_id, | ||
32 | url: `https://i.redd.it/${item.media_id}.${imgExt(p, item.media_id)}`, | ||
33 | total: p.gallery_data.items.length, | ||
34 | idx: idx+1, | ||
35 | })); | ||
36 | } else { | ||
37 | return null; | ||
38 | } | ||
39 | } | ||
diff --git a/src/routes/index.js b/src/routes/index.js index 9a415be..e585d3d 100644 --- a/src/routes/index.js +++ b/src/routes/index.js | |||
@@ -136,15 +136,6 @@ router.get("/create-invite", authenticateAdmin, async (req, res) => { | |||
136 | } | 136 | } |
137 | 137 | ||
138 | try { | 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(); | 139 | createInvite(); |
149 | return res.redirect("/dashboard"); | 140 | return res.redirect("/dashboard"); |
150 | } catch (err) { | 141 | } catch (err) { |
@@ -201,19 +192,22 @@ router.post("/register", validateInviteToken, async (req, res) => { | |||
201 | try { | 192 | try { |
202 | const hashedPassword = await Bun.password.hash(password); | 193 | const hashedPassword = await Bun.password.hash(password); |
203 | 194 | ||
204 | db.query( | 195 | if (!req.isFirstUser) { |
205 | "UPDATE invites SET usedAt = CURRENT_TIMESTAMP WHERE id = $id", | 196 | db.query( |
206 | ).run({ | 197 | "UPDATE invites SET usedAt = CURRENT_TIMESTAMP WHERE id = $id", |
207 | id: req.invite.id, | 198 | ).run({ |
208 | }); | 199 | id: req.invite.id, |
200 | }); | ||
201 | } | ||
209 | 202 | ||
210 | const insertedRecord = db | 203 | const insertedRecord = db |
211 | .query( | 204 | .query( |
212 | "INSERT INTO users (username, password_hash) VALUES ($username, $hashedPassword)", | 205 | "INSERT INTO users (username, password_hash, isAdmin) VALUES ($username, $hashedPassword, $isAdmin)", |
213 | ) | 206 | ) |
214 | .run({ | 207 | .run({ |
215 | username, | 208 | username, |
216 | hashedPassword, | 209 | hashedPassword, |
210 | isAdmin: req.isFirstUser ? 1 : 0, | ||
217 | }); | 211 | }); |
218 | const id = insertedRecord.lastInsertRowid; | 212 | const id = insertedRecord.lastInsertRowid; |
219 | const token = jwt.sign({ username, id }, JWT_KEY, { expiresIn: "5d" }); | 213 | const token = jwt.sign({ username, id }, JWT_KEY, { expiresIn: "5d" }); |
diff --git a/src/views/comments.pug b/src/views/comments.pug index 541a7bd..e9bd332 100644 --- a/src/views/comments.pug +++ b/src/views/comments.pug | |||
@@ -1,6 +1,7 @@ | |||
1 | include ../mixins/comment | 1 | include ../mixins/comment |
2 | include ../mixins/header | 2 | include ../mixins/header |
3 | include ../mixins/head | 3 | include ../mixins/head |
4 | include ../mixins/postUtils | ||
4 | include ../utils | 5 | include ../utils |
5 | 6 | ||
6 | - var post = data.post | 7 | - var post = data.post |
@@ -33,25 +34,20 @@ html | |||
33 | h2.post-title | 34 | h2.post-title |
34 | != post.title | 35 | != post.title |
35 | 36 | ||
36 | if post.is_gallery && post.is_gallery == true | 37 | if isPostGallery(post) |
37 | if post.gallery_data | 38 | div.gallery |
38 | if post.gallery_data.items | 39 | each item in postGalleryItems(post) |
39 | div.gallery | 40 | div.gallery-item |
40 | - var total = post.gallery_data.items.length | 41 | div.gallery-item-idx |
41 | - var idx = 0 | 42 | | #{`${item.idx}/${item.total}`} |
42 | each item in post.gallery_data.items | 43 | a(href=`/media/${item.url}`) |
43 | - var url = `https://i.redd.it/${item.media_id}.jpg` | 44 | img(src=item.url loading="lazy") |
44 | div.gallery-item | 45 | else if isPostImage(post) |
45 | div.gallery-item-idx | ||
46 | | #{`${++idx}/${total}`} | ||
47 | a(href=`/media/${url}`) | ||
48 | img(src=url loading="lazy") | ||
49 | else if post.post_hint == "image" && post.thumbnail && post.thumbnail != "self" && post.thumbnail != "default" | ||
50 | img(src=post.url).post-media | 46 | img(src=post.url).post-media |
51 | else if post.post_hint == 'hosted:video' | 47 | else if isPostVideo(post) |
52 | - var url = post.secure_media.reddit_video.dash_url | 48 | - var url = post.secure_media.reddit_video.dash_url |
53 | video(controls data-dashjs-player src=`${url}`).post-media | 49 | video(controls data-dashjs-player src=`${url}`).post-media |
54 | else if post.post_hint == "link" | 50 | else if isPostLink(post) |
55 | a(href=post.url) | 51 | a(href=post.url) |
56 | | #{post.url} | 52 | | #{post.url} |
57 | 53 | ||