Skip to content

Commit

Permalink
Merge branch 'main' into 11-restrict-creation
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell authored Mar 12, 2024
2 parents d6a824d + b412f3c commit db78835
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 34 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Property | Type | Description
`contact_types.replacement_property` | Property | Defines how this `contact_type` is described when being replaced. The `property_name` is always `replacement`. See [ConfigProperty](#ConfigProperty).
`contact_types.place_properties` | Array<ConfigProperty> | Defines the attributes which are collected and set on the user's created place. See [ConfigProperty](#ConfigProperty).
`contact_types.contact_properties` | Array<ConfigProperty> | Defines the attributes which are collected and set on the user's primary contact doc. See [ConfigProperty](#ConfigProperty).
`contact_types.deactivate_users_on_replace` | boolean | Controls what should happen to the defunct contact and user documents when a user is replaced. When `false`, the contact and user account will be deleted. When `true`, the contact will be unaltered and the user account will be assigned the role `deactivated`. This allows for account restoration.
`logoBase64` | Image in base64 | Logo image for your project

#### ConfigProperty
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cht-user-management",
"version": "1.0.18",
"version": "1.1.1",
"main": "dist/index.js",
"dependencies": {
"@fastify/autoload": "^5.8.0",
Expand Down
2 changes: 2 additions & 0 deletions src/config/chis-ke/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"contact_type": "person",
"user_role": "community_health_assistant",
"username_from_place": true,
"deactivate_users_on_replace": false,
"hierarchy": [
{
"friendly_name": "Sub County",
Expand Down Expand Up @@ -283,6 +284,7 @@
"contact_type": "person",
"user_role": "community_health_volunteer",
"username_from_place": false,
"deactivate_users_on_replace": false,
"hierarchy": [
{
"friendly_name": "Sub County",
Expand Down
2 changes: 1 addition & 1 deletion src/config/chis-ug/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
"domain": "moh-ug-test.dev.medicmobile.org"
}
],

"contact_types": [
{
"name": "health_center",
"friendly": "VHT Area",
"contact_type": "person",
"user_role": "vht",
"username_from_place": true,
"deactivate_users_on_replace": true,
"hierarchy": [
{
"friendly_name": "Health Center",
Expand Down
1 change: 1 addition & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type ContactType = {
replacement_property: ContactProperty;
place_properties: ContactProperty[];
contact_properties: ContactProperty[];
deactivate_users_on_replace: boolean;
};

export type HierarchyConstraint = {
Expand Down
70 changes: 55 additions & 15 deletions src/lib/cht-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,42 @@ export class ChtApi {
return resp.data.id;
};

updatePlace = async (payload: PlacePayload, contactId: string): Promise<string> => {
updatePlace = async (payload: PlacePayload, contactId: string): Promise<any> => {
const doc: any = await this.getDoc(payload._id);

const payloadClone:any = _.cloneDeep(payload);
delete payloadClone.contact;
delete payloadClone.parent;

const previousPrimaryContact = doc.contact._id;
Object.assign(doc, payloadClone, { contact: { _id: contactId }});
doc.user_attribution ||= {};
doc.user_attribution.previousPrimaryContacts ||= [];
doc.user_attribution.previousPrimaryContacts.push(previousPrimaryContact);

const url = `${this.protocolAndHost}/medic/${payload._id}`;
console.log('axios.put', url);
const resp = await axios.put(url, doc, this.authorizationOptions());
return resp.data.id;
const putUrl = `${this.protocolAndHost}/medic/${payload._id}`;
console.log('axios.put', putUrl);
const resp = await axios.put(putUrl, doc, this.authorizationOptions());
if (!resp.data.ok) {
throw Error('response from chtApi.updatePlace was not OK');
}

return doc;
};

disableUsersWithPlace = async (placeId: string): Promise<string[]> => {
const url = `${this.protocolAndHost}/_users/_find`;
const payload = {
selector: {
facility_id: placeId,
},
};
deleteDoc = async (docId: string): Promise<void> => {
const doc: any = await this.getDoc(docId);

console.log('axios.post', url);
const resp = await axios.post(url, payload, this.authorizationOptions());
const usersToDisable: string[] = resp.data?.docs?.map((d: any) => d._id);
const deleteContactUrl = `${this.protocolAndHost}/medic/${doc._id}?rev=${doc._rev}`;
console.log('axios.delete', deleteContactUrl);
const resp = await axios.delete(deleteContactUrl, this.authorizationOptions());
if (!resp.data.ok) {
throw Error('response from chtApi.deleteDoc was not OK');
}
};

disableUsersWithPlace = async (placeId: string): Promise<string[]> => {
const usersToDisable: string[] = await this.getUsersAtPlace(placeId);
for (const userDocId of usersToDisable) {
await this.disableUser(userDocId);
}
Expand All @@ -120,6 +131,22 @@ export class ChtApi {
return axios.delete(url, this.authorizationOptions());
};

deactivateUsersWithPlace = async (placeId: string): Promise<string[]> => {
const usersToDeactivate: string[] = await this.getUsersAtPlace(placeId);
for (const userDocId of usersToDeactivate) {
await this.deactivateUser(userDocId);
}
return usersToDeactivate;
};

deactivateUser = async (docId: string): Promise<void> => {
const username = docId.substring('org.couchdb.user:'.length);
const url = `${this.protocolAndHost}/api/v1/users/${username}`;
console.log('axios.post', url);
const deactivationPayload = { roles: ['deactivated' ]};
return axios.post(url, deactivationPayload, this.authorizationOptions());
};

createUser = async (user: UserPayload): Promise<void> => {
const url = `${this.protocolAndHost}/api/v1/users`;
console.log('axios.post', url);
Expand Down Expand Up @@ -183,6 +210,19 @@ export class ChtApi {
return resp.data;
};

private async getUsersAtPlace(placeId: string): Promise<string[]> {
const url = `${this.protocolAndHost}/_users/_find`;
const payload = {
selector: {
facility_id: placeId,
},
};

console.log('axios.post', url);
const resp = await axios.post(url, payload, this.authorizationOptions());
return resp.data?.docs?.map((d: any) => d._id);
}

private authorizationOptions(): any {
return {
headers: { Cookie: this.session.sessionToken },
Expand Down
4 changes: 3 additions & 1 deletion src/lib/validator-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export default class ValidatorName implements IValidator {

private titleCase(value: string): string {
const words = value.toLowerCase().split(' ');
const titleCased = words.map(word => word[0].toUpperCase() + word.slice(1)).join(' ');
const titleCased = words
.filter(x => x)
.map(word => word[0].toUpperCase() + word.slice(1)).join(' ');
return titleCased.replace(/ '/g, '\'');
}
}
1 change: 0 additions & 1 deletion src/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default async function events(fastify: FastifyInstance) {

resp.hijack();
const placesChangeListener = (arg: string = '*') => {
console.log('place_state_change', arguments);
resp.sse({ event: 'place_state_change', data: arg });
};

Expand Down
3 changes: 1 addition & 2 deletions src/services/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export type UserCreationDetails = {
password?: string;
placeId?: string;
contactId?: string;
disabledUsers?: string[];
};

export enum PlaceUploadState {
Expand Down Expand Up @@ -118,7 +117,7 @@ export default class Place {

public asChtPayload(username: string): PlacePayload {
const user_attribution = {
tool: `cht_usr-${appVersion}`,
tool: `cht-user-management-${appVersion}`,
username,
created_time: Date.now(),
replacement: this.resolvedHierarchy[0],
Expand Down
16 changes: 14 additions & 2 deletions src/services/upload-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Config } from '../config';
import Place, { PlaceUploadState } from './place';
import RemotePlaceCache from '../lib/remote-place-cache';
import { UploadNewPlace } from './upload.new';
import { UploadReplacementPlace } from './upload.replacement';
import { UploadReplacementWithDeletion } from './upload.replacement';
import { UploadReplacementWithDeactivation } from './upload.deactivate';
import { UserPayload } from './user-payload';

const UPLOAD_BATCH_SIZE = 1;
Expand Down Expand Up @@ -40,7 +41,7 @@ export class UploadManager extends EventEmitter {
this.eventedPlaceStateChange(place, PlaceUploadState.IN_PROGRESS);

try {
const uploader: Uploader = place.hierarchyProperties.replacement ? new UploadReplacementPlace(chtApi) : new UploadNewPlace(chtApi);
const uploader: Uploader = pickUploader(place, chtApi);
const payload = place.asChtPayload(chtApi.chtSession.username);
await Config.mutate(payload, chtApi, !!place.properties.replacement);

Expand Down Expand Up @@ -105,3 +106,14 @@ export class UploadManager extends EventEmitter {
});
};
}

function pickUploader(place: Place, chtApi: ChtApi): Uploader {
if (!place.hierarchyProperties.replacement) {
return new UploadNewPlace(chtApi);
}

return place.type.deactivate_users_on_replace ?
new UploadReplacementWithDeactivation(chtApi) :
new UploadReplacementWithDeletion(chtApi);
}

34 changes: 34 additions & 0 deletions src/services/upload.deactivate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChtApi, PlacePayload } from '../lib/cht-api';
import Place from './place';
import { retryOnUpdateConflict } from '../lib/retry-logic';
import { Uploader } from './upload-manager';

export class UploadReplacementWithDeactivation implements Uploader {
private readonly chtApi: ChtApi;

constructor(chtApi: ChtApi) {
this.chtApi = chtApi;
}

handleContact = async (payload: PlacePayload): Promise<string | undefined> => {
return await this.chtApi.createContact(payload);
};

handlePlacePayload = async (place: Place, payload: PlacePayload): Promise<string> => {
const contactId = place.creationDetails?.contactId;
const placeId = place.resolvedHierarchy[0]?.id;

if (!contactId || !placeId) {
throw Error('contactId and placeId are required');
}

const updatedPlaceDoc = await retryOnUpdateConflict<any>(() => this.chtApi.updatePlace(payload, contactId));
await this.chtApi.deactivateUsersWithPlace(placeId);
return updatedPlaceDoc._id;
};

linkContactAndPlace = async (place: Place, placeId: string): Promise<void> => {
const contactId = await this.chtApi.updateContactParent(placeId);
place.creationDetails.contactId = contactId;
};
}
14 changes: 8 additions & 6 deletions src/services/upload.replacement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Place from './place';
import { retryOnUpdateConflict } from '../lib/retry-logic';
import { Uploader } from './upload-manager';

export class UploadReplacementPlace implements Uploader {
export class UploadReplacementWithDeletion implements Uploader {
private readonly chtApi: ChtApi;

constructor(chtApi: ChtApi) {
Expand All @@ -22,12 +22,14 @@ export class UploadReplacementPlace implements Uploader {
throw Error('contactId and placeId are required');
}

const updatedPlaceId = await retryOnUpdateConflict<string>(() => this.chtApi.updatePlace(payload, contactId));
const disabledUsers = await this.chtApi.disableUsersWithPlace(placeId);
place.creationDetails.disabledUsers = disabledUsers;
const updatedPlaceDoc = await retryOnUpdateConflict<any>(() => this.chtApi.updatePlace(payload, contactId));
const previousPrimaryContact = updatedPlaceDoc.user_attribution.previousPrimaryContacts?.pop();
if (previousPrimaryContact) {
await retryOnUpdateConflict<any>(() => this.chtApi.deleteDoc(previousPrimaryContact));
}

// (optional) mute and rename contacts associated to the disabled users
return updatedPlaceId;
await this.chtApi.disableUsersWithPlace(placeId);
return updatedPlaceDoc._id;
};

linkContactAndPlace = async (place: Place, placeId: string): Promise<void> => {
Expand Down
3 changes: 2 additions & 1 deletion test/lib/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const scenarios: Scenario[] = [
{ type: 'name', prop: 'NZATANI / ILALAMBYU', isValid: true, altered: 'Nzatani / Ilalambyu' },
{ type: 'name', prop: 'Sam\'s CHU', propertyParameter: ['CHU', 'Comm Unit'], isValid: true, altered: 'Sam\'s' },
{ type: 'name', prop: 'Jonathan M.Barasa', isValid: true, altered: 'Jonathan M Barasa' },
{ type: 'name', prop: ' ', isValid: true, altered: '' },

{ type: 'dob', prop: '', isValid: false },
{ type: 'dob', prop: '2016/05/25', isValid: false },
Expand Down Expand Up @@ -74,7 +75,7 @@ describe('lib/validation.ts', () => {
}

const actualAltered = Validation.format(place);
expect(actualAltered.properties.prop).to.eq(scenario.altered || scenario.prop);
expect(actualAltered.properties.prop).to.eq(scenario.altered ?? scenario.prop);
});
}

Expand Down
Loading

0 comments on commit db78835

Please sign in to comment.