From 31ea2e2183b3dd511666062fb14377b81c36c3ae Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sat, 17 Jul 2021 19:07:10 -0400 Subject: [PATCH] refactor(api): asEditor instead of copy-pasted admin/manager/root check --- packages/api/handlers/clear-guild-cache.ts | 51 ++---------- .../api/handlers/create-roleypoly-data.ts | 83 ------------------- packages/api/handlers/sync-from-legacy.ts | 70 ++-------------- packages/api/handlers/update-guild.ts | 41 ++------- packages/api/utils/guild.ts | 83 ++++++++++++++++++- 5 files changed, 105 insertions(+), 223 deletions(-) delete mode 100644 packages/api/handlers/create-roleypoly-data.ts diff --git a/packages/api/handlers/clear-guild-cache.ts b/packages/api/handlers/clear-guild-cache.ts index 0649b87..b122ad2 100644 --- a/packages/api/handlers/clear-guild-cache.ts +++ b/packages/api/handlers/clear-guild-cache.ts @@ -1,51 +1,18 @@ -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'; +import { asEditor, getGuild, GuildRateLimiterKey } from '../utils/guild'; +import { notFound, ok } from '../utils/responses'; -export const ClearGuildCache = withSession( - (session) => +export const ClearGuildCache = asEditor( + { + rateLimitKey: GuildRateLimiterKey.cacheClear, + rateLimitTimeoutSeconds: 60 * 5, + }, + (session, { guildID }) => async (request: Request): Promise => { - 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(); } ); diff --git a/packages/api/handlers/create-roleypoly-data.ts b/packages/api/handlers/create-roleypoly-data.ts deleted file mode 100644 index 23853c2..0000000 --- a/packages/api/handlers/create-roleypoly-data.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CategoryType, Features, GuildData as GuildDataT } from '@roleypoly/types'; -import KSUID from 'ksuid'; -import { onlyRootUsers, respond } from '../utils/api-tools'; -import { GuildData } from '../utils/kv'; - -// Temporary use. -export const CreateRoleypolyData = onlyRootUsers( - async (request: Request): Promise => { - const data: GuildDataT = { - id: '386659935687147521', - message: - 'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3', - features: Features.Preview, - auditLogWebhook: null, - categories: [ - { - id: KSUID.randomSync().string, - name: 'Demo Roles', - type: CategoryType.Multi, - hidden: false, - position: 0, - roles: [ - '557825026406088717', - '557824994269200384', - '557824893241131029', - '557812915386843170', - '557812901717737472', - '557812805546541066', - ], - }, - { - id: KSUID.randomSync().string, - name: 'Colors', - type: CategoryType.Single, - hidden: false, - position: 1, - roles: ['394060232893923349', '394060145799331851', '394060192846839809'], - }, - { - id: KSUID.randomSync().string, - name: 'Test Roles', - type: CategoryType.Multi, - hidden: false, - position: 5, - roles: ['558104828216213505', '558103534453653514', '558297233582194728'], - }, - { - id: KSUID.randomSync().string, - name: 'Region', - type: CategoryType.Multi, - hidden: false, - position: 3, - roles: [ - '397296181803483136', - '397296137066774529', - '397296218809827329', - '397296267283267605', - ], - }, - { - id: KSUID.randomSync().string, - name: 'Opt-in Channels', - type: CategoryType.Multi, - hidden: false, - position: 4, - roles: ['414514823959674890', '764230661904007219'], - }, - { - id: KSUID.randomSync().string, - name: 'Pronouns', - type: CategoryType.Multi, - hidden: false, - position: 2, - roles: ['485916566790340608', '485916566941335583', '485916566311927808'], - }, - ], - }; - - await GuildData.put(data.id, data); - - return respond({ ok: true }); - } -); diff --git a/packages/api/handlers/sync-from-legacy.ts b/packages/api/handlers/sync-from-legacy.ts index 3752e3f..0fbd080 100644 --- a/packages/api/handlers/sync-from-legacy.ts +++ b/packages/api/handlers/sync-from-legacy.ts @@ -1,69 +1,15 @@ -import { - Features, - GuildData as GuildDataT, - UserGuildPermissions, -} from '@roleypoly/types'; -import { isRoot, withSession } from '../utils/api-tools'; -import { GuildRateLimiterKey, useGuildRateLimiter } from '../utils/guild'; +import { asEditor, GuildRateLimiterKey } from '../utils/guild'; import { fetchLegacyServer, transformLegacyGuild } from '../utils/import-from-legacy'; import { GuildData } from '../utils/kv'; -import { - conflict, - lowPermissions, - missingParameters, - notFound, - ok, - rateLimited, -} from '../utils/responses'; +import { notFound, ok } from '../utils/responses'; -export const SyncFromLegacy = withSession( - (session) => +export const SyncFromLegacy = asEditor( + { + rateLimitKey: GuildRateLimiterKey.legacyImport, + rateLimitTimeoutSeconds: 60 * 20, + }, + (session, { guildID }) => async (request: Request): Promise => { - 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)) { - 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 shouldForce = url.searchParams.get('force') === 'yes'; - - // Not using getGuildData as we want null feedback, not a zeroed out object. - const checkGuild = await GuildData.get(guildID); - // Don't force, and guild exists in our side, but LegacyGuild flag is set, - // fail this request. - if ( - !shouldForce && - checkGuild && - (checkGuild.features & Features.LegacyGuild) === Features.LegacyGuild - ) { - return conflict(); - } - const legacyGuild = await fetchLegacyServer(guildID); if (!legacyGuild) { return notFound(); diff --git a/packages/api/handlers/update-guild.ts b/packages/api/handlers/update-guild.ts index c4985b8..d04d56e 100644 --- a/packages/api/handlers/update-guild.ts +++ b/packages/api/handlers/update-guild.ts @@ -1,44 +1,15 @@ import { sendAuditLog, validateAuditLogWebhook } from '@roleypoly/api/utils/audit-log'; -import { - GuildDataUpdate, - SessionData, - UserGuildPermissions, - WebhookValidationStatus, -} from '@roleypoly/types'; -import { withSession } from '../utils/api-tools'; -import { getGuildData } from '../utils/guild'; +import { GuildDataUpdate, WebhookValidationStatus } from '@roleypoly/types'; +import { asEditor, getGuildData } from '../utils/guild'; import { GuildData } from '../utils/kv'; -import { - invalid, - lowPermissions, - missingParameters, - notFound, - ok, -} from '../utils/responses'; +import { invalid, ok } from '../utils/responses'; -export const UpdateGuild = withSession( - (session: SessionData) => +export const UpdateGuild = asEditor( + {}, + (session, { guildID, guild }) => async (request: Request): Promise => { - const url = new URL(request.url); - const [, , guildID] = url.pathname.split('/'); - if (!guildID) { - return missingParameters(); - } - const guildUpdate = (await request.json()) as GuildDataUpdate; - const guild = session.guilds.find((guild) => guild.id === guildID); - if (!guild) { - return notFound(); - } - - if ( - guild?.permissionLevel !== UserGuildPermissions.Manager && - guild?.permissionLevel !== UserGuildPermissions.Admin - ) { - return lowPermissions(); - } - const oldGuildData = await getGuildData(guildID); const newGuildData = { ...oldGuildData, diff --git a/packages/api/utils/guild.ts b/packages/api/utils/guild.ts index 0f37438..cfefd7d 100644 --- a/packages/api/utils/guild.ts +++ b/packages/api/utils/guild.ts @@ -1,13 +1,23 @@ +import { Handler } from '@roleypoly/api/router'; +import { + lowPermissions, + missingParameters, + notFound, + rateLimited, +} from '@roleypoly/api/utils/responses'; import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission'; import { Features, Guild, GuildData as GuildDataT, + GuildSlug, OwnRoleInfo, Role, RoleSafety, + SessionData, + UserGuildPermissions, } from '@roleypoly/types'; -import { AuthType, cacheLayer, discordFetch } from './api-tools'; +import { AuthType, cacheLayer, discordFetch, isRoot, withSession } from './api-tools'; import { botClientID, botToken } from './config'; import { GuildData, Guilds } from './kv'; import { useRateLimiter } from './rate-limiting'; @@ -178,3 +188,74 @@ export const useGuildRateLimiter = ( key: GuildRateLimiterKey, timeoutSeconds: number ) => useRateLimiter(Guilds, `guilds/${guildID}/rate-limit/${key}`, timeoutSeconds); + +type AsEditorOptions = { + rateLimitKey?: GuildRateLimiterKey; + rateLimitTimeoutSeconds?: number; +}; + +type UserGuildContext = { + guildID: string; + guild: GuildSlug; + url: URL; +}; + +export const asEditor = ( + options: AsEditorOptions = {}, + wrappedHandler: (session: SessionData, userGuildContext: UserGuildContext) => Handler +): Handler => + withSession((session: SessionData) => async (request: Request): Promise => { + const { rateLimitKey, rateLimitTimeoutSeconds } = options; + const url = new URL(request.url); + const [, , guildID] = url.pathname.split('/'); + if (!guildID) { + return missingParameters(); + } + + let rateLimit: null | ReturnType = null; + if (rateLimitKey) { + rateLimit = await useGuildRateLimiter( + guildID, + rateLimitKey, + rateLimitTimeoutSeconds || 60 + ); + } + + const userIsRoot = isRoot(session.user.id); + + let guild = session.guilds.find((guild) => guild.id === guildID); + if (!guild) { + if (!userIsRoot) { + return notFound(); + } + + const fullGuild = await getGuild(guildID); + if (!fullGuild) { + return notFound(); + } + + guild = { + id: fullGuild.id, + name: fullGuild.name, + icon: fullGuild.icon, + permissionLevel: UserGuildPermissions.Admin, // root will always be considered admin + }; + } + + const userIsManager = guild.permissionLevel === UserGuildPermissions.Manager; + const userIsAdmin = guild.permissionLevel === UserGuildPermissions.Admin; + + if (!userIsAdmin && !userIsManager) { + return lowPermissions(); + } + + if (!userIsRoot && rateLimit && (await rateLimit())) { + return rateLimited(); + } + + return await wrappedHandler(session, { + guildID, + guild, + url, + })(request); + });