Skip to content

Commit

Permalink
fix(#12): address feedback part 5
Browse files Browse the repository at this point in the history
  • Loading branch information
paulpascal committed Jun 20, 2024
1 parent feffcf1 commit d88902a
Show file tree
Hide file tree
Showing 22 changed files with 223 additions and 217 deletions.
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ FROM node:20-alpine

ENV EXTERNAL_PORT 3000
ENV PORT 3000
ENV NODE_ENV production

WORKDIR /app

Expand All @@ -14,7 +13,7 @@ RUN apk add git
RUN npm ci --omit=dev

COPY src ./src
COPY tsconfig.json .
COPY tsconfig.json ./
RUN npm run build

CMD npm start
2 changes: 0 additions & 2 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ FROM node:20-alpine

WORKDIR /app

ENV NODE_ENV production

COPY package*.json ./
RUN apk add git
RUN npm ci --omit=dev
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Variable | Description | Sample
`EXTERNAL_PORT` | Port to use in docker compose when starting the web server | `3000`
`PORT` | For localhost development environment | `3000`
`COOKIE_PRIVATE_KEY` | A string used to two-way encryption of main app cookies. Production values need to be a secret. Suggest `uuidgen` to generate | `589a7f23-5bb2-4b77-ac78-f202b9b6d5e3`
`QUEUE_PRIVATE_KEY` | A string used to two-way encryption sensitive data passed to workers. Production values need to be a secret. Suggest `uuidgen` to generate | `589a7f23-5bb2-4b77-ac78-f202b9b6d5e3`
`QUEUE_PRIVATE_KEY` | A string used to two-way encryption sensitive data passed to workers. Recommend to be different from `COOKIE_PRIVATE_KEY`. Production values need to be a secret. Suggest `uuidgen` to generate | `2b57pd5e-f272-og90-8u97-89a7589a7f23`
`INTERFACE` | Interface to bind to. Leave as '0.0.0.0' for prod, suggest '127.0.0.1' for development | `127.0.0.1`
`CHT_DEV_URL_PORT` | CHT instance when in `NODE_ENV===dev`. Needs URL and port | `192-168-1-26.local-ip.medicmobile.org:10463`
`CHT_DEV_HTTP` | 'false' for http 'true' for https | `false`
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ services:
cht-user-management:
image: ${CHT_USER_MANAGEMENT_IMAGE:-public.ecr.aws/medic/cht-user-management:latest}
environment:
- NODE_ENV=${NODE_ENV:-production}
- EXTERNAL_PORT=${EXTERNAL_PORT:-3000}
- COOKIE_PRIVATE_KEY=${COOKIE_PRIVATE_KEY}
- QUEUE_PRIVATE_KEY=${QUEUE_PRIVATE_KEY}
- CONFIG_NAME=${CONFIG_NAME}
- CHT_DEV_URL_PORT=${CHT_DEV_URL_PORT}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
ports:
Expand All @@ -26,6 +28,7 @@ services:
restart: always
command: npm run start:worker
environment:
- NODE_ENV=${NODE_ENV:-production}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- QUEUE_PRIVATE_KEY=${QUEUE_PRIVATE_KEY}
Expand Down
5 changes: 3 additions & 2 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
NODE_ENV=
COOKIE_PRIVATE_KEY=
QUEUE_PRIVATE_KEY= # should be different from COOKIE_PRIVATE_KEY
CONFIG_NAME=chis-ke # Name of the configuration
PORT=3000 # for development environmentcontainer)
EXTERNAL_PORT=3000 # for docker
INTERFACE=0.0.0.0 # Leave as '0.0.0.0' for prod, suggest '127.0.0.1' for development
#REDIS_HOST=redis # Redis server hostname - only uncomment if you know what you're doing
#REDIS_PORT=6378 # Redis server port - only uncomment if you know what you're doing
#REDIS_HOST=redis # Redis server hostname - only uncomment if you know what you're doing
#REDIS_PORT=6378 # Redis server port - only uncomment if you know what you're doing
CHT_DEV_HTTP=false # 'true' for http 'false' for https
CHT_DEV_URL_PORT=localhost:5984 # where your dev CHT instance is, hostname:port
#CHT_USER_MANAGEMENT_IMAGE=cht-user-management:local # docker image for cht-user-management service - uncomment to use with local development
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"dev": "tsc-watch --onSuccess \"node dist/index.js\"",
"publish:cht-user-management": "SERVICE=cht-user-management node scripts/publish.js",
"publish:cht-user-management-worker": "SERVICE=cht-user-management-worker node scripts/publish.js",
"publish": "npm run publish:cht-user-management && npm run publish:cht-user-management-worker",
"start:worker": "node dist/worker/main.js"
},
"repository": {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken';
import ChtSession from './cht-session';

const LOGIN_EXPIRES_AFTER_MS = 2 * 24 * 60 * 60 * 1000;
const QUEUE_SESSION_EXPIRATION = '48h';
const { COOKIE_PRIVATE_KEY, QUEUE_PRIVATE_KEY } = process.env;
const PRIVATE_KEY_SALT = '_'; // change to logout all users
const COOKIE_SIGNING_KEY = COOKIE_PRIVATE_KEY + PRIVATE_KEY_SALT;
Expand Down Expand Up @@ -43,7 +44,7 @@ export default class Auth {
}

public static encodeTokenForQueue(session: ChtSession) {
return this.encodeToken(session, QUEUE_SIGNING_KEY, '5h');
return this.encodeToken(session, QUEUE_SIGNING_KEY, QUEUE_SESSION_EXPIRATION);
}

public static decodeTokenForQueue(token: string): ChtSession {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { ChtApi } from './cht-api';
import RemotePlaceResolver from './remote-place-resolver';
import Place from '../services/place';

import { JobParams, IQueueManager } from '../shared/queues';
import { JobParams, IQueue } from '../lib/queues';
import Auth from './authentication';
import { MoveContactData } from '../worker/move-contact-worker';

export default class MoveLib {
constructor() { }

public static async move(formData: any, contactType: ContactType, sessionCache: SessionCache, chtApi: ChtApi, queueManager: IQueueManager) {
public static async move(formData: any, contactType: ContactType, sessionCache: SessionCache, chtApi: ChtApi, moveContactQueue: IQueue) {
const fromLineage = await resolve('from_', formData, contactType, sessionCache, chtApi);
const toLineage = await resolve('to_', formData, contactType, sessionCache, chtApi);

Expand All @@ -31,7 +31,7 @@ export default class MoveLib {
jobName,
jobData,
};
await queueManager.addJob(jobParam);
await moveContactQueue.add(jobParam);

return {
toLineage,
Expand Down
50 changes: 50 additions & 0 deletions src/lib/queues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { v4 } from 'uuid';
import { env } from 'process';
import { JobsOptions, Queue, ConnectionOptions } from 'bullmq';

const MOVE_CONTACT_QUEUE = 'MOVE_CONTACT_QUEUE';

const environment = env as unknown as {
REDIS_HOST: string;
REDIS_PORT: string;
QUEUE_PRIVATE_KEY: string;
};

export const QUEUE_PRIVATE_KEY = environment.QUEUE_PRIVATE_KEY;

export const redisConnection: ConnectionOptions = {
host: environment.REDIS_HOST,
port: Number(environment.REDIS_PORT)
};

export interface IQueue {
name: string;
add(jobParams: any): Promise<string>;
}

export interface JobParams {
jobName: string;
jobData: any;
jobOpts?: JobsOptions;
}

export class BullQueue implements IQueue {
public readonly name: string;
public readonly bullQueue: Queue;

constructor(queueName: string) {
this.name = queueName;
this.bullQueue = new Queue(queueName, { connection: redisConnection });
}

public async add(jobParams: JobParams): Promise<string> {
const jobId = v4();
const { jobName, jobData, jobOpts } = jobParams;

await this.bullQueue.add(jobName, jobData, { jobId, ...jobOpts });
return jobId;
}
}

// Create a singleton instance of QueueManager
export const moveContactQueue = new BullQueue(MOVE_CONTACT_QUEUE);
2 changes: 2 additions & 0 deletions src/liquid/place/move_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ <h1 class="subtitle">To</h1>
position: 'top-center',
message: `<p>This will be moved shortly! It may take up to 24 hours to process.</p>`,
});
} else {
console.error('Something went wrong, this contact cannot be moved.');
}
}
</script>
5 changes: 2 additions & 3 deletions src/plugins/bullmq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { createBullBoard } from '@bull-board/api';
import { FastifyAdapter } from '@bull-board/fastify';

import { queueManager } from '../shared/queues';
import { moveContactQueue } from '../lib/queues';


async function bullMQBoardPlugin(fastify: FastifyInstance) {
const serverAdapter = new FastifyAdapter();

const moveContactQueue = queueManager.getQueue();

createBullBoard({
queues: [
new BullMQAdapter(
moveContactQueue
moveContactQueue.bullQueue
),
],
serverAdapter,
Expand Down
4 changes: 2 additions & 2 deletions src/routes/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ChtApi } from '../lib/cht-api';
import { FastifyInstance } from 'fastify';
import MoveLib from '../lib/move';
import SessionCache from '../services/session-cache';
import { queueManager } from '../shared/queues';
import { moveContactQueue } from '../lib/queues';

export default async function sessionCache(fastify: FastifyInstance) {
fastify.get('/move/:placeType', async (req, resp) => {
Expand Down Expand Up @@ -35,7 +35,7 @@ export default async function sessionCache(fastify: FastifyInstance) {
const chtApi = new ChtApi(req.chtSession);

try {
const result = await MoveLib.move(formData, contactType, sessionCache, chtApi, queueManager);
const result = await MoveLib.move(formData, contactType, sessionCache, chtApi, moveContactQueue);

const tmplData = {
view: 'move',
Expand Down
10 changes: 0 additions & 10 deletions src/shared/queue-config.ts

This file was deleted.

50 changes: 0 additions & 50 deletions src/shared/queues.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/worker/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { config } from 'dotenv';

config();

import { queueManager } from '../shared/queues';
import { moveContactQueue } from '../lib/queues';
import { MoveContactWorker } from './move-contact-worker';

(async () => {
new MoveContactWorker(queueManager.getQueue().name);
(() => {
new MoveContactWorker(moveContactQueue.name);
console.log(`🚀 Move Contact Worker is listening`);
})();
6 changes: 3 additions & 3 deletions src/worker/move-contact-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { spawn } from 'child_process';
import { Worker, Job, DelayedError } from 'bullmq';

import Auth from '../lib/authentication';
import { redisConnection } from '../shared/queue-config';
import { redisConnection } from '../lib/queues';
import { DateTime } from 'luxon';

export interface MoveContactData {
Expand All @@ -16,8 +16,8 @@ export interface MoveContactData {
export type JobResult = { success: boolean; message: string };

export class MoveContactWorker {
private readonly DELAY_IN_MILLIS = 60 * 60 * 1000; // 60 minutes
private readonly MAX_TIMEOUT_IN_MILLIS = 60 * 60 * 1000; // 60 minutes
private readonly DELAY_IN_MILLIS = 4 * 60 * 60 * 1000; // 4 hours
private readonly MAX_TIMEOUT_IN_MILLIS = 4 * 60 * 60 * 1000; // 4 hours
private readonly MAX_CONCURRENCY = 1; // Limit concurrency to 1 job at a time
private readonly MAX_SENTINEL_BACKLOG = 7000; // ensure we don't take down the server

Expand Down
24 changes: 21 additions & 3 deletions test/lib/authentication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,40 @@ chai.use(chaiExclude);
const { expect } = chai;

describe('lib/authentication.ts', () => {
it('encode and decode', () => {
it('encode and decode for cookie', () => {
const session = mockChtSession();
const encoded = Auth.encodeTokenForCookie(session);
const decoded = Auth.decodeTokenForCookie(encoded);
expect(session).excluding('axiosInstance').to.deep.eq(decoded);
});

it('invalid token cannot be decoded', () => {
it('encode and decode for workers', () => {
const session = mockChtSession();
const encoded = Auth.encodeTokenForQueue(session);
const decoded = Auth.decodeTokenForQueue(encoded);
expect(session).excluding('axiosInstance').to.deep.eq(decoded);
});

it('invalid token cannot be decoded for cookie', () => {
expect(() => Auth.decodeTokenForCookie('encoded')).to.throw('jwt malformed');
});

it('invalid session cannot be decoded', () => {
it('invalid token cannot be decoded for workers', () => {
expect(() => Auth.decodeTokenForQueue('encoded')).to.throw('jwt malformed');
});

it('invalid session cannot be decoded for cookie', () => {
const session = mockChtSession();
delete session.username;
const encoded = Auth.encodeTokenForCookie(session);
expect(() => Auth.decodeTokenForCookie(encoded)).to.throw('invalid CHT session information');
});

it('invalid session cannot be decoded for workers', () => {
const session = mockChtSession();
delete session.username;
const encoded = Auth.encodeTokenForQueue(session);
expect(() => Auth.decodeTokenForQueue(encoded)).to.throw('invalid CHT session information');
});
});

Loading

0 comments on commit d88902a

Please sign in to comment.