feat: Add invite token-based user registration and an admin interface for token management.
This commit is contained in:
@@ -2,6 +2,7 @@ import db from "../sql.mjs";
|
||||
import lib from "../lib.mjs";
|
||||
import { exec } from "child_process";
|
||||
import { promises as fs } from "fs";
|
||||
import cfg from "../config.mjs";
|
||||
|
||||
export default (router, tpl) => {
|
||||
router.get(/^\/login(\/)?$/, async (req, res) => {
|
||||
@@ -291,5 +292,52 @@ export default (router, tpl) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Token Routes
|
||||
router.get(/^\/admin\/tokens\/?$/, lib.auth, async (req, res) => {
|
||||
res.reply({
|
||||
body: tpl.render("admin/tokens", { session: req.session, tmp: null }, req)
|
||||
});
|
||||
});
|
||||
|
||||
router.get(/^\/api\/v2\/admin\/tokens\/?$/, lib.auth, async (req, res) => {
|
||||
const tokens = await db`
|
||||
select invite_tokens.*, "user".user as used_by_name
|
||||
from invite_tokens
|
||||
left join "user" on "user".id = invite_tokens.used_by
|
||||
order by created_at desc
|
||||
`;
|
||||
if (res.json) {
|
||||
return res.json({ success: true, tokens });
|
||||
}
|
||||
// Fallback if res.json is not available
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, tokens }));
|
||||
});
|
||||
|
||||
router.post(/^\/api\/v2\/admin\/tokens\/create\/?$/, lib.auth, async (req, res) => {
|
||||
try {
|
||||
const secret = cfg.main.invite_secret || 'defaultsecret';
|
||||
const token = lib.md5(lib.createID() + secret).substring(0, 10).toUpperCase(); // Short readable token
|
||||
await db`
|
||||
insert into invite_tokens (token, created_at, created_by)
|
||||
values (${token}, ${~~(Date.now() / 1e3)}, ${req.session.id})
|
||||
`;
|
||||
if (res.json) return res.json({ success: true, token });
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, token }));
|
||||
} catch (err) {
|
||||
if (res.json) return res.json({ success: false, msg: err.message });
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: err.message }));
|
||||
}
|
||||
});
|
||||
|
||||
router.post(/^\/api\/v2\/admin\/tokens\/delete\/?$/, lib.auth, async (req, res) => {
|
||||
if (!req.post.id) {
|
||||
if (res.json) return res.json({ success: false });
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false }));
|
||||
}
|
||||
await db`delete from invite_tokens where id = ${req.post.id}`;
|
||||
if (res.json) return res.json({ success: true });
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true }));
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
76
src/inc/routes/register.mjs
Normal file
76
src/inc/routes/register.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import db from "../sql.mjs";
|
||||
import lib from "../lib.mjs";
|
||||
|
||||
export default (router, tpl) => {
|
||||
router.get(/^\/register(\/)?$/, async (req, res) => {
|
||||
if (req.cookies.session) {
|
||||
return res.writeHead(302, { "Location": "/" }).end();
|
||||
}
|
||||
res.reply({
|
||||
body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck" })
|
||||
});
|
||||
});
|
||||
|
||||
router.post(/^\/register(\/)?$/, async (req, res) => {
|
||||
const { username, password, password_confirm, token } = req.post;
|
||||
|
||||
const renderError = (msg) => {
|
||||
return res.reply({
|
||||
body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck", error: msg })
|
||||
});
|
||||
};
|
||||
|
||||
if (!username || !password || !token) return renderError("All fields are required");
|
||||
if (password !== password_confirm) return renderError("Passwords do not match");
|
||||
if (username.length < 3) return renderError("Username too short");
|
||||
|
||||
// Check token
|
||||
const tokenRow = await db`
|
||||
select * from invite_tokens where token = ${token} and is_used = false
|
||||
`;
|
||||
|
||||
if (tokenRow.length === 0) {
|
||||
return renderError("Invalid or used invite token");
|
||||
}
|
||||
|
||||
// Check user existence
|
||||
const existing = await db`select id from "user" where "login" = ${username.toLowerCase()}`;
|
||||
if (existing.length > 0) return renderError("Username taken");
|
||||
|
||||
// Create User
|
||||
const hash = await lib.hash(password);
|
||||
const ts = ~~(Date.now() / 1e3);
|
||||
|
||||
// Note: Creating user. Assuming columns based on typical structure.
|
||||
// Need to check 'user' table columns to be safe, but usually: login, password, user (display name), created_at, admin
|
||||
// I'll assume 'user' is display name and 'login' is lowercase
|
||||
|
||||
const newUser = await db`
|
||||
insert into "user" ("login", "password", "user", "created_at", "admin")
|
||||
values (${username.toLowerCase()}, ${hash}, ${username}, ${ts}, false)
|
||||
returning id
|
||||
`;
|
||||
const userId = newUser[0].id;
|
||||
|
||||
// Mark token used
|
||||
await db`
|
||||
update invite_tokens
|
||||
set is_used = true, used_by = ${userId}
|
||||
where id = ${tokenRow[0].id}
|
||||
`;
|
||||
|
||||
// Get a valid avatar ID (default to 1 or whatever exists)
|
||||
const avatarRow = await db`select id from items limit 1`;
|
||||
const avatarId = avatarRow.length > 0 ? avatarRow[0].id : 1; // Fallback to 1, though checking length is safer
|
||||
|
||||
await db`
|
||||
insert into user_options (user_id, mode, theme, fullscreen, avatar)
|
||||
values (${userId}, 0, 'f0ck', 0, ${avatarId})
|
||||
`;
|
||||
|
||||
// Redirect to login
|
||||
return res.writeHead(302, { "Location": "/login" }).end();
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
Reference in New Issue
Block a user