Skip to content

Commit

Permalink
chore: suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
Benmuiruri committed Feb 17, 2025
1 parent 85c63f7 commit cee7190
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 66 deletions.
37 changes: 32 additions & 5 deletions src/lib/authentication-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,36 @@ export class AuthError extends Error {
super(errorMessage);
this.name = 'AuthError';
}
}

export const AuthErrors = {
INVALID_CREDENTIALS: () => new AuthError(401, 'Invalid username or password'),
MISSING_CREDENTIALS: () => new AuthError(401, 'Missing username or password'),
};
static INVALID_CREDENTIALS() {
return new AuthError(401, 'Invalid username or password');
}

static MISSING_CREDENTIALS() {
return new AuthError(401, 'Missing username or password');
}

static TOKEN_CREATION_FAILED(username: string, domain: string) {
return new AuthError(401, `Failed to obtain token for ${username} at ${domain}`);
}

static MISSING_FACILITY(username: string) {
return new AuthError(401, `User ${username} does not have a facility_id connected to their user doc`);
}

static INCOMPATIBLE_CHT_CORE_VERSION(domain: string, chtCoreVersion: string) {
return new AuthError(401, `CHT Core Version must be 4.7.0 or higher. "${domain}" is running ${chtCoreVersion}.`);
}

static CANNOT_PARSE_CHT_VERSION(chtCoreVersion: string, domain: string) {
return new AuthError(401, `Cannot parse cht core version ${chtCoreVersion} for instance "${domain}"`);
}

static assertSessionCreationError(e: unknown): never {
// @ts-expect-error
if (e?.response?.status === 401) {
throw AuthError.INVALID_CREDENTIALS();
}
throw e;
}
}
63 changes: 30 additions & 33 deletions src/lib/cht-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AxiosHeaders, AxiosInstance } from 'axios';
import axiosRetry from 'axios-retry';
import * as semver from 'semver';

import { AuthErrors } from './authentication-error';
import { AuthError } from './authentication-error';
import { AuthenticationInfo } from '../config';
import { axiosRetryConfig } from './retry-logic';
import { RemotePlace } from './remote-place-cache';
Expand Down Expand Up @@ -53,20 +53,9 @@ export default class ChtSession {
}

public static async create(authInfo: AuthenticationInfo, username: string, password: string): Promise<ChtSession> {
try {
const sessionToken = await ChtSession.createSessionToken(authInfo, username, password);
if (!sessionToken) {
throw new Error(`failed to obtain token for ${username} at ${authInfo.domain}`);
}

const creationDetails = await ChtSession.fetchCreationDetails(authInfo, username, sessionToken);
return new ChtSession(creationDetails);
} catch (e: any) {
if (e.response?.status === 401) {
throw AuthErrors.INVALID_CREDENTIALS();
}
throw e;
}
const sessionToken = await ChtSession.createSessionToken(authInfo, username, password);
const creationDetails = await ChtSession.fetchCreationDetails(authInfo, username, sessionToken);
return new ChtSession(creationDetails);
}

public static createFromDataString(data: string): ChtSession {
Expand All @@ -84,23 +73,31 @@ export default class ChtSession {
}

private static async createSessionToken(authInfo: AuthenticationInfo, username: string, password: string): Promise<string> {
const sessionUrl = ChtSession.createUrl(authInfo, '_session');
const resp = await axios.post(
sessionUrl,
{
name: username,
password,
},
{
auth: {
username,
password
try {
const sessionUrl = ChtSession.createUrl(authInfo, '_session');
const resp = await axios.post(
sessionUrl,
{
name: username,
password,
},
{
auth: {
username,
password
},
}
);
const setCookieHeader = (resp.headers as AxiosHeaders).get('set-cookie') as AxiosHeaders;
const token = setCookieHeader?.[0]?.split(';')
.find((header: string) => header.startsWith(COUCH_AUTH_COOKIE_NAME));
if (!token) {
throw AuthError.TOKEN_CREATION_FAILED(username, authInfo.domain);
}
);
const setCookieHeader = (resp.headers as AxiosHeaders).get('set-cookie') as AxiosHeaders;
return setCookieHeader?.[0]?.split(';')
.find((header: string) => header.startsWith(COUCH_AUTH_COOKIE_NAME));
return token;
} catch (e) {
AuthError.assertSessionCreationError(e);
}
}

private static async fetchCreationDetails(authInfo: AuthenticationInfo, username: string, sessionToken: string): Promise<SessionCreationDetails> {
Expand All @@ -122,7 +119,7 @@ export default class ChtSession {

const facilityIds = isAdmin ? [ADMIN_FACILITY_ID] : _.flatten([userDoc?.facility_id]).filter(Boolean);
if (!facilityIds?.length) {
throw Error(`User ${username} does not have a facility_id connected to their user doc`);
throw AuthError.MISSING_FACILITY(username);
}

ChtSession.assertCoreVersion(chtCoreVersion, authInfo.domain);
Expand All @@ -139,11 +136,11 @@ export default class ChtSession {
private static assertCoreVersion(chtCoreVersion: any, domain: string) {
const coercedVersion = semver.valid(semver.coerce(chtCoreVersion));
if (!coercedVersion) {
throw Error(`Cannot parse cht core version ${chtCoreVersion} for instance "${domain}"`);
throw AuthError.CANNOT_PARSE_CHT_VERSION(chtCoreVersion, domain);
}

if (semver.lt(coercedVersion, '4.7.0')) {
throw Error(`CHT Core Version must be 4.7.0 or higher. "${domain}" is running ${chtCoreVersion}.`);
throw AuthError.INCOMPATIBLE_CHT_CORE_VERSION(domain, chtCoreVersion);
}
}

Expand Down
28 changes: 16 additions & 12 deletions src/routes/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

import Auth from '../lib/authentication';
import { AuthError, AuthErrors } from '../lib/authentication-error';
import { AuthError } from '../lib/authentication-error';
import { Config } from '../config';
import { version as appVersion } from '../package.json';
import ChtSession from '../lib/cht-session';
Expand Down Expand Up @@ -38,11 +38,24 @@ export default async function authentication(fastify: FastifyInstance) {
const data: any = req.body;
const { username, password, domain } = data;

const getLoginErrorMessage = (error: unknown): string => {
if (error instanceof AuthError) {
console.error('Login error:', {
status: error.status,
message: error.errorMessage,
});
return error.errorMessage;
}

console.error('Login error:', error);
return 'Unexpected error logging in';
};

try {
const authInfo = Config.getAuthenticationInfo(domain);

if (!username || !password) {
throw AuthErrors.MISSING_CREDENTIALS();
throw AuthError.MISSING_CREDENTIALS();
}

const chtSession = await ChtSession.create(authInfo, username, password);
Expand All @@ -57,16 +70,7 @@ export default async function authentication(fastify: FastifyInstance) {

resp.header('HX-Redirect', '/');
} catch (e: any ) {
if (e instanceof AuthError) {
console.error('Login error:', {
status: e.status,
message: e.errorMessage,
});
return renderAuthForm(resp, e.errorMessage);
}

console.error('Login error:', e);
return renderAuthForm(resp, 'Unexpected error logging in');
return renderAuthForm(resp, getLoginErrorMessage(e));
}
});

Expand Down
16 changes: 1 addition & 15 deletions test/lib/authentication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chai from 'chai';
import chaiExclude from 'chai-exclude';
import { mockChtSession } from '../mocks';
import Auth from '../../src/lib/authentication';
import { AuthError, AuthErrors } from '../../src/lib/authentication-error';
import { AuthError } from '../../src/lib/authentication-error';

chai.use(chaiExclude);
const { expect } = chai;
Expand Down Expand Up @@ -43,19 +43,5 @@ describe('lib/authentication/authentication.ts', () => {
const encoded = Auth.encodeTokenForWorker(session);
expect(() => Auth.createWorkerSession(encoded)).to.throw('invalid CHT session information');
});

it('should create INVALID_CREDENTIALS error', () => {
const error = AuthErrors.INVALID_CREDENTIALS();
expect(error).to.be.instanceof(AuthError);
expect(error.status).to.equal(401);
expect(error.errorMessage).to.equal('Invalid username or password');
});

it('should create MISSING_CREDENTIALS error', () => {
const error = AuthErrors.MISSING_CREDENTIALS();
expect(error).to.be.instanceof(AuthError);
expect(error.status).to.equal(401);
expect(error.errorMessage).to.equal('Missing username or password');
});
});

12 changes: 11 additions & 1 deletion test/lib/cht-session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ describe('lib/cht-session.ts', () => {

it('throw cht yields no authtoken', async () => {
mockAxios.post.resolves(mockSessionResponse([]));
await expect(ChtSession.default.create(mockAuthInfo, 'user', 'pwd')).to.eventually.be.rejectedWith('failed to obtain token');
await expect(ChtSession.default.create(mockAuthInfo, 'user', 'pwd'))
.to.eventually.be.rejectedWith(`Failed to obtain token for user at ${mockAuthInfo.domain}`);
});

it('throw if no user doc', async () => {
Expand All @@ -91,6 +92,15 @@ describe('lib/cht-session.ts', () => {
mockAxios.get.onSecondCall().resolves({ data: { version: { app: '4.6.5' } } });
await expect(ChtSession.default.create(mockAuthInfo, 'user', 'pwd')).to.eventually.be.rejectedWith('CHT Core Version must be');
});

it('throws if invalid credentials', async () => {
mockAxios.post.rejects({ response: { status: 401 } });

await expect(ChtSession.default.create(mockAuthInfo, 'user', 'wrong_pwd'))
.to.be.rejectedWith('Invalid username or password')
.and.to.eventually.be.instanceof(AuthError)
.and.to.have.property('status', 401);
});
});

it('createFromDataString', async () => {
Expand Down

0 comments on commit cee7190

Please sign in to comment.