Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stage/pubtype restrictions to pub read api #988

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
76 changes: 67 additions & 9 deletions core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,12 @@ const getAuthorization = async () => {
return {
user,
authorization: rules.reduce((acc, curr) => {
const { scope, constraints, accessType } = curr;
if (!constraints) {
acc[scope][accessType] = true;
if (!curr.constraints) {
acc[curr.scope][curr.accessType] = true;
return acc;
}

acc[scope][accessType] = constraints ?? true;
acc[curr.scope][curr.accessType] = curr.constraints ?? true;
return acc;
}, baseAuthorizationObject),
apiAccessTokenId: validatedAccessToken.id,
Expand Down Expand Up @@ -245,7 +244,7 @@ const handler = createNextHandler(
},

get: async ({ params, query }) => {
const { user, community } = await checkAuthorization({
const { user, community, authorization } = await checkAuthorization({
token: { scope: ApiAccessScope.pub, type: ApiAccessType.read },
cookies: {
capability: Capabilities.viewPub,
Expand All @@ -262,25 +261,84 @@ const handler = createNextHandler(
query
);

if (typeof authorization === "object") {
const allowedStages = authorization.stages;
if (
pub.stageId &&
allowedStages &&
allowedStages.length > 0 &&
!allowedStages.includes(pub.stageId)
) {
throw new ForbiddenError(
`You are not authorized to view this pub in stage ${pub.stageId}`
);
}

const allowedPubTypes = authorization.pubTypes;
if (
allowedPubTypes &&
allowedPubTypes.length > 0 &&
!allowedPubTypes.includes(pub.pubTypeId)
) {
throw new ForbiddenError(
`You are not authorized to view this pub in pub type ${pub.pubTypeId}`
);
}
}

return {
status: 200,
body: pub,
};
},
getMany: async ({ query }) => {
const { user, community } = await checkAuthorization({
const { user, community, authorization } = await checkAuthorization({
token: { scope: ApiAccessScope.pub, type: ApiAccessType.read },
// TODO: figure out capability here
cookies: false,
});

const { pubTypeId, stageId, ...rest } = query;
const allowedPubTypes =
typeof authorization === "object" ? authorization.pubTypes : undefined;
const allowedStages =
typeof authorization === "object" ? authorization.stages : undefined;

let { pubTypeId, stageId, pubIds, ...rest } = query;

const requestedPubTypes = pubTypeId
? Array.isArray(pubTypeId)
? pubTypeId
: [pubTypeId]
: undefined;
const requestedStages = stageId
? Array.isArray(stageId)
? stageId
: [stageId]
: undefined;
const requestedPubIds = pubIds
? Array.isArray(pubIds)
? pubIds
: [pubIds]
: undefined;

const pubTypes =
requestedPubTypes?.length && allowedPubTypes?.length
? requestedPubTypes.filter((pubType) => allowedPubTypes?.includes(pubType))
: allowedPubTypes && allowedPubTypes.length > 0
? allowedPubTypes
: requestedPubTypes;

const stages =
requestedStages?.length && allowedStages?.length
? requestedStages.filter((stage) => allowedStages?.includes(stage))
: (allowedStages ?? requestedStages);

const pubs = await getPubsWithRelatedValuesAndChildren(
{
communityId: community.id,
pubTypeId,
stageId,
pubTypeId: pubTypes,
stageId: stages,
pubIds: requestedPubIds,
userId: user.id,
},
rest
Expand Down
33 changes: 29 additions & 4 deletions core/app/c/[communitySlug]/settings/tokens/CreateTokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import type { CreateTokenFormContext } from "db/types";
import type { CreateTokenFormContext as CreateTokenFormContextType } from "db/types";
import { ApiAccessScope, apiAccessTokensInitializerSchema } from "db/public";
import { permissionsSchema } from "db/types";
import { Button } from "ui/button";
Expand All @@ -19,6 +19,7 @@ import { Separator } from "ui/separator";

import { useServerAction } from "~/lib/serverActions";
import * as actions from "./actions";
import { CreateTokenFormContext } from "./CreateTokenFormContext";
import { PermissionField } from "./PermissionField";

export const createTokenFormSchema = apiAccessTokensInitializerSchema
Expand All @@ -27,6 +28,7 @@ export const createTokenFormSchema = apiAccessTokensInitializerSchema
issuedById: true,
})
.extend({
name: z.string().min(1, "Name is required").max(255, "Name is too long"),
description: z.string().max(255).optional(),
token: apiAccessTokensInitializerSchema.shape.token.optional(),
expiration: z
Expand Down Expand Up @@ -58,10 +60,12 @@ export const createTokenFormSchema = apiAccessTokensInitializerSchema
export type CreateTokenFormSchema = z.infer<typeof createTokenFormSchema>;
export type CreateTokenForm = ReturnType<typeof useForm<CreateTokenFormSchema>>;

export const CreateTokenForm = ({ context }: { context: CreateTokenFormContext }) => {
export const CreateTokenForm = () => {
const form = useForm<CreateTokenFormSchema>({
resolver: zodResolver(createTokenFormSchema),
defaultValues: {
name: "",
description: "",
// default to 1 day from now, mostly to make testing easier
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24),
},
Expand Down Expand Up @@ -140,7 +144,6 @@ export const CreateTokenForm = ({ context }: { context: CreateTokenFormContext }
key={scope}
name={scope}
form={form}
context={context}
prettyName={`${scope[0].toUpperCase()}${scope.slice(1)}`}
/>
</React.Fragment>
Expand All @@ -165,7 +168,7 @@ export const CreateTokenForm = ({ context }: { context: CreateTokenFormContext }
<Button
type="submit"
className="justify-self-end"
disabled={!form.formState.isValid}
// disabled={!form.formState.isValid}
data-testid="create-token-button"
>
Create Token
Expand Down Expand Up @@ -194,3 +197,25 @@ export const CreateTokenForm = ({ context }: { context: CreateTokenFormContext }
</Form>
);
};

/**
* Exported here instead of just importing CreateTokenFormContext.tsx
* in page.tsx, because doing so triggers the following strange error
*
* React.jsx: type is invalid -- expected a string (for built-in components)
* or a class/function (for composite components) but got: undefined.
* You likely forgot to export your component from the file it's defined in,
* or you might have mixed up default and named imports
*/
export const CreateTokenFormWithContext = ({ stages, pubTypes }: CreateTokenFormContextType) => {
return (
<CreateTokenFormContext.Provider
value={{
stages,
pubTypes,
}}
>
<CreateTokenForm />
</CreateTokenFormContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { createContext } from "react";

import type { CreateTokenFormContext as CreateTokenFormContextType } from "db/types";
import { NO_STAGE_OPTION } from "db/types";

export const CreateTokenFormContext = createContext<CreateTokenFormContextType>({
stages: {
stages: [],
allOptions: [NO_STAGE_OPTION],
allValues: [NO_STAGE_OPTION.value],
},
pubTypes: {
pubTypes: [],
allOptions: [],
allValues: [],
},
});
Loading
Loading