diff --git a/public/s/css/f0ck.css b/public/s/css/f0ck.css index 370eaa7..8345ddf 100644 --- a/public/s/css/f0ck.css +++ b/public/s/css/f0ck.css @@ -2204,7 +2204,7 @@ body[type='login'] { align-items: center; padding: 20px 20px; background: rgba(0, 0, 0, 0.8); - border-radius: 10px; + border-radius: 0; border: 1px solid var(--accent); } @@ -3365,7 +3365,8 @@ input#s_avatar { } /* Login Modal */ -#login-modal { +#login-modal, +#register-modal { position: fixed; top: 0; left: 0; @@ -3437,7 +3438,8 @@ input#s_avatar { filter: brightness(1.1); } -#login-modal-close { +#login-modal-close, +#register-modal-close { position: absolute; top: 10px; right: 15px; @@ -3449,7 +3451,8 @@ input#s_avatar { opacity: 0.7; } -#login-modal-close:hover { +#login-modal-close:hover, +#register-modal-close:hover { opacity: 1; color: var(--accent); } \ No newline at end of file diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js index a37f270..9587f23 100644 --- a/public/s/js/f0ck.js +++ b/public/s/js/f0ck.js @@ -71,6 +71,38 @@ window.requestAnimFrame = (function () { }); } + // Register Modal Logic + const registerBtn = document.getElementById('nav-register-btn'); + const registerModal = document.getElementById('register-modal'); + const registerClose = document.getElementById('register-modal-close'); + + if (registerBtn && registerModal) { + registerBtn.addEventListener('click', (e) => { + e.preventDefault(); + registerModal.style.display = 'flex'; + // Close dropdown + if (visitorMenu) visitorMenu.classList.remove('show'); + }); + + if (registerClose) { + registerClose.addEventListener('click', () => { + registerModal.style.display = 'none'; + }); + } + + registerModal.addEventListener('click', (e) => { + if (e.target === registerModal) { + registerModal.style.display = 'none'; + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && registerModal.style.display === 'flex') { + registerModal.style.display = 'none'; + } + }); + } + // Initialize background preference if (localStorage.getItem('background') == undefined) { localStorage.setItem('background', 'true'); diff --git a/src/inc/routes/admin.mjs b/src/inc/routes/admin.mjs index 7fc4923..d535490 100644 --- a/src/inc/routes/admin.mjs +++ b/src/inc/routes/admin.mjs @@ -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; }; diff --git a/src/inc/routes/register.mjs b/src/inc/routes/register.mjs new file mode 100644 index 0000000..cb54aa0 --- /dev/null +++ b/src/inc/routes/register.mjs @@ -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; +}; diff --git a/views/admin.html b/views/admin.html index 354ad00..60486c6 100644 --- a/views/admin.html +++ b/views/admin.html @@ -16,6 +16,7 @@
  • Approval Queue
  • Sessions
  • +
  • Invite Tokens
  • diff --git a/views/admin/tokens.html b/views/admin/tokens.html new file mode 100644 index 0000000..c5649b7 --- /dev/null +++ b/views/admin/tokens.html @@ -0,0 +1,89 @@ +@include(snippets/header) + +
    +

    Invite Tokens

    + +
    + +
    + +
    + + + + + + + + + + + + + +
    TokenStatusUsed ByCreatedActions
    +
    +
    + + + +@include(snippets/footer) \ No newline at end of file diff --git a/views/register.html b/views/register.html new file mode 100644 index 0000000..f4bc59f --- /dev/null +++ b/views/register.html @@ -0,0 +1,28 @@ + + + + + + + register + + + + +
    +

    Register

    + @if(typeof error !== 'undefined') +
    {{ error }}
    + @endif + + + + + +
    + Back to Login +
    +
    + + + \ No newline at end of file diff --git a/views/snippets/navbar.html b/views/snippets/navbar.html index 724edfb..17e9ceb 100644 --- a/views/snippets/navbar.html +++ b/views/snippets/navbar.html @@ -56,10 +56,11 @@ + + + \ No newline at end of file