init f0ckm
This commit is contained in:
340
src/inc/lib.mjs
Normal file
340
src/inc/lib.mjs
Normal file
@@ -0,0 +1,340 @@
|
||||
import crypto from "crypto";
|
||||
import util from "util";
|
||||
import db from "./sql.mjs";
|
||||
|
||||
import cfg from "./config.mjs";
|
||||
|
||||
|
||||
|
||||
const scrypt = util.promisify(crypto.scrypt);
|
||||
|
||||
const epochs = [
|
||||
["year", 31536000],
|
||||
["month", 2592000],
|
||||
["day", 86400],
|
||||
["hour", 3600],
|
||||
["minute", 60],
|
||||
["second", 1]
|
||||
];
|
||||
const getDuration = timeAgoInSeconds => {
|
||||
for (let [name, seconds] of epochs) {
|
||||
const interval = ~~(timeAgoInSeconds / seconds);
|
||||
if (interval >= 1) return {
|
||||
interval: interval,
|
||||
epoch: name
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default new class {
|
||||
escapeHTML(str) {
|
||||
if (!str) return "";
|
||||
return str.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
|
||||
};
|
||||
calcSpeed(b, s) {
|
||||
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
|
||||
};
|
||||
timeAgo(date) {
|
||||
const duration = getDuration(~~((new Date() - new Date(date)) / 1e3));
|
||||
if (!duration) return "just now";
|
||||
const { interval, epoch } = duration;
|
||||
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
|
||||
};
|
||||
md5(str) {
|
||||
return crypto.createHash('md5').update(str).digest("hex");
|
||||
};
|
||||
sha256(str) {
|
||||
return crypto.createHash('sha256').update(str).digest("hex");
|
||||
};
|
||||
getMode(mode) {
|
||||
let tmp;
|
||||
mode = Number(mode);
|
||||
switch (mode) {
|
||||
case 1: // nsfw
|
||||
tmp = "items.id in (select item_id from tags_assign where tag_id = 2)";
|
||||
break;
|
||||
case 2: // untagged
|
||||
tmp = "not exists (select 1 from tags_assign where item_id = items.id)";
|
||||
break;
|
||||
case 3: // all
|
||||
tmp = "1 = 1";
|
||||
break;
|
||||
case 4: // nsfl
|
||||
tmp = cfg.enable_nsfl ? `items.id in (select item_id from tags_assign where tag_id = ${parseInt(cfg.nsfl_tag_id, 10) || 3})` : "1 = 0";
|
||||
break;
|
||||
default: // sfw
|
||||
tmp = "items.id in (select item_id from tags_assign where tag_id = 1)";
|
||||
break;
|
||||
}
|
||||
return tmp;
|
||||
};
|
||||
createID() {
|
||||
return crypto.randomBytes(16).toString("hex") + Date.now().toString(24);
|
||||
};
|
||||
generateToken() {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
};
|
||||
genLink(env) {
|
||||
const link = [];
|
||||
if (env.tag) link.push("tag", env.tag);
|
||||
if (env.hall) link.push("h", env.hall);
|
||||
if (env.user) link.push("user", env.user, env.type ?? 'uploads');
|
||||
|
||||
let tmp = link.length === 0 ? '/' : link.join('/');
|
||||
if (!tmp.endsWith('/'))
|
||||
tmp = tmp + '/';
|
||||
if (!tmp.startsWith('/'))
|
||||
tmp = '/' + tmp;
|
||||
|
||||
// Build suffix with query params
|
||||
let suffix = env.strict ? '?strict=1' : '';
|
||||
|
||||
return {
|
||||
main: tmp,
|
||||
path: env.path ? env.path : '',
|
||||
suffix: suffix
|
||||
};
|
||||
};
|
||||
parseTag(tag) {
|
||||
if (!tag)
|
||||
return null;
|
||||
return decodeURIComponent(tag);
|
||||
}
|
||||
slugify(str) {
|
||||
if (!str) return "";
|
||||
return str.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
// Escape ILIKE wildcard characters in user-supplied strings
|
||||
escapeLike(str) {
|
||||
if (!str) return str;
|
||||
return str.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
||||
}
|
||||
|
||||
// async funcs
|
||||
async countf0cks() {
|
||||
const tagged = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where id in (select item_id from tags_assign group by item_id) and active = true
|
||||
`)[0].total;
|
||||
const untagged = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where not exists (select 1 from tags_assign where item_id = items.id) and active = true
|
||||
`)[0].total;
|
||||
const sfw = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true
|
||||
`)[0].total;
|
||||
const nsfw = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true
|
||||
`)[0].total;
|
||||
const nsfl = cfg.enable_nsfl ? +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true
|
||||
`)[0].total : 0;
|
||||
const deleted = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where active = false and is_deleted = true
|
||||
`)[0].total;
|
||||
const pending = +(await db`
|
||||
select count(*) as total
|
||||
from "items"
|
||||
where active = false and is_deleted = false
|
||||
`)[0].total;
|
||||
const lastf0ck = +(await db`
|
||||
select max(id) as id
|
||||
from "items"
|
||||
`)[0].id;
|
||||
return {
|
||||
tagged,
|
||||
untagged,
|
||||
total: tagged + untagged,
|
||||
deleted,
|
||||
pending,
|
||||
untracked: lastf0ck - (tagged + untagged + deleted + pending),
|
||||
sfw,
|
||||
nsfw,
|
||||
nsfl: cfg.enable_nsfl ? nsfl : 0,
|
||||
};
|
||||
};
|
||||
async hash(str) {
|
||||
const salt = crypto.randomBytes(16).toString("hex");
|
||||
const derivedKey = await scrypt(str, salt, 64);
|
||||
return "$f0ck$" + salt + ":" + derivedKey.toString("hex");
|
||||
};
|
||||
async verify(str, hash) {
|
||||
const [salt, key] = hash.substring(6).split(":");
|
||||
const keyBuffer = Buffer.from(key, "hex");
|
||||
const derivedKey = await scrypt(str, salt, 64);
|
||||
return crypto.timingSafeEqual(keyBuffer, derivedKey);
|
||||
};
|
||||
async getTags(itemid) {
|
||||
const tags = await db`
|
||||
select "tags".id, "tags".tag, "tags".normalized, "user".user, uo.display_name
|
||||
from "tags_assign"
|
||||
left join "tags" on "tags".id = "tags_assign".tag_id
|
||||
left join "user" on "user".id = "tags_assign".user_id
|
||||
left join user_options uo on uo.user_id = "user".id
|
||||
where "tags_assign".item_id = ${+itemid}
|
||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".id = ${cfg.nsfl_tag_id || 3} then 2 else 3 end) asc, "tags".id asc
|
||||
`;
|
||||
for (let t = 0; t < tags.length; t++) {
|
||||
tags[t].badge = this.getBadge(tags[t]);
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
getBadge(tagObj) {
|
||||
if (tagObj.tag.startsWith(">"))
|
||||
return "badge-greentext badge-light";
|
||||
else if (tagObj.normalized === "ukraine")
|
||||
return "badge-ukraine badge-light";
|
||||
else if (/[а-яё]/.test(tagObj.normalized) || tagObj.normalized === "russia")
|
||||
return "badge-russia badge-light";
|
||||
else if (tagObj.normalized === "german")
|
||||
return "badge-german badge-light";
|
||||
else if (tagObj.normalized === "dutch")
|
||||
return "badge-dutch badge-light";
|
||||
else if (tagObj.normalized === "sfw")
|
||||
return "badge-success";
|
||||
else if (tagObj.normalized === "nsfw")
|
||||
return "badge-danger";
|
||||
else if (tagObj.normalized === "nsfl")
|
||||
return cfg.enable_nsfl ? "badge-nsfl" : "badge-light";
|
||||
else
|
||||
return "badge-light";
|
||||
};
|
||||
async hasTag(itemid, tagid) {
|
||||
const tag = (await db`
|
||||
select *
|
||||
from "tags_assign"
|
||||
where
|
||||
item_id = ${+itemid} and
|
||||
tag_id = ${+tagid}
|
||||
limit 1
|
||||
`).length;
|
||||
return !!tag;
|
||||
};
|
||||
// detectNSFW: removed — contained shell injection via exec() with unescaped `dest` parameter.
|
||||
// If re-implemented, use execFile() with argument arrays and a dedicated Python script.
|
||||
async getDefaultAvatar() {
|
||||
return (await db`
|
||||
select column_default as avatar
|
||||
from "information_schema"."columns"
|
||||
where
|
||||
TABLE_SCHEMA='public' and
|
||||
TABLE_NAME='user_options' and
|
||||
COLUMN_NAME = 'avatar'
|
||||
`)[0].avatar;
|
||||
};
|
||||
|
||||
// meddlware admin
|
||||
async auth(req, res, next) {
|
||||
if (!req.session || !req.session.admin) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: "401 - Unauthorized"
|
||||
});
|
||||
}
|
||||
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
// meddlware user
|
||||
async userauth(req, res, next) {
|
||||
if (!req.session) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: "401 - Unauthorized"
|
||||
});
|
||||
}
|
||||
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout' && req.url.pathname !== '/settings') {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
async loggedin(req, res, next) {
|
||||
if (!req.session) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: "401 - Unauthorized"
|
||||
});
|
||||
}
|
||||
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
async modAuth(req, res, next) {
|
||||
if (!req.session || (!req.session.admin && !req.session.is_moderator)) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: "401 - Unauthorized"
|
||||
});
|
||||
}
|
||||
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
async adminAuth(req, res, next) {
|
||||
if (!req.session || !req.session.admin) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: "401 - Unauthorized"
|
||||
});
|
||||
}
|
||||
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
getCookieOptions(expires = null, httpOnly = true) {
|
||||
const isSecure = cfg.main.url.full && cfg.main.url.full.startsWith('https');
|
||||
let options = "Path=/; SameSite=Lax";
|
||||
if (httpOnly) options += "; HttpOnly";
|
||||
if (isSecure) options += "; Secure";
|
||||
if (expires) {
|
||||
if (typeof expires === 'number') {
|
||||
options += `; Max-Age=${expires}`;
|
||||
} else {
|
||||
options += `; Expires=${expires}`;
|
||||
}
|
||||
}
|
||||
this.debug(`[COOKIE DEBUG] full=${cfg.main.url.full}, isSecure=${isSecure}, options=${options}`);
|
||||
return options;
|
||||
}
|
||||
|
||||
debug(...args) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
logError(err, context = "Internal Error") {
|
||||
const errId = crypto.randomUUID();
|
||||
console.error(`[ERROR REF ${errId}] ${context}:`, err);
|
||||
return `Internal Error. Reference: ${errId}`;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user