Skip to content

Commit

Permalink
Merge branch 'main' into 18-upload-progress-bar
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell authored Feb 23, 2024
2 parents 332a8ca + cf4446e commit 3413ce8
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 34 deletions.
81 changes: 79 additions & 2 deletions package-lock.json

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

3 changes: 2 additions & 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.10",
"version": "1.0.14",
"main": "dist/index.js",
"dependencies": {
"@fastify/autoload": "^5.8.0",
Expand All @@ -21,6 +21,7 @@
"fastify": "^4.23.2",
"fastify-sse-v2": "^3.1.2",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"libphonenumber-js": "^1.10.48",
"liquidjs": "^10.9.2",
"lodash": "^4.17.21",
Expand Down
18 changes: 10 additions & 8 deletions src/lib/retry-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@ import { UserPayload } from '../services/user-payload';
import { ChtApi } from './cht-api';

const RETRY_COUNT = 4;
const RETRYABLE_STATUS_CODES = [500, 502, 503, 504, 511];

export const axiosRetryConfig = {
retries: RETRY_COUNT,
retryDelay: () => 1000,
retryCondition: (err: AxiosError) => {
const status = err.response?.status;
return (!status || status >= 500) && isRetryAllowed(err);
return (!status || RETRYABLE_STATUS_CODES.includes(status)) && isRetryAllowed(err);
},
onRetry: (retryCount: number, error: AxiosError, requestConfig: AxiosRequestConfig) => {
console.log(`${requestConfig.url} failure. Retrying (${retryCount})`);
},
};

export async function retryOnUpdateConflict<T>(funcWithPut: () => Promise<T>): Promise<T> {
export async function retryOnUpdateConflict<T>(funcWithGetAndPut: () => Promise<T>): Promise<T> {
for (let retryCount = 0; retryCount < RETRY_COUNT; retryCount++) {
try {
return await funcWithPut();
return await funcWithGetAndPut();
} catch (err : any) {
const statusCode = err.response?.status;
if (statusCode === 409) {
Expand Down Expand Up @@ -49,15 +50,16 @@ export async function createUserWithRetries(userPayload: UserPayload, chtApi: Ch
throw err;
}

const translationKey = err.response?.data?.error?.translationKey;
console.error('createUser retry because', translationKey);
if (translationKey === 'username.taken') {
// no idea when/why some instances yield "response.data" as JSON vs some as string
const errorMessage = err.response?.data?.error?.message || err.response?.data;
console.error('createUser retry because', errorMessage);
if (errorMessage.includes('already taken.')) {
userPayload.makeUsernameMoreComplex();
continue;
}

const RETRY_PASSWORD_TRANSLATIONS = ['password.length.minimum', 'password.weak'];
if (RETRY_PASSWORD_TRANSLATIONS.includes(translationKey)) {
const RETRY_PASSWORD_STRINGS = ['The password must be at least', 'The password is too easy to guess.'];
if (RETRY_PASSWORD_STRINGS.find(str => errorMessage.includes(str))) {
userPayload.regeneratePassword();
continue;
}
Expand Down
3 changes: 1 addition & 2 deletions src/public/app/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@

<div class="navbar-dropdown is-right">
{% include "components/button_upload.html" className="navbar-item" %}
{% include "components/button_save_credentials.html" className="navbar-item" %}

{% include "components/button_save_credentials.html" className="navbar-item" %}
<a class="navbar-item" href="/logout">
<span class="material-symbols-outlined">logout</span> Logout
</a>
Expand Down
5 changes: 0 additions & 5 deletions src/public/components/list_cell.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,5 @@
{% if include.linkTo.type == 'local' %}
<span class="material-symbols-outlined">cloud_off</span>
{% endif %}
{% else %}
{% if include.values.replacement %}
<i><small>Inherit</small></i>
{% endif %}
{% endif %}

</td>
45 changes: 35 additions & 10 deletions src/routes/files.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import _ from 'lodash';
import { FastifyInstance } from 'fastify';
import { transform, stringify } from 'csv/sync';
import { Config } from '../config';
import { stringify } from 'csv/sync';
import { Config, ContactType } from '../config';
import SessionCache from '../services/session-cache';
import Place from '../services/place';
import JSZip from 'jszip';

export default async function files(fastify: FastifyInstance) {
fastify.get('/files/template/:placeType', async (req) => {
Expand All @@ -20,19 +20,44 @@ export default async function files(fastify: FastifyInstance) {
return stringify([columns]);
});

fastify.get('/files/credentials', async (req) => {
fastify.get('/files/credentials', async (req, reply) => {
const sessionCache: SessionCache = req.sessionCache;
const results = new Map<ContactType, String[][]>();
const places = sessionCache.getPlaces();
const refinedRecords = transform(places, (place: Place) => {
return [
place.type.friendly,
places.forEach(place => {
const parent = Config.getParentProperty(place.type);
const record = [
place.hierarchyProperties[parent.property_name],
place.name,
place.contact.properties.name,
place.contact.properties.phone,
place.creationDetails.username,
place.creationDetails.password,
place.creationDetails.disabledUsers,
];
const result = results.get(place.type) || [];
result.push(record);
results.set(place.type, result);
});

return stringify(refinedRecords);
const zip = new JSZip();
results.forEach((places, contactType) => {
const parent = Config.getParentProperty(contactType);
const columns = [
parent.friendly_name,
contactType.friendly,
'name',
'phone',
'username',
'password'
];
zip.file(
`${contactType.name}.csv`,
stringify(places, {
columns: columns,
header: true,
})
);
});
reply.header('Content-Disposition', `attachment; filename="${Date.now()}_${req.chtSession.authInfo.friendly}_users.zip"`);
return zip.generateNodeStream();
});
}
18 changes: 14 additions & 4 deletions src/services/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,28 @@ export default class Place {
}, {});
};

const contactAttributes = (contactType: string) => {
const RESERVED_CONTACT_TYPES = ['district_hospital', 'health_center', 'clinic', 'person'];

if (RESERVED_CONTACT_TYPES.includes(contactType)) {
return { type: contactType };
}

return {
type: 'contact',
contact_type: contactType,
};
};
return {
...filteredProperties(this.properties),
...contactAttributes(this.type.name),
_id: this.isReplacement ? this.resolvedHierarchy[0]?.id : this.id,
type: 'contact',
contact_type: this.type.name,
parent: this.resolvedHierarchy[1]?.id,
user_attribution,
contact: {
...filteredProperties(this.contact.properties),
...contactAttributes(this.contact.type.contact_type),
name: this.contact.name,
type: 'contact',
contact_type: this.contact.type.contact_type,
user_attribution,
}
};
Expand Down
1 change: 1 addition & 0 deletions src/services/upload-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChtApi, PlacePayload } from '../lib/cht-api';
import { Config } from '../config';
import Place, { PlaceUploadState } from './place';
import RemotePlaceCache from '../lib/remote-place-cache';
import SessionCache, { SessionCacheUploadState } from './session-cache';
import { UploadNewPlace } from './upload.new';
import { UploadReplacementPlace } from './upload.replacement';
import { UserPayload } from './user-payload';
Expand Down
13 changes: 12 additions & 1 deletion test/lib/retry-logic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const RetryScenarios = [
retry: 'upload-manager'
},
{
desc: 'password too weak',
desc: 'password too weak (json)',
axiosError: {
code: 'ERR_BAD_REQUEST',
response: {
Expand All @@ -57,6 +57,17 @@ const RetryScenarios = [
},
retry: 'upload-manager'
},
{
desc: 'password too weak (string)',
axiosError: {
code: 'ERR_BAD_REQUEST',
response: {
status: 400,
data: 'The password is too easy to guess. Include a range of types of characters to increase the score.',
}
},
retry: 'upload-manager'
},
];

export const UploadManagerRetryScenario = RetryScenarios[RetryScenarios.length - 1];
Expand Down
Loading

0 comments on commit 3413ce8

Please sign in to comment.