mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39: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 { 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();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue