mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
refactor(api): asEditor instead of copy-pasted admin/manager/root check
This commit is contained in:
parent
0ed5d696df
commit
31ea2e2183
5 changed files with 105 additions and 223 deletions
|
@ -1,51 +1,18 @@
|
||||||
import { UserGuildPermissions } from '@roleypoly/types';
|
import { asEditor, getGuild, GuildRateLimiterKey } from '../utils/guild';
|
||||||
import { isRoot, withSession } from '../utils/api-tools';
|
import { notFound, ok } from '../utils/responses';
|
||||||
import { getGuild, GuildRateLimiterKey, useGuildRateLimiter } from '../utils/guild';
|
|
||||||
import {
|
|
||||||
lowPermissions,
|
|
||||||
missingParameters,
|
|
||||||
notFound,
|
|
||||||
ok,
|
|
||||||
rateLimited,
|
|
||||||
} from '../utils/responses';
|
|
||||||
|
|
||||||
export const ClearGuildCache = withSession(
|
export const ClearGuildCache = asEditor(
|
||||||
(session) =>
|
{
|
||||||
|
rateLimitKey: GuildRateLimiterKey.cacheClear,
|
||||||
|
rateLimitTimeoutSeconds: 60 * 5,
|
||||||
|
},
|
||||||
|
(session, { guildID }) =>
|
||||||
async (request: Request): Promise<Response> => {
|
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 });
|
const result = await getGuild(guildID, { skipCachePull: true });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok();
|
return ok();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -1,69 +1,15 @@
|
||||||
import {
|
import { asEditor, GuildRateLimiterKey } from '../utils/guild';
|
||||||
Features,
|
|
||||||
GuildData as GuildDataT,
|
|
||||||
UserGuildPermissions,
|
|
||||||
} from '@roleypoly/types';
|
|
||||||
import { isRoot, withSession } from '../utils/api-tools';
|
|
||||||
import { GuildRateLimiterKey, useGuildRateLimiter } from '../utils/guild';
|
|
||||||
import { fetchLegacyServer, transformLegacyGuild } from '../utils/import-from-legacy';
|
import { fetchLegacyServer, transformLegacyGuild } from '../utils/import-from-legacy';
|
||||||
import { GuildData } from '../utils/kv';
|
import { GuildData } from '../utils/kv';
|
||||||
import {
|
import { notFound, ok } from '../utils/responses';
|
||||||
conflict,
|
|
||||||
lowPermissions,
|
|
||||||
missingParameters,
|
|
||||||
notFound,
|
|
||||||
ok,
|
|
||||||
rateLimited,
|
|
||||||
} from '../utils/responses';
|
|
||||||
|
|
||||||
export const SyncFromLegacy = withSession(
|
export const SyncFromLegacy = asEditor(
|
||||||
(session) =>
|
{
|
||||||
|
rateLimitKey: GuildRateLimiterKey.legacyImport,
|
||||||
|
rateLimitTimeoutSeconds: 60 * 20,
|
||||||
|
},
|
||||||
|
(session, { guildID }) =>
|
||||||
async (request: Request): Promise<Response> => {
|
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);
|
const legacyGuild = await fetchLegacyServer(guildID);
|
||||||
if (!legacyGuild) {
|
if (!legacyGuild) {
|
||||||
return notFound();
|
return notFound();
|
||||||
|
|
|
@ -1,44 +1,15 @@
|
||||||
import { sendAuditLog, validateAuditLogWebhook } from '@roleypoly/api/utils/audit-log';
|
import { sendAuditLog, validateAuditLogWebhook } from '@roleypoly/api/utils/audit-log';
|
||||||
import {
|
import { GuildDataUpdate, WebhookValidationStatus } from '@roleypoly/types';
|
||||||
GuildDataUpdate,
|
import { asEditor, getGuildData } from '../utils/guild';
|
||||||
SessionData,
|
|
||||||
UserGuildPermissions,
|
|
||||||
WebhookValidationStatus,
|
|
||||||
} from '@roleypoly/types';
|
|
||||||
import { withSession } from '../utils/api-tools';
|
|
||||||
import { getGuildData } from '../utils/guild';
|
|
||||||
import { GuildData } from '../utils/kv';
|
import { GuildData } from '../utils/kv';
|
||||||
import {
|
import { invalid, ok } from '../utils/responses';
|
||||||
invalid,
|
|
||||||
lowPermissions,
|
|
||||||
missingParameters,
|
|
||||||
notFound,
|
|
||||||
ok,
|
|
||||||
} from '../utils/responses';
|
|
||||||
|
|
||||||
export const UpdateGuild = withSession(
|
export const UpdateGuild = asEditor(
|
||||||
(session: SessionData) =>
|
{},
|
||||||
|
(session, { guildID, guild }) =>
|
||||||
async (request: Request): Promise<Response> => {
|
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 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 oldGuildData = await getGuildData(guildID);
|
||||||
const newGuildData = {
|
const newGuildData = {
|
||||||
...oldGuildData,
|
...oldGuildData,
|
||||||
|
|
|
@ -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 { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||||
import {
|
import {
|
||||||
Features,
|
Features,
|
||||||
Guild,
|
Guild,
|
||||||
GuildData as GuildDataT,
|
GuildData as GuildDataT,
|
||||||
|
GuildSlug,
|
||||||
OwnRoleInfo,
|
OwnRoleInfo,
|
||||||
Role,
|
Role,
|
||||||
RoleSafety,
|
RoleSafety,
|
||||||
|
SessionData,
|
||||||
|
UserGuildPermissions,
|
||||||
} from '@roleypoly/types';
|
} 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 { botClientID, botToken } from './config';
|
||||||
import { GuildData, Guilds } from './kv';
|
import { GuildData, Guilds } from './kv';
|
||||||
import { useRateLimiter } from './rate-limiting';
|
import { useRateLimiter } from './rate-limiting';
|
||||||
|
@ -178,3 +188,74 @@ export const useGuildRateLimiter = (
|
||||||
key: GuildRateLimiterKey,
|
key: GuildRateLimiterKey,
|
||||||
timeoutSeconds: number
|
timeoutSeconds: number
|
||||||
) => useRateLimiter(Guilds, `guilds/${guildID}/rate-limit/${key}`, timeoutSeconds);
|
) => 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);
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue