Skip to content

Commit

Permalink
feat(backend/frontend): 3256 able to view deleted tenant (#3299)
Browse files Browse the repository at this point in the history
* feat(3256): able to view deleted tenant

* feat(3256): fix backend tests.

* feat(3256): allow for switching between deleted and not.

* feat(3256): review comments.
  • Loading branch information
koekiebox authored Feb 14, 2025
1 parent 0b0e8a3 commit f3cae25
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 35 deletions.
2 changes: 1 addition & 1 deletion packages/backend/src/graphql/resolvers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getTenant: QueryResolvers<TenantedApolloContext>['tenant'] =
}

const tenantService = await ctx.container.use('tenantService')
const tenant = await tenantService.get(args.id)
const tenant = await tenantService.get(args.id, isOperator)
if (!tenant) {
throw new GraphQLError('tenant does not exist', {
extensions: {
Expand Down
14 changes: 13 additions & 1 deletion packages/backend/src/tenants/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('Tenant Service', (): void => {
expect(tenant).toEqual(createdTenant)
})

test('returns undefined if tenant is deleted', async (): Promise<void> => {
test('returns deletedAt set if tenant is deleted', async (): Promise<void> => {
const dbTenant = await Tenant.query(knex).insertAndFetch({
apiSecret: 'test-secret',
email: faker.internet.email(),
Expand All @@ -86,6 +86,10 @@ describe('Tenant Service', (): void => {

const tenant = await tenantService.get(dbTenant.id)
expect(tenant).toBeUndefined()

// Ensure Operator is able to access tenant even if deleted:
const tenantDel = await tenantService.get(dbTenant.id, true)
expect(tenantDel?.deletedAt).toBeDefined()
})

test('returns undefined if tenant is deleted', async (): Promise<void> => {
Expand All @@ -99,6 +103,10 @@ describe('Tenant Service', (): void => {

const tenant = await tenantService.get(dbTenant.id)
expect(tenant).toBeUndefined()

// Ensure Operator is able to access tenant even if deleted:
const tenantDel = await tenantService.get(dbTenant.id, true)
expect(tenantDel?.deletedAt).toBeDefined()
})
})

Expand Down Expand Up @@ -414,6 +422,10 @@ describe('Tenant Service', (): void => {
// Ensure that cache was set for deletion
expect(spyCacheDelete).toHaveBeenCalledTimes(1)
expect(spyCacheDelete).toHaveBeenCalledWith(tenant.id)

// Ensure Operator is able to access tenant even if deleted:
const tenantDel = await tenantService.get(tenant.id, true)
expect(tenantDel?.deletedAt).toBeDefined()
}
)
)
Expand Down
20 changes: 13 additions & 7 deletions packages/backend/src/tenants/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CacheDataStore } from '../middleware/cache/data-stores'
import type { AuthServiceClient } from '../auth-service-client/client'

export interface TenantService {
get: (id: string) => Promise<Tenant | undefined>
get: (id: string, includeDeleted?: boolean) => Promise<Tenant | undefined>
create: (options: CreateTenantOptions) => Promise<Tenant>
update: (options: UpdateTenantOptions) => Promise<Tenant>
delete: (id: string) => Promise<void>
Expand All @@ -28,7 +28,8 @@ export async function createTenantService(
}

return {
get: (id: string) => getTenant(deps, id),
get: (id: string, includeDeleted?: boolean) =>
getTenant(deps, id, includeDeleted),
create: (options) => createTenant(deps, options),
update: (options) => updateTenant(deps, options),
delete: (id) => deleteTenant(deps, id),
Expand All @@ -39,13 +40,18 @@ export async function createTenantService(

async function getTenant(
deps: ServiceDependencies,
id: string
id: string,
includeDeleted: boolean = false
): Promise<Tenant | undefined> {
const inMem = await deps.tenantCache.get(id)
if (inMem) return inMem
const tenant = await Tenant.query(deps.knex)
.findById(id)
.whereNull('deletedAt')
if (inMem) {
if (!includeDeleted && inMem.deletedAt) return undefined
return inMem
}
let query = Tenant.query(deps.knex)
if (!includeDeleted) query = query.whereNull('deletedAt')

const tenant = await query.findById(id)
if (tenant) await deps.tenantCache.set(tenant.id, tenant)

return tenant
Expand Down
55 changes: 35 additions & 20 deletions packages/frontend/app/routes/tenants.$tenantId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
if (!tenant)
throw json(null, { status: 404, statusText: 'Tenant not found.' })

const tenantDeleted = tenant.deletedAt ? tenant.deletedAt.length > 0 : false
const me = await whoAmI(request)
return json({ tenant, me })
return json({ tenant, me, tenantDeleted })
}

export default function ViewTenantPage() {
const { tenant, me } = useLoaderData<typeof loader>()
const { tenant, me, tenantDeleted } = useLoaderData<typeof loader>()
const response = useActionData<typeof action>()
const navigation = useNavigation()
const [formData, setFormData] = useState<FormData>()
Expand Down Expand Up @@ -78,7 +79,13 @@ export default function ViewTenantPage() {
<div className='col-span-1 pt-3'>
<h3 className='text-lg font-medium'>General Information</h3>
<p className='text-sm'>
Created at {new Date(tenant.createdAt).toLocaleString()}
{`Created at ${new Date(tenant.createdAt).toLocaleString()}`}
{tenantDeleted && tenant.deletedAt && (
<>
<br />
{`Deleted at ${new Date(tenant.deletedAt).toLocaleString()}`}
</>
)}
</p>
<ErrorPanel errors={response?.errors.message} />
</div>
Expand All @@ -102,25 +109,29 @@ export default function ViewTenantPage() {
<Input
label='Public Name'
name='publicName'
disabled={tenantDeleted}
defaultValue={tenant.publicName ?? undefined}
error={response?.errors?.fieldErrors.publicName}
/>
<Input
label='Email'
name='email'
disabled={tenantDeleted}
defaultValue={tenant.email ?? undefined}
error={response?.errors?.fieldErrors.email}
/>
</div>
<div className='flex justify-end p-4'>
<Button
aria-label='save general information'
type='submit'
name='intent'
value='general'
>
{isSubmitting ? 'Saving ...' : 'Save'}
</Button>
{!tenantDeleted && (
<Button
aria-label='save general information'
type='submit'
name='intent'
value='general'
>
{isSubmitting ? 'Saving ...' : 'Save'}
</Button>
)}
</div>
</fieldset>
</Form>
Expand All @@ -147,25 +158,29 @@ export default function ViewTenantPage() {
<Input
name='idpConsentUrl'
label='Consent URL'
disabled={tenantDeleted}
defaultValue={tenant.idpConsentUrl ?? undefined}
error={response?.errors?.fieldErrors.idpConsentUrl}
/>
<PasswordInput
name='idpSecret'
label='Secret'
disabled={tenantDeleted}
defaultValue={tenant.idpSecret ?? undefined}
error={response?.errors?.fieldErrors.idpSecret}
/>
</div>
<div className='flex justify-end p-4'>
<Button
aria-label='save ip information'
type='submit'
name='intent'
value='ip'
>
{isSubmitting ? 'Saving ...' : 'Save'}
</Button>
{!tenantDeleted && (
<Button
aria-label='save ip information'
type='submit'
name='intent'
value='ip'
>
{isSubmitting ? 'Saving ...' : 'Save'}
</Button>
)}
</div>
</fieldset>
</Form>
Expand Down Expand Up @@ -197,7 +212,7 @@ export default function ViewTenantPage() {
</div>
{/* Sensitive - END */}
{/* DELETE TENANT - Danger zone */}
{me.isOperator && me.id !== tenant.id && (
{!tenantDeleted && me.isOperator && me.id !== tenant.id && (
<DangerZone title='Delete Tenant'>
<Form method='post' onSubmit={submitHandler}>
<Input type='hidden' name='id' value={tenant.id} />
Expand Down
8 changes: 2 additions & 6 deletions packages/frontend/app/routes/tenants._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,8 @@ export default function TenantsPage() {
tenantEdges.map((tenant) => (
<Table.Row
key={tenant.node.id}
className={tenant.node.deletedAt ? '' : 'cursor-pointer'}
onClick={() =>
tenant.node.deletedAt
? 'return'
: navigate(`/tenants/${tenant.node.id}`)
}
className='cursor-pointer'
onClick={() => navigate(`/tenants/${tenant.node.id}`)}
>
<Table.Cell>
<div className='flex flex-col'>
Expand Down

0 comments on commit f3cae25

Please sign in to comment.