Skip to content

Commit

Permalink
WAITP-1255: Tupaia web server report and dashboard routes (#4602)
Browse files Browse the repository at this point in the history
* WAITP-1255 Add web config to api client

* WAITP-1255 Add user and report route to tupaia-web-server

* WAITP-1255 Update SessionSwitchingAuthHandler to use util functions

* WAITP-1255 Add fetchReport to ReportApi

* WAITP-1255 Add temporary logout route

* WAITP-1255 Allow no session in attach function

* WAITP-1255 Update user route default to match current web config server
behaviour

* WAITP-1255 Add dashboard route to tupaia-web-server

* WAITP-1255 Add mock web config api

* WAITP-1255 Remove unnecessary SessionCookie redefinition

* WAITP-1255 PR Fixups

* WAITP-1255 Add mock export

* WAITP-1255 Only discard 401s from attachSession

* WAITP-1255 Do it the recommended way instead
  • Loading branch information
EMcQ-BES authored Jun 7, 2023
1 parent a30f89c commit 6be041a
Show file tree
Hide file tree
Showing 21 changed files with 226 additions and 14 deletions.
5 changes: 5 additions & 0 deletions packages/api-client/src/MockTupaiaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DataTableApiInterface,
EntityApiInterface,
ReportApiInterface,
WebConfigApiInterface,
} from './connections';

import {
Expand All @@ -17,6 +18,7 @@ import {
MockDataTableApi,
MockEntityApi,
MockReportApi,
MockWebConfigApi,
} from './connections/mocks';

export class MockTupaiaApiClient {
Expand All @@ -25,18 +27,21 @@ export class MockTupaiaApiClient {
public readonly dataTable: DataTableApiInterface;
public readonly entity: EntityApiInterface;
public readonly report: ReportApiInterface;
public readonly webConfig: WebConfigApiInterface;

public constructor({
auth = new MockAuthApi(),
central = new MockCentralApi(),
dataTable = new MockDataTableApi(),
entity = new MockEntityApi(),
report = new MockReportApi(),
webConfig = new MockWebConfigApi(),
} = {}) {
this.auth = auth;
this.central = central;
this.dataTable = dataTable;
this.entity = entity;
this.report = report;
this.webConfig = webConfig
}
}
4 changes: 4 additions & 0 deletions packages/api-client/src/TupaiaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
DataTableApi,
EntityApi,
ReportApi,
WebConfigApi,
AuthApiInterface,
CentralApiInterface,
DataTableApiInterface,
EntityApiInterface,
ReportApiInterface,
WebConfigApiInterface,
} from './connections';
import { PRODUCTION_BASE_URLS, ServiceBaseUrlSet } from './constants';

Expand All @@ -26,12 +28,14 @@ export class TupaiaApiClient {
public readonly dataTable: DataTableApiInterface;
public readonly auth: AuthApiInterface;
public readonly report: ReportApiInterface;
public readonly webConfig: WebConfigApiInterface;

public constructor(authHandler: AuthHandler, baseUrls: ServiceBaseUrlSet = PRODUCTION_BASE_URLS) {
this.auth = new AuthApi(new ApiConnection(authHandler, baseUrls.auth));
this.entity = new EntityApi(new ApiConnection(authHandler, baseUrls.entity));
this.central = new CentralApi(new ApiConnection(authHandler, baseUrls.central));
this.report = new ReportApi(new ApiConnection(authHandler, baseUrls.report));
this.dataTable = new DataTableApi(new ApiConnection(authHandler, baseUrls.dataTable));
this.webConfig = new WebConfigApi(new ApiConnection(authHandler, baseUrls.webConfig));
}
}
13 changes: 5 additions & 8 deletions packages/api-client/src/auth/SessionSwitchingAuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import { createBasicHeader } from '@tupaia/utils';
import { createBasicHeader, getEnvVarOrDefault } from '@tupaia/utils';
import { AuthHandler, SessionType } from '../types';

const { API_CLIENT_NAME = '', API_CLIENT_PASSWORD = '' } = process.env;
const DEFAULT_AUTH_HEADER = createBasicHeader(
API_CLIENT_NAME,
API_CLIENT_PASSWORD,
);

// Handles switching between microservice client and user login sessions
export class SessionSwitchingAuthHandler implements AuthHandler {
session?: SessionType;
Expand All @@ -29,6 +23,9 @@ export class SessionSwitchingAuthHandler implements AuthHandler {
return this.session.getAuthHeader();
}

return DEFAULT_AUTH_HEADER;
return createBasicHeader(
getEnvVarOrDefault('API_CLIENT_NAME', ''),
getEnvVarOrDefault('API_CLIENT_PASSWORD', ''),
);
}
}
4 changes: 4 additions & 0 deletions packages/api-client/src/connections/ReportApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class ReportApi extends BaseApi {
public async fetchTransformSchemas() {
return this.connection.get('fetchTransformSchemas');
}

public async fetchReport(reportCode: string, query?: QueryParameters | null) {
return this.connection.get(`fetchReport/${reportCode}`, query);
}
}

export interface ReportApiInterface extends PublicInterface<ReportApi> {}
16 changes: 16 additions & 0 deletions packages/api-client/src/connections/WebConfigApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import { QueryParameters } from '../types';
import { BaseApi } from './BaseApi';
import { PublicInterface } from './types';

export class WebConfigApi extends BaseApi {
public async fetchReport(reportCode: string, query?: QueryParameters | null) {
return this.connection.get(`report/${reportCode}`, query);
}
}

export interface WebConfigApiInterface extends PublicInterface<WebConfigApi> {};
1 change: 1 addition & 0 deletions packages/api-client/src/connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { DataTableApi, DataTableApiInterface } from './DataTableApi';
export { EntityApi, EntityApiInterface } from './EntityApi';
export { CentralApi, CentralApiInterface } from './CentralApi';
export { ReportApi, ReportApiInterface } from './ReportApi';
export { WebConfigApi, WebConfigApiInterface } from './WebConfigApi';
3 changes: 3 additions & 0 deletions packages/api-client/src/connections/mocks/MockReportApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ export class MockReportApi implements ReportApiInterface {
public fetchTransformSchemas(): Promise<any> {
throw new Error('Method not implemented.');
}
public fetchReport(): Promise<any> {
throw new Error('Method not implemented.');
}
}
12 changes: 12 additions & 0 deletions packages/api-client/src/connections/mocks/MockWebConfigApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import { WebConfigApiInterface } from '..';

export class MockWebConfigApi implements WebConfigApiInterface {
public fetchReport(): Promise<any> {
throw new Error('Method not implemented.');
}
}
1 change: 1 addition & 0 deletions packages/api-client/src/connections/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { MockCentralApi } from './MockCentralApi';
export { MockDataTableApi } from './MockDataTableApi';
export { MockEntityApi } from './MockEntityApi';
export { MockReportApi } from './MockReportApi';
export { MockWebConfigApi } from './MockWebConfigApi';
14 changes: 12 additions & 2 deletions packages/api-client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export const DATA_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';

type ServiceName = 'auth' | 'entity' | 'central' | 'report' | 'dataTable';
type ServiceName = 'auth' | 'entity' | 'central' | 'report' | 'dataTable' | 'webConfig';
export type ServiceBaseUrlSet = Record<ServiceName, string>;

const productionSubdomains = [
Expand Down Expand Up @@ -52,6 +52,11 @@ const SERVICES = {
version: 'v1',
localPort: '8010',
},
webConfig: {
subdomain: 'config',
version: 'v1',
localPort: '8000',
},
};

const getLocalUrl = (service: ServiceName): string =>
Expand All @@ -62,6 +67,7 @@ export const LOCALHOST_BASE_URLS: ServiceBaseUrlSet = {
central: getLocalUrl('central'),
report: getLocalUrl('report'),
dataTable: getLocalUrl('dataTable'),
webConfig: getLocalUrl('webConfig'),
};

const getServiceUrl = (service: ServiceName, subdomainPrefix?: string): string => {
Expand All @@ -76,6 +82,7 @@ export const DEV_BASE_URLS: ServiceBaseUrlSet = {
central: getServiceUrl('central', 'dev'),
report: getServiceUrl('report', 'dev'),
dataTable: getServiceUrl('dataTable', 'dev'),
webConfig: getServiceUrl('webConfig', 'dev'),
};

export const PRODUCTION_BASE_URLS: ServiceBaseUrlSet = {
Expand All @@ -84,6 +91,7 @@ export const PRODUCTION_BASE_URLS: ServiceBaseUrlSet = {
central: getServiceUrl('central'),
report: getServiceUrl('report'),
dataTable: getServiceUrl('dataTable'),
webConfig: getServiceUrl('webConfig'),
};

const getServiceUrlForSubdomain = (service: ServiceName, originalSubdomain: string): string => {
Expand Down Expand Up @@ -122,16 +130,18 @@ const getDefaultBaseUrls = (hostname: string): ServiceBaseUrlSet => {
central: getServiceUrlForSubdomain('central', subdomain),
report: getServiceUrlForSubdomain('report', subdomain),
dataTable: getServiceUrlForSubdomain('dataTable', subdomain),
webConfig: getServiceUrlForSubdomain('webConfig', subdomain),
};
};

export const getBaseUrlsForHost = (hostname: string): ServiceBaseUrlSet => {
const { auth, entity, central, report, dataTable } = getDefaultBaseUrls(hostname);
const { auth, entity, central, report, dataTable, webConfig } = getDefaultBaseUrls(hostname);
return {
auth: process.env.AUTH_API_URL || auth,
entity: process.env.ENTITY_API_URL || entity,
central: process.env.CENTRAL_API_URL || central,
report: process.env.REPORT_API_URL || report,
dataTable: process.env.DATA_TABLE_API_URL || dataTable,
webConfig: process.env.WEB_CONFIG_API_URL || webConfig,
};
};
1 change: 1 addition & 0 deletions packages/server-boilerplate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
SessionType,
SessionCookie,
attachSession,
attachSessionIfAvailable,
} from './orchestrator';
export * from './types';
export * from './models';
Expand Down
2 changes: 1 addition & 1 deletion packages/server-boilerplate/src/orchestrator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
export { ApiBuilder } from './api';
export { SessionModel, SessionType } from './models';
export { SessionCookie } from './types';
export { attachSession } from './session';
export { attachSession, attachSessionIfAvailable } from './session';
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ export const attachSession = async (req: Request, res: Response, next: NextFunct
next(error);
}
};

export const attachSessionIfAvailable = async (req: Request, res: Response, next: NextFunction) => {
// Discard authorization errors from attach session so function succeeds even if session doesn't exist
try {
attachSession(req, res, () => { next() });
} catch(error: any) {
if (error instanceof UnauthenticatedError) {
next();
} else {
throw error;
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

export { attachSession } from './attachSession';
export { attachSession, attachSessionIfAvailable } from './attachSession';
5 changes: 4 additions & 1 deletion packages/tupaia-web-server/src/@types/express/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

import { AccessPolicy } from '@tupaia/access-policy';
import { TupaiaApiClient } from '@tupaia/api-client';
import { SessionCookie } from '@tupaia/server-boilerplate';

import { TupaiaWebSessionType } from '../../models';
import { TupaiaWebSessionType, TupaiaWebSessionModel } from '../../models';

declare global {
namespace Express {
export interface Request {
accessPolicy: AccessPolicy;
sessionModel: TupaiaWebSessionModel;
sessionCookie?: SessionCookie;
session: TupaiaWebSessionType;
ctx: {
services: TupaiaApiClient;
Expand Down
21 changes: 20 additions & 1 deletion packages/tupaia-web-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,40 @@
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import { TupaiaDatabase } from '@tupaia/database';
import {
OrchestratorApiBuilder,
handleWith,
useForwardUnhandledRequests,
attachSessionIfAvailable,
} from '@tupaia/server-boilerplate';
import { SessionSwitchingAuthHandler } from '@tupaia/api-client';
import { TupaiaWebSessionModel } from '../models';
import {
DashboardsRoute,
ReportRoute,
UserRoute,
TempLogoutRoute,

DashboardsRequest,
ReportRequest,
UserRequest,
TempLogoutRequest,
} from '../routes';

const { WEB_CONFIG_API_URL = 'http://localhost:8000/api/v1' } = process.env;

export function createApp() {
const app = new OrchestratorApiBuilder(new TupaiaDatabase(), 'tupaia-web')
.attachApiClientToContext(req => new SessionSwitchingAuthHandler(req.session))
.useSessionModel(TupaiaWebSessionModel)
.useAttachSession(attachSessionIfAvailable)
.attachApiClientToContext(req => new SessionSwitchingAuthHandler(req.session))
.get<ReportRequest>('report/:reportCode', handleWith(ReportRoute))
.get<UserRequest>('getUser', handleWith(UserRoute))
.get<DashboardsRequest>('dashboards', handleWith(DashboardsRoute))
// TODO: Stop using get for logout, then delete this
.get<TempLogoutRequest>('logout', handleWith(TempLogoutRoute))
.build();

useForwardUnhandledRequests(app, WEB_CONFIG_API_URL);
Expand Down
31 changes: 31 additions & 0 deletions packages/tupaia-web-server/src/routes/DashboardsRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';

export type DashboardsRequest = Request<
any,
any,
any,
any
>;

export class DashboardsRoute extends Route<DashboardsRequest> {
public async buildResponse() {
const { query, ctx } = this.req;
const { organisationUnitCode, projectCode } = query;

const project = (await ctx.services.central.fetchResources('projects', { filter: { code: projectCode }, columns: JSON.stringify(['entity_id', 'entity_hierarchy.name']) }))[0];
const baseEntity = await ctx.services.entity.getEntity(project['entity_hierarchy.name'], organisationUnitCode);
// TODO: Add a better getAncestors function to the EntityApi
const entities = await ctx.services.entity.getRelationshipsOfEntity(project['entity_hierarchy.name'], project.entity_id, 'descendant', {}, {} , { filter: { type: baseEntity.type } });
const dashboards = await ctx.services.central.fetchResources('dashboards', { filter: { root_entity_code: entities.ancestors }});
return Promise.all(dashboards.map(async (dash: any) => ({
...dash,
items: await ctx.services.central.fetchResources(`dashboards/${dash.id}/dashboardRelations`)
})));
}
}
30 changes: 30 additions & 0 deletions packages/tupaia-web-server/src/routes/ReportRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*
*/

import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';

export type ReportRequest = Request<
{ reportCode: string },
any,
any,
any
>;

export class ReportRoute extends Route<ReportRequest> {
public async buildResponse() {
const { query, ctx } = this.req;
const { reportCode } = this.req.params;
const { legacy } = query;

// Legacy data builders are handled through the web config server still
if (legacy === 'true') {
return ctx.services.webConfig.fetchReport(reportCode, query);
}

return ctx.services.report.fetchReport(reportCode, query);
}
}
Loading

0 comments on commit 6be041a

Please sign in to comment.