Skip to content

Commit

Permalink
Add disabled users
Browse files Browse the repository at this point in the history
  • Loading branch information
pauljohanneskraft committed Jan 14, 2025
1 parent 0223b8e commit 55e2338
Show file tree
Hide file tree
Showing 16 changed files with 199 additions and 21 deletions.
17 changes: 17 additions & 0 deletions functions/models/src/functions/disableUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import { z } from 'zod'

export const disableUserInputSchema = z.object({
userId: z.string(),
})

export type DisableUserInput = z.input<typeof disableUserInputSchema>

export type DisableUserOutput = undefined
17 changes: 17 additions & 0 deletions functions/models/src/functions/enableUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import { z } from 'zod'

export const enableUserInputSchema = z.object({
userId: z.string(),
})

export type EnableUserInput = z.input<typeof enableUserInputSchema>

export type EnableUserOutput = undefined
2 changes: 2 additions & 0 deletions functions/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export * from './functions/customSeed.js'
export * from './functions/defaultSeed.js'
export * from './functions/deleteInvitation.js'
export * from './functions/deleteUser.js'
export * from './functions/disableUser.js'
export * from './functions/dismissMessage.js'
export * from './functions/enableUser.js'
export * from './functions/enrollUser.js'
export * from './functions/exportHealthSummary.js'
export * from './functions/getUsersInformation.js'
Expand Down
1 change: 1 addition & 0 deletions functions/models/src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class User extends UserRegistration {

constructor(input: {
type: UserType
disabled: boolean
organization?: string
dateOfBirth?: Date
clinician?: string
Expand Down
17 changes: 13 additions & 4 deletions functions/models/src/types/userRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const userRegistrationInputConverter = new Lazy(
new SchemaConverter({
schema: z.object({
type: z.nativeEnum(UserType),
disabled: optionalishDefault(z.boolean(), false),
organization: optionalish(z.string()),
dateOfBirth: optionalish(dateConverter.schema),
clinician: optionalish(z.string()),
Expand All @@ -33,6 +34,7 @@ export const userRegistrationInputConverter = new Lazy(
}),
encode: (object) => ({
type: object.type,
disabled: object.disabled,
organization: object.organization ?? null,
dateOfBirth:
object.dateOfBirth ? dateConverter.encode(object.dateOfBirth) : null,
Expand Down Expand Up @@ -60,15 +62,19 @@ export const userRegistrationConverter = new Lazy(
}),
)

export interface UserClaims {
type: UserType
organization?: string
}
export const userClaimsSchema = z.object({
type: z.nativeEnum(UserType),
organization: optionalish(z.string()),
disabled: optionalishDefault(z.boolean(), false),
})

export type UserClaims = z.output<typeof userClaimsSchema>

export class UserRegistration {
// Stored Properties

readonly type: UserType
readonly disabled: boolean
readonly organization?: string

readonly dateOfBirth?: Date
Expand All @@ -90,6 +96,7 @@ export class UserRegistration {
get claims(): UserClaims {
const result: UserClaims = {
type: this.type,
disabled: this.disabled,
}
if (this.organization !== undefined) {
result.organization = this.organization
Expand All @@ -101,6 +108,7 @@ export class UserRegistration {

constructor(input: {
type: UserType
disabled: boolean
organization?: string
dateOfBirth?: Date
clinician?: string
Expand All @@ -115,6 +123,7 @@ export class UserRegistration {
timeZone?: string
}) {
this.type = input.type
this.disabled = input.disabled
this.organization = input.organization
this.dateOfBirth = input.dateOfBirth
this.clinician = input.clinician
Expand Down
1 change: 1 addition & 0 deletions functions/src/functions/deleteInvitation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describeWithEmulators('function: deleteInvitation', (env) => {
code: 'TESTCODE',
user: new UserRegistration({
type: UserType.patient,
disabled: false,
organization: 'stanford',
receivesAppointmentReminders: false,
receivesInactivityReminders: true,
Expand Down
41 changes: 41 additions & 0 deletions functions/src/functions/disableUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import {

Check warning on line 9 in functions/src/functions/disableUser.ts

View workflow job for this annotation

GitHub Actions / Lint

Imports "DisableUserOutput" are only used as type
disableUserInputSchema,
DisableUserOutput,
} from '@stanfordbdhg/engagehf-models'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'
import { validatedOnCall } from './helpers.js'

Check warning on line 15 in functions/src/functions/disableUser.ts

View workflow job for this annotation

GitHub Actions / Lint

`./helpers.js` import should occur before import of `../services/credential/credential.js`

export const disableUser = validatedOnCall(
'disableUser',
disableUserInputSchema,
async (request): Promise<DisableUserOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userId = request.data.userId
const userService = factory.user()

await credential.checkAsync(
() => [UserRole.admin],
async () => {
const user = await userService.getUser(credential.userId)
return user?.content.organization !== undefined ?
[
UserRole.owner(user.content.organization),
UserRole.clinician(user.content.organization),
]
: []
},
)

await userService.disableUser(userId)
},
)
41 changes: 41 additions & 0 deletions functions/src/functions/enableUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import {

Check warning on line 9 in functions/src/functions/enableUser.ts

View workflow job for this annotation

GitHub Actions / Lint

Imports "EnableUserOutput" are only used as type
enableUserInputSchema,
EnableUserOutput,
} from '@stanfordbdhg/engagehf-models'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'
import { validatedOnCall } from './helpers.js'

Check warning on line 15 in functions/src/functions/enableUser.ts

View workflow job for this annotation

GitHub Actions / Lint

`./helpers.js` import should occur before import of `../services/credential/credential.js`

export const enableUser = validatedOnCall(
'enableUser',
enableUserInputSchema,
async (request): Promise<EnableUserOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userId = request.data.userId
const userService = factory.user()

await credential.checkAsync(
() => [UserRole.admin],
async () => {
const user = await userService.getUser(credential.userId)
return user?.content.organization !== undefined ?
[
UserRole.owner(user.content.organization),
UserRole.clinician(user.content.organization),
]
: []
},
)

await userService.enableUser(userId)
},
)
1 change: 1 addition & 0 deletions functions/src/functions/enrollUser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describeWithEmulators('function: enrollUser', (env) => {
code: 'TESTCODE',
user: new UserRegistration({
type: UserType.patient,
disabled: false,
organization: 'stanford',
receivesAppointmentReminders: true,
receivesInactivityReminders: true,
Expand Down
44 changes: 27 additions & 17 deletions functions/src/services/credential/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
// SPDX-License-Identifier: MIT
//

import { UserType } from '@stanfordbdhg/engagehf-models'
import {

Check warning on line 9 in functions/src/services/credential/credential.ts

View workflow job for this annotation

GitHub Actions / Lint

Imports "UserClaims" are only used as type
UserClaims,
userClaimsSchema,
UserType,
} from '@stanfordbdhg/engagehf-models'
import { https } from 'firebase-functions/v2'
import { type AuthData } from 'firebase-functions/v2/tasks'

Expand Down Expand Up @@ -67,13 +71,8 @@ export class UserRole {
export class Credential {
// Stored Properties

private readonly authData: AuthData

// Computed Properties

get userId(): string {
return this.authData.uid
}
readonly userId: string
private readonly claims: UserClaims

// Constructor

Expand All @@ -83,7 +82,12 @@ export class Credential {
'unauthenticated',
'User is not authenticated.',
)
this.authData = authData
try {
this.claims = userClaimsSchema.parse(authData.token)
} catch {
throw this.permissionDeniedError()
}
this.userId = authData.uid
}

// Methods
Expand Down Expand Up @@ -112,33 +116,39 @@ export class Credential {
)
}

disabledError(): https.HttpsError {
return new https.HttpsError('permission-denied', 'User is disabled.')
}

// Helpers

private checkSingle(role: UserRole): boolean {
if (this.claims.disabled) throw this.disabledError()

switch (role.type) {
case UserRoleType.admin: {
return this.authData.token.type === UserType.admin
return this.claims.type === UserType.admin
}
case UserRoleType.owner: {
return (
this.authData.token.type === UserType.owner &&
this.authData.token.organization === role.organization
this.claims.type === UserType.owner &&
this.claims.organization === role.organization
)
}
case UserRoleType.clinician: {
return (
this.authData.token.type === UserType.clinician &&
this.authData.token.organization === role.organization
this.claims.type === UserType.clinician &&
this.claims.organization === role.organization
)
}
case UserRoleType.patient: {
return (
this.authData.token.type === UserType.patient &&
this.authData.token.organization === role.organization
this.claims.type === UserType.patient &&
this.claims.organization === role.organization
)
}
case UserRoleType.user: {
return this.authData.uid === role.userId
return this.userId === role.userId
}
}
}
Expand Down
1 change: 1 addition & 0 deletions functions/src/services/trigger/triggerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ export class TriggerService {
code: '<your admin email>',
user: new UserRegistration({
type: UserType.admin,
disabled: false,
receivesAppointmentReminders: false,
receivesInactivityReminders: false,
receivesMedicationUpdates: false,
Expand Down
3 changes: 3 additions & 0 deletions functions/src/services/user/databaseUserService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('DatabaseUserService', () => {
expect(userData?.dateOfEnrollment).to.exist
expect(userData?.claims).to.deep.equal({
type: UserType.admin,
disabled: false,
})
})

Expand Down Expand Up @@ -114,6 +115,7 @@ describe('DatabaseUserService', () => {
expect(userData?.claims).to.deep.equal({
type: UserType.clinician,
organization: 'mockOrganization',
disabled: false,
})
})

Expand Down Expand Up @@ -161,6 +163,7 @@ describe('DatabaseUserService', () => {
expect(userData?.claims).to.deep.equal({
type: UserType.patient,
organization: 'mockOrganization',
disabled: false,
})
})
})
Expand Down
20 changes: 20 additions & 0 deletions functions/src/services/user/databaseUserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,26 @@ export class DatabaseUserService implements UserService {

// Users

async disableUser(userId: string): Promise<void> {
await this.databaseService.runTransaction((collections, transaction) => {
transaction.update(collections.users.doc(userId), {
disabled: true,
})
})

await this.updateClaims(userId)
}

async enableUser(userId: string): Promise<void> {
await this.databaseService.runTransaction((collections, transaction) => {
transaction.update(collections.users.doc(userId), {
disabled: false,
})
})

await this.updateClaims(userId)
}

async getAllOwners(organizationId: string): Promise<Array<Document<User>>> {
return this.databaseService.getQuery<User>((collections) =>
collections.users
Expand Down
10 changes: 10 additions & 0 deletions functions/src/services/user/userService.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class MockUserService implements UserService {
content: new Invitation({
user: new UserRegistration({
type: UserType.patient,
disabled: false,
dateOfBirth: new Date('1970-01-02'),
clinician: 'mockPatient',
receivesAppointmentReminders: true,
Expand Down Expand Up @@ -127,6 +128,14 @@ export class MockUserService implements UserService {

// Methods - User

async disableUser(userId: string): Promise<void> {
return
}

async enableUser(userId: string): Promise<void> {
return
}

async getAllOwners(organizationId: string): Promise<Array<Document<User>>> {
return []
}
Expand All @@ -142,6 +151,7 @@ export class MockUserService implements UserService {
lastUpdate: new Date(),
content: new User({
type: UserType.clinician,
disabled: false,
dateOfBirth: new Date('1970-01-02'),
clinician: 'mockClinician',
lastActiveDate: new Date('2024-04-04'),
Expand Down
Loading

0 comments on commit 55e2338

Please sign in to comment.