selfhostable, read-only reddit client

add invite system

Changed files
+98 -5
scripts
src
+38
scripts/gen-invite.js
··· 1 + import { Database } from "bun:sqlite"; 2 + 3 + const db = new Database("readit.db", { 4 + strict: true, 5 + }); 6 + 7 + // Create the invites table if it doesn't exist 8 + db.run(` 9 + CREATE TABLE IF NOT EXISTS invites ( 10 + id INTEGER PRIMARY KEY AUTOINCREMENT, 11 + token TEXT NOT NULL, 12 + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 13 + usedAt TIMESTAMP 14 + ) 15 + `); 16 + 17 + // Generate a new invite token 18 + function generateInviteToken() { 19 + const hasher = new Bun.CryptoHasher("sha256", "super-secret-invite-key"); 20 + return hasher.update(Math.random().toString()).digest("hex"); 21 + } 22 + 23 + // Store the token in the database 24 + function createInvite() { 25 + const token = generateInviteToken(); 26 + db.run("INSERT INTO invites (token) VALUES ($token)", { token }); 27 + console.log(`Invite token created: ${token}`); 28 + } 29 + 30 + // CLI usage 31 + const command = process.argv[2]; 32 + const arg = process.argv[3]; 33 + 34 + if (command === "create") { 35 + createInvite(); 36 + } else { 37 + console.log("requires an arg"); 38 + }
+37
src/invite.js
··· 1 + const { db } = require("./db"); 2 + 3 + const validateInviteToken = async (req, res, next) => { 4 + const token = req.query.token; 5 + 6 + if (!token) { 7 + return res.render("register", { 8 + message: "this instance requires an invite", 9 + isDisabled: true, 10 + }); 11 + } 12 + 13 + const invite = db 14 + .query("SELECT * FROM invites WHERE token = $token AND usedAt IS null") 15 + .get({ token }); 16 + 17 + if (!invite) { 18 + return res.render("register", { 19 + message: "this invite token is invalid", 20 + isDisabled: true, 21 + }); 22 + } 23 + 24 + if (invite.usedAt) { 25 + return res.render("register", { 26 + message: "this invite has been claimed", 27 + isDisabled: true, 28 + }); 29 + } 30 + 31 + req.invite = invite; 32 + next(); 33 + }; 34 + 35 + module.exports = { 36 + validateInviteToken, 37 + };
+7
src/public/styles.css
··· 491 491 color: var(--bg-color); 492 492 } 493 493 494 + .submit-button button:disabled { 495 + width: 100%; 496 + padding: 12px; 497 + background-color: var(--bg-color-muted); 498 + color: var(--text-color-muted); 499 + } 500 + 494 501 .register-error-message { 495 502 flex-flow: row wrap; 496 503 color: var(--error-text-color);
+10 -3
src/routes/index.js
··· 6 6 const { JWT_KEY } = require("../"); 7 7 const { db } = require("../db"); 8 8 const { authenticateToken } = require("../auth"); 9 + const { validateInviteToken } = require("../invite"); 9 10 10 11 const router = express.Router(); 11 12 const G = new geddit.Geddit(); ··· 113 114 res.render("media", { kind, url }); 114 115 }); 115 116 116 - router.get("/register", async (req, res) => { 117 - res.render("register"); 117 + router.get("/register", validateInviteToken, async (req, res) => { 118 + res.render("register", { isDisabled: false, token: req.query.token }); 118 119 }); 119 120 120 - router.post("/register", async (req, res) => { 121 + router.post("/register", validateInviteToken, async (req, res) => { 121 122 const { username, password, confirm_password } = req.body; 122 123 123 124 if (!username || !password || !confirm_password) { ··· 141 142 142 143 try { 143 144 const hashedPassword = await Bun.password.hash(password); 145 + 146 + db.query("UPDATE invites SET usedAt = CURRENT_TIMESTAMP WHERE id = $id", { 147 + id: req.invite.id, 148 + }); 149 + 144 150 const insertedRecord = db 145 151 .query( 146 152 "INSERT INTO users (username, password_hash) VALUES ($username, $hashedPassword)", ··· 159 165 }) 160 166 .redirect("/"); 161 167 } catch (err) { 168 + console.log(err); 162 169 return res.render("register", { 163 170 message: "error registering user, try again later", 164 171 });
+6 -2
src/views/register.pug
··· 1 1 include ../mixins/head 2 2 3 + - var action = "/register" + (token?`?token=${token}`:'') 3 4 doctype html 4 5 html 5 6 +head("register") ··· 9 10 if message 10 11 div.register-error-message 11 12 | #{message} 12 - form(action="/register" method="post") 13 + form(action=`${action}` method="post") 13 14 div.input-text 14 15 label(for="username") username 15 16 input(type="text" name="username" required) ··· 20 21 label(for="confirm_password") confirm password 21 22 input(type="password" name="confirm_password" required) 22 23 div.submit-button 23 - button(type="submit") register 24 + if isDisabled 25 + button(type="submit" disabled) register'nt :( 26 + else 27 + button(type="submit") register 24 28 div 25 29 p 26 30 | already have an account?