sirx: und weg damit :D

This commit is contained in:
Flummi 2021-05-25 14:44:35 +02:00
parent 83fbc557e8
commit f3e357d8a4
17 changed files with 577 additions and 54 deletions

228
public/s/js/admin.js Normal file
View File

@ -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();
});
}
})();

View File

@ -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);

View File

@ -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;

11
src/inc/meddlware.mjs Normal file
View File

@ -0,0 +1,11 @@
export const auth = (req, res) => {
if(!req.session) {
return {
success: false,
redirect: "/"
};
}
return {
success: true
};
};

View File

@ -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];
};

View File

@ -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)
});
});

View File

@ -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
})});
});

View File

@ -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);
};

View File

@ -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));

View File

@ -44,9 +44,9 @@
<p>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.</p>
<h5>Known Bugs</h5>
<p style="text-decoration: line-through">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™</p>
<p>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™</p>
<p style="text-decoration: line-through">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™</p>
<h5>f0ck Privacy?</h5>
<p>Cookies: Yes, we set 1 cookie for your prefered stylesheet, for the _cfduid cookie please see <a href="https://blog.cloudflare.com/deprecating-cfduid-cookie/">https://blog.cloudflare.com/deprecating-cfduid-cookie/</a></p>
<p>Cookies: Yes, we set 1 cookie for your prefered stylesheet, for the _cfduid cookie please see <a href="https://blog.cloudflare.com/deprecating-cfduid-cookie/" target="_blank">https://blog.cloudflare.com/deprecating-cfduid-cookie/</a></p>
<p>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</p>
<p>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!</p>
</div>

27
views/admin_search.html Normal file
View File

@ -0,0 +1,27 @@
{{include main/header_admin}}
<form action="/admin/test" style="margin-top: 15px;">
<input type="text" name="tag" /><button type="submit">search</button>
</form>
<hr />
{{if result}}
<h1>{{=result.length}} f0cks given</h1>
<table style="width: 100%;">
<tr>
<td style="text-align: center;">Thumbnail</td>
<td style="text-align: center;">ID</td>
<td style="text-align: center;">Tag</td>
<td style="text-align: center;">Username</td>
<td style="text-align: center;">Score</td>
</tr>
{{each result as line}}
<tr>
<td style="width: 128px;"><a href="/{{=line.id}}" target="_blank"><img src="/t/{{=line.id}}.png" /></a></td>
<td style="text-align: center;"><a href="/{{=line.id}}" target="_blank">{{=line.id}}</a></td>
<td style="text-align: center;"><a href="/admin/test?tag={{=line.tag.replace(/\s/g, "+")}}">{{=line.tag}}</a></td>
<td style="text-align: center;">{{=line.username}}</td>
<td style="text-align: center;">{{=line.score}}</td>
</tr>
{{/each}}
</table>
{{/if}}
{{include main/footer}}

22
views/admin_sessions.html Normal file
View File

@ -0,0 +1,22 @@
{{include main/header_admin}}
<table style="width: 100%;">
<tr>
<td>ID</td>
<td>userid</td>
<td>user</td>
<td>browser</td>
<td>created_at</td>
<td>last_used</td>
</tr>
{{each sessions as session}}
<tr>
<td>{{=session.id}}</td>
<td>{{=session.user_id}}</td>
<td>{{=session.user}}</td>
<td>{{=session.browser}}</td>
<td>{{=new Date(session.created_at * 1e3).toLocaleString("de-DE")}}</td>
<td>{{=new Date(session.last_used * 1e3).toLocaleString("de-DE")}}</td>
</tr>
{{/each}}
</table>
{{include main/footer}}

View File

@ -46,14 +46,16 @@
</div>
</div>
<div class="metadata">
<span class="badge badge-dark"><a href="/{{=item.id}}" style="--hover-image: url('/t/{{=item.id}}.png');" class="id-link">{{=item.id}}</a></span>
<span class="badge badge-dark">
<a href="/{{=item.id}}" style="--hover-image: url('/t/{{=item.id}}.png');" class="id-link">{{=item.id}}</a>{{if session}} (<span id="a_username">{{=user.name}}</span>){{/if}}
</span>
<span class="badge badge-dark">{{=user.network}} / {{=user.channel}}</span>
<span class="badge badge-dark image-source"><a class="post_source" title="{{=item.src.long}}" href="{{=item.src.long}}" target="_blank">{{=item.src.short}}</a></span>
<span class="badge badge-dark"><a class="dest-link" href="{{=item.dest}}" target="_blank">{{=item.mime}}</a> / {{=item.size}}</span>
<span class="badge badge-dark"><time class="timeago" title="{{=item.timestamp}}" datetime="{{=item.timestamp}}">{{=item.timestamp}}</time></span>
<span class="badge badge-dark">
{{if session}}
<a href="#">delete</a>
<a href="#" id="a_delete">delete</a>
{{else}}
{{=lul}}
{{/if}}
@ -61,9 +63,16 @@
<span class="badge badge-dark" id="tags">
{{if typeof item.tags !== "undefined"}}
{{each item.tags as tag}}
<span class="badge badge-{{=(tag.tag === "nsfw" ? "danger" : tag.tag === "sfw" ? "success" : "light")}} mr-2">{{=tag.tag}}</span>
<span {{if session}}title="{{=tag.prefix}}"{{/if}} class="badge {{if tag.tag[0] == "&"}}badge-greentext{{/if}} badge-{{=(tag.tag === "nsfw" ? "danger" : tag.tag === "sfw" ? "success" : "light")}} mr-2">
{{if session}}<a href="/admin/test?tag={{=tag.tag.replace(/\s/g, "%20")}}" target="_blank" style="color: inherit !important;">{{/if}}{{=tag.tag}}{{if session}}</a>&nbsp;<a href="#">&#215;</a>{{/if}}
</span>
{{/each}}
{{/if}}</span>
{{/if}}
{{if session}}
<a href="#" id="a_addtag">add tag</a>
&nbsp;-&nbsp;<a href="#" id="a_toggle">toggle</a>
{{/if}}
</span>
</div>
</div>
{{include main/footer}}

View File

@ -1,5 +1,6 @@
<script async src="/s/js/theme.js"></script>
<script src="/s/js/v0ck.js"></script>
<script src="/s/js/f0ck.js"></script>
{{if session}}<script src="/s/js/admin.js?v=3"></script>{{/if}}
</body>
</html>

View File

@ -6,12 +6,15 @@
<link rel="stylesheet" href="/s/css/f0ck.css">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="f0ck.me is the place where internet purists gather to celebrate content of all kinds">
{{if data.item}}
<meta property="og:site_name" content="f0ck.me" />
<meta property="og:description"/>
<meta name="Description"/>
{{if item.tags}}
<meta property="og:description" content="{{each item.tags as tag}}{{=tag.tag.replace(/[\\$'"]/g, "\\$&")}},{{/each}}f0ck" />
<meta name="description" content="{{each item.tags as tag}}{{=tag.tag.replace(/[\\$'"]/g, "\\$&")}},{{/each}}f0ck" />
{{/if}}
<meta property="og:image" content="{{=item.thumbnail}}" />
{{else}}
<meta name="description" content="f0ck.me is the place where internet purists gather to celebrate content of all kinds">
{{/if}}
</head>
<body>

View File

@ -7,8 +7,10 @@
<li class="nav-item dropdown">
<a class="nav-link" href="/admin" content="{{=session.user}}" data-toggle="dropdown">Admin</a>
<ul class="dropdown-menu">
<li><a href="/admin">Adminpanel</a></li>
<li><a href="/logout">Logout</a></li>
<li><a href="/admin">adminpanel</a></li>
<li><a href="/admin/sessions">sessions</a></li>
<li><a href="/admin/test">search (wip)</a></li>
<li><a href="/logout">logout</a></li>
</ul>
</li>
{{else}}

View File

@ -11,13 +11,13 @@
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span class="nav-link-identifier">blah</span>
<a class="nav-link" href="/admin/sessions">
<span class="nav-link-identifier">sessions</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span class="nav-link-identifier">blah</span>
<a class="nav-link" href="/admin/test">
<span class="nav-link-identifier">search&nbsp;(wip)</span>
</a>
</li>
<li class="nav-item">