mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 09:54:36 -05:00
Handle 429 & add admin webhook support (#2775)
This commit is contained in:
parent
57892fb571
commit
e7feef15af
|
|
@ -5,6 +5,7 @@ SESSION_SECRET=secret
|
|||
// Auth https://discord.com/developers
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
DISCORD_ADMIN_WEBHOOK_URL=
|
||||
|
||||
// Patreon integration to sync supporter status https://www.patreon.com/portal/registration/register-clients
|
||||
PATREON_ACCESS_TOKEN=
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { _action, actualNumber, friendCode } from "~/utils/zod";
|
||||
import * as AdminNotifications from "../core/admin-notifications.server";
|
||||
import { plusTiersFromVotingAndLeaderboard } from "../core/plus-tier.server";
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
|
|
@ -167,6 +168,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
message = "API access granted";
|
||||
break;
|
||||
}
|
||||
case "TEST_ADMIN_NOTIFICATION": {
|
||||
requireRole(user, "ADMIN");
|
||||
|
||||
await AdminNotifications.send("Test notification from admin panel");
|
||||
|
||||
message = "Test notification sent";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
|
|
@ -229,4 +238,7 @@ export const adminActionSchema = z.union([
|
|||
_action: _action("API_ACCESS"),
|
||||
user: z.preprocess(actualNumber, z.number().positive()),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("TEST_ADMIN_NOTIFICATION"),
|
||||
}),
|
||||
]);
|
||||
|
|
|
|||
29
app/features/admin/core/admin-notifications.server.ts
Normal file
29
app/features/admin/core/admin-notifications.server.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { logger } from "~/utils/logger";
|
||||
|
||||
const DISCORD_ADMIN_WEBHOOK_URL = process.env.DISCORD_ADMIN_WEBHOOK_URL;
|
||||
|
||||
if (!DISCORD_ADMIN_WEBHOOK_URL) {
|
||||
logger.info(
|
||||
"DISCORD_ADMIN_WEBHOOK_URL not set, admin notifications disabled",
|
||||
);
|
||||
}
|
||||
|
||||
export async function send(message: string): Promise<void> {
|
||||
if (!DISCORD_ADMIN_WEBHOOK_URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(DISCORD_ADMIN_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content: message }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(`Failed to send admin notification: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to send admin notification", error);
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,8 @@ function AdminActions() {
|
|||
|
||||
return (
|
||||
<div className="stack lg">
|
||||
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS && <Seed />}
|
||||
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS ? <Seed /> : null}
|
||||
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS ? <TestAdminNotification /> : null}
|
||||
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS || isAdmin ? <Impersonate /> : null}
|
||||
|
||||
{isStaff ? <LinkPlayer /> : null}
|
||||
|
|
@ -459,4 +460,21 @@ function Seed() {
|
|||
);
|
||||
}
|
||||
|
||||
function TestAdminNotification() {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post">
|
||||
<h2>Test Admin Notification</h2>
|
||||
<SubmitButton
|
||||
type="submit"
|
||||
_action="TEST_ADMIN_NOTIFICATION"
|
||||
state={fetcher.state}
|
||||
>
|
||||
Send Test
|
||||
</SubmitButton>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
export const ErrorBoundary = Catcher;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { add } from "date-fns";
|
||||
import { OAuth2Strategy } from "remix-auth-oauth2";
|
||||
import { z } from "zod";
|
||||
import * as AdminNotifications from "~/features/admin/core/admin-notifications.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
let discordApiCooldownUntil: number | null = null;
|
||||
|
||||
const partialDiscordUserSchema = z.object({
|
||||
avatar: z.string().nullish(),
|
||||
discriminator: z.string(),
|
||||
|
|
@ -25,11 +29,28 @@ const discordUserDetailsSchema = z.tuple([
|
|||
partialDiscordUserSchema,
|
||||
partialDiscordConnectionsSchema,
|
||||
]);
|
||||
const discordRateLimitSchema = z.object({
|
||||
retry_after: z.number(),
|
||||
});
|
||||
|
||||
export const DiscordStrategy = () => {
|
||||
const envVars = authEnvVars();
|
||||
|
||||
const jsonIfOk = (res: Response) => {
|
||||
const jsonIfOk = async (res: Response) => {
|
||||
if (res.status === 429) {
|
||||
const body = discordRateLimitSchema.safeParse(await res.clone().json());
|
||||
const retryAfterSeconds = body.success ? body.data.retry_after : 60;
|
||||
discordApiCooldownUntil = add(new Date(), {
|
||||
seconds: retryAfterSeconds,
|
||||
}).getTime();
|
||||
logger.warn(
|
||||
`Discord API rate limited, cooldown for ${retryAfterSeconds}s${body.success ? "" : " (failed to parse retry_after)"}`,
|
||||
);
|
||||
AdminNotifications.send(
|
||||
`Discord API rate limited, cooldown for ${retryAfterSeconds}s`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Auth related call failed with status code ${res.status}`,
|
||||
|
|
@ -40,6 +61,10 @@ export const DiscordStrategy = () => {
|
|||
};
|
||||
|
||||
const fetchProfileViaDiscordApi = (token: string) => {
|
||||
if (discordApiCooldownUntil && Date.now() < discordApiCooldownUntil) {
|
||||
throw new Error("Discord API is rate limited");
|
||||
}
|
||||
|
||||
const authHeader: [string, string] = ["Authorization", `Bearer ${token}`];
|
||||
|
||||
return Promise.all([
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user