mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
♻️ Refactor safe action middleware (#1809)
This commit is contained in:
parent
6b4e5b3540
commit
c2701a4d4f
7 changed files with 89 additions and 78 deletions
|
@ -5,8 +5,9 @@ import { signOut } from "@/next-auth";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
|
||||||
export const deleteCurrentUserAction = authActionClient.action(
|
export const deleteCurrentUserAction = authActionClient
|
||||||
async ({ ctx }) => {
|
.metadata({ actionName: "delete_current_user" })
|
||||||
|
.action(async ({ ctx }) => {
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
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();
|
await signOut();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
|
@ -5,22 +5,24 @@ import { subject } from "@casl/ability";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export const makeMeAdminAction = authActionClient.action(async ({ ctx }) => {
|
export const makeMeAdminAction = authActionClient
|
||||||
if (ctx.ability.cannot("update", subject("User", ctx.user), "role")) {
|
.metadata({ actionName: "make_admin" })
|
||||||
throw new ActionError({
|
.action(async ({ ctx }) => {
|
||||||
code: "UNAUTHORIZED",
|
if (ctx.ability.cannot("update", subject("User", ctx.user), "role")) {
|
||||||
message: "You are not authorized to update your role",
|
throw new ActionError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to update your role",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
role: "admin",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.user.update({
|
redirect("/control-panel");
|
||||||
where: {
|
|
||||||
id: ctx.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
redirect("/control-panel");
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
import { requireUserAbility } from "@/auth/queries";
|
import { requireUserAbility } from "@/auth/queries";
|
||||||
import { posthog } from "@rallly/posthog/server";
|
import { posthog } from "@rallly/posthog/server";
|
||||||
import { waitUntil } from "@vercel/functions";
|
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 =
|
type ActionErrorCode =
|
||||||
| "UNAUTHORIZED"
|
| "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({
|
export const actionClient = createSafeActionClient({
|
||||||
|
defineMetadataSchema: () =>
|
||||||
|
z.object({
|
||||||
|
actionName: z.string(),
|
||||||
|
}),
|
||||||
handleServerError: async (error) => {
|
handleServerError: async (error) => {
|
||||||
if (error instanceof ActionError) {
|
if (error instanceof ActionError) {
|
||||||
return error.code;
|
return error.code;
|
||||||
|
@ -36,22 +76,14 @@ export const actionClient = createSafeActionClient({
|
||||||
|
|
||||||
return "INTERNAL_SERVER_ERROR";
|
return "INTERNAL_SERVER_ERROR";
|
||||||
},
|
},
|
||||||
}).use(({ next }) => {
|
}).use(autoRevalidateMiddleware);
|
||||||
const result = next({
|
|
||||||
ctx: {
|
|
||||||
posthog,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
waitUntil(Promise.all([posthog?.shutdown()]));
|
export const authActionClient = actionClient
|
||||||
|
.use(async ({ next }) => {
|
||||||
|
const { user, ability } = await requireUserAbility();
|
||||||
|
|
||||||
return result;
|
return next({
|
||||||
});
|
ctx: { user, ability },
|
||||||
|
});
|
||||||
export const authActionClient = actionClient.use(async ({ next }) => {
|
})
|
||||||
const { user, ability } = await requireUserAbility();
|
.use(posthogMiddleware);
|
||||||
|
|
||||||
return next({
|
|
||||||
ctx: { user, ability },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { ActionError, authActionClient } from "@/features/safe-action/server";
|
||||||
import { getEmailClient } from "@/utils/emails";
|
import { getEmailClient } from "@/utils/emails";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { formatEventDateTime } from "./utils";
|
import { formatEventDateTime } from "./utils";
|
||||||
|
|
||||||
export const cancelEventAction = authActionClient
|
export const cancelEventAction = authActionClient
|
||||||
|
.metadata({ actionName: "cancel_event" })
|
||||||
.inputSchema(
|
.inputSchema(
|
||||||
z.object({
|
z.object({
|
||||||
eventId: z.string(),
|
eventId: z.string(),
|
||||||
|
@ -43,14 +43,8 @@ export const cancelEventAction = authActionClient
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/", "layout");
|
ctx.captureProperties({
|
||||||
|
eventId: parsedInput.eventId,
|
||||||
ctx.posthog?.capture({
|
|
||||||
event: "cancel_event",
|
|
||||||
distinctId: ctx.user.id,
|
|
||||||
properties: {
|
|
||||||
event_id: parsedInput.eventId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// notify attendees
|
// notify attendees
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { authActionClient } from "@/features/safe-action/server";
|
import { authActionClient } from "@/features/safe-action/server";
|
||||||
import { setupSchema } from "./schema";
|
import { setupSchema } from "./schema";
|
||||||
|
|
||||||
export const updateUserAction = authActionClient
|
export const completeSetupAction = authActionClient
|
||||||
|
.metadata({ actionName: "complete_setup" })
|
||||||
.inputSchema(setupSchema)
|
.inputSchema(setupSchema)
|
||||||
.action(async ({ parsedInput, ctx }) => {
|
.action(async ({ parsedInput, ctx }) => {
|
||||||
const { name, timeZone, locale } = parsedInput;
|
const { name, timeZone, locale } = parsedInput;
|
||||||
|
@ -20,19 +20,13 @@ export const updateUserAction = authActionClient
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.posthog?.capture({
|
ctx.captureProperties({
|
||||||
event: "user_setup_completed",
|
$set: {
|
||||||
distinctId: ctx.user.id,
|
name,
|
||||||
properties: {
|
timeZone,
|
||||||
$set: {
|
locale,
|
||||||
name,
|
|
||||||
timeZone,
|
|
||||||
locale,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/", "layout");
|
|
||||||
|
|
||||||
redirect("/");
|
redirect("/");
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { useSafeAction } from "@/features/safe-action/client";
|
||||||
import { useTimezone } from "@/features/timezone";
|
import { useTimezone } from "@/features/timezone";
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
|
|
||||||
import { updateUserAction } from "../actions";
|
import { completeSetupAction } from "../actions";
|
||||||
import { type SetupFormValues, setupSchema } from "../schema";
|
import { type SetupFormValues, setupSchema } from "../schema";
|
||||||
|
|
||||||
interface SetupFormProps {
|
interface SetupFormProps {
|
||||||
|
@ -30,7 +30,7 @@ interface SetupFormProps {
|
||||||
export function SetupForm({ defaultValues }: SetupFormProps) {
|
export function SetupForm({ defaultValues }: SetupFormProps) {
|
||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const userSetupAction = useSafeAction(updateUserAction);
|
const completeSetup = useSafeAction(completeSetupAction);
|
||||||
|
|
||||||
const form = useForm<SetupFormValues>({
|
const form = useForm<SetupFormValues>({
|
||||||
resolver: zodResolver(setupSchema),
|
resolver: zodResolver(setupSchema),
|
||||||
|
@ -45,7 +45,7 @@ export function SetupForm({ defaultValues }: SetupFormProps) {
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (data) => {
|
onSubmit={form.handleSubmit(async (data) => {
|
||||||
await userSetupAction.executeAsync(data);
|
await completeSetup.executeAsync(data);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -108,8 +108,8 @@ export function SetupForm({ defaultValues }: SetupFormProps) {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{userSetupAction.result.serverError && (
|
{completeSetup.result.serverError && (
|
||||||
<FormMessage>{userSetupAction.result.serverError}</FormMessage>
|
<FormMessage>{completeSetup.result.serverError}</FormMessage>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
import { ActionError, authActionClient } from "@/features/safe-action/server";
|
import { ActionError, authActionClient } from "@/features/safe-action/server";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getUser } from "./queries";
|
import { getUser } from "./queries";
|
||||||
|
|
||||||
export const changeRoleAction = authActionClient
|
export const changeRoleAction = authActionClient
|
||||||
|
.metadata({ actionName: "change_role" })
|
||||||
.inputSchema(
|
.inputSchema(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
|
@ -47,11 +47,10 @@ export const changeRoleAction = authActionClient
|
||||||
role,
|
role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/control-panel");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteUserAction = authActionClient
|
export const deleteUserAction = authActionClient
|
||||||
|
.metadata({ actionName: "delete_user" })
|
||||||
.inputSchema(
|
.inputSchema(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
|
@ -86,8 +85,6 @@ export const deleteUserAction = authActionClient
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/control-panel");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue