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(2915): admin front-end for tenant support #3254

Open
wants to merge 18 commits into
base: 2893/multi-tenancy-v1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions localenv/mock-account-servicing-entity/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions packages/backend/src/graphql/generated/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/backend/src/graphql/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/backend/src/graphql/resolvers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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<TenantedApolloContext>['whoami'] = async (
parent,
Expand Down Expand Up @@ -174,6 +175,7 @@ export function tenantToGraphQl(tenant: Tenant): SchemaTenant {
createdAt: new Date(+tenant.createdAt).toISOString(),
deletedAt: tenant.deletedAt
? new Date(+tenant.deletedAt).toISOString()
: null
: null,
isOperator: tenant.apiSecret === Config.adminApiSecret
Copy link
Contributor

@BlairCurrey BlairCurrey Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really in the scope of this PR but related: #3276

not suggesting any changes here

}
}
2 changes: 2 additions & 0 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,8 @@ 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 {
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const navigation = [
name: 'Home',
href: '/'
},
{
name: 'Tenants',
href: '/tenants'
},
{
name: 'Assets',
href: '/assets'
Expand Down
41 changes: 41 additions & 0 deletions packages/frontend/app/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

171 changes: 171 additions & 0 deletions packages/frontend/app/lib/api/tenant.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { gql } from '@apollo/client'
import type {
CreateTenantInput,
CreateTenantMutation,
UpdateTenantMutationVariables,
UpdateTenantInput,
UpdateTenantMutation,
CreateTenantMutationVariables,
QueryTenantsArgs,
ListTenantsQuery,
ListTenantsQueryVariables,
DeleteTenantMutationVariables,
DeleteTenantMutation,
QueryTenantArgs,
GetTenantQuery,
GetTenantQueryVariables
} from '~/generated/graphql'
import { getApolloClient } from '../apollo.server'

export const listTenants = async (request: Request, args: QueryTenantsArgs) => {
const apolloClient = await getApolloClient(request)
const response = await apolloClient.query<
ListTenantsQuery,
ListTenantsQueryVariables
>({
query: gql`
query ListTenantsQuery(
$after: String
$before: String
$first: Int
$last: Int
) {
tenants(after: $after, before: $before, first: $first, last: $last) {
edges {
node {
id
email
apiSecret
idpConsentUrl
idpSecret
publicName
createdAt
deletedAt
isOperator
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
`,
variables: args
})
return response.data.tenants
}

export const createTenant = async (
request: Request,
args: CreateTenantInput
) => {
const apolloClient = await getApolloClient(request)
const response = await apolloClient.mutate<
CreateTenantMutation,
CreateTenantMutationVariables
>({
mutation: gql`
mutation CreateTenantMutation($input: CreateTenantInput!) {
createTenant(input: $input) {
tenant {
id
publicName
email
apiSecret
idpConsentUrl
idpSecret
}
}
}
`,
variables: {
input: args
}
})

return response.data?.createTenant
}

export const updateTenant = async (
request: Request,
args: UpdateTenantInput
koekiebox marked this conversation as resolved.
Show resolved Hide resolved
) => {
const apolloClient = await getApolloClient(request)
const response = await apolloClient.mutate<
UpdateTenantMutation,
UpdateTenantMutationVariables
>({
mutation: gql`
mutation UpdateTenantMutation($input: UpdateTenantInput!) {
updateTenant(input: $input) {
tenant {
id
email
apiSecret
idpConsentUrl
idpSecret
publicName
}
}
}
`,
variables: {
input: args
}
})

return response.data?.updateTenant
}

export const deleteTenant = async (request: Request, args: string) => {
const apolloClient = await getApolloClient(request)
const response = await apolloClient.mutate<
DeleteTenantMutation,
DeleteTenantMutationVariables
>({
mutation: gql`
mutation DeleteTenantMutation($id: String!) {
deleteTenant(id: $id) {
success
}
}
`,
variables: {
id: args
}
})

return response.data?.deleteTenant
}

export const getTenantInfo = async (
request: Request,
args: QueryTenantArgs
) => {
const apolloClient = await getApolloClient(request)
const response = await apolloClient.query<
GetTenantQuery,
GetTenantQueryVariables
>({
query: gql`
query GetTenantQuery($id: String!) {
tenant(id: $id) {
id
email
apiSecret
idpConsentUrl
idpSecret
publicName
createdAt
deletedAt
isOperator
}
}
`,
variables: args
})
return response.data.tenant
}
30 changes: 29 additions & 1 deletion packages/frontend/app/lib/validate.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const createAssetSchema = z
.object({
code: z
.string()
.min(3, { message: 'Code should be atleast 3 characters long' })
.min(3, { message: 'Code should be at least 3 characters long' })
.max(6, { message: 'Maximum length of Code is 6 characters' })
.regex(/^[a-zA-Z]+$/, { message: 'Code should only contain letters.' })
.transform((code) => code.toUpperCase()),
Expand Down Expand Up @@ -127,3 +127,31 @@ export const updateWalletAddressSchema = z
status: z.enum([WalletAddressStatus.Active, WalletAddressStatus.Inactive])
})
.merge(uuidSchema)

export const updateTenantSchema = z
.object({
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(),
idpConsentUrl: z.string().optional(),
idpSecret: z.string().optional()
})
.merge(uuidSchema)

export const createTenantSchema = 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.' }
)
})
.merge(updateTenantSchema)
.omit({ id: true })
Loading
Loading