diff --git a/.github/workflows/master_pantrydeploy.yml b/.github/workflows/master_pantrydeploy.yml index 920e5c4..65fe558 100644 --- a/.github/workflows/master_pantrydeploy.yml +++ b/.github/workflows/master_pantrydeploy.yml @@ -34,14 +34,14 @@ jobs: username: ${{ secrets.AzureAppService_ContainerUsername_28d39508e5734f558fc68a409d6d0fac }} password: ${{ secrets.AzureAppService_ContainerPassword_807a7babf8fc4bbab3814a0bd2d77375 }} - - name: Build and push container image to registry + - name: Build and push backend container image to registry uses: docker/build-push-action@v2 with: push: true tags: collectivereg.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_28d39508e5734f558fc68a409d6d0fac }}/pantry-backend:${{ github.sha }} file: ./backend/Dockerfile - - name: Build and push container image to registry + - name: Build and push frontend container image to registry uses: docker/build-push-action@v2 with: push: true diff --git a/apps/user-portal/components/ManageStripeButton.tsx b/apps/user-portal/components/ManageStripeButton.tsx index 90f58fc..c307d88 100644 --- a/apps/user-portal/components/ManageStripeButton.tsx +++ b/apps/user-portal/components/ManageStripeButton.tsx @@ -13,8 +13,8 @@ export function ManageStripeButton() { const router = useRouter(); const MANAGE_STRIPE_MUTATION = gql` - mutation MANAGE_STRIPE_MUTATION($userId: ID!, $returnUrl: String!) { - manageStripe(userId: $userId, returnUrl: $returnUrl) + mutation MANAGE_STRIPE_MUTATION($returnUrl: String!) { + manageStripe(returnUrl: $returnUrl) } `; @@ -32,7 +32,6 @@ export function ManageStripeButton() { const portalSession = await getPortalSession({ variables: { - userId: userSession.id, returnUrl: `${window.location.origin}/profile`, }, }); diff --git a/apps/user-portal/components/SubscribeButton.tsx b/apps/user-portal/components/SubscribeButton.tsx index 7068315..dae5d25 100644 --- a/apps/user-portal/components/SubscribeButton.tsx +++ b/apps/user-portal/components/SubscribeButton.tsx @@ -13,16 +13,8 @@ export function SubscribeButton({ ...props }) { const { variation, subscription, club } = props; const SUBSCRIPTION_MUTATION = gql` - mutation SUBSCRIPTION_MUTATION( - $variationId: ID! - $userId: ID! - $returnUrl: String! - ) { - membershipSignup( - userId: $userId - returnUrl: $returnUrl - variationId: $variationId - ) + mutation SUBSCRIPTION_MUTATION($variationId: ID!, $returnUrl: String!) { + membershipSignup(returnUrl: $returnUrl, variationId: $variationId) } `; @@ -66,7 +58,6 @@ export function SubscribeButton({ ...props }) { const session = await getStripeSession({ variables: { variationId: variation.id, - userId: userSession.id, returnUrl: `${window.location.origin}/${club}/${subscription}`, }, }); diff --git a/backend/access.ts b/backend/access.ts index 4af9351..da126a0 100644 --- a/backend/access.ts +++ b/backend/access.ts @@ -32,7 +32,16 @@ export const rules = { return true; } // 2. If not, do they own this item? - return { user: { id: session?.itemId } }; + return { user: { id: { equals: session?.itemId }} }; + }, + canManageSubscriptions({ session }: ListAccessArgs) { + + // 1. Do they have the permission of canManageVariations + if (permissions.canManageProducts({ session })) { + return true; + } + // 2. If not, do they own this item? + return false; }, canManageOrgs({ session }: ListAccessArgs) { if (!isSignedIn({ session })) { @@ -54,7 +63,7 @@ export const rules = { return true; } // 2. If not, do they own this item? - return { user: { id: session?.itemId } }; + return { user: { id: { equals: session?.itemId }} }; }, canManageOrderItems({ session }: ListAccessArgs) { if (!isSignedIn({ session })) { @@ -75,7 +84,7 @@ export const rules = { return true; // They can read everything! } // They should only see available products (based on the status field) - return { status: 'AVAILABLE' }; + return {status: {equals: 'active'} }; }, canManageUsers({ session }: ListAccessArgs) { if (!isSignedIn({ session })) { @@ -85,6 +94,6 @@ export const rules = { return true; } // Otherwise they may only update themselves! - return { id: session?.itemId }; + return { id: { equals: session?.itemId }}; }, }; diff --git a/backend/keystone.ts b/backend/keystone.ts index 9298a2b..afc0fc2 100644 --- a/backend/keystone.ts +++ b/backend/keystone.ts @@ -6,6 +6,8 @@ import { statelessSessions } from '@keystone-6/core/session'; import { createAuth } from '@opensaas/keystone-nextjs-auth'; import AzureB2C from '@opensaas/keystone-nextjs-auth/providers/azure-ad-b2c'; import { stripeHook } from './lib/stripe'; +import { permissionsList } from './schemas/roleFields'; + import express from 'express'; import url from 'url'; @@ -30,7 +32,7 @@ let sessionMaxAge = 60 * 60 * 24 * 30; // 30 days const auth = createAuth({ listKey: 'User', identityField: 'subjectId', - sessionData: `id name email memberships { id name status startDate renewalDate variation { id name subscription { id name }}}`, + sessionData: `id name email isAdmin role { ${permissionsList.join(' ')} } memberships { id name status startDate renewalDate variation { id name subscription { id name }}}`, autoCreate: true, userMap: { subjectId: 'id', email: 'email', name: 'name',}, accountMap: {}, @@ -58,7 +60,7 @@ export default auth.withAuth( 'postgres://postgres:mysecretpassword@localhost:55000', }, ui: { - isAccessAllowed: (context) => !!context.session?.data, + isAccessAllowed: (context) => context.session?.data?.isAdmin, }, lists, extendGraphqlSchema, diff --git a/backend/lib/stripe.ts b/backend/lib/stripe.ts index 3bf0dcb..7c8c96d 100644 --- a/backend/lib/stripe.ts +++ b/backend/lib/stripe.ts @@ -57,6 +57,7 @@ export async function stripeHook(req: Request, res: Response) { const context = (req as any).context as KeystoneContext; let data; let eventType; + const sudo = context.sudo(); // Check if webhook signing is configured. const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_1234567890123456789012345678901234567890'; @@ -83,13 +84,12 @@ export async function stripeHook(req: Request, res: Response) { } else { // Webhook signing is recommended, but if the secret is not configured in `config.js`, // retrieve the event data directly from the request body. - data = req.body.data; eventType = req.body.type; } switch (eventType) { case 'checkout.session.completed': - const membership = await context.query.Membership.findOne({ + const membership = await sudo.query.Membership.findOne({ where: { signupSessionId: data.object.id }, query: graphql` id @@ -98,7 +98,12 @@ export async function stripeHook(req: Request, res: Response) { } `, }); - await context.query.Membership.updateOne({ + + if (!membership) { + console.log('⚠️ No membership found for checkout.session.completed'); + return res.sendStatus(404); + } + await sudo.query.Membership.updateOne({ where: { id: membership.id }, data: { status: 'PAID', @@ -115,7 +120,7 @@ export async function stripeHook(req: Request, res: Response) { // This approach helps you avoid hitting rate limits. break; case 'invoice.payment_failed': - const failedMembership = await context.query.Membership.findOne({ + const failedMembership = await sudo.query.Membership.findOne({ where: { stripeSubscriptionId: data.object.subscription }, query: graphql` id @@ -124,7 +129,11 @@ export async function stripeHook(req: Request, res: Response) { } `, }); - await context.query.Membership.updateOne({ + if (!failedMembership.id) { + console.log('⚠️ No membership found for invoice.payment_failed'); + return res.sendStatus(404); + } + await sudo.query.Membership.updateOne({ where: { id: failedMembership.id }, data: { status: 'FAILED', @@ -138,6 +147,6 @@ export async function stripeHook(req: Request, res: Response) { default: // Unhandled event type } - + sudo.exitSudo; res.sendStatus(200); } diff --git a/backend/migrations/20220318084652_is_admin_flag/migration.sql b/backend/migrations/20220318084652_is_admin_flag/migration.sql new file mode 100644 index 0000000..ea3f6ce --- /dev/null +++ b/backend/migrations/20220318084652_is_admin_flag/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Role" ADD COLUMN "canManageProducts" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/migrations/20220321230524_add_status/migration.sql b/backend/migrations/20220321230524_add_status/migration.sql new file mode 100644 index 0000000..fba6a3c --- /dev/null +++ b/backend/migrations/20220321230524_add_status/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Subscription" ADD COLUMN "status" TEXT DEFAULT E'active'; + +-- AlterTable +ALTER TABLE "Variation" ADD COLUMN "status" TEXT DEFAULT E'active'; diff --git a/backend/mutations/index.ts b/backend/mutations/index.ts index ee1801d..1c39b97 100644 --- a/backend/mutations/index.ts +++ b/backend/mutations/index.ts @@ -9,7 +9,7 @@ const graphql = String.raw; export const extendGraphqlSchema = graphQLSchemaExtension({ typeDefs: graphql` type Mutation { - membershipSignup(variationId: ID!, userId: ID!, returnUrl: String!): JSON + membershipSignup(variationId: ID!, returnUrl: String!): JSON customSignup( email: String! name: String! @@ -22,7 +22,7 @@ export const extendGraphqlSchema = graphQLSchemaExtension({ createUser: Boolean suburb: String ): JSON - stripeManage(userId: ID!, returnUrl: String!): JSON + stripeManage(returnUrl: String!): JSON } `, resolvers: { diff --git a/backend/mutations/membershipSignup.ts b/backend/mutations/membershipSignup.ts index a57e454..f46c2d5 100644 --- a/backend/mutations/membershipSignup.ts +++ b/backend/mutations/membershipSignup.ts @@ -12,9 +12,14 @@ const graphql = String.raw; async function membershipSignup( root: any, - { variationId, userId, returnUrl }: Arguments, + { variationId, returnUrl }: Arguments, context: KeystoneContext ) { + const userId = context.session?.itemId; + if (!userId) { + console.log("No user signed in"); + return {error: "No user signed in"}; + }; // get the subscription from the id const variation = await context.query.Variation.findOne({ where: { id: variationId }, diff --git a/backend/mutations/stripeManage.ts b/backend/mutations/stripeManage.ts index 7d73569..4179ae6 100644 --- a/backend/mutations/stripeManage.ts +++ b/backend/mutations/stripeManage.ts @@ -11,9 +11,14 @@ const graphql = String.raw; async function stripeManage( root: any, - { userId, returnUrl }: Arguments, + { returnUrl }: Arguments, context: KeystoneContext ) { + const userId = context.session?.itemId; + if (!userId) { + console.log("No user signed in"); + return {error: "No user signed in"}; + }; const user = await context.query.User.findOne({ where: { id: userId }, query: graphql` diff --git a/backend/schema.graphql b/backend/schema.graphql index ab9355a..0e71d7e 100644 --- a/backend/schema.graphql +++ b/backend/schema.graphql @@ -2,7 +2,7 @@ # Modify your Keystone config when you want to change this. type Mutation { - membershipSignup(variationId: ID!, userId: ID!, returnUrl: String!): JSON + membershipSignup(variationId: ID!, returnUrl: String!): JSON customSignup( email: String! name: String! @@ -15,7 +15,7 @@ type Mutation { createUser: Boolean suburb: String ): JSON - stripeManage(userId: ID!, returnUrl: String!): JSON + stripeManage(returnUrl: String!): JSON createClub(data: ClubCreateInput!): Club createClubs(data: [ClubCreateInput!]!): [Club] updateClub(where: ClubWhereUniqueInput!, data: ClubUpdateInput!): Club @@ -390,6 +390,7 @@ type Variation { id: ID! name: String subscription: Subscription + status: String memberships( where: MembershipWhereInput! = {} orderBy: [MembershipOrderByInput!]! = [] @@ -421,6 +422,7 @@ input VariationWhereInput { id: IDFilter name: StringFilter subscription: SubscriptionWhereInput + status: StringNullableFilter memberships: MembershipManyRelationFilter price: IntFilter chargeInterval: StringFilter @@ -460,6 +462,7 @@ input IntNullableFilter { input VariationOrderByInput { id: OrderDirection name: OrderDirection + status: OrderDirection price: OrderDirection chargeInterval: OrderDirection chargeIntervalCount: OrderDirection @@ -470,6 +473,7 @@ input VariationOrderByInput { input VariationUpdateInput { name: String subscription: SubscriptionRelateToOneForUpdateInput + status: String memberships: MembershipRelateToManyForUpdateInput price: Int about: JSON @@ -500,6 +504,7 @@ input VariationUpdateArgs { input VariationCreateInput { name: String subscription: SubscriptionRelateToOneForCreateInput + status: String memberships: MembershipRelateToManyForCreateInput price: Int about: JSON @@ -537,6 +542,7 @@ type Subscription { autoRenew: Boolean about: Subscription_about_Document stripeProductId: String + status: String } type Subscription_about_Document { @@ -563,6 +569,7 @@ input SubscriptionWhereInput { slug: StringFilter autoRenew: BooleanFilter stripeProductId: StringFilter + status: StringNullableFilter } input VariationManyRelationFilter { @@ -580,6 +587,7 @@ input SubscriptionOrderByInput { slug: OrderDirection autoRenew: OrderDirection stripeProductId: OrderDirection + status: OrderDirection } input SubscriptionUpdateInput { @@ -593,6 +601,7 @@ input SubscriptionUpdateInput { autoRenew: Boolean about: JSON stripeProductId: String + status: String } input ClubRelateToOneForUpdateInput { @@ -624,6 +633,7 @@ input SubscriptionCreateInput { autoRenew: Boolean about: JSON stripeProductId: String + status: String } input ClubRelateToOneForCreateInput { @@ -727,6 +737,7 @@ input TagRelateToManyForCreateInput { type Role { id: ID! name: String + canManageProducts: Boolean canManageClubs: Boolean canSeeOtherUsers: Boolean canManageUsers: Boolean @@ -752,6 +763,7 @@ input RoleWhereInput { NOT: [RoleWhereInput!] id: IDFilter name: StringFilter + canManageProducts: BooleanFilter canManageClubs: BooleanFilter canSeeOtherUsers: BooleanFilter canManageUsers: BooleanFilter @@ -770,6 +782,7 @@ input UserManyRelationFilter { input RoleOrderByInput { id: OrderDirection name: OrderDirection + canManageProducts: OrderDirection canManageClubs: OrderDirection canSeeOtherUsers: OrderDirection canManageUsers: OrderDirection @@ -780,6 +793,7 @@ input RoleOrderByInput { input RoleUpdateInput { name: String + canManageProducts: Boolean canManageClubs: Boolean canSeeOtherUsers: Boolean canManageUsers: Boolean @@ -803,6 +817,7 @@ input RoleUpdateArgs { input RoleCreateInput { name: String + canManageProducts: Boolean canManageClubs: Boolean canSeeOtherUsers: Boolean canManageUsers: Boolean @@ -876,6 +891,7 @@ type User { skip: Int! = 0 ): [Post!] postsCount(where: PostWhereInput! = {}): Int + isAdmin: Boolean role: Role householdMembers: JSON stripeCustomerId: String @@ -905,6 +921,7 @@ input UserWhereInput { preferredName: StringFilter phone: StringFilter posts: PostManyRelationFilter + isAdmin: BooleanFilter role: RoleWhereInput stripeCustomerId: StringFilter memberships: MembershipManyRelationFilter @@ -917,6 +934,7 @@ input UserOrderByInput { subjectId: OrderDirection preferredName: OrderDirection phone: OrderDirection + isAdmin: OrderDirection stripeCustomerId: OrderDirection } @@ -927,6 +945,7 @@ input UserUpdateInput { preferredName: String phone: String posts: PostRelateToManyForUpdateInput + isAdmin: Boolean role: RoleRelateToOneForUpdateInput householdMembers: JSON stripeCustomerId: String @@ -951,6 +970,7 @@ input UserCreateInput { preferredName: String phone: String posts: PostRelateToManyForCreateInput + isAdmin: Boolean role: RoleRelateToOneForCreateInput householdMembers: JSON stripeCustomerId: String diff --git a/backend/schema.prisma b/backend/schema.prisma index b5f42bf..29d4506 100644 --- a/backend/schema.prisma +++ b/backend/schema.prisma @@ -45,6 +45,7 @@ model Variation { name String @default("") subscription Subscription? @relation("Variation_subscription", fields: [subscriptionId], references: [id]) subscriptionId String? @map("subscription") + status String? @default("active") memberships Membership[] @relation("Membership_variation") price Int about Json @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") @@ -69,6 +70,7 @@ model Subscription { autoRenew Boolean @default(false) about Json @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") stripeProductId String @unique @default("") + status String? @default("active") @@index([clubId]) } @@ -92,6 +94,7 @@ model Post { model Role { id String @id @default(cuid()) name String @default("") + canManageProducts Boolean @default(false) canManageClubs Boolean @default(false) canSeeOtherUsers Boolean @default(false) canManageUsers Boolean @default(false) @@ -115,6 +118,7 @@ model User { preferredName String @default("") phone String @default("") posts Post[] @relation("Post_author") + isAdmin Boolean @default(false) role Role? @relation("User_role", fields: [roleId], references: [id]) roleId String? @map("role") householdMembers Json? diff --git a/backend/schemas/Membership.ts b/backend/schemas/Membership.ts index 8405c20..ff73031 100644 --- a/backend/schemas/Membership.ts +++ b/backend/schemas/Membership.ts @@ -18,7 +18,11 @@ export const Membership = list({ create: () => true, // only people with the permission can delete themselves! // You can't delete yourself - delete: permissions.canManageUsers, + delete: permissions.canManageProducts, + }, + filter: { + update: rules.canManageProducts, + query: rules.canManageProducts, }, }, hooks: { diff --git a/backend/schemas/Role.ts b/backend/schemas/Role.ts index f0fbcb9..9e801a0 100644 --- a/backend/schemas/Role.ts +++ b/backend/schemas/Role.ts @@ -5,6 +5,13 @@ import { permissionFields } from './roleFields'; export const Role = list({ + access: { + operation: { + create: permissions.canManageRoles, + delete: permissions.canManageRoles, + update: permissions.canManageRoles, + } + }, fields: { name: text({ validation: { isRequired: true }}), ...permissionFields, diff --git a/backend/schemas/Subscription.ts b/backend/schemas/Subscription.ts index 31de43e..fc82b07 100644 --- a/backend/schemas/Subscription.ts +++ b/backend/schemas/Subscription.ts @@ -12,6 +12,17 @@ import { document } from "@keystone-6/fields-document"; import stripeConfig from "../lib/stripe"; export const Subscription = list({ + access: { + operation: { + create: permissions.canManageProducts, + delete: permissions.canManageProducts, + update: permissions.canManageProducts, + }, + filter: { + update: rules.canManageSubscriptions, + query: rules.canReadProducts + }, + }, hooks: { resolveInput: async ({ resolvedData, item }) => { // If the subscription is being created and no stripeProductId is provided, create a new stripe product @@ -23,6 +34,16 @@ export const Subscription = list({ } return resolvedData; }, + afterOperation: async ({ listKey, operation, resolvedData, context }) => { + // Update Stripe Product if the subscription is being updated + if (operation === "update") { + await stripeConfig.products.update( + resolvedData.stripeProductId, { + name: resolvedData.name, + } + ); + } + }, }, fields: { name: text({ validation: { isRequired: true } }), @@ -66,5 +87,12 @@ export const Subscription = list({ stripeProductId: text({ isIndexed: "unique", }), + status: select({ + options: [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + ], + defaultValue: "active", + }), }, }); diff --git a/backend/schemas/User.ts b/backend/schemas/User.ts index df83c69..4f830a4 100644 --- a/backend/schemas/User.ts +++ b/backend/schemas/User.ts @@ -1,7 +1,10 @@ import { list } from '@keystone-6/core'; -import { text, relationship, json } from '@keystone-6/core/fields'; +import { text, relationship, json, checkbox } from '@keystone-6/core/fields'; import stripeConfig from '../lib/stripe'; +import { rules, isSignedIn, permissions } from "../access"; + + export const User = list({ hooks: { resolveInput: async ({ resolvedData, item }) => { @@ -19,6 +22,16 @@ export const User = list({ return resolvedData; }, }, + access: { + operation: { + create: () => true, + delete: permissions.canManageUsers, + }, + filter: { + update: rules.canManageUsers, + query: rules.canManageUsers, + }, + }, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: true }), @@ -26,7 +39,19 @@ export const User = list({ preferredName: text(), phone: text(), posts: relationship({ ref: 'Post.author', many: true }), + isAdmin: checkbox({ + access: { + update: permissions.canManageUsers, + create: permissions.canManageUsers, + }, + defaultValue: false, + label: 'User can access admin portal', + }), role: relationship({ + access: { + update: permissions.canManageUsers, + create: permissions.canManageUsers, + }, ref: 'Role.assignedTo', many: false, }), diff --git a/backend/schemas/Variation.ts b/backend/schemas/Variation.ts index e0c6571..b3b11de 100644 --- a/backend/schemas/Variation.ts +++ b/backend/schemas/Variation.ts @@ -13,6 +13,16 @@ import stripeConfig from "../lib/stripe"; export const Variation = list({ hooks: { + afterOperation: async ({ listKey, operation, resolvedData, context }) => { + // Update Stripe Price if the variation is being updated + if (operation === "update") { + const active = resolvedData.status === 'active' ? true : false; + await stripeConfig.prices.update( + resolvedData.stripePriceId, { + active + }); + } + }, resolveInput: async ({ resolvedData, item, context }) => { // If the User is being created and no stripeCutomerId is provided create the stripe customer if (!resolvedData.stripePriceId && !item?.stripePriceId) { @@ -23,11 +33,15 @@ export const Variation = list({ id stripeProductId`, }); + const active = resolvedData.status === 'active' || item?.active === 'active' ? true : false; const stripeProductId = subscription.stripeProductId; + const unitPriceDollars = resolvedData.price || item?.price; + const unitPriceCents = unitPriceDollars * 100; const price = await stripeConfig.prices.create({ product: stripeProductId, + active: active, currency: "aud", - unit_amount: resolvedData.price || item?.price, + unit_amount: unitPriceCents, recurring: { interval: resolvedData.chargeInterval || item?.chargeInterval, interval_count: @@ -61,7 +75,8 @@ export const Variation = list({ update: permissions.canManageProducts, }, filter: { - update: rules.canManageProducts, + update: rules.canManageSubscriptions, + query: rules.canReadProducts }, }, fields: { @@ -70,11 +85,20 @@ export const Variation = list({ ref: "Subscription.variations", many: false, }), + status: select({ + options: [ + { value: "active", label: "Active"}, + { value: "inactive", label: "Inactive"} ], + defaultValue: "active", + }), memberships: relationship({ ref: "Membership.variation", many: true, }), price: integer({ + access: { + update: () => false, + }, validation: { isRequired: true, }, @@ -92,6 +116,9 @@ export const Variation = list({ dividers: true, }), chargeInterval: select({ + access: { + update: () => false, + }, options: [ { value: "day", label: "Day" }, { value: "week", label: "Week" }, @@ -103,12 +130,18 @@ export const Variation = list({ }, }), chargeIntervalCount: integer({ + access: { + update: () => false, + }, validation: { isRequired: true, }, }), totalCount: integer(), stripePriceId: text({ + access: { + update: () => false, + }, isIndexed: "unique", }), }, diff --git a/backend/schemas/roleFields.ts b/backend/schemas/roleFields.ts index f515c1b..e6ccd2b 100644 --- a/backend/schemas/roleFields.ts +++ b/backend/schemas/roleFields.ts @@ -1,6 +1,10 @@ import { checkbox } from '@keystone-6/core/fields'; export const permissionFields = { + canManageProducts: checkbox({ + label: 'Can manage Variatons and Subscriptions', + defaultValue: false, + }), canManageClubs: checkbox({ defaultValue: false, label: 'User can see and manage add Clubs', diff --git a/backend/types.ts b/backend/types.ts index b039608..7792e2a 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -33,6 +33,6 @@ export type AccessControl = { }; export type ListAccessArgs = { - itemId?: string; + listKey?: string; session?: Session; };