diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index efcaca00e0..f441f81038 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -8081,22 +8081,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "isOperator", - "description": "Is the tenant an Operator tenant.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "publicName", "description": "Public name for the tenant.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index fafd95ffab..19e86bd077 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -10,7 +10,6 @@ import { GraphQLErrorCode } from '../errors' import { Tenant } from '../../tenants/model' import { Pagination, SortOrder } from '../../shared/baseModel' import { getPageInfo } from '../../shared/pagination' -import { Config } from '../../config/app' export const whoami: QueryResolvers['whoami'] = async ( parent, @@ -175,7 +174,6 @@ export function tenantToGraphQl(tenant: Tenant): SchemaTenant { createdAt: new Date(+tenant.createdAt).toISOString(), deletedAt: tenant.deletedAt ? new Date(+tenant.deletedAt).toISOString() - : null, - isOperator: tenant.apiSecret === Config.adminApiSecret + : null } } diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index b73e960170..78218b01a1 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1551,8 +1551,6 @@ type Tenant implements Model { createdAt: String! "The date and time that this tenant was deleted." deletedAt: String - "Is the tenant an Operator tenant." - isOperator: Boolean! } type TenantsConnection { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 2c4d6669c4..4668c59a95 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2932,7 +2929,7 @@ export type ListTenantsQueryVariables = Exact<{ }>; -export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; export type CreateTenantMutationVariables = Exact<{ input: CreateTenantInput; @@ -2960,7 +2957,7 @@ export type GetTenantQueryVariables = Exact<{ }>; -export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, isOperator: boolean } }; +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }; export type WhoAmIQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/packages/frontend/app/lib/api/tenant.server.ts b/packages/frontend/app/lib/api/tenant.server.ts index b4139b4b1f..9e01d9d8b2 100644 --- a/packages/frontend/app/lib/api/tenant.server.ts +++ b/packages/frontend/app/lib/api/tenant.server.ts @@ -41,7 +41,6 @@ export const listTenants = async (request: Request, args: QueryTenantsArgs) => { publicName createdAt deletedAt - isOperator } } pageInfo { @@ -161,7 +160,6 @@ export const getTenantInfo = async ( publicName createdAt deletedAt - isOperator } } `, diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index d15668419f..973d8bcc5b 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -132,19 +132,10 @@ export const updateTenantSchema = z .object({ apiSecret: z .string() - .min(10, { message: 'API Secret should be at least 3 characters long' }) - .max(255, { message: 'Maximum length of API Secret is 255 characters' }) - .regex( - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, - { message: 'API Secret should be Base64 encoded.' } - ), + .min(10, { message: 'API Secret should be at least 10 characters long' }) + .max(255, { message: 'Maximum length of API Secret is 255 characters' }), publicName: z.string().optional(), - email: z - .string() - .regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, { - message: 'Invalid email address.' - }) - .optional(), + email: z.string().optional(), idpConsentUrl: z.string().optional(), idpSecret: z.string().optional() }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index cb96a5efb6..240503849b 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -31,7 +31,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { await checkAuthAndRedirect(request.url, cookies) const tenantId = params.tenantId - const result = z.string().uuid().safeParse(tenantId) if (!result.success) { throw json(null, { status: 400, statusText: 'Invalid tenant ID.' }) @@ -42,13 +41,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { throw json(null, { status: 404, statusText: 'Tenant not found.' }) const me = await whoAmI(request) - const isOperator = me.isOperator - - return json({ tenant, isOperator }) + return json({ tenant, me }) } export default function ViewTenantPage() { - const { tenant, isOperator } = useLoaderData() + const { tenant, me } = useLoaderData() const response = useActionData() const navigation = useNavigation() const [formData, setFormData] = useState() @@ -98,12 +95,6 @@ export default function ViewTenantPage() { disabled readOnly /> -
@@ -205,20 +197,17 @@ export default function ViewTenantPage() { {/* Identity Provider Information - END */} {/* DELETE TENANT - Danger zone */} - -
- - - -
-
+ {me.isOperator && me.id !== tenant.id && ( + +
+ + + +
+
+ )}
{ tenantPageInfo, previousPageUrl, nextPageUrl, - isOperator + me }) } export default function TenantsPage() { - const { - tenantEdges, - tenantPageInfo, - previousPageUrl, - nextPageUrl, - isOperator - } = useLoaderData() + const { tenantEdges, tenantPageInfo, previousPageUrl, nextPageUrl, me } = + useLoaderData() const navigate = useNavigate() return ( @@ -71,7 +66,7 @@ export default function TenantsPage() {

Tenants

- {isOperator && ( + {me.isOperator && ( @@ -106,7 +101,7 @@ export default function TenantsPage() { )} - {tenant.node.isOperator && ( + {me.isOperator && me.id == tenant.node.id && ( Operator )}
diff --git a/packages/frontend/app/routes/tenants.create.tsx b/packages/frontend/app/routes/tenants.create.tsx index e579be7ccf..fb8a1c1732 100644 --- a/packages/frontend/app/routes/tenants.create.tsx +++ b/packages/frontend/app/routes/tenants.create.tsx @@ -1,8 +1,13 @@ -import { json, type ActionFunctionArgs } from '@remix-run/node' -import { Form, useActionData, useNavigation } from '@remix-run/react' +import { json, type ActionFunctionArgs, redirect } from '@remix-run/node' +import { + Form, + useActionData, + useLoaderData, + useNavigation +} from '@remix-run/react' import { PageHeader } from '~/components' import { Button, ErrorPanel, Input, PasswordInput } from '~/components/ui' -import { createTenant } from '~/lib/api/tenant.server' +import { createTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' import { createTenantSchema } from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' @@ -12,13 +17,16 @@ import { type LoaderFunctionArgs } from '@remix-run/node' export const loader = async ({ request }: LoaderFunctionArgs) => { const cookies = request.headers.get('cookie') await checkAuthAndRedirect(request.url, cookies) - return null + const me = await whoAmI(request) + return json({ me }) } export default function CreateTenantPage() { const response = useActionData() const { state } = useNavigation() const isSubmitting = state === 'submitting' + const { me } = useLoaderData() + if (!me || !me.isOperator) throw redirect('tenants') return (
@@ -126,14 +134,22 @@ export async function action({ request }: ActionFunctionArgs) { const formData = Object.fromEntries(await request.formData()) const result = createTenantSchema.safeParse(formData) - if (!result.success) { errors.fieldErrors = result.error.flatten().fieldErrors return json({ errors }, { status: 400 }) } + if ( + result.data.email && + result.data.email.trim().length > 0 && + !new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/).test( + result.data.email + ) + ) { + errors.message = ['Email is invalid.'] + return json({ errors }, { status: 400 }) + } const response = await createTenant(request, { ...result.data }) - if (!response?.tenant) { errors.message = ['Could not create tenant. Please try again!'] return json({ errors }, { status: 400 }) diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 31b6c2cff5..5296c25729 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1454,8 +1454,6 @@ export type Tenant = Model & { idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ idpSecret?: Maybe; - /** Is the tenant an Operator tenant. */ - isOperator: Scalars['Boolean']['output']; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2546,7 +2544,6 @@ export type TenantResolvers; idpConsentUrl?: Resolver, ParentType, ContextType>; idpSecret?: Resolver, ParentType, ContextType>; - isOperator?: Resolver; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; };