♻️ Refactor safe action middleware (#1809)

This commit is contained in:
Luke Vella 2025-07-11 11:29:36 +01:00 committed by GitHub
parent 6b4e5b3540
commit c2701a4d4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 89 additions and 78 deletions

View file

@ -5,8 +5,9 @@ import { signOut } from "@/next-auth";
import { subject } from "@casl/ability";
import { prisma } from "@rallly/database";
export const deleteCurrentUserAction = authActionClient.action(
async ({ ctx }) => {
export const deleteCurrentUserAction = authActionClient
.metadata({ actionName: "delete_current_user" })
.action(async ({ ctx }) => {
const userId = ctx.user.id;
const user = await prisma.user.findUnique({
@ -34,18 +35,9 @@ export const deleteCurrentUserAction = authActionClient.action(
},
});
ctx.posthog?.capture({
event: "delete_account",
distinctId: ctx.user.id,
properties: {
email: ctx.user.email,
},
});
await signOut();
return {
success: true,
};
},
);
});

View file

@ -5,7 +5,9 @@ import { subject } from "@casl/ability";
import { prisma } from "@rallly/database";
import { redirect } from "next/navigation";
export const makeMeAdminAction = authActionClient.action(async ({ ctx }) => {
export const makeMeAdminAction = authActionClient
.metadata({ actionName: "make_admin" })
.action(async ({ ctx }) => {
if (ctx.ability.cannot("update", subject("User", ctx.user), "role")) {
throw new ActionError({
code: "UNAUTHORIZED",
@ -23,4 +25,4 @@ export const makeMeAdminAction = authActionClient.action(async ({ ctx }) => {
});
redirect("/control-panel");
});
});

View file

@ -2,7 +2,9 @@
import { requireUserAbility } from "@/auth/queries";
import { posthog } from "@rallly/posthog/server";
import { waitUntil } from "@vercel/functions";
import { createSafeActionClient } from "next-safe-action";
import { createMiddleware, createSafeActionClient } from "next-safe-action";
import { revalidatePath } from "next/cache";
import z from "zod";
type ActionErrorCode =
| "UNAUTHORIZED"
@ -28,7 +30,45 @@ export class ActionError extends Error {
}
}
const autoRevalidateMiddleware = createMiddleware().define(async ({ next }) => {
const result = await next();
revalidatePath("/", "layout");
return result;
});
const posthogMiddleware = createMiddleware<{
ctx: { user: { id: string } };
metadata: { actionName: string };
}>().define(async ({ ctx, next, metadata }) => {
let properties: Record<string, unknown> | undefined;
const result = await next({
ctx: {
posthog,
captureProperties: (props?: Record<string, unknown>) => {
properties = props;
},
},
});
posthog?.capture({
distinctId: ctx.user.id,
event: metadata.actionName,
properties,
});
if (posthog) {
waitUntil(posthog.shutdown());
}
return result;
});
export const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({
actionName: z.string(),
}),
handleServerError: async (error) => {
if (error instanceof ActionError) {
return error.code;
@ -36,22 +76,14 @@ export const actionClient = createSafeActionClient({
return "INTERNAL_SERVER_ERROR";
},
}).use(({ next }) => {
const result = next({
ctx: {
posthog,
},
});
}).use(autoRevalidateMiddleware);
waitUntil(Promise.all([posthog?.shutdown()]));
return result;
});
export const authActionClient = actionClient.use(async ({ next }) => {
export const authActionClient = actionClient
.use(async ({ next }) => {
const { user, ability } = await requireUserAbility();
return next({
ctx: { user, ability },
});
});
})
.use(posthogMiddleware);

View file

@ -3,11 +3,11 @@ import { ActionError, authActionClient } from "@/features/safe-action/server";
import { getEmailClient } from "@/utils/emails";
import { subject } from "@casl/ability";
import { prisma } from "@rallly/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { formatEventDateTime } from "./utils";
export const cancelEventAction = authActionClient
.metadata({ actionName: "cancel_event" })
.inputSchema(
z.object({
eventId: z.string(),
@ -43,14 +43,8 @@ export const cancelEventAction = authActionClient
},
});
revalidatePath("/", "layout");
ctx.posthog?.capture({
event: "cancel_event",
distinctId: ctx.user.id,
properties: {
event_id: parsedInput.eventId,
},
ctx.captureProperties({
eventId: parsedInput.eventId,
});
// notify attendees

View file

@ -1,12 +1,12 @@
"use server";
import { prisma } from "@rallly/database";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { authActionClient } from "@/features/safe-action/server";
import { setupSchema } from "./schema";
export const updateUserAction = authActionClient
export const completeSetupAction = authActionClient
.metadata({ actionName: "complete_setup" })
.inputSchema(setupSchema)
.action(async ({ parsedInput, ctx }) => {
const { name, timeZone, locale } = parsedInput;
@ -20,19 +20,13 @@ export const updateUserAction = authActionClient
},
});
ctx.posthog?.capture({
event: "user_setup_completed",
distinctId: ctx.user.id,
properties: {
ctx.captureProperties({
$set: {
name,
timeZone,
locale,
},
},
});
revalidatePath("/", "layout");
redirect("/");
});

View file

@ -20,7 +20,7 @@ import { useSafeAction } from "@/features/safe-action/client";
import { useTimezone } from "@/features/timezone";
import { useTranslation } from "@/i18n/client";
import { updateUserAction } from "../actions";
import { completeSetupAction } from "../actions";
import { type SetupFormValues, setupSchema } from "../schema";
interface SetupFormProps {
@ -30,7 +30,7 @@ interface SetupFormProps {
export function SetupForm({ defaultValues }: SetupFormProps) {
const { timezone } = useTimezone();
const { i18n } = useTranslation();
const userSetupAction = useSafeAction(updateUserAction);
const completeSetup = useSafeAction(completeSetupAction);
const form = useForm<SetupFormValues>({
resolver: zodResolver(setupSchema),
@ -45,7 +45,7 @@ export function SetupForm({ defaultValues }: SetupFormProps) {
<Form {...form}>
<form
onSubmit={form.handleSubmit(async (data) => {
await userSetupAction.executeAsync(data);
await completeSetup.executeAsync(data);
})}
>
<div className="space-y-4">
@ -108,8 +108,8 @@ export function SetupForm({ defaultValues }: SetupFormProps) {
</FormItem>
)}
/>
{userSetupAction.result.serverError && (
<FormMessage>{userSetupAction.result.serverError}</FormMessage>
{completeSetup.result.serverError && (
<FormMessage>{completeSetup.result.serverError}</FormMessage>
)}
</div>
<div className="mt-6">

View file

@ -2,11 +2,11 @@
import { ActionError, authActionClient } from "@/features/safe-action/server";
import { subject } from "@casl/ability";
import { prisma } from "@rallly/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { getUser } from "./queries";
export const changeRoleAction = authActionClient
.metadata({ actionName: "change_role" })
.inputSchema(
z.object({
userId: z.string(),
@ -47,11 +47,10 @@ export const changeRoleAction = authActionClient
role,
},
});
revalidatePath("/control-panel");
});
export const deleteUserAction = authActionClient
.metadata({ actionName: "delete_user" })
.inputSchema(
z.object({
userId: z.string(),
@ -86,8 +85,6 @@ export const deleteUserAction = authActionClient
},
});
revalidatePath("/control-panel");
return {
success: true,
};