fix(webapp): org scoping issues in plan selection, alerts, pats and usage (#2549)

* fix: org scoping in the select plan flow

Adds proper org scoping in the loader and action in the plans page.

* Fix billing alerts scope

* Fix org usage page scope

* Fix token revoking flow scope check

* Throw error for failed PAT revokes instead of silent failure
This commit is contained in:
Saadi Myftija
2025-09-25 19:17:51 +02:00
committed by GitHub
parent a3cea1302e
commit 69d52db856
6 changed files with 32 additions and 27 deletions
@@ -47,7 +47,7 @@ export const meta: MetaFunction = () => {
};
export async function loader({ params, request }: LoaderFunctionArgs) {
await requireUserId(request);
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const { isManagedCloud } = featuresForRequest(request);
@@ -55,8 +55,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
return redirect(organizationPath({ slug: organizationSlug }));
}
const organization = await prisma.organization.findUnique({
where: { slug: organizationSlug },
const organization = await prisma.organization.findFirst({
where: { slug: organizationSlug, members: { some: { userId } } },
});
if (!organization) {
@@ -28,7 +28,7 @@ export const meta: MetaFunction = () => {
};
export async function loader({ params, request }: LoaderFunctionArgs) {
await requireUserId(request);
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const { isManagedCloud } = featuresForRequest(request);
@@ -41,8 +41,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
throw new Response(null, { status: 404, statusText: "Plans not found" });
}
const organization = await prisma.organization.findUnique({
where: { slug: organizationSlug },
const organization = await prisma.organization.findFirst({
where: { slug: organizationSlug, members: { some: { userId } } },
});
if (!organization) {
@@ -46,7 +46,7 @@ export const meta: MetaFunction = () => {
};
export async function loader({ params, request }: LoaderFunctionArgs) {
await requireUserId(request);
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const { isManagedCloud } = featuresForRequest(request);
@@ -54,8 +54,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
return redirect(organizationPath({ slug: organizationSlug }));
}
const organization = await prisma.organization.findUnique({
where: { slug: organizationSlug },
const organization = await prisma.organization.findFirst({
where: { slug: organizationSlug, members: { some: { userId } } },
});
if (!organization) {
@@ -3,8 +3,8 @@ import { parse } from "@conform-to/zod";
import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid";
import { ShieldExclamationIcon } from "@heroicons/react/24/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import { Form, MetaFunction, useActionData, useFetcher } from "@remix-run/react";
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { Form, type MetaFunction, useActionData, useFetcher } from "@remix-run/react";
import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
@@ -16,7 +16,6 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
@@ -36,8 +35,8 @@ import {
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { redirectWithSuccessMessage } from "~/models/message.server";
import {
CreatedPersonalAccessToken,
ObfuscatedPersonalAccessToken,
type CreatedPersonalAccessToken,
type ObfuscatedPersonalAccessToken,
createPersonalAccessToken,
getValidPersonalAccessTokens,
revokePersonalAccessToken,
@@ -53,7 +52,7 @@ export const meta: MetaFunction = () => {
];
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
try {
@@ -113,7 +112,7 @@ export const action: ActionFunction = async ({ request }) => {
}
case "revoke": {
try {
await revokePersonalAccessToken(submission.value.tokenId);
await revokePersonalAccessToken(submission.value.tokenId, userId);
return redirectWithSuccessMessage(
personalAccessTokensPath(),
@@ -125,6 +124,7 @@ export const action: ActionFunction = async ({ request }) => {
}
}
default: {
submission.value satisfies never;
return json({ errors: { body: "Invalid action" } }, { status: 400 });
}
}
@@ -7,16 +7,16 @@ import {
} from "@heroicons/react/20/solid";
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { Form, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs } from "@remix-run/server-runtime";
import { type ActionFunctionArgs } from "@remix-run/server-runtime";
import { uiComponent } from "@team-plain/typescript-sdk";
import { GitHubLightIcon } from "@trigger.dev/companyicons";
import {
FreePlanDefinition,
Limits,
PaidPlanDefinition,
Plans,
SetPlanBody,
SubscriptionResult,
type FreePlanDefinition,
type Limits,
type PaidPlanDefinition,
type Plans,
type SetPlanBody,
type SubscriptionResult,
} from "@trigger.dev/platform";
import React, { useEffect, useState } from "react";
import { z } from "zod";
@@ -75,8 +75,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
message: message || undefined,
});
const organization = await prisma.organization.findUnique({
where: { slug: organizationSlug },
const organization = await prisma.organization.findFirst({
where: { slug: organizationSlug, members: { some: { userId: user.id } } },
});
if (!organization) {
@@ -79,15 +79,20 @@ export async function getPersonalAccessTokenFromAuthorizationCode(authorizationC
};
}
export async function revokePersonalAccessToken(tokenId: string) {
await prisma.personalAccessToken.update({
export async function revokePersonalAccessToken(tokenId: string, userId: string) {
const result = await prisma.personalAccessToken.updateMany({
where: {
id: tokenId,
userId,
},
data: {
revokedAt: new Date(),
},
});
if (result.count === 0) {
throw new Error("PAT not found or already revoked");
}
}
export type PersonalAccessTokenAuthenticationResult = {