mirror of
https://github.com/Spengreb/sync.git
synced 2026-06-10 07:12:05 +00:00
Merge pull request #14 from Spengreb/broadspectrum-code-analysis
Update to node 20 and jQuery 3
This commit is contained in:
commit
2788dae3c8
11 changed files with 166 additions and 24 deletions
|
|
@ -39,7 +39,7 @@ class Database {
|
||||||
database: Config.get('mysql.database'),
|
database: Config.get('mysql.database'),
|
||||||
multipleStatements: true, // Legacy thing
|
multipleStatements: true, // Legacy thing
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4'
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
connection = {
|
connection = {
|
||||||
host: Config.get('mysql.server'),
|
host: Config.get('mysql.server'),
|
||||||
|
|
@ -49,9 +49,9 @@ class Database {
|
||||||
database: Config.get('mysql.database'),
|
database: Config.get('mysql.database'),
|
||||||
multipleStatements: true, // Legacy thing
|
multipleStatements: true, // Legacy thing
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4'
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
knexConfig = {
|
knexConfig = {
|
||||||
client: 'mysql',
|
client: 'mysql',
|
||||||
connection,
|
connection,
|
||||||
|
|
|
||||||
88
src/shows.js
88
src/shows.js
|
|
@ -22,15 +22,93 @@ function makeSystemProxy(name) {
|
||||||
|
|
||||||
function computeNextRunAt(show) {
|
function computeNextRunAt(show) {
|
||||||
const base = Number(show.next_run_at || show.scheduled_for || Date.now());
|
const base = Number(show.next_run_at || show.scheduled_for || Date.now());
|
||||||
if (show.recurrence === 'daily') {
|
const recurrence = show.recurrence;
|
||||||
return base + 24 * 60 * 60 * 1000;
|
const timezone = show.timezone || 'UTC';
|
||||||
|
|
||||||
|
if (recurrence !== 'daily' && recurrence !== 'weekly') {
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show.recurrence === 'weekly') {
|
const daysToAdd = recurrence === 'weekly' ? 7 : 1;
|
||||||
return base + 7 * 24 * 60 * 60 * 1000;
|
const source = new Date(base);
|
||||||
|
const local = toZonedParts(source, timezone);
|
||||||
|
if (!local) {
|
||||||
|
return base + (daysToAdd * 24 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return base;
|
const targetDate = addDaysUTC(local.year, local.month, local.day, daysToAdd);
|
||||||
|
const zonedTarget = {
|
||||||
|
year: targetDate.year,
|
||||||
|
month: targetDate.month,
|
||||||
|
day: targetDate.day,
|
||||||
|
hour: local.hour,
|
||||||
|
minute: local.minute,
|
||||||
|
second: local.second
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = zonedDateTimeToUtc(zonedTarget, timezone);
|
||||||
|
return next || (base + (daysToAdd * 24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toZonedParts(date, timezone) {
|
||||||
|
try {
|
||||||
|
const dtf = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: timezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
const parts = dtf.formatToParts(date);
|
||||||
|
const out = {};
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.type === 'literal') continue;
|
||||||
|
out[part.type] = parseInt(part.value, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: out.year,
|
||||||
|
month: out.month,
|
||||||
|
day: out.day,
|
||||||
|
hour: out.hour,
|
||||||
|
minute: out.minute,
|
||||||
|
second: out.second
|
||||||
|
};
|
||||||
|
} catch (_err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDaysUTC(year, month, day, days) {
|
||||||
|
const d = new Date(Date.UTC(year, month - 1, day + days));
|
||||||
|
return {
|
||||||
|
year: d.getUTCFullYear(),
|
||||||
|
month: d.getUTCMonth() + 1,
|
||||||
|
day: d.getUTCDate()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function zonedDateTimeToUtc(local, timezone) {
|
||||||
|
let guess = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute, local.second);
|
||||||
|
|
||||||
|
// Iterate to resolve timezone offset for the target wall-clock time (handles DST shifts).
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const zoned = toZonedParts(new Date(guess), timezone);
|
||||||
|
if (!zoned) return null;
|
||||||
|
|
||||||
|
const desired = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute, local.second);
|
||||||
|
const current = Date.UTC(zoned.year, zoned.month - 1, zoned.day, zoned.hour, zoned.minute, zoned.second);
|
||||||
|
const delta = desired - current;
|
||||||
|
if (delta === 0) {
|
||||||
|
return guess;
|
||||||
|
}
|
||||||
|
guess += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return guess;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePlaylist(rawPlaylist) {
|
function normalizePlaylist(rawPlaylist) {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,13 @@ exports.init = function csrfInit (domain) {
|
||||||
var secret = req.signedCookies._csrf;
|
var secret = req.signedCookies._csrf;
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
secret = tokens.secretSync();
|
secret = tokens.secretSync();
|
||||||
|
const secure = req.realProtocol === 'https' || req.secure === true;
|
||||||
res.cookie("_csrf", secret, {
|
res.cookie("_csrf", secret, {
|
||||||
domain: domain,
|
domain: domain,
|
||||||
signed: true,
|
signed: true,
|
||||||
httpOnly: true
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +40,9 @@ exports.init = function csrfInit (domain) {
|
||||||
|
|
||||||
exports.verify = function csrfVerify(req) {
|
exports.verify = function csrfVerify(req) {
|
||||||
var secret = req.signedCookies._csrf;
|
var secret = req.signedCookies._csrf;
|
||||||
var token = req.body._csrf || req.query._csrf;
|
var token = (req.body && req.body._csrf) ||
|
||||||
|
(req.query && req.query._csrf) ||
|
||||||
|
req.header('x-csrf-token');
|
||||||
|
|
||||||
if (!tokens.verify(secret, token)) {
|
if (!tokens.verify(secret, token)) {
|
||||||
throw new CSRFError('Invalid CSRF token');
|
throw new CSRFError('Invalid CSRF token');
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,13 @@ export function ipSessionCookieMiddleware(req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
|
const secure = req.realProtocol === 'https' || req.secure === true;
|
||||||
res.cookie('ip-session', createIPSessionCookie(req.realIP, firstSeen), {
|
res.cookie('ip-session', createIPSessionCookie(req.realIP, firstSeen), {
|
||||||
signed: true,
|
signed: true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
expires: NO_EXPIRATION
|
expires: NO_EXPIRATION,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,11 @@ async function getChannelEmotes(channelId) {
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (err) return reject(new Error(err));
|
if (err) return reject(new Error(err));
|
||||||
if (!rows || rows.length === 0) return resolve([]);
|
if (!rows || rows.length === 0) return resolve([]);
|
||||||
try { resolve(JSON.parse(rows[0].value)); }
|
try {
|
||||||
catch (e) { resolve([]); }
|
resolve(JSON.parse(rows[0].value));
|
||||||
|
} catch (e) {
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -92,7 +95,6 @@ router.put('/:name', botAuth, requireRank(4), async (req, res) => {
|
||||||
return res.status(409).json({ error: 'An emote with that name already exists' });
|
return res.status(409).json({ error: 'An emote with that name already exists' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const old = emotes[idx];
|
|
||||||
emotes[idx] = validated;
|
emotes[idx] = validated;
|
||||||
await saveChannelEmotes(req.bot.channel_id, emotes);
|
await saveChannelEmotes(req.bot.channel_id, emotes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const csrf = require('../../csrf');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
function isMutatingMethod(method) {
|
||||||
|
return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBotBearerToken(req) {
|
||||||
|
const authHeader = req.headers && req.headers.authorization;
|
||||||
|
return typeof authHeader === 'string' && /^Bearer\s+cbt_/.test(authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
if (!isMutatingMethod(req.method) || hasBotBearerToken(req)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
csrf.verify(req);
|
||||||
|
next();
|
||||||
|
} catch (_err) {
|
||||||
|
return res.status(403).json({ error: 'Invalid CSRF token' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.use('/channels/:channel/bots', require('./bots'));
|
router.use('/channels/:channel/bots', require('./bots'));
|
||||||
router.use('/channels/:channel/emotes', require('./emotes'));
|
router.use('/channels/:channel/emotes', require('./emotes'));
|
||||||
router.use('/channels/:channel/playlist', require('./playlist'));
|
router.use('/channels/:channel/playlist', require('./playlist'));
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ async function getChannelRow(channelName) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.channels.lookup(channelName, (err, row) => {
|
db.channels.lookup(channelName, (err, row) => {
|
||||||
if (err) reject(new Error(err));
|
if (err) reject(new Error(err));
|
||||||
|
else if (!row) reject(new Error('Channel not found'));
|
||||||
else resolve(row);
|
else resolve(row);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,11 @@ async function authorizeChannel(req, res) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bot.rank < 2) {
|
||||||
|
res.status(403).json({ error: 'Insufficient rank' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actorName: bot.name,
|
actorName: bot.name,
|
||||||
rank: bot.rank,
|
rank: bot.rank,
|
||||||
|
|
|
||||||
|
|
@ -241,21 +241,23 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
setAuthCookie: function setAuthCookie(req, res, expiration, auth) {
|
setAuthCookie: function setAuthCookie(req, res, expiration, auth) {
|
||||||
|
const secure = req.realProtocol === 'https' || req.secure === true;
|
||||||
|
const baseCookieOptions = {
|
||||||
|
expires: expiration,
|
||||||
|
httpOnly: true,
|
||||||
|
signed: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure
|
||||||
|
};
|
||||||
|
|
||||||
if (req.hostname.indexOf(Config.get("http.root-domain")) >= 0) {
|
if (req.hostname.indexOf(Config.get("http.root-domain")) >= 0) {
|
||||||
// Prevent non-root cookie from screwing things up
|
// Prevent non-root cookie from screwing things up
|
||||||
res.clearCookie("auth");
|
res.clearCookie("auth");
|
||||||
res.cookie("auth", auth, {
|
res.cookie("auth", auth, Object.assign({}, baseCookieOptions, {
|
||||||
domain: Config.get("http.root-domain-dotted"),
|
domain: Config.get("http.root-domain-dotted"),
|
||||||
expires: expiration,
|
}));
|
||||||
httpOnly: true,
|
|
||||||
signed: true
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
res.cookie("auth", auth, {
|
res.cookie("auth", auth, baseCookieOptions);
|
||||||
expires: expiration,
|
|
||||||
httpOnly: true,
|
|
||||||
signed: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@ mixin head()
|
||||||
var DEFAULT_THEME = '#{DEFAULT_THEME}';
|
var DEFAULT_THEME = '#{DEFAULT_THEME}';
|
||||||
var CHANNELPATH = '#{channelPath}';
|
var CHANNELPATH = '#{channelPath}';
|
||||||
var CHANNELNAME = '#{channelName}';
|
var CHANNELNAME = '#{channelName}';
|
||||||
|
var CSRF_TOKEN = '#{csrfToken}';
|
||||||
else
|
else
|
||||||
script(type="text/javascript").
|
script(type="text/javascript").
|
||||||
var DEFAULT_THEME = '#{DEFAULT_THEME}';
|
var DEFAULT_THEME = '#{DEFAULT_THEME}';
|
||||||
var CHANNELPATH = '#{channelPath}';
|
var CHANNELPATH = '#{channelPath}';
|
||||||
|
var CSRF_TOKEN = '#{csrfToken}';
|
||||||
|
|
||||||
script(src="/js/theme.js")
|
script(src="/js/theme.js")
|
||||||
//[if lt IE 9]
|
//[if lt IE 9]
|
||||||
|
|
|
||||||
21
www/js/ui.js
21
www/js/ui.js
|
|
@ -1257,6 +1257,27 @@ $("#resize-video-smaller").on('click', function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$.ajaxPrefilter(function (options, _originalOptions, _jqXHR) {
|
||||||
|
var url = String(options.url || '');
|
||||||
|
if (!/\/api\/v1\//.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var method = String(options.type || options.method || 'GET').toUpperCase();
|
||||||
|
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headers = options.headers || {};
|
||||||
|
if (options.headers.Authorization || options.headers.authorization) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof CSRF_TOKEN === 'string' && CSRF_TOKEN.length > 0) {
|
||||||
|
options.headers['X-CSRF-Token'] = CSRF_TOKEN;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var CSTBots = (function () {
|
var CSTBots = (function () {
|
||||||
function apiBase() {
|
function apiBase() {
|
||||||
return '/api/v1/channels/' + CHANNEL.name;
|
return '/api/v1/channels/' + CHANNEL.name;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue