Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin hooks for public and protected web routes #215

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading