Skip to content

Commit

Permalink
Plugin hooks for public and protected web routes
Browse files Browse the repository at this point in the history
  • Loading branch information
newmanw committed Sep 25, 2024
1 parent 7a0a59b commit 52e0a04
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 101 deletions.
103 changes: 59 additions & 44 deletions plugins/arcgis/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SettingPermission } from '@ngageoint/mage.service/lib/entities/authoriz
import express from 'express'
import { ArcGISPluginConfig } from './ArcGISPluginConfig'
import { ObservationProcessor } from './ObservationProcessor'
import {HttpClient} from './HttpClient'
import { HttpClient } from './HttpClient'
import { FeatureServiceResult } from './FeatureServiceResult'

const logPrefix = '[mage.arcgis]'
Expand Down Expand Up @@ -50,52 +50,67 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
const processor = new ObservationProcessor(stateRepo, eventRepo, obsRepoForEvent, userRepo, console);
processor.start();
return {
webRoutes(requestContext: GetAppRequestContext) {
const routes = express.Router()
.use(express.json())
.use(async (req, res, next) => {
const context = requestContext(req)
const user = context.requestingPrincipal()
if (!user.role.permissions.find(x => x === SettingPermission.UPDATE_SETTINGS)) {
return res.status(403).json({ message: 'unauthorized' })
}
next()
webRoutes: {
public: (requestContext: GetAppRequestContext) => {
const routes = express.Router().use(express.json())
routes.post('/oauth/signin', async (req, res, next) => {
// TODO implement
})
routes.route('/config')
.get(async (req, res, next) => {
console.info('Getting ArcGIS plugin config...')
const config = await processor.safeGetConfig();
res.json(config)
})
.put(async (req, res, next) => {
console.info('Applying ArcGIS plugin config...')
const arcConfig = req.body as ArcGISPluginConfig
const configString = JSON.stringify(arcConfig)
console.info(configString)
processor.putConfig(arcConfig)
res.status(200).json({})

routes.post('/oauth/authenticate', async (req, res, next) => {
// TODO implement
})
routes.route('/arcgisLayers')
.get(async (req, res, next) => {
const featureUrl = req.query.featureUrl as string;
console.info('Getting ArcGIS layer info for ' + featureUrl)
const httpClient = new HttpClient(console);
httpClient.sendGetHandleResponse(featureUrl, (chunk) => {
console.info('ArcGIS layer info response ' + chunk);
try {
const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult;
res.json(featureServiceResult);
} catch(e) {
if(e instanceof SyntaxError) {
console.error('Problem with url response for url ' + featureUrl + ' error ' + e)
res.status(200).json({})
} else {
throw e;
}

return routes
},
protected: (requestContext: GetAppRequestContext) => {
const routes = express.Router()
.use(express.json())
.use(async (req, res, next) => {
const context = requestContext(req)
const user = context.requestingPrincipal()
if (!user.role.permissions.find(x => x === SettingPermission.UPDATE_SETTINGS)) {
return res.status(403).json({ message: 'unauthorized' })
}
});
})
return routes
next()
})
routes.route('/config')
.get(async (req, res, next) => {
console.info('Getting ArcGIS plugin config...')
const config = await processor.safeGetConfig();
res.json(config)
})
.put(async (req, res, next) => {
console.info('Applying ArcGIS plugin config...')
const arcConfig = req.body as ArcGISPluginConfig
const configString = JSON.stringify(arcConfig)
console.info(configString)
processor.putConfig(arcConfig)
res.status(200).json({})
})
routes.route('/arcgisLayers')
.get(async (req, res, next) => {
const featureUrl = req.query.featureUrl as string;
console.info('Getting ArcGIS layer info for ' + featureUrl)
const httpClient = new HttpClient(console);
httpClient.sendGetHandleResponse(featureUrl, (chunk) => {
console.info('ArcGIS layer info response ' + chunk);
try {
const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult;
res.json(featureServiceResult);
} catch (e) {
if (e instanceof SyntaxError) {
console.error('Problem with url response for url ' + featureUrl + ' error ' + e)
res.status(200).json({})
} else {
throw e;
}
}
});
})

return routes
}
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions plugins/image/service/package-lock.json

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

68 changes: 35 additions & 33 deletions plugins/image/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,39 +54,41 @@ const imagePluginHooks: InitPluginHook<typeof InjectedServices> = {
const control = await createImagePluginControl(stateRepo, eventRepo, obsRepoForEvent, attachmentStore, queryAttachments, imageService, console)
control.start()
return {
webRoutes(requestContext: GetAppRequestContext): express.Router {
// TODO: add api routes to save image processing settings
const routes = express.Router()
.use(express.json())
.use(async (req, res, next) => {
const context = requestContext(req)
const user = context.requestingPrincipal()
if (!user.role.permissions.find(x => x === SettingPermission.UPDATE_SETTINGS)) {
return res.status(403).json({ message: 'unauthorized' })
}
next()
})
routes.route('/config')
.get(async (req, res, next) => {
const config = await control.getConfig()
res.json(config)
})
.put(async (req, res, next) => {
const bodyConfig = req.body as any
const configPatch: Partial<ImagePluginConfig> = {
enabled: typeof bodyConfig.enabled === 'boolean' ? bodyConfig.enabled : undefined,
intervalBatchSize: typeof bodyConfig.intervalBatchSize === 'number' ? bodyConfig.intervalBatchSize : undefined,
intervalSeconds: typeof bodyConfig.intervalSeconds === 'number' ? bodyConfig.intervalSeconds : undefined,
thumbnailSizes: Array.isArray(bodyConfig.thumbnailSizes) ?
bodyConfig.thumbnailSizes.reduce((sizes: number[], size: any) => {
return typeof size === 'number' ? [ ...sizes, size ] : sizes
}, [] as number[])
: []
}
const config = await control.applyConfig(configPatch)
res.json(config)
})
return routes
webRoutes: {
protected(requestContext: GetAppRequestContext): express.Router {
// TODO: add api routes to save image processing settings
const routes = express.Router()
.use(express.json())
.use(async (req, res, next) => {
const context = requestContext(req)
const user = context.requestingPrincipal()
if (!user.role.permissions.find(x => x === SettingPermission.UPDATE_SETTINGS)) {
return res.status(403).json({ message: 'unauthorized' })
}
next()
})
routes.route('/config')
.get(async (req, res, next) => {
const config = await control.getConfig()
res.json(config)
})
.put(async (req, res, next) => {
const bodyConfig = req.body as any
const configPatch: Partial<ImagePluginConfig> = {
enabled: typeof bodyConfig.enabled === 'boolean' ? bodyConfig.enabled : undefined,
intervalBatchSize: typeof bodyConfig.intervalBatchSize === 'number' ? bodyConfig.intervalBatchSize : undefined,
intervalSeconds: typeof bodyConfig.intervalSeconds === 'number' ? bodyConfig.intervalSeconds : undefined,
thumbnailSizes: Array.isArray(bodyConfig.thumbnailSizes) ?
bodyConfig.thumbnailSizes.reduce((sizes: number[], size: any) => {
return typeof size === 'number' ? [...sizes, size] : sizes
}, [] as number[])
: []
}
const config = await control.applyConfig(configPatch)
res.json(config)
})
return routes
}
}
}
}
Expand Down
30 changes: 20 additions & 10 deletions service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ export const boot = async function(config: BootConfig): Promise<MageService> {
const dbLayer = await initDatabase()
const repos = await initRepositories(dbLayer, config)
const appLayer = await initAppLayer(repos)
const { webController, addAuthenticatedPluginRoutes } = await initWebLayer(repos, appLayer, config.plugins?.webUIPlugins || [])
const routesForPluginId: { [pluginId: string]: WebRoutesHooks['webRoutes'] } = {}
const collectPluginRoutesToSort = (pluginId: string, initPluginRoutes: WebRoutesHooks['webRoutes']) => {
const { webController, addPluginRoutes } = await initWebLayer(repos, appLayer, config.plugins?.webUIPlugins || [])
const routesForPluginId: {[pluginId: string]: WebRoutesHooks } = {}
const collectPluginRoutesToSort = (pluginId: string, initPluginRoutes: WebRoutesHooks): void => {
routesForPluginId[pluginId] = initPluginRoutes
}
const globalScopeServices = new Map<InjectionToken<any>, any>([
Expand Down Expand Up @@ -191,7 +191,7 @@ export const boot = async function(config: BootConfig): Promise<MageService> {
}
const pluginRoutePathsDescending = Object.keys(routesForPluginId).sort().reverse()
for (const pluginId of pluginRoutePathsDescending) {
addAuthenticatedPluginRoutes(pluginId, routesForPluginId[pluginId])
addPluginRoutes(pluginId, routesForPluginId[pluginId])
}

try {
Expand Down Expand Up @@ -532,8 +532,11 @@ interface MageEventRequestContext extends AppRequestContext<UserDocument> {

const observationEventScopeKey = 'observationEventScope' as const

async function initWebLayer(repos: Repositories, app: AppLayer, webUIPlugins: string[]):
Promise<{ webController: express.Application, addAuthenticatedPluginRoutes: (pluginId: string, pluginRoutes: WebRoutesHooks['webRoutes']) => void }> {
async function initWebLayer(
repos: Repositories,
app: AppLayer,
webUIPlugins: string[]
): Promise<{ webController: express.Application, addPluginRoutes: (pluginId: string, initPluginRoutes: WebRoutesHooks) => void }> {
// load routes the old way
const webLayer = await import('./express')
const webController = webLayer.app
Expand Down Expand Up @@ -648,13 +651,20 @@ async function initWebLayer(repos: Repositories, app: AppLayer, webUIPlugins: st
}
return {
webController,
addAuthenticatedPluginRoutes: (pluginId: string, initPluginRoutes: WebRoutesHooks['webRoutes']) => {
const routes = initPluginRoutes(pluginAppRequestContext)
webController.use(`/plugins/${pluginId}`, [ bearerAuth, routes ])
addPluginRoutes: (pluginId: string, initPluginRoutes: WebRoutesHooks): void => {
if (initPluginRoutes.webRoutes.public) {
const routes = initPluginRoutes.webRoutes.public(pluginAppRequestContext)
webController.use(`/plugins/${pluginId}`, [routes])
}

if (initPluginRoutes.webRoutes.protected) {
const routes = initPluginRoutes.webRoutes.protected(pluginAppRequestContext)
webController.use(`/plugins/${pluginId}`, [bearerAuth, routes])
}
}
}
}

function baseAppRequestContext(req: express.Request): AppRequestContext<UserWithRole> {
return {
requestToken: Symbol(),
Expand Down
11 changes: 8 additions & 3 deletions service/src/main.impl/main.impl.plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ export interface InjectableServices {
<Service>(token: InjectionToken<Service>): Service
}

export type AddPluginWebRoutes = (pluginId: string, initRoutes: WebRoutesHooks['webRoutes']) => void
export type AddPluginWebRoutes = (pluginId: string, webRoutes: WebRoutesHooks) => void

export async function integratePluginHooks(pluginId: string, plugin: InitPluginHook<any>, injectService: InjectableServices, addWebRoutesFromPlugin: AddPluginWebRoutes): Promise<void> {
export async function integratePluginHooks(
pluginId: string,
plugin: InitPluginHook<any>,
injectService: InjectableServices,
addWebRoutesFromPlugin: AddPluginWebRoutes,
): Promise<void> {
let injection: Injection<any> | null = null
let hooks: PluginHooks
if (plugin.inject) {
Expand All @@ -32,6 +37,6 @@ export async function integratePluginHooks(pluginId: string, plugin: InitPluginH
await loadIconsHooks(pluginId, hooks, injectService(StaticIconRepositoryToken))
await loadFeedsHooks(pluginId, hooks, injectService(FeedServiceTypeRepositoryToken))
if (hooks.webRoutes) {
await addWebRoutesFromPlugin(pluginId, hooks.webRoutes)
await addWebRoutesFromPlugin(pluginId, hooks)
}
}
9 changes: 6 additions & 3 deletions service/src/plugins.api/plugins.api.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export interface GetAppRequestContext {
(req: express.Request): AppRequestContext<UserExpanded>
}

export interface WebRoutesHooks {
webRoutes(requestContext: GetAppRequestContext): express.Router
}
export type WebRoutesHooks = {
webRoutes: {
public?: (requestContext: GetAppRequestContext) => express.Router,
protected?: (requestContext: GetAppRequestContext) => express.Router
}
}
2 changes: 1 addition & 1 deletion service/test/app/systemInfo/app.systemInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('CreateReadSystemInfo', () => {
.getSetting('disclaimer')
.returns(Promise.resolve(mockDisclaimer as any));
mockedSettingsModule
.getSetting('contactInfo')
.getSetting('contactinfo')
.returns(Promise.resolve(mockContactInfo as any));
mockedAuthConfigModule.getAllConfigurations().returns(Promise.resolve([]));
mockedAuthConfigTransformerModule.transform(Arg.any()).returns([]);
Expand Down
14 changes: 9 additions & 5 deletions service/test/main/main.plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Service1Impl implements Service1 {}
class Service2Impl implements Service2 {}
const serviceMap = new Map([[ Service1Token, new Service1Impl() ], [ Service2Token, new Service2Impl() ]])
const injectService: plugins.InjectableServices = (token: any) => serviceMap.get(token) as any
const initPluginRoutes: AddPluginWebRoutes = (pluginId: string, initPluginRoutes: WebRoutesHooks['webRoutes']) => void(0)
const initPluginRoutes: AddPluginWebRoutes = (pluginId: string, initPluginRoutes: WebRoutesHooks) => void(0)

interface InjectServiceHandle {
injectService: typeof injectService
Expand Down Expand Up @@ -71,17 +71,21 @@ describe('loading plugins', function() {
service2: Service2Token
}
const routes = express.Router()
const hook: WebRoutesHooks['webRoutes'] = (appRequestContext: (req: express.Request) => AppRequestContext<UserExpanded>) => routes
const hook = {
webRoutes: {
public: (appRequestContext: (req: express.Request) => AppRequestContext<UserExpanded>) => routes,
protected: (appRequestContext: (req: express.Request) => AppRequestContext<UserExpanded>) => routes
}
}

let injected: any = null
const initPlugin: InitPluginHook<typeof injectRequest> = {
inject: {

},
init: async (services): Promise<WebRoutesHooks> => {
injected = services
return {
webRoutes: hook
}
return hook
}
}
initPlugin.inject = injectRequest
Expand Down

0 comments on commit 52e0a04

Please sign in to comment.