mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-22 02:37:23 +02:00
♻️ Clean up trpc code and add global client error handling (#1549)
This commit is contained in:
parent
4e603d737f
commit
5437b91c10
17 changed files with 167 additions and 163 deletions
|
@ -39,7 +39,6 @@
|
||||||
"@tanstack/react-query": "^4.0.0",
|
"@tanstack/react-query": "^4.0.0",
|
||||||
"@tanstack/react-table": "^8.9.1",
|
"@tanstack/react-table": "^8.9.1",
|
||||||
"@trpc/client": "^10.13.0",
|
"@trpc/client": "^10.13.0",
|
||||||
"@trpc/next": "^10.13.0",
|
|
||||||
"@trpc/react-query": "^10.13.0",
|
"@trpc/react-query": "^10.13.0",
|
||||||
"@trpc/server": "^10.13.0",
|
"@trpc/server": "^10.13.0",
|
||||||
"@upstash/qstash": "^2.7.17",
|
"@upstash/qstash": "^2.7.17",
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
"emailNotAllowed": "This email is not allowed.",
|
"emailNotAllowed": "This email is not allowed.",
|
||||||
"emailPlaceholder": "jessie.smith@example.com",
|
"emailPlaceholder": "jessie.smith@example.com",
|
||||||
"exportToCsv": "Export to CSV",
|
"exportToCsv": "Export to CSV",
|
||||||
"forgetMe": "Forget me",
|
|
||||||
"guest": "Guest",
|
"guest": "Guest",
|
||||||
"ifNeedBe": "If need be",
|
"ifNeedBe": "If need be",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
|
@ -199,9 +198,6 @@
|
||||||
"pollStatusFinalized": "Finalized",
|
"pollStatusFinalized": "Finalized",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"noParticipants": "No participants",
|
"noParticipants": "No participants",
|
||||||
"userId": "User ID",
|
|
||||||
"aboutGuest": "Guest User",
|
|
||||||
"aboutGuestDescription": "Profile settings are not available for guest users. <0>Sign in</0> to your existing account or <1>create a new account</1> to customize your profile.",
|
|
||||||
"logoutDescription": "Sign out of your existing session",
|
"logoutDescription": "Sign out of your existing session",
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
|
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
|
||||||
|
@ -305,5 +301,7 @@
|
||||||
"registerVerifyDescription": "Check your email for the verification code",
|
"registerVerifyDescription": "Check your email for the verification code",
|
||||||
"loginVerifyTitle": "Finish Logging In",
|
"loginVerifyTitle": "Finish Logging In",
|
||||||
"loginVerifyDescription": "Check your email for the verification code",
|
"loginVerifyDescription": "Check your email for the verification code",
|
||||||
"createAccount": "Create Account"
|
"createAccount": "Create Account",
|
||||||
|
"tooManyRequests": "Too many requests",
|
||||||
|
"tooManyRequestsDescription": "Please try again later."
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ import {
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { DuplicateForm } from "@/app/[locale]/poll/[urlId]/duplicate-form";
|
import { DuplicateForm } from "@/app/[locale]/poll/[urlId]/duplicate-form";
|
||||||
import { trpc } from "@/app/providers";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
const formName = "duplicate-form";
|
const formName = "duplicate-form";
|
||||||
export function DuplicateDialog({
|
export function DuplicateDialog({
|
||||||
|
|
|
@ -31,7 +31,8 @@ const handler = (req: NextRequest) => {
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
locale,
|
locale,
|
||||||
ip: ipAddress(req) ?? undefined,
|
ip:
|
||||||
|
process.env.NODE_ENV === "development" ? "127.0.0.1" : ipAddress(req),
|
||||||
} satisfies TRPCContext;
|
} satisfies TRPCContext;
|
||||||
},
|
},
|
||||||
onError({ error }) {
|
onError({ error }) {
|
||||||
|
|
|
@ -1,51 +1,32 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { PostHogProvider } from "@rallly/posthog/client";
|
import { PostHogProvider } from "@rallly/posthog/client";
|
||||||
import { TooltipProvider } from "@rallly/ui/tooltip";
|
import { TooltipProvider } from "@rallly/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { createTRPCReact } from "@trpc/react-query";
|
|
||||||
import { domMax, LazyMotion } from "framer-motion";
|
import { domMax, LazyMotion } from "framer-motion";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { UserProvider } from "@/components/user-provider";
|
import { UserProvider } from "@/components/user-provider";
|
||||||
import { I18nProvider } from "@/i18n/client";
|
import { I18nProvider } from "@/i18n/client";
|
||||||
import { trpcConfig } from "@/trpc/client/config";
|
import { TRPCProvider } from "@/trpc/client/provider";
|
||||||
import type { AppRouter } from "@/trpc/routers";
|
|
||||||
import { ConnectedDayjsProvider } from "@/utils/dayjs";
|
import { ConnectedDayjsProvider } from "@/utils/dayjs";
|
||||||
|
|
||||||
import { PostHogPageView } from "./posthog-page-view";
|
import { PostHogPageView } from "./posthog-page-view";
|
||||||
|
|
||||||
export const trpc = createTRPCReact<AppRouter>({
|
|
||||||
overrides: {
|
|
||||||
useMutation: {
|
|
||||||
async onSuccess(opts) {
|
|
||||||
await opts.originalFn();
|
|
||||||
await opts.queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Providers(props: { children: React.ReactNode }) {
|
export function Providers(props: { children: React.ReactNode }) {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
|
||||||
const [trpcClient] = useState(() => trpc.createClient(trpcConfig));
|
|
||||||
return (
|
return (
|
||||||
<LazyMotion features={domMax}>
|
<LazyMotion features={domMax}>
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<TooltipProvider>
|
|
||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<PostHogPageView />
|
<PostHogPageView />
|
||||||
|
<TRPCProvider>
|
||||||
|
<TooltipProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ConnectedDayjsProvider>
|
<ConnectedDayjsProvider>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ConnectedDayjsProvider>
|
</ConnectedDayjsProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</PostHogProvider>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</TRPCProvider>
|
||||||
|
</PostHogProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</QueryClientProvider>
|
|
||||||
</trpc.Provider>
|
|
||||||
</LazyMotion>
|
</LazyMotion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,13 @@ import Link from "next/link";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { DuplicateDialog } from "@/app/[locale]/poll/[urlId]/duplicate-dialog";
|
import { DuplicateDialog } from "@/app/[locale]/poll/[urlId]/duplicate-dialog";
|
||||||
import { trpc } from "@/app/providers";
|
|
||||||
import { PayWallDialog } from "@/components/pay-wall-dialog";
|
import { PayWallDialog } from "@/components/pay-wall-dialog";
|
||||||
import { FinalizePollDialog } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
import { FinalizePollDialog } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
||||||
import { ProFeatureBadge } from "@/components/pro-feature-badge";
|
import { ProFeatureBadge } from "@/components/pro-feature-badge";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePlan } from "@/contexts/plan";
|
import { usePlan } from "@/contexts/plan";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
import { DeletePollDialog } from "./manage-poll/delete-poll-dialog";
|
import { DeletePollDialog } from "./manage-poll/delete-poll-dialog";
|
||||||
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
||||||
|
|
|
@ -24,12 +24,12 @@ import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { trpc } from "@/app/providers";
|
|
||||||
import { DateIconInner } from "@/components/date-icon";
|
import { DateIconInner } from "@/components/date-icon";
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
||||||
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
|
import { trpc } from "@/trpc/client";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import { createTRPCNext } from "@trpc/next";
|
import { createTRPCReact } from "@trpc/react-query";
|
||||||
|
|
||||||
import { trpcConfig } from "@/trpc/client/config";
|
|
||||||
import type { AppRouter } from "@/trpc/routers";
|
import type { AppRouter } from "@/trpc/routers";
|
||||||
|
|
||||||
export const trpc = createTRPCNext<AppRouter>({
|
export const trpc = createTRPCReact<AppRouter>({
|
||||||
config() {
|
overrides: {
|
||||||
return trpcConfig;
|
|
||||||
},
|
|
||||||
unstable_overrides: {
|
|
||||||
useMutation: {
|
useMutation: {
|
||||||
async onSuccess(opts) {
|
async onSuccess(opts) {
|
||||||
await opts.originalFn();
|
await opts.originalFn();
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { MutationCache } from "@tanstack/react-query";
|
|
||||||
import { type TRPCLink, httpBatchLink, TRPCClientError } from "@trpc/client";
|
|
||||||
import { observable } from "@trpc/server/observable";
|
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
import type { AppRouter } from "../routers";
|
|
||||||
|
|
||||||
const errorHandlingLink: TRPCLink<AppRouter> = () => {
|
|
||||||
return ({ next, op }) => {
|
|
||||||
return observable((observer) => {
|
|
||||||
const unsubscribe = next(op).subscribe({
|
|
||||||
next: (result) => observer.next(result),
|
|
||||||
error: (error) => {
|
|
||||||
if (
|
|
||||||
error instanceof TRPCClientError &&
|
|
||||||
error.data?.code === "UNAUTHORIZED"
|
|
||||||
) {
|
|
||||||
window.location.href = "/login";
|
|
||||||
}
|
|
||||||
observer.error(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const trpcConfig = {
|
|
||||||
links: [
|
|
||||||
errorHandlingLink,
|
|
||||||
httpBatchLink({
|
|
||||||
url: "/api/trpc",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
transformer: superjson,
|
|
||||||
queryClientConfig: {
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
cacheTime: Infinity,
|
|
||||||
staleTime: 1000 * 60,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mutationCache: new MutationCache({
|
|
||||||
onError: (error) => {
|
|
||||||
Sentry.captureException(error);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
79
apps/web/src/trpc/client/provider.tsx
Normal file
79
apps/web/src/trpc/client/provider.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
import { usePostHog } from "@rallly/posthog/client";
|
||||||
|
import { useToast } from "@rallly/ui/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
MutationCache,
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { httpBatchLink, TRPCClientError } from "@trpc/client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { useTranslation } from "@/i18n/client";
|
||||||
|
|
||||||
|
import { trpc } from "../client";
|
||||||
|
|
||||||
|
export function TRPCProvider(props: { children: React.ReactNode }) {
|
||||||
|
const posthog = usePostHog();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
cacheTime: Infinity,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutationCache: new MutationCache({
|
||||||
|
onError(error) {
|
||||||
|
if (error instanceof TRPCClientError) {
|
||||||
|
posthog.capture("failed api request", {
|
||||||
|
path: error.data.path,
|
||||||
|
code: error.data.code,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
switch (error.data.code) {
|
||||||
|
case "UNAUTHORIZED":
|
||||||
|
window.location.href = "/login";
|
||||||
|
break;
|
||||||
|
case "TOO_MANY_REQUESTS":
|
||||||
|
toast({
|
||||||
|
title: t("tooManyRequests", {
|
||||||
|
defaultValue: "Too many requests",
|
||||||
|
}),
|
||||||
|
description: t("tooManyRequestsDescription", {
|
||||||
|
defaultValue: "Please try again later.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [trpcClient] = useState(() =>
|
||||||
|
trpc.createClient({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: "/api/trpc",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
transformer: superjson,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{props.children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
</trpc.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { mergeGuestsIntoUser } from "@/auth/merge-user";
|
||||||
import { getEmailClient } from "@/utils/emails";
|
import { getEmailClient } from "@/utils/emails";
|
||||||
import { createToken, decryptToken } from "@/utils/session";
|
import { createToken, decryptToken } from "@/utils/session";
|
||||||
|
|
||||||
import { publicProcedure, rateLimitMiddleware, router } from "../trpc";
|
import { createRateLimitMiddleware, publicProcedure, router } from "../trpc";
|
||||||
import type { RegistrationTokenPayload } from "../types";
|
import type { RegistrationTokenPayload } from "../types";
|
||||||
|
|
||||||
export const auth = router({
|
export const auth = router({
|
||||||
|
@ -29,7 +29,7 @@ export const auth = router({
|
||||||
return { isRegistered: count > 0 };
|
return { isRegistered: count > 0 };
|
||||||
}),
|
}),
|
||||||
requestRegistration: publicProcedure
|
requestRegistration: publicProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(5, "1 m"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
|
|
|
@ -12,11 +12,11 @@ import { getEmailClient } from "@/utils/emails";
|
||||||
|
|
||||||
import { getTimeZoneAbbreviation } from "../../utils/date";
|
import { getTimeZoneAbbreviation } from "../../utils/date";
|
||||||
import {
|
import {
|
||||||
|
createRateLimitMiddleware,
|
||||||
possiblyPublicProcedure,
|
possiblyPublicProcedure,
|
||||||
privateProcedure,
|
privateProcedure,
|
||||||
proProcedure,
|
proProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
rateLimitMiddleware,
|
|
||||||
requireUserMiddleware,
|
requireUserMiddleware,
|
||||||
router,
|
router,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
@ -130,7 +130,7 @@ export const polls = router({
|
||||||
|
|
||||||
// START LEGACY ROUTES
|
// START LEGACY ROUTES
|
||||||
create: possiblyPublicProcedure
|
create: possiblyPublicProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(20, "1 h"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -233,6 +233,7 @@ export const polls = router({
|
||||||
return { id: poll.id };
|
return { id: poll.id };
|
||||||
}),
|
}),
|
||||||
update: possiblyPublicProcedure
|
update: possiblyPublicProcedure
|
||||||
|
.use(createRateLimitMiddleware(60, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
urlId: z.string(),
|
urlId: z.string(),
|
||||||
|
@ -305,6 +306,7 @@ export const polls = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
delete: possiblyPublicProcedure
|
delete: possiblyPublicProcedure
|
||||||
|
.use(createRateLimitMiddleware(30, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
urlId: z.string(),
|
urlId: z.string(),
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { getEmailClient } from "@/utils/emails";
|
||||||
import { createToken } from "@/utils/session";
|
import { createToken } from "@/utils/session";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createRateLimitMiddleware,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
rateLimitMiddleware,
|
|
||||||
requireUserMiddleware,
|
requireUserMiddleware,
|
||||||
router,
|
router,
|
||||||
} from "../../trpc";
|
} from "../../trpc";
|
||||||
|
@ -72,7 +72,7 @@ export const comments = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
add: publicProcedure
|
add: publicProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(5, "1 m"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { getEmailClient } from "@/utils/emails";
|
||||||
import { createToken } from "@/utils/session";
|
import { createToken } from "@/utils/session";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createRateLimitMiddleware,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
rateLimitMiddleware,
|
|
||||||
requireUserMiddleware,
|
requireUserMiddleware,
|
||||||
router,
|
router,
|
||||||
} from "../../trpc";
|
} from "../../trpc";
|
||||||
|
@ -105,6 +105,7 @@ export const participants = router({
|
||||||
return participants;
|
return participants;
|
||||||
}),
|
}),
|
||||||
delete: publicProcedure
|
delete: publicProcedure
|
||||||
|
.use(createRateLimitMiddleware(20, "1 m"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
participantId: z.string(),
|
participantId: z.string(),
|
||||||
|
@ -122,7 +123,7 @@ export const participants = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
add: publicProcedure
|
add: publicProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(20, "1 m"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -217,6 +218,7 @@ export const participants = router({
|
||||||
return participant;
|
return participant;
|
||||||
}),
|
}),
|
||||||
rename: publicProcedure
|
rename: publicProcedure
|
||||||
|
.use(createRateLimitMiddleware(20, "1 m"))
|
||||||
.input(z.object({ participantId: z.string(), newName: z.string() }))
|
.input(z.object({ participantId: z.string(), newName: z.string() }))
|
||||||
.mutation(async ({ input: { participantId, newName } }) => {
|
.mutation(async ({ input: { participantId, newName } }) => {
|
||||||
await prisma.participant.update({
|
await prisma.participant.update({
|
||||||
|
@ -230,6 +232,7 @@ export const participants = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
update: publicProcedure
|
update: publicProcedure
|
||||||
|
.use(createRateLimitMiddleware(20, "1 m"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
pollId: z.string(),
|
pollId: z.string(),
|
||||||
|
|
|
@ -12,9 +12,9 @@ import { createToken } from "@/utils/session";
|
||||||
import { getSubscriptionStatus } from "@/utils/subscription";
|
import { getSubscriptionStatus } from "@/utils/subscription";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createRateLimitMiddleware,
|
||||||
privateProcedure,
|
privateProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
rateLimitMiddleware,
|
|
||||||
router,
|
router,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
|
||||||
|
@ -53,7 +53,9 @@ export const user = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
delete: privateProcedure.mutation(async ({ ctx }) => {
|
delete: privateProcedure
|
||||||
|
.use(createRateLimitMiddleware(5, "1 h"))
|
||||||
|
.mutation(async ({ ctx }) => {
|
||||||
if (ctx.user.isGuest) {
|
if (ctx.user.isGuest) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
|
@ -80,6 +82,7 @@ export const user = router({
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
changeName: privateProcedure
|
changeName: privateProcedure
|
||||||
|
.use(createRateLimitMiddleware(20, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
|
@ -96,6 +99,7 @@ export const user = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
updatePreferences: privateProcedure
|
updatePreferences: privateProcedure
|
||||||
|
.use(createRateLimitMiddleware(30, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
|
@ -122,7 +126,7 @@ export const user = router({
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
requestEmailChange: privateProcedure
|
requestEmailChange: privateProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(10, "1 h"))
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const currentUser = await prisma.user.findUnique({
|
const currentUser = await prisma.user.findUnique({
|
||||||
|
@ -174,7 +178,7 @@ export const user = router({
|
||||||
return { success: true as const };
|
return { success: true as const };
|
||||||
}),
|
}),
|
||||||
getAvatarUploadUrl: privateProcedure
|
getAvatarUploadUrl: privateProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(20, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
fileType: z.enum(["image/jpeg", "image/png"]),
|
fileType: z.enum(["image/jpeg", "image/png"]),
|
||||||
|
@ -220,7 +224,7 @@ export const user = router({
|
||||||
}),
|
}),
|
||||||
updateAvatar: privateProcedure
|
updateAvatar: privateProcedure
|
||||||
.input(z.object({ imageKey: z.string().max(255) }))
|
.input(z.object({ imageKey: z.string().max(255) }))
|
||||||
.use(rateLimitMiddleware)
|
.use(createRateLimitMiddleware(10, "1 h"))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
const oldImageKey = ctx.user.image;
|
const oldImageKey = ctx.user.image;
|
||||||
|
|
|
@ -89,22 +89,25 @@ export const proProcedure = privateProcedure.use(async ({ ctx, next }) => {
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rateLimitMiddleware = middleware(async ({ ctx, next }) => {
|
export const createRateLimitMiddleware = (
|
||||||
|
requests: number,
|
||||||
|
duration: "1 m" | "1 h",
|
||||||
|
) => {
|
||||||
|
return middleware(async ({ ctx, next }) => {
|
||||||
if (!process.env.KV_REST_API_URL) {
|
if (!process.env.KV_REST_API_URL) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratelimit = new Ratelimit({
|
|
||||||
redis: kv,
|
|
||||||
limiter: Ratelimit.slidingWindow(5, "1 m"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ctx.ip) {
|
if (!ctx.ip) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to get client IP",
|
message: "Failed to get client IP",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const ratelimit = new Ratelimit({
|
||||||
|
redis: kv,
|
||||||
|
limiter: Ratelimit.slidingWindow(requests, duration),
|
||||||
|
});
|
||||||
|
|
||||||
const res = await ratelimit.limit(ctx.ip);
|
const res = await ratelimit.limit(ctx.ip);
|
||||||
|
|
||||||
|
@ -117,5 +120,6 @@ export const rateLimitMiddleware = middleware(async ({ ctx, next }) => {
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const mergeRouters = t.mergeRouters;
|
export const mergeRouters = t.mergeRouters;
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -6052,13 +6052,6 @@
|
||||||
resolved "https://registry.npmjs.org/@trpc/client/-/client-10.41.0.tgz"
|
resolved "https://registry.npmjs.org/@trpc/client/-/client-10.41.0.tgz"
|
||||||
integrity sha512-W4lYULb7//2yXkULCKim49slXsBwiBq48rfge1yOWXdq0Ed8VxzXvZt8+uWOkxmHbQAw4lq8G5fCNYFB+Za6vQ==
|
integrity sha512-W4lYULb7//2yXkULCKim49slXsBwiBq48rfge1yOWXdq0Ed8VxzXvZt8+uWOkxmHbQAw4lq8G5fCNYFB+Za6vQ==
|
||||||
|
|
||||||
"@trpc/next@^10.13.0":
|
|
||||||
version "10.41.0"
|
|
||||||
resolved "https://registry.npmjs.org/@trpc/next/-/next-10.41.0.tgz"
|
|
||||||
integrity sha512-QwvZrvDjRFEzErmLZ4hMdYfX13nsH0SpijjuTNPIlSIyFISCIfDCqmBvWC07O6fCG/swh+XM19FhJN6RMqTlKQ==
|
|
||||||
dependencies:
|
|
||||||
react-ssr-prepass "^1.5.0"
|
|
||||||
|
|
||||||
"@trpc/react-query@^10.13.0":
|
"@trpc/react-query@^10.13.0":
|
||||||
version "10.41.0"
|
version "10.41.0"
|
||||||
resolved "https://registry.npmjs.org/@trpc/react-query/-/react-query-10.41.0.tgz"
|
resolved "https://registry.npmjs.org/@trpc/react-query/-/react-query-10.41.0.tgz"
|
||||||
|
@ -12511,11 +12504,6 @@ react-remove-scroll@^2.5.6:
|
||||||
use-callback-ref "^1.3.0"
|
use-callback-ref "^1.3.0"
|
||||||
use-sidecar "^1.1.2"
|
use-sidecar "^1.1.2"
|
||||||
|
|
||||||
react-ssr-prepass@^1.5.0:
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz"
|
|
||||||
integrity sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==
|
|
||||||
|
|
||||||
react-style-singleton@^2.2.1:
|
react-style-singleton@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz"
|
resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue