diff --git a/apps/web/src/app/api/stripe/webhook/handlers/checkout/completed.ts b/apps/web/src/app/api/stripe/webhook/handlers/checkout/completed.ts index 1b560dd1a..13d198d51 100644 --- a/apps/web/src/app/api/stripe/webhook/handlers/checkout/completed.ts +++ b/apps/web/src/app/api/stripe/webhook/handlers/checkout/completed.ts @@ -2,8 +2,8 @@ import type { Stripe } from "@rallly/billing"; import { stripe } from "@rallly/billing"; import { posthog } from "@rallly/posthog/server"; import { env } from "@/env"; -import { licensingClient } from "@/features/licensing/client"; import { licenseCheckoutMetadataSchema } from "@/features/licensing/schema"; +import { licenseManager } from "@/features/licensing/server"; import { subscriptionCheckoutMetadataSchema } from "@/features/subscription/schema"; import { getEmailClient } from "@/utils/emails"; @@ -61,7 +61,7 @@ async function handleSelfHostedCheckoutSessionCompleted( ); } - const license = await licensingClient.createLicense({ + const license = await licenseManager.createLicense({ type: licenseType, licenseeEmail: email, licenseeName: customerDetails.name ?? undefined, diff --git a/apps/web/src/features/licensing/actions.ts b/apps/web/src/features/licensing/actions.ts new file mode 100644 index 000000000..88e1e1afb --- /dev/null +++ b/apps/web/src/features/licensing/actions.ts @@ -0,0 +1,24 @@ +"use server"; + +import { prisma } from "@rallly/database"; +import { adminActionClient } from "@/features/safe-action/server"; + +export const removeInstanceLicenseAction = adminActionClient + .metadata({ + actionName: "remove_instance_license", + }) + .action(async () => { + try { + await prisma.instanceLicense.deleteMany(); + } catch (_error) { + return { + success: false, + message: "Failed to delete license", + }; + } + + return { + success: true, + message: "License deleted successfully", + }; + }); diff --git a/apps/web/src/features/licensing/actions/validate-license.ts b/apps/web/src/features/licensing/actions/validate-license.ts index df2532486..135466dc8 100644 --- a/apps/web/src/features/licensing/actions/validate-license.ts +++ b/apps/web/src/features/licensing/actions/validate-license.ts @@ -2,7 +2,7 @@ import { prisma } from "@rallly/database"; import { rateLimit } from "@/features/rate-limit"; -import { licensingClient } from "../client"; +import { licenseManager } from "../server"; export async function validateLicenseKey(key: string) { const { success } = await rateLimit("validate_license_key", 10, "1 m"); @@ -14,7 +14,7 @@ export async function validateLicenseKey(key: string) { }; } - const { data } = await licensingClient.validateLicenseKey({ + const { data } = await licenseManager.validateLicenseKey({ key, }); diff --git a/apps/web/src/features/licensing/client.ts b/apps/web/src/features/licensing/client.ts deleted file mode 100644 index cc999e185..000000000 --- a/apps/web/src/features/licensing/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { env } from "@/env"; -import { LicensingClient } from "./lib/licensing-client"; - -export const licensingClient = new LicensingClient({ - apiUrl: env.LICENSE_API_URL, - authToken: env.LICENSE_API_AUTH_TOKEN, -}); diff --git a/apps/web/src/features/licensing/components/remove-license-button.tsx b/apps/web/src/features/licensing/components/remove-license-button.tsx index eb462804b..dd55b5b25 100644 --- a/apps/web/src/features/licensing/components/remove-license-button.tsx +++ b/apps/web/src/features/licensing/components/remove-license-button.tsx @@ -14,15 +14,15 @@ import { } from "@rallly/ui/dialog"; import { Icon } from "@rallly/ui/icon"; import { XIcon } from "lucide-react"; -import { useRouter } from "next/navigation"; import { useTransition } from "react"; import { Trans } from "@/components/trans"; -import { removeInstanceLicense } from "../mutations"; +import { useSafeAction } from "@/features/safe-action/client"; +import { removeInstanceLicenseAction } from "../actions"; export function RemoveLicenseButton() { const [isPending, startTransition] = useTransition(); - const router = useRouter(); const dialog = useDialog(); + const removeInstanceLicense = useSafeAction(removeInstanceLicenseAction); return ( @@ -56,8 +56,7 @@ export function RemoveLicenseButton() { variant="destructive" onClick={() => startTransition(async () => { - await removeInstanceLicense(); - router.refresh(); + await removeInstanceLicense.executeAsync(); dialog.dismiss(); }) } diff --git a/apps/web/src/features/licensing/index.ts b/apps/web/src/features/licensing/index.ts deleted file mode 100644 index d40bfc15c..000000000 --- a/apps/web/src/features/licensing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LicensingClient } from "./lib/licensing-client"; diff --git a/apps/web/src/features/licensing/lib/licensing-client.ts b/apps/web/src/features/licensing/lib/licensing-manager.ts similarity index 97% rename from apps/web/src/features/licensing/lib/licensing-client.ts rename to apps/web/src/features/licensing/lib/licensing-manager.ts index 68b329379..8ae0adb00 100644 --- a/apps/web/src/features/licensing/lib/licensing-client.ts +++ b/apps/web/src/features/licensing/lib/licensing-manager.ts @@ -7,7 +7,7 @@ import { validateLicenseKeyResponseSchema, } from "../schema"; -export class LicensingClient { +export class LicenseManager { apiUrl: string; authToken?: string; diff --git a/apps/web/src/features/licensing/server.ts b/apps/web/src/features/licensing/server.ts new file mode 100644 index 000000000..bc56d19ca --- /dev/null +++ b/apps/web/src/features/licensing/server.ts @@ -0,0 +1,7 @@ +import { env } from "@/env"; +import { LicenseManager } from "./lib/licensing-manager"; + +export const licenseManager = new LicenseManager({ + apiUrl: env.LICENSE_API_URL, + authToken: env.LICENSE_API_AUTH_TOKEN, +}); diff --git a/apps/web/src/features/safe-action/server.ts b/apps/web/src/features/safe-action/server.ts index 5719db68a..37fcfaed6 100644 --- a/apps/web/src/features/safe-action/server.ts +++ b/apps/web/src/features/safe-action/server.ts @@ -88,3 +88,14 @@ export const authActionClient = actionClient }); }) .use(posthogMiddleware); + +export const adminActionClient = authActionClient.use(async ({ ctx, next }) => { + if (ctx.user.role !== "admin") { + throw new ActionError({ + code: "FORBIDDEN", + message: "You do not have permission to perform this action.", + }); + } + + return next(); +});