Skip to content

Commit

Permalink
breaking(#234): replace users when core supports multiple facility_ids (
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell authored Dec 27, 2024
1 parent 4ffd976 commit 68d771d
Show file tree
Hide file tree
Showing 20 changed files with 419 additions and 247 deletions.
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.6.1",
"version": "2.0.0",
"main": "dist/index.js",
"dependencies": {
"@bull-board/api": "^5.17.0",
Expand Down
39 changes: 20 additions & 19 deletions src/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ChtSession from './cht-session';
const LOGIN_EXPIRES_AFTER_MS = 4 * 24 * 60 * 60 * 1000;
const QUEUE_SESSION_EXPIRATION = '96h';
const { COOKIE_PRIVATE_KEY, WORKER_PRIVATE_KEY } = process.env;
const PRIVATE_KEY_SALT = '_'; // change to logout all users
const PRIVATE_KEY_SALT = '$'; // change to logout all users
const COOKIE_SIGNING_KEY = COOKIE_PRIVATE_KEY + PRIVATE_KEY_SALT;

export default class Auth {
Expand All @@ -23,37 +23,38 @@ export default class Auth {
}
}

private static encodeToken(session: ChtSession, signingKey: string, expiresIn: string) {
const data = JSON.stringify(session);
return jwt.sign({ data }, signingKey, { expiresIn });
}

private static decodeToken(token: string, signingKey: string): ChtSession {
if (!token) {
throw new Error('invalid authentication token');
}

const { data } = jwt.verify(token, signingKey) as any;
return ChtSession.createFromDataString(data);
}

public static encodeTokenForCookie(session: ChtSession) {
return this.encodeToken(session, COOKIE_SIGNING_KEY, '1 day');
}

public static decodeTokenForCookie(token: string): ChtSession {
return this.decodeToken(token, COOKIE_SIGNING_KEY);
public static createCookieSession(token: string): ChtSession {
return this.createSessionFromToken(token, COOKIE_SIGNING_KEY);
}

public static encodeTokenForWorker(session: ChtSession) {
return this.encodeToken(session, `${WORKER_PRIVATE_KEY}`, QUEUE_SESSION_EXPIRATION);
}

public static decodeTokenForWorker(token: string): ChtSession {
return this.decodeToken(token, `${WORKER_PRIVATE_KEY}`);
public static createWorkerSession(token: string): ChtSession {
return this.createSessionFromToken(token, `${WORKER_PRIVATE_KEY}`);
}

public static cookieExpiry() {
return new Date(new Date().getTime() + LOGIN_EXPIRES_AFTER_MS);
}


private static encodeToken(session: ChtSession, signingKey: string, expiresIn: string) {
const data = JSON.stringify(session);
return jwt.sign({ data }, signingKey, { expiresIn });
}

private static createSessionFromToken(token: string, signingKey: string): ChtSession {
if (!token) {
throw new Error('invalid authentication token');
}

const { data } = jwt.verify(token, signingKey) as any;
return ChtSession.createFromDataString(data);
}
}
169 changes: 63 additions & 106 deletions src/lib/cht-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ export type PlacePayload = {
[key: string]: any;
};

export type CreatedPlaceResult = {
placeId: string;
contactId?: string;
};

export type CouchDoc = {
_id: string;
};

export type UserInfo = {
username: string;
place?: CouchDoc[] | CouchDoc | string[] | string;
roles?: string[];
};

export class ChtApi {
public readonly chtSession: ChtSession;
private axiosInstance: AxiosInstance;
Expand All @@ -28,47 +43,30 @@ export class ChtApi {
this.axiosInstance = session.axiosInstance;
}

// workaround https://github.com/medic/cht-core/issues/8674
updateContactParent = async (parentId: string): Promise<string> => {
const parentDoc = await this.getDoc(parentId);
const contactId = parentDoc?.contact?._id;
if (!contactId) {
throw Error('cannot find id of contact');
}

const contactDoc = await this.getDoc(contactId);
if (!contactDoc || !parentDoc) {
throw Error('cannot find parent or contact docs');
}

contactDoc.parent = minify(parentDoc);

const putUrl = `medic/${contactId}`;
console.log('axios.put', putUrl);
const putResp = await this.axiosInstance.put(putUrl, contactDoc);
if (putResp.status !== 201) {
throw new Error(putResp.data);
}

return contactDoc._id;
};

createPlace = async (payload: PlacePayload): Promise<string> => {
async createPlace(payload: PlacePayload): Promise<CreatedPlaceResult> {
const url = `api/v1/places`;
console.log('axios.post', url);
const resp = await this.axiosInstance.post(url, payload);
return resp.data.id;
};
return {
placeId: resp.data.id,
contactId: resp.data.contact?.id,
};
}

// because there is no PUT for /api/v1/places
createContact = async (payload: PlacePayload): Promise<string> => {
async createContact(payload: PlacePayload): Promise<string> {
const payloadWithPlace = {
...payload.contact,
place: payload._id,
};

const url = `api/v1/people`;
console.log('axios.post', url);
const resp = await this.axiosInstance.post(url, payload.contact);
const resp = await this.axiosInstance.post(url, payloadWithPlace);
return resp.data.id;
};
}

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

const payloadClone:any = _.cloneDeep(payload);
Expand All @@ -91,9 +89,9 @@ export class ChtApi {
}

return doc;
};
}

deleteDoc = async (docId: string): Promise<void> => {
async deleteDoc(docId: string): Promise<void> {
const doc: any = await this.getDoc(docId);

const deleteContactUrl = `medic/${doc._id}?rev=${doc._rev}`;
Expand All @@ -102,40 +100,21 @@ export class ChtApi {
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);
}
return usersToDisable;
};
}

disableUser = async (docId: string): Promise<void> => {
const username = docId.substring('org.couchdb.user:'.length);
async disableUser(username: string): Promise<void> {
const url = `api/v1/users/${username}`;
console.log('axios.delete', url);
return this.axiosInstance.delete(url);
};

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 = `api/v1/users/${username}`;
async updateUser(userInfo: UserInfo): Promise<void> {
const url = `api/v1/users/${userInfo.username}`;
console.log('axios.post', url);
const deactivationPayload = { roles: ['deactivated' ]};
return this.axiosInstance.post(url, deactivationPayload);
};
return this.axiosInstance.post(url, userInfo);
}

countContactsUnderPlace = async (docId: string): Promise<number> => {
async countContactsUnderPlace(docId: string): Promise<number> {
const url = `medic/_design/medic/_view/contacts_by_depth`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url, {
Expand All @@ -147,18 +126,18 @@ export class ChtApi {
});

return resp.data?.rows?.length || 0;
};
}

createUser = async (user: UserPayload): Promise<void> => {
async createUser(user: UserPayload): Promise<void> {
const url = `api/v1/users`;
console.log('axios.post', url);
const axiosRequestionConfig = {
'axios-retry': { retries: 0 }, // upload-manager handles retries for this
};
await this.axiosInstance.post(url, user, axiosRequestionConfig);
};
}

getParentAndSibling = async (parentId: string, contactType: ContactType): Promise<{ parent: any; sibling: any }> => {
async getParentAndSibling(parentId: string, contactType: ContactType): Promise<{ parent: any; sibling: any }> {
const url = `medic/_design/medic/_view/contacts_by_depth`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url, {
Expand All @@ -175,10 +154,9 @@ export class ChtApi {
const parent = docs.find((d: any) => d.contact_type === parentType);
const sibling = docs.find((d: any) => d.contact_type === contactType.name);
return { parent, sibling };
};
}

getPlacesWithType = async (placeType: string)
: Promise<any[]> => {
async getPlacesWithType(placeType: string): Promise<any[]> {
const url = `medic/_design/medic-client/_view/contacts_by_type`;
const params = {
key: JSON.stringify([placeType]),
Expand All @@ -187,20 +165,31 @@ export class ChtApi {
console.log('axios.get', url, params);
const resp = await this.axiosInstance.get(url, { params });
return resp.data.rows.map((row: any) => row.doc);
};
}

getDoc = async (id: string): Promise<any> => {
async getDoc(id: string): Promise<any> {
const url = `medic/${id}`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url);
return resp.data;
};
}

async getUsersAtPlace(placeId: string): Promise<UserInfo[]> {
const url = `api/v2/users?facility_id=${placeId}`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url);
return resp.data?.map((doc: any): UserInfo => ({
username: doc.username,
place: doc.place,
}));
}

lastSyncAtPlace = async (placeId: string): Promise<DateTime> => {
async lastSyncAtPlace(placeId: string): Promise<DateTime> {
const userIds = await this.getUsersAtPlace(placeId);
const result = await this.getLastSyncForUsers(userIds);
const usernames = userIds.map(userId => userId.username);
const result = await this.getLastSyncForUsers(usernames);
return result || DateTime.invalid('unknown');
};
}

private getLastSyncForUsers = async (usernames: string[]): Promise<DateTime | undefined> => {
if (!usernames?.length) {
Expand All @@ -225,36 +214,4 @@ export class ChtApi {
const maxTimestamp = Math.max(timestamps);
return DateTime.fromMillis(maxTimestamp);
};

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

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

function minify(doc: any): any {
if (!doc) {
return;
}

return {
_id: doc._id,
parent: minify(doc.parent),
};
}

Loading

0 comments on commit 68d771d

Please sign in to comment.