diff --git a/public/s/js/admin.js b/public/s/js/admin.js new file mode 100644 index 0000000..0f1dd7f --- /dev/null +++ b/public/s/js/admin.js @@ -0,0 +1,228 @@ +let flashActive = false; +const flashTypes = [ "error", "success", "warn" ]; + +const flash = ({ type, msg }) => { + let flashContainer; + if(tmp = document.querySelector("div#flash")) + flashContainer = tmp; + else { + flashContainer = document.createElement("div"); + flashContainer.id = "flash"; + document.body.insertAdjacentElement("afterbegin", flashContainer); + } + + flashContainer.innerHTML = msg; + if(flashTypes.includes(type)) { + flashContainer.className = ""; + flashContainer.classList.add(type); + } + if(flashActive) + return false; + + flashActive = true; + flashContainer.animate( + [ { bottom: "-28px" }, { bottom: 0 } ], { + duration: 400, + fill: "both" + } + ).onfinish = () => setTimeout(() => { + flashContainer.animate( + [ { bottom: 0 }, { bottom: "-28px" } ], { + duration: 400, + fill: "both" + } + ).onfinish = () => flashActive = false; + }, 4 * 1e3); + return true; +}; + +(async () => { + if(_addtag = document.querySelector("a#a_addtag")) { + const postid = +document.querySelector("a.id-link").innerText; + const poster = document.querySelector("span#a_username").innerText; + let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)); + + const deleteEvent = async e => { + e.preventDefault(); + if(!confirm("Do you really want to delete this tag?")) + return; + const tag = e.target.parentElement.innerText.slice(0, -2); + if(!tags.includes(tag)) + return alert("wtf"); + const res = await deleteTag(postid, tag); + if(!res.success) + return alert("uff"); + tags = res.tags.map(t => t.tag); + renderTags(res.tags); + }; + + const post = async (url, data) => fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }); + + const getTags = async postid => await (await fetch("/api/v2/admin/tags/get/" + postid)).json(); + + const deletePost = async postid => await (await fetch("/api/v2/admin/deletepost/" + postid)).json(); + + const addTag = async (postid, tag) => await (await post("/api/v2/admin/tags/add", { + postid: postid, + tag: tag + })).json(); + + const deleteTag = async (postid, tag) => await (await post("/api/v2/admin/tags/delete", { + postid: postid, + tag: tag + })).json(); + + const renderTags = _tags => { + [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); + _tags.reverse().forEach(tag => { + const a = document.createElement("a"); + a.href = `/admin/test?tag=${tag.tag.replace(/\s/g, "%20")}`; + a.target = "_blank"; + a.style = "color: inherit !important"; + a.innerText = tag.tag; + + const span = document.createElement("span"); + span.classList.add("badge", "badge-light", "mr-2"); + span.title = tag.prefix; + if(tag.tag == "sfw") { + span.classList.remove("badge-light"); + span.classList.add("badge-success"); + } + if(tag.tag == "nsfw") { + span.classList.remove("badge-light"); + span.classList.add("badge-danger"); + } + if(tag.tag.startsWith(">")) { + span.classList.add("badge-greentext"); + } + + const delbutton = document.createElement("a"); + delbutton.innerHTML = " ×"; + delbutton.href = "#"; + delbutton.addEventListener("click", deleteEvent); + span.insertAdjacentElement("beforeend", a); + span.insertAdjacentElement("beforeend", delbutton); + + document.querySelector("#tags").insertAdjacentElement("afterbegin", span); + }); + }; + + const addtagClick = (ae = false) => { + if(ae) + ae.preventDefault(); + + const insert = document.querySelector("a#a_addtag"); + const span = document.createElement("span"); + span.classList.add("badge", "badge-light", "mr-2"); + + const input = document.createElement("input"); + input.size = "10"; + input.value = ""; + + let tt; + input.addEventListener("keydown", async e => { + if(e.key === "Enter") { + const tmptag = input.value; + if(tags.includes(tmptag)) + return alert("tag already exists"); + const res = await addTag(postid, tmptag); + if(!res.success) { + return flash({ + type: "error", + msg: res.msg + }); + } + tags = res.tags.map(t => t.tag); + renderTags(res.tags); + addtagClick(); + } + else if(e.key === "Escape") { + span.parentElement.removeChild(span); + } + return true; + }); + + span.insertAdjacentElement("afterbegin", input); + insert.insertAdjacentElement("beforebegin", span); + + input.focus(); + + input.addEventListener("focusout", ie => { + if(input.value.length === 0) + input.parentElement.parentElement.removeChild(input.parentElement); + }); + }; + + const toggleEvent = async (e = false) => { + if(e) + e.preventDefault(); + let res; + if(tags.includes("sfw")) { + await deleteTag(postid, "sfw"); + res = await addTag(postid, "nsfw"); + } + else if(tags.includes("nsfw")) { + await deleteTag(postid, "nsfw"); + res = await addTag(postid, "sfw"); + } + else + res = await addTag(postid, "sfw"); + + if(!res.success) + return flash({ + type: "error", + msg: res.msg + }); + + renderTags(res.tags); + tags = res.tags.map(t => t.tag); + + flash({ + type: "success", + msg: tags.join() + }); + }; + + const deleteButtonEvent = async e => { + if(e) + e.preventDefault(); + if(!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`)) + return; + const res = await deletePost(postid); + if(res.success) { + flash({ + type: "success", + msg: "post was successfully deleted" + }); + } + else { + flash({ + type: "error", + msg: res.msg + }); + } + }; + + _addtag.addEventListener("click", addtagClick); + document.querySelector("a#a_toggle").addEventListener("click", toggleEvent); + [...document.querySelectorAll("#tags > .badge > a:last-child")].map(t => t.addEventListener("click", deleteEvent)); + document.querySelector("a#a_delete").addEventListener("click", deleteButtonEvent); + + document.addEventListener("keyup", e => { + if(e.target.tagName === "INPUT") + return; + if(e.key === "p") + toggleEvent(); + else if(e.key === "i") + addtagClick(); + else if(e.key === "x") + deleteButtonEvent(); + }); + } +})(); diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js index 26a94bc..fd7afd7 100644 --- a/public/s/js/f0ck.js +++ b/public/s/js/f0ck.js @@ -2,7 +2,7 @@ if(elem = document.querySelector("#my-video")) { const video = new v0ck(elem); document.addEventListener("keydown", e => { - if(e.key === " ") { + if(e.key === " " && e.target.tagName !== "INPUT") { video[video.paused ? 'play' : 'pause'](); document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden'); } @@ -26,7 +26,7 @@ "r": clickOnElementBinding("#random") }; document.addEventListener("keydown", e => { - if(e.key in keybindings) { + if(e.key in keybindings && e.target.tagName !== "INPUT") { e.preventDefault(); keybindings[e.key](); } @@ -144,19 +144,6 @@ analyser.getByteFrequencyData(data); draw(data); } - /*function draw(data) { - data = [ ...data ]; - ctx.clearRect(0, 0, canvas.width, canvas.height); - const space = canvas.width / data.length; - const sstyle = getComputedStyle(document.body).getPropertyValue("--accent"); - data.forEach((value, i) => { - ctx.beginPath(); - ctx.moveTo(space * i, canvas.height); - ctx.lineTo(space * i, canvas.height - value); - ctx.strokeStyle = sstyle; - ctx.stroke(); - }); - }*/ function draw(data) { data = [ ...data ]; ctx.clearRect(0, 0, canvas.width, canvas.height); diff --git a/public/s/js/theme.js b/public/s/js/theme.js index 0e4c717..f7de92c 100644 --- a/public/s/js/theme.js +++ b/public/s/js/theme.js @@ -29,6 +29,8 @@ const Cookie = { })); document.addEventListener("keydown", e => { + if(e.target.tagName === "INPUT") + return; const acttheme = Cookie.get('theme') ?? "f0ck"; const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase()); const k = e.key; diff --git a/src/inc/meddlware.mjs b/src/inc/meddlware.mjs new file mode 100644 index 0000000..36ae165 --- /dev/null +++ b/src/inc/meddlware.mjs @@ -0,0 +1,11 @@ +export const auth = (req, res) => { + if(!req.session) { + return { + success: false, + redirect: "/" + }; + } + return { + success: true + }; +}; diff --git a/src/inc/router.mjs b/src/inc/router.mjs index 9291131..e3612d9 100644 --- a/src/inc/router.mjs +++ b/src/inc/router.mjs @@ -7,8 +7,21 @@ export default new class Router { this.routes = new Map(); }; route(method, args) { - this.routes.set(args[0], { method: method, f: args[1] }); - console.info("route set", method, args[0]); + // args[0]: route + // args[1]: func || meddlware + const route = args[0]; + let func; + let meddlware = null; + if(args.length === 2) { + func = args[1]; + } + else { + meddlware = args[1]; + func = args[2]; + } + + this.routes.set(route, { method: method, meddlware: meddlware, f: func }); + console.info("route set", method, route); }; get() { this.route("GET", arguments); @@ -81,5 +94,5 @@ export default new class Router { }; Map.prototype.getRoute = function(path, method, tmp) { - return (!(tmp = [...this.entries()].filter(r => ( r[0] === path || r[0].exec?.(path) ) && r[1].method.includes(method) )[0])) ? false : tmp[1].f; + return (!(tmp = [...this.entries()].filter(r => ( r[0] === path || r[0].exec?.(path) ) && r[1].method.includes(method) )[0])) ? false : tmp[1]; }; diff --git a/src/inc/routes/admin.mjs b/src/inc/routes/admin.mjs index 269d6d3..595f7c3 100644 --- a/src/inc/routes/admin.mjs +++ b/src/inc/routes/admin.mjs @@ -4,7 +4,8 @@ import tpl from "../tpl.mjs"; import lib from "../lib.mjs"; import util from "util"; import crypto from "crypto"; -import cfg from "../../../config.json"; +import { auth } from "../meddlware.mjs"; +import search from "./inc/search.mjs"; const scrypt = util.promisify(crypto.scrypt); @@ -50,15 +51,12 @@ router.post(/^\/login(\/)?$/, async (req, res) => { return res.writeHead(301, { "Cache-Control": "no-cache, public", - "Set-Cookie": `session=${session}; Path=/`, + "Set-Cookie": `session=${session}; Path=/; Expires=Fri, 31 Dec 9999 23:59:59 GMT`, "Location": "/" }).end(); }); -router.get(/^\/logout$/, async (req, res) => { - if(!req.session) - return res.redirect("/"); - +router.get(/^\/logout$/, auth, async (req, res) => { const usersession = await sql("user_sessions").where("id", req.session.sess_id); if(usersession.length === 0) return res.reply({ body: "nope 2" }); @@ -66,7 +64,7 @@ router.get(/^\/logout$/, async (req, res) => { await sql("user_sessions").where("id", req.session.sess_id).del(); return res.writeHead(301, { "Cache-Control": "no-cache, public", - "Set-Cookie": "session=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT", + "Set-Cookie": "session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" }).end(); }); @@ -88,11 +86,42 @@ router.get(/^\/login\/test$/, async (req, res) => { }); }); -router.get(/^\/admin(\/)?$/, async (req, res) => { - if(!req.session) - return res.redirect("/"); - +router.get(/^\/admin(\/)?$/, auth, async (req, res) => { // frontpage res.reply({ body: tpl.render("views/admin", {}, req) }); }); + +router.get(/^\/admin\/sessions(\/)?$/, auth, async (req, res) => { + const rows = await sql("user_sessions") + .leftJoin("user", "user.id", "user_sessions.user_id") + .select("user_sessions.*", "user.user") + .orderBy("user.id"); + + res.reply({ + body: tpl.render("views/admin_sessions", { + sessions: rows + }, req) + }); +}); + +router.get(/^\/admin\/test(\/)?$/, auth, async (req, res) => { + let ret; + if(Object.keys(req.url.qs).length > 0) { + const tag = req.url.qs.tag; + + const rows = await sql("tags") + .select("items.id", "items.username", "tags.tag") + .leftJoin("tags_assign", "tags_assign.tag_id", "tags.id") + .leftJoin("items", "items.id", "tags_assign.item_id") + .where("tags.tag", "regexp", tag); + + ret = search(rows, tag); + } + + res.reply({ + body: tpl.render("views/admin_search", { + result: ret + }, req) + }); +}); diff --git a/src/inc/routes/apiv2.mjs b/src/inc/routes/apiv2.mjs index 9c80c1f..b587095 100644 --- a/src/inc/routes/apiv2.mjs +++ b/src/inc/routes/apiv2.mjs @@ -1,5 +1,7 @@ import router from "../router.mjs"; import sql from "../sql.mjs"; +import { auth } from "../meddlware.mjs"; +import util from "util"; const allowedMimes = [ "audio", "image", "video", "%" ]; @@ -71,3 +73,132 @@ router.get(/^\/api\/v2\/user\/.*(\/\d+)?$/, async (req, res) => { // auf qs umst body: JSON.stringify(rows.length > 0 ? rows : []) }); }); + +// adminzeugs +router.post(/^\/api\/v2\/admin\/tags\/add$/, auth, async (req, res) => { + if(!req.post.postid || !req.post.tag) { + return res.reply({ body: JSON.stringify({ + success: false, + msg: "missing postid or tag" + })}); + } + + const postid = +req.post.postid; + const tag = req.post.tag; + + if(tag.length >= 45) { + return res.reply({ body: JSON.stringify({ + success: false, + msg: "tag is too long!" + })}); + } + + try { + let tagid; + const tag_exists = await sql("tags").select("id", "tag").where("tag", tag); + if(tag_exists.length === 0) { // create new tag + tagid = (await sql("tags").insert({ + tag: tag + }))[0]; + } + else { + tagid = tag_exists[0].id; + } + await sql("tags_assign").insert({ + tag_id: tagid, + item_id: postid, + prefix: `${req.session.user}@webinterface` + }); + } catch(err) { + return res.reply({ body: JSON.stringify({ + success: false, + msg: err.message, + tags: await getTags(postid) + })}); + } + + return res.reply({ body: JSON.stringify({ + success: true, + postid: req.post.postid, + tag: req.post.tag, + tags: await getTags(postid) + })}); +}); + +const getTags = async itemid => await sql("tags_assign") + .leftJoin("tags", "tags.id", "tags_assign.tag_id") + .where("tags_assign.item_id", itemid) + .select("tags.id", "tags.tag", "tags_assign.prefix"); + +const cleanTags = async () => { + const tags = await sql("tags").leftJoin("tags_assign", "tags_assign.tag_id", "tags.id").whereNull("tags_assign.item_id"); + if(tags.length === 0) + return; + + let deleteTag = sql("tags"); + tags.forEach(tag => { + if(["sfw", "nsfw"].includes(tag.tag.toLowerCase())) + return; + deleteTag = deleteTag.orWhere("id", tag.id); + }); + await deleteTag.del(); +}; + +router.post(/^\/api\/v2\/admin\/tags\/delete$/, auth, async (req, res) => { + if(!req.post.postid || !req.post.tag) { + return res.reply({ body: JSON.stringify({ + success: false, + msg: "missing postid or tag" + })}); + } + + const postid = +req.post.postid; + const tag = req.post.tag; + + const tags = await getTags(postid); + + const tagid = tags.filter(t => t.tag === tag)[0]?.id ?? null; + if(!tagid || tagid?.length === 0) { + return res.reply({ body: JSON.stringify({ + success: false, + tag: tag, + msg: "tag is not assigned", + tags: await getTags(postid) + })}); + } + + let q = sql("tags_assign").where("tag_id", tagid).andWhere("item_id", postid).del(); + if(req.session.level < 50) + q = q.andWhere("prefix", `${req.session.user}@webinterface`); + const reply = !!(await q); + + await cleanTags(); + + return res.reply({ body: JSON.stringify({ + success: reply, + tag: tag, + tagid: tagid, + tags: await getTags(postid) + })}); +}); + +router.get(/^\/api\/v2\/admin\/tags\/get\/\d+$/, auth, async (req, res) => { + return res.reply({ body: JSON.stringify({ + tags: await getTags(+req.url.split[5]) + })}); +}); + +router.get(/^\/api\/v2\/admin\/deletepost\/\d+$/, auth, async (req, res) => { + if(!req.url.split[4]) { + return res.reply({ body: JSON.stringify({ + success: true, + msg: "no postid" + })}); + } + const postid = +req.url.split[4]; + + const rows = await sql("items").where("id", postid).del(); + res.reply({ body: JSON.stringify({ + success: true + })}); +}); diff --git a/src/inc/routes/inc/search.mjs b/src/inc/routes/inc/search.mjs new file mode 100644 index 0000000..1ddbc72 --- /dev/null +++ b/src/inc/routes/inc/search.mjs @@ -0,0 +1,35 @@ +export default (obj, word) => { + if(typeof obj !== "object") + return false; + return obj.map(tmp => { + let rscore = 0 + , startat = 0 + , string = tmp.tag + , cscore + , score; + for(let i = 0; i < word.length; i++) { + const idxOf = string.toLowerCase().indexOf(word.toLowerCase()[i], startat); + if(-1 === idxOf) + return 0; + if(startat === idxOf) + cscore = 0.7; + else { + cscore = 0.1; + if(string[idxOf - 1] === ' ') + cscore += 0.8; + } + if(string[idxOf] === word[i]) + cscore += 0.1; + rscore += cscore; + startat = idxOf + 1; + } + score = 0.5 * (rscore / string.length + rscore / word.length); + if(word.toLowerCase()[0] === string.toLowerCase()[0] && score < 0.85) + score += 0.15; + + return { + ...tmp, + score: score + }; + }).sort((a, b) => b.score - a.score); +}; diff --git a/src/websrv.mjs b/src/websrv.mjs index 7ab5805..63ba8ec 100644 --- a/src/websrv.mjs +++ b/src/websrv.mjs @@ -17,17 +17,24 @@ import router from "./inc/router.mjs"; req.url = url.parse(req.url.replace(/(?!^.)(\/+)?$/, '')); req.url.split = req.url.pathname.split("/").slice(1); - req.url.qs = querystring.parse(req.url.query); + req.url.qs = {...querystring.parse(req.url.query)}; req.post = await new Promise((resolve, _, data = "") => req .on("data", d => void (data += d)) - .on("end", () => void resolve(Object.fromEntries(Object.entries(querystring.parse(data)).map(([k, v]) => { - try { - return [k, decodeURIComponent(v)]; - } catch(err) { - return [k, v]; + .on("end", () => { + if(req.headers['content-type'] == "application/json") { + try { + return void resolve(JSON.parse(data)); + } catch(err) {} } - }))))); + void resolve(Object.fromEntries(Object.entries(querystring.parse(data)).map(([k, v]) => { + try { + return [k, decodeURIComponent(v)]; + } catch(err) { + return [k, v]; + } + }))); + })); res.reply = ({ code = 200, @@ -68,13 +75,29 @@ import router from "./inc/router.mjs"; }).end(); } } - - !(r = router.routes.getRoute(req.url.pathname, req.method)) ? res.writeHead(404).end(`404 - ${req.method} ${req.url.pathname}`) : await r(req, res); + console.log([ `[${(new Date()).toLocaleTimeString()}]`, `${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`, `${req.method} ${res.statusCode}`, - req.url.pathname].map(e=>e.toString().padEnd(15)).join("")); + req.url.pathname + ].map(e => e.toString().padEnd(15)).join("")); + + if(r = router.routes.getRoute(req.url.pathname, req.method)) { + if(r.meddlware) { + const tmp = r.meddlware(req, req); + if(tmp.redirect) + return res.redirect(tmp.redirect); + if(!tmp.success) + return res.reply({ body: tmp.msg }); + } + await r.f(req, res); + } + else { + res + .writeHead(404) + .end(`404 - ${req.method} ${req.url.pathname}`); + } }).listen(cfg.websrv.port, () => setTimeout(() => { console.log(`f0ck is listening on port ${cfg.websrv.port}.`); }, 500)); diff --git a/views/about.html b/views/about.html index 4713170..3792aec 100644 --- a/views/about.html +++ b/views/about.html @@ -44,9 +44,9 @@
f0ck is completely functional without javascript enabled, you can be the beardiest neckbeard of all, we got you m'gentleman, if you want to use a custom theme you gotta allow our style cookie.
Theme plopper (If theme is selected the default f0ck sheme will appear for a small amount of time until the custom stylesheet is applied) - ETA: Christmas™
-Magical video seeker (If you hold mouseclick for too long on the video timeline and drag to a specific position it will go crazy, don't!) - ETA: Christmas™
+Magical video seeker (If you hold mouseclick for too long on the video timeline and drag to a specific position it will go crazy, don't!) - ETA: Christmas™
Cookies: Yes, we set 1 cookie for your prefered stylesheet, for the _cfduid cookie please see https://blog.cloudflare.com/deprecating-cfduid-cookie/
+Cookies: Yes, we set 1 cookie for your prefered stylesheet, for the _cfduid cookie please see https://blog.cloudflare.com/deprecating-cfduid-cookie/
Logs: No for Tor - Yes for cloudflare and cloudflare probably sells your soul to the devil, however our webserver doesn't log cloudflare connecting to our webserver, if you want to lurk without being flared by the cloud, see the above tor section my man
But let me tell you something about internet "privacy". At first you need to understand what it means to have "privacy", for me as a human the word privacy means that I am by myself, private not observable by others, on the internet this concept does not work, in real life you might lock your door and then no one can enter the normal way to your room and you have some good old fashioned privacy, but on the internet various applications on your computer including extensions for your browser might make connections without you knowing or even giving consent if you knew, most applications send heartbeats, store information, read files on your computer, they might even process the gained data with or without you knowing or consenting to any of it, you probably accepted in good faith the ToS of many services without ever reading them, in the end it's up to you if you give a shit about your privacy, btw a VPN wont help if you still got all the tracking cookies and shit in your browser, they will just add 1+1 and you are identified again. My honest advice for anyone who seeks total privacy without bullshit, disconnect from the internet, remove the internet from your life, it's a bulletproof solution! With that being said, have a good day!
diff --git a/views/admin_search.html b/views/admin_search.html new file mode 100644 index 0000000..c076d20 --- /dev/null +++ b/views/admin_search.html @@ -0,0 +1,27 @@ +{{include main/header_admin}} + +Thumbnail | +ID | +Tag | +Username | +Score | +
![]() |
+ {{=line.id}} | +{{=line.tag}} | +{{=line.username}} | +{{=line.score}} | +
ID | +userid | +user | +browser | +created_at | +last_used | +
{{=session.id}} | +{{=session.user_id}} | +{{=session.user}} | +{{=session.browser}} | +{{=new Date(session.created_at * 1e3).toLocaleString("de-DE")}} | +{{=new Date(session.last_used * 1e3).toLocaleString("de-DE")}} | +