Skip to content

Commit c9ccc2e

Browse files
committed
Merge branch 'feat/df-623-payment' of https://github.com/DEFRA/forms-engine-plugin into feat/df-623-payment
2 parents c4409b3 + 7db18ca commit c9ccc2e

14 files changed

Lines changed: 256 additions & 69 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@defra/forms-engine-plugin",
3-
"version": "4.0.42",
3+
"version": "4.0.43",
44
"description": "Defra forms engine",
55
"type": "module",
66
"files": [

src/client/javascripts/location-map.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,18 @@ export function makeTileRequestTransformer(apiPath) {
138138
* @param {string} resourceType - the resource type
139139
*/
140140
return function transformTileRequest(url, resourceType) {
141-
// Only proxy OS API requests that don't already have a key
142-
if (resourceType !== 'Style' && url.startsWith('https://api.os.uk')) {
143-
const urlObj = new URL(url)
144-
if (!urlObj.searchParams.has('key')) {
141+
if (url.startsWith('https://api.os.uk')) {
142+
if (resourceType === 'Tile') {
143+
return {
144+
url: url.replace(
145+
'https://api.os.uk/maps/vector/v1/vts',
146+
`${window.location.origin}${apiPath}`
147+
),
148+
headers: {}
149+
}
150+
}
151+
152+
if (resourceType !== 'Style') {
145153
return {
146154
url: `${apiPath}/map-proxy?url=${encodeURIComponent(url)}`,
147155
headers: {}

src/server/plugins/engine/configureEnginePlugin.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export const configureEnginePlugin = async (
2222
preparePageEventRequestOptions,
2323
onRequest,
2424
saveAndExit,
25-
ordnanceSurveyApiKey
25+
ordnanceSurveyApiKey,
26+
ordnanceSurveyApiSecret
2627
}: RouteConfig = {},
2728
cache?: CacheService
2829
): Promise<{
@@ -65,7 +66,8 @@ export const configureEnginePlugin = async (
6566
onRequest,
6667
baseUrl: 'http://localhost:3009', // always runs locally
6768
saveAndExit,
68-
ordnanceSurveyApiKey
69+
ordnanceSurveyApiKey,
70+
ordnanceSurveyApiSecret
6971
}
7072
}
7173
}

src/server/plugins/engine/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
2626
onRequest: Joi.function().optional(),
2727
baseUrl: Joi.string().uri().required(),
2828
saveAndExit: Joi.function().optional(),
29-
ordnanceSurveyApiKey: Joi.string().optional()
29+
ordnanceSurveyApiKey: Joi.string().optional(),
30+
ordnanceSurveyApiSecret: Joi.string().optional()
3031
})
3132

3233
/**

src/server/plugins/engine/plugin.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const plugin = {
4040
preparePageEventRequestOptions,
4141
onRequest,
4242
ordnanceSurveyApiKey,
43-
baseUrl
43+
baseUrl,
44+
ordnanceSurveyApiSecret
4445
} = options
4546

4647
const cacheService =
@@ -60,12 +61,13 @@ export const plugin = {
6061
})
6162
}
6263

63-
// Register the maps plugin only if we have an OS api key
64-
if (ordnanceSurveyApiKey) {
64+
// Register the maps plugin only if we have an OS api key & secret
65+
if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) {
6566
await server.register({
6667
plugin: mapPlugin,
6768
options: {
68-
ordnanceSurveyApiKey
69+
ordnanceSurveyApiKey,
70+
ordnanceSurveyApiSecret
6971
}
7072
})
7173
}

src/server/plugins/engine/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ export interface PluginOptions {
430430
onRequest?: OnRequestCallback
431431
baseUrl: string // base URL of the application, protocol and hostname e.g. "https://myapp.com"
432432
ordnanceSurveyApiKey?: string
433+
ordnanceSurveyApiSecret?: string
433434
}
434435

435436
export interface FormAdapterSubmissionMessageMeta {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { post } from '~/src/server/services/httpService.js'
2+
3+
/**
4+
* @type {string}
5+
*/
6+
let cachedToken
7+
let tokenExpiry = 0
8+
9+
/**
10+
* Get Ordnance Survey OAuth token
11+
* @param {MapConfiguration} options - Ordnance survey map options
12+
*/
13+
export async function getAccessToken(options) {
14+
const { ordnanceSurveyApiKey: key, ordnanceSurveyApiSecret: secret } = options
15+
const now = Date.now()
16+
17+
if (cachedToken && now < tokenExpiry) {
18+
return cachedToken
19+
}
20+
21+
const creds = `${key}:${secret}`
22+
const result = await post('https://api.os.uk/oauth2/token/v1', {
23+
headers: {
24+
Authorization: `Basic ${btoa(creds)}`,
25+
'Content-Type': 'application/x-www-form-urlencoded'
26+
},
27+
payload: 'grant_type=client_credentials',
28+
json: true
29+
})
30+
31+
const data = result.payload
32+
33+
cachedToken = data.access_token
34+
tokenExpiry = now + (data.expires_in - 60) * 1000 // refresh early
35+
36+
return cachedToken
37+
}
38+
39+
/**
40+
* @import { MapConfiguration } from '~/src/server/plugins/map/types.js'
41+
*/
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js'
2+
import { post } from '~/src/server/services/httpService.js'
3+
4+
jest.mock('~/src/server/services/httpService.ts')
5+
6+
describe('OS OAuth token', () => {
7+
describe('getAccessToken', () => {
8+
it('should get access token', async () => {
9+
jest.mocked(post).mockResolvedValueOnce({
10+
res: /** @type {IncomingMessage} */ ({
11+
statusCode: 200,
12+
headers: {}
13+
}),
14+
payload: {
15+
access_token: 'access_token',
16+
expires_in: '299',
17+
issued_at: '1770036762387',
18+
token_type: 'Bearer'
19+
},
20+
error: undefined
21+
})
22+
23+
const token = await getAccessToken({
24+
ordnanceSurveyApiKey: 'apikey',
25+
ordnanceSurveyApiSecret: 'apisecret'
26+
})
27+
28+
expect(token).toBe('access_token')
29+
30+
expect(post).toHaveBeenCalledWith('https://api.os.uk/oauth2/token/v1', {
31+
headers: {
32+
Authorization: `Basic ${btoa('apikey:apisecret')}`,
33+
'Content-Type': 'application/x-www-form-urlencoded'
34+
},
35+
payload: 'grant_type=client_credentials',
36+
json: true
37+
})
38+
expect(post).toHaveBeenCalledTimes(1)
39+
})
40+
41+
it('should return an cached token', async () => {
42+
const token = await getAccessToken({
43+
ordnanceSurveyApiKey: 'apikey',
44+
ordnanceSurveyApiSecret: 'apisecret'
45+
})
46+
47+
expect(token).toBe('access_token')
48+
expect(post).toHaveBeenCalledTimes(0)
49+
})
50+
})
51+
})
52+
53+
/**
54+
* @import { IncomingMessage } from 'node:http'
55+
*/

src/server/plugins/map/routes/index.js

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
import { resolve } from 'node:path'
22

3+
import { StatusCodes } from 'http-status-codes'
34
import Joi from 'joi'
45

6+
import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js'
57
import { find, nearest } from '~/src/server/plugins/map/service.js'
6-
import { request as httpRequest } from '~/src/server/services/httpService.js'
8+
import {
9+
get,
10+
request as httpRequest
11+
} from '~/src/server/services/httpService.js'
712

813
/**
914
* Gets the map support routes
1015
* @param {MapConfiguration} options - ordnance survey names api key
1116
*/
1217
export function getRoutes(options) {
1318
return [
19+
mapStyleResourceRoutes(),
1420
mapProxyRoute(options),
21+
tileProxyRoute(options),
1522
geocodeProxyRoute(options),
16-
reverseGeocodeProxyRoute(options),
17-
...tileRoutes()
23+
reverseGeocodeProxyRoute(options)
1824
]
1925
}
2026

2127
/**
2228
* Proxies ordnance survey requests from the front end to api.os.com
23-
* Used for VTS map tiles, sprites and fonts by forwarding on the request
24-
* and adding the apikey and optionally an SRS (spatial reference system)
29+
* Used for the VTS map source by forwarding on the request
30+
* and adding the auth token and SRS (spatial reference system)
2531
* @param {MapConfiguration} options - the map options
2632
* @returns {ServerRoute<MapProxyGetRequestRefs>}
2733
*/
@@ -32,14 +38,15 @@ function mapProxyRoute(options) {
3238
handler: async (request, h) => {
3339
const { query } = request
3440
const targetUrl = new URL(decodeURIComponent(query.url))
41+
const token = await getAccessToken(options)
3542

36-
// Add API key server-side and set SRS
37-
targetUrl.searchParams.set('key', options.ordnanceSurveyApiKey)
38-
if (!targetUrl.searchParams.has('srs')) {
39-
targetUrl.searchParams.set('srs', '3857')
40-
}
43+
targetUrl.searchParams.set('srs', '3857')
4144

42-
const proxyResponse = await httpRequest('get', targetUrl.toString())
45+
const proxyResponse = await httpRequest('get', targetUrl.toString(), {
46+
headers: {
47+
Authorization: `Bearer ${token}`
48+
}
49+
})
4350
const buffer = proxyResponse.payload
4451
const contentType = proxyResponse.res.headers['content-type']
4552
const response = h.response(buffer)
@@ -63,7 +70,44 @@ function mapProxyRoute(options) {
6370
}
6471

6572
/**
66-
* Proxies ordnance survey geocode requests from the front end to api.os.com
73+
* Proxies ordnance survey requests from the front end to api.os.uk
74+
* Used for VTS map tiles forwarding on the request and adding the auth token
75+
* @param {MapConfiguration} options - the map options
76+
* @returns {ServerRoute<MapProxyGetRequestRefs>}
77+
*/
78+
function tileProxyRoute(options) {
79+
return {
80+
method: 'GET',
81+
path: '/api/tile/{z}/{y}/{x}.pbf',
82+
handler: async (request, h) => {
83+
const { z, y, x } = request.params
84+
const token = await getAccessToken(options)
85+
86+
const url = `https://api.os.uk/maps/vector/v1/vts/tile/${z}/${y}/${x}.pbf?srs=3857`
87+
88+
const { payload, res } = await get(url, {
89+
headers: {
90+
Authorization: `Bearer ${token}`,
91+
Accept: 'application/x-protobuf'
92+
},
93+
json: false,
94+
gunzip: true
95+
})
96+
97+
if (res.statusCode && res.statusCode !== StatusCodes.OK.valueOf()) {
98+
return h.response('Tile fetch failed').code(res.statusCode)
99+
}
100+
101+
return h
102+
.response(payload)
103+
.type('application/x-protobuf')
104+
.header('Cache-Control', 'public, max-age=86400')
105+
}
106+
}
107+
}
108+
109+
/**
110+
* Proxies ordnance survey geocode requests from the front end to api.os.uk
67111
* Used for the gazzeteer address lookup to find name from query strings like postcode and place names
68112
* @param {MapConfiguration} options - the map options
69113
* @returns {ServerRoute<MapGeocodeGetRequestRefs>}
@@ -91,7 +135,7 @@ function geocodeProxyRoute(options) {
91135
}
92136

93137
/**
94-
* Proxies ordnance survey reverse geocode requests from the front end to api.os.com
138+
* Proxies ordnance survey reverse geocode requests from the front end to api.os.uk
95139
* Used to find name from easting and northing points.
96140
* N.B this endpoint is currently not used by the front end but will be soon in "maps V2"
97141
* @param {MapConfiguration} options - the map options
@@ -124,20 +168,22 @@ function reverseGeocodeProxyRoute(options) {
124168
}
125169
}
126170

127-
function tileRoutes() {
128-
return [
129-
{
130-
method: 'GET',
131-
path: '/api/maps/vts/{path*}',
132-
options: {
133-
handler: {
134-
directory: {
135-
path: resolve(import.meta.dirname, './vts')
136-
}
171+
/**
172+
* Resource routes to return sprites and glyphs
173+
* @returns {ServerRoute<MapProxyGetRequestRefs>}
174+
*/
175+
function mapStyleResourceRoutes() {
176+
return {
177+
method: 'GET',
178+
path: '/api/maps/vts/{path*}',
179+
options: {
180+
handler: {
181+
directory: {
182+
path: resolve(import.meta.dirname, './vts')
137183
}
138184
}
139185
}
140-
]
186+
}
141187
}
142188

143189
/**

0 commit comments

Comments
 (0)