Fix shows/bot API auth gaps, handle missing channels as 404, make recurrence DST-safe, and clear lint regressions

This commit is contained in:
Speng Reb 2026-05-21 16:13:56 +02:00
parent e3dd961430
commit 12696452aa
5 changed files with 97 additions and 11 deletions

View file

@ -39,7 +39,7 @@ class Database {
database: Config.get('mysql.database'),
multipleStatements: true, // Legacy thing
charset: 'utf8mb4'
}
};
} else {
connection = {
host: Config.get('mysql.server'),
@ -49,7 +49,7 @@ class Database {
database: Config.get('mysql.database'),
multipleStatements: true, // Legacy thing
charset: 'utf8mb4'
}
};
}
knexConfig = {

View file

@ -22,15 +22,93 @@ function makeSystemProxy(name) {
function computeNextRunAt(show) {
const base = Number(show.next_run_at || show.scheduled_for || Date.now());
if (show.recurrence === 'daily') {
return base + 24 * 60 * 60 * 1000;
const recurrence = show.recurrence;
const timezone = show.timezone || 'UTC';
if (recurrence !== 'daily' && recurrence !== 'weekly') {
return base;
}
if (show.recurrence === 'weekly') {
return base + 7 * 24 * 60 * 60 * 1000;
const daysToAdd = recurrence === 'weekly' ? 7 : 1;
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) {

View file

@ -26,8 +26,11 @@ async function getChannelEmotes(channelId) {
(err, rows) => {
if (err) return reject(new Error(err));
if (!rows || rows.length === 0) return resolve([]);
try { resolve(JSON.parse(rows[0].value)); }
catch (e) { resolve([]); }
try {
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' });
}
const old = emotes[idx];
emotes[idx] = validated;
await saveChannelEmotes(req.bot.channel_id, emotes);

View file

@ -63,6 +63,7 @@ async function getChannelRow(channelName) {
return new Promise((resolve, reject) => {
db.channels.lookup(channelName, (err, row) => {
if (err) reject(new Error(err));
else if (!row) reject(new Error('Channel not found'));
else resolve(row);
});
});

View file

@ -138,6 +138,11 @@ async function authorizeChannel(req, res) {
return null;
}
if (bot.rank < 2) {
res.status(403).json({ error: 'Insufficient rank' });
return null;
}
return {
actorName: bot.name,
rank: bot.rank,