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(backend/frontend): 3256 able to view deleted tenant #3299

Merged
merged 4 commits into from
Feb 14, 2025
Merged
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
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
Loading