mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
feat(api): add rate-limiting and /clear-guild-cache (#198)
This commit is contained in:
parent
57d83699d5
commit
a4fd37d71c
7 changed files with 138 additions and 12 deletions
50
packages/api/handlers/clear-guild-cache.ts
Normal file
50
packages/api/handlers/clear-guild-cache.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { UserGuildPermissions } from '@roleypoly/types';
|
||||
import { isRoot, withSession } from '../utils/api-tools';
|
||||
import { getGuild, GuildRateLimiterKey, useGuildRateLimiter } from '../utils/guild';
|
||||
import {
|
||||
lowPermissions,
|
||||
missingParameters,
|
||||
notFound,
|
||||
ok,
|
||||
rateLimited,
|
||||
} from '../utils/responses';
|
||||
|
||||
export const ClearGuildCache = withSession(
|
||||
(session) => async (request: Request): Promise<Response> => {
|
||||
const url = new URL(request.url);
|
||||
const [, , guildID] = url.pathname.split('/');
|
||||
if (!guildID) {
|
||||
return missingParameters();
|
||||
}
|
||||
|
||||
const rateLimit = useGuildRateLimiter(
|
||||
guildID,
|
||||
GuildRateLimiterKey.cacheClear,
|
||||
60 * 5
|
||||
); // 5 minute RL TTL, 288 times per day.
|
||||
|
||||
if (!isRoot(session.user.id)) {
|
||||
const guild = session.guilds.find((guild) => guild.id === guildID);
|
||||
if (!guild) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (
|
||||
guild?.permissionLevel !== UserGuildPermissions.Manager &&
|
||||
guild?.permissionLevel !== UserGuildPermissions.Admin
|
||||
) {
|
||||
return lowPermissions();
|
||||
}
|
||||
|
||||
if (await rateLimit()) {
|
||||
return rateLimited();
|
||||
}
|
||||
}
|
||||
|
||||
const result = await getGuild(guildID, { skipCachePull: true });
|
||||
if (!result) {
|
||||
return notFound();
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
);
|
|
@ -3,20 +3,32 @@ import {
|
|||
GuildData as GuildDataT,
|
||||
UserGuildPermissions,
|
||||
} from '@roleypoly/types';
|
||||
import { isRoot, respond, withSession } from '../utils/api-tools';
|
||||
import { isRoot, withSession } from '../utils/api-tools';
|
||||
import { GuildRateLimiterKey, useGuildRateLimiter } from '../utils/guild';
|
||||
import { fetchLegacyServer, transformLegacyGuild } from '../utils/import-from-legacy';
|
||||
import { GuildData } from '../utils/kv';
|
||||
|
||||
const noPermissions = () =>
|
||||
respond({ error: 'no permissions to this guild' }, { status: 403 });
|
||||
const notFound = () => respond({ error: 'guild not found' }, { status: 404 });
|
||||
const alreadyImported = () =>
|
||||
respond({ error: 'guild already imported' }, { status: 400 });
|
||||
import {
|
||||
conflict,
|
||||
lowPermissions,
|
||||
missingParameters,
|
||||
notFound,
|
||||
ok,
|
||||
rateLimited,
|
||||
} from '../utils/responses';
|
||||
|
||||
export const SyncFromLegacy = withSession(
|
||||
(session) => async (request: Request): Promise<Response> => {
|
||||
const url = new URL(request.url);
|
||||
const [, , guildID] = url.pathname.split('/');
|
||||
if (!guildID) {
|
||||
return missingParameters();
|
||||
}
|
||||
|
||||
const rateLimit = useGuildRateLimiter(
|
||||
guildID,
|
||||
GuildRateLimiterKey.legacyImport,
|
||||
60 * 20
|
||||
); // 20 minute RL TTL, 72 times per day.
|
||||
|
||||
// Allow root users to trigger this too, just in case.
|
||||
if (!isRoot(session.user.id)) {
|
||||
|
@ -26,10 +38,14 @@ export const SyncFromLegacy = withSession(
|
|||
}
|
||||
|
||||
if (
|
||||
guild?.permissionLevel !== UserGuildPermissions.Manager ||
|
||||
guild?.permissionLevel !== UserGuildPermissions.Manager &&
|
||||
guild?.permissionLevel !== UserGuildPermissions.Admin
|
||||
) {
|
||||
return noPermissions();
|
||||
return lowPermissions();
|
||||
}
|
||||
|
||||
if (await rateLimit()) {
|
||||
return rateLimited();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +60,7 @@ export const SyncFromLegacy = withSession(
|
|||
checkGuild &&
|
||||
(checkGuild.features & Features.LegacyGuild) === Features.LegacyGuild
|
||||
) {
|
||||
return alreadyImported();
|
||||
return conflict();
|
||||
}
|
||||
|
||||
const legacyGuild = await fetchLegacyServer(guildID);
|
||||
|
@ -55,6 +71,6 @@ export const SyncFromLegacy = withSession(
|
|||
const newGuildData = transformLegacyGuild(legacyGuild);
|
||||
await GuildData.put(guildID, newGuildData);
|
||||
|
||||
return respond({ ok: true });
|
||||
return ok();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BotJoin } from './handlers/bot-join';
|
||||
import { ClearGuildCache } from './handlers/clear-guild-cache';
|
||||
import { CreateRoleypolyData } from './handlers/create-roleypoly-data';
|
||||
import { GetPickerData } from './handlers/get-picker-data';
|
||||
import { GetSession } from './handlers/get-session';
|
||||
|
@ -28,6 +29,7 @@ router.add('GET', 'get-slug', GetSlug);
|
|||
router.add('GET', 'get-picker-data', GetPickerData);
|
||||
router.add('PATCH', 'update-roles', UpdateRoles);
|
||||
router.add('POST', 'sync-from-legacy', SyncFromLegacy);
|
||||
router.add('POST', 'clear-guild-cache', ClearGuildCache);
|
||||
|
||||
// Root users only
|
||||
router.add('GET', 'x-create-roleypoly-data', CreateRoleypolyData);
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { AuthType, cacheLayer, discordFetch } from './api-tools';
|
||||
import { botClientID, botToken } from './config';
|
||||
import { GuildData, Guilds } from './kv';
|
||||
import { useRateLimiter } from './rate-limiting';
|
||||
|
||||
type APIGuild = {
|
||||
// Only relevant stuff
|
||||
|
@ -161,3 +162,14 @@ const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: numbe
|
|||
|
||||
return safety;
|
||||
};
|
||||
|
||||
export enum GuildRateLimiterKey {
|
||||
legacyImport = 'legacyImport',
|
||||
cacheClear = 'cacheClear',
|
||||
}
|
||||
|
||||
export const useGuildRateLimiter = (
|
||||
guildID: string,
|
||||
key: GuildRateLimiterKey,
|
||||
timeoutSeconds: number
|
||||
) => useRateLimiter(Guilds, `guilds/${guildID}/rate-limit/${key}`, timeoutSeconds);
|
||||
|
|
15
packages/api/utils/rate-limiting.ts
Normal file
15
packages/api/utils/rate-limiting.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { WrappedKVNamespace } from './kv';
|
||||
|
||||
export const useRateLimiter = (
|
||||
kv: WrappedKVNamespace,
|
||||
key: string,
|
||||
timeoutSeconds: number
|
||||
) => async (): Promise<boolean> => {
|
||||
const value = await kv.get<boolean>(key);
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await kv.put(key, true, timeoutSeconds);
|
||||
return false;
|
||||
};
|
16
packages/api/utils/responses.ts
Normal file
16
packages/api/utils/responses.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { respond } from './api-tools';
|
||||
|
||||
export const ok = () => respond({ ok: true });
|
||||
|
||||
export const missingParameters = () =>
|
||||
respond({ error: 'missing parameters' }, { status: 400 });
|
||||
|
||||
export const lowPermissions = () =>
|
||||
respond({ error: 'no permissions for this action' }, { status: 403 });
|
||||
|
||||
export const notFound = () => respond({ error: 'not found' }, { status: 404 });
|
||||
|
||||
export const conflict = () => respond({ error: 'conflict' }, { status: 409 });
|
||||
|
||||
export const rateLimited = () =>
|
||||
respond({ error: 'rate limit hit, enhance your calm' }, { status: 419 });
|
|
@ -171,10 +171,23 @@ const rebuild = () =>
|
|||
|
||||
const watcher = chokidar.watch(path.resolve(__dirname, basePath), {
|
||||
ignoreInitial: true,
|
||||
ignore: '**/{dist,node_modules}',
|
||||
ignore: '**/dist',
|
||||
});
|
||||
|
||||
let currentlyRebuilding = false;
|
||||
|
||||
watcher.on('all', async (type, path) => {
|
||||
if (path.includes('node_modules') || path.includes('dist')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentlyRebuilding) {
|
||||
console.info('change skipped...', { type, path });
|
||||
return;
|
||||
}
|
||||
|
||||
currentlyRebuilding = true;
|
||||
|
||||
if (path.includes('dist')) {
|
||||
return;
|
||||
}
|
||||
|
@ -183,6 +196,8 @@ watcher.on('all', async (type, path) => {
|
|||
|
||||
await rebuild();
|
||||
reload();
|
||||
|
||||
currentlyRebuilding = false;
|
||||
});
|
||||
|
||||
fork(async () => {
|
||||
|
|
Loading…
Add table
Reference in a new issue