Skip to content

feat(2915): admin front-end for tenant support #3254

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

Merged
merged 27 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
99d6982
feat: backend tenant graphql resolvers
njlie Jan 21, 2025
344e21a
chore: formatting
njlie Jan 21, 2025
657780f
fix: extra testing db for tenants
njlie Jan 22, 2025
3ce9128
feat: bruno collection
njlie Jan 23, 2025
15cb387
feat: update graphql schema comments
njlie Jan 24, 2025
4aafebb
fix: review comments
njlie Jan 27, 2025
9072e02
feat: optional idp secret & consent url
njlie Jan 28, 2025
e4a6e6c
feat: tenant response requirement
njlie Jan 28, 2025
45dcb5b
feat: make delete operator-only
njlie Jan 28, 2025
59d4638
Merge branch 'nl/3124/backend-tenant-resolvers' into issues/2915-mt-f…
koekiebox Jan 29, 2025
993e4c4
feat(2915): admin front-end for tenant support
koekiebox Jan 29, 2025
ac04fa6
feat(2915): apply permissions for tenant screens
koekiebox Jan 30, 2025
9cd9899
feat(2915): improvements
koekiebox Jan 30, 2025
8bc7d7f
chore: cleanup
njlie Jan 31, 2025
b7ea593
Merge branch 'nl/3124/backend-tenant-resolvers' into issues/2915-mt-f…
koekiebox Feb 3, 2025
ef3601f
Merge branch '2893/multi-tenancy-v1' into issues/2915-mt-frontend
koekiebox Feb 4, 2025
4de527d
feat(2915): merged with tenant branch. updates to update screen.
koekiebox Feb 4, 2025
3258767
feat(2915): update fixes.
koekiebox Feb 6, 2025
cc03979
Merge branch '2893/multi-tenancy-v1' into issues/2915-mt-frontend
koekiebox Feb 7, 2025
8f78849
feat(3180): bug fixes and testing with non operator tenant.
koekiebox Feb 7, 2025
ae76925
feat(3180): bug fixes and testing with non operator tenant.
koekiebox Feb 7, 2025
719bb18
Merge branch '2893/multi-tenancy-v1' into issues/2915-mt-frontend
koekiebox Feb 12, 2025
e718b64
feat(2915): review feedback
koekiebox Feb 12, 2025
f90efd0
feat(2915): fix update. field validation for email.
koekiebox Feb 12, 2025
267ef6c
feat(2915): formatting.
koekiebox Feb 12, 2025
f15bad3
feat(2915): fix.
koekiebox Feb 12, 2025
970fcbf
feat(2915): review fixes.
koekiebox Feb 12, 2025
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
}
}
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.

163 changes: 163 additions & 0 deletions packages/frontend/app/lib/api/tenant.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { gql } from '@apollo/client'
import type {
CreateTenantInput,
CreateTenantMutation,
CreateTenantMutationVariables,
QueryTenantsArgs,
ListTenantsQuery,
ListTenantsQueryVariables
} 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
) => {
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