refactor(api): asEditor instead of copy-pasted admin/manager/root check

This commit is contained in:
41666 2021-07-17 19:07:10 -04:00
parent 0ed5d696df
commit 31ea2e2183
5 changed files with 105 additions and 223 deletions

View file

@ -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<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();
}
);

View file

@ -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<Response> => {
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 });
}
);

View file

@ -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<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)) {
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<GuildDataT>(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();

View file

@ -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<Response> => {
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,

View file

@ -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<Response> => {
const { rateLimitKey, rateLimitTimeoutSeconds } = options;
const url = new URL(request.url);
const [, , guildID] = url.pathname.split('/');
if (!guildID) {
return missingParameters();
}
let rateLimit: null | ReturnType<typeof useGuildRateLimiter> = 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);
});