@docbox-nz/hapi-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +6 -0
- package/.eslintrc +131 -0
- package/.prettierignore +11 -0
- package/.prettierrc +6 -0
- package/LICENSE.md +21 -0
- package/README.md +152 -0
- package/cspell.json +6 -0
- package/dist/api/adminService.d.ts +16 -0
- package/dist/api/boxService.d.ts +43 -0
- package/dist/api/client.d.ts +45 -0
- package/dist/api/docboxFile.d.ts +15 -0
- package/dist/api/fileService.d.ts +251 -0
- package/dist/api/folderService.d.ts +45 -0
- package/dist/api/linkService.d.ts +115 -0
- package/dist/api/taskService.d.ts +23 -0
- package/dist/api/utils.d.ts +1 -0
- package/dist/error.d.ts +11 -0
- package/dist/index.browser.cjs +988 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.esm.js +984 -0
- package/dist/index.browser.esm.js.map +1 -0
- package/dist/index.browser.js +4390 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.node.cjs +22660 -0
- package/dist/index.node.cjs.map +1 -0
- package/dist/index.node.esm.js +22657 -0
- package/dist/index.node.esm.js.map +1 -0
- package/dist/index.node.js +20809 -0
- package/dist/index.node.js.map +1 -0
- package/dist/options.d.ts +79 -0
- package/dist/types/box.d.ts +59 -0
- package/dist/types/file.d.ts +371 -0
- package/dist/types/folder.d.ts +153 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/link.d.ts +136 -0
- package/dist/types/search.d.ts +177 -0
- package/dist/types/shared.d.ts +94 -0
- package/dist/types/user.d.ts +20 -0
- package/package.json +56 -0
- package/rollup.config.js +36 -0
- package/src/error.ts +46 -0
- package/src/index.ts +244 -0
- package/src/options.ts +95 -0
- package/tsconfig.json +15 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { badRequest, forbidden } from '@hapi/boom';
|
|
2
|
+
import type { Plugin, Request, ResponseToolkit, RouteOptions, ServerRoute } from '@hapi/hapi';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import type { AxiosInstance } from 'axios';
|
|
5
|
+
import { createHandleAxiosError } from './error';
|
|
6
|
+
import type { DocboxRequestTenant, DocboxRequestUser, PluginOptions } from './options';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_PATH: string = '/';
|
|
9
|
+
|
|
10
|
+
export const DocboxGateway: Plugin<PluginOptions> = {
|
|
11
|
+
name: '@docbox-nz/hapi-gateway',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
register: function (server, options) {
|
|
14
|
+
const routes = createDocboxRoutes(options);
|
|
15
|
+
for (const route of routes) {
|
|
16
|
+
server.route(route);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createDocboxRoutes(options: PluginOptions) {
|
|
22
|
+
let basePath = options.basePath ?? DEFAULT_BASE_PATH;
|
|
23
|
+
|
|
24
|
+
// Remove trailing slashes from base path
|
|
25
|
+
if (basePath.endsWith('/')) basePath = basePath.substring(0, basePath.length - 1);
|
|
26
|
+
|
|
27
|
+
const baseRouteOptions: RouteOptions = options.baseRouteOptions ?? {};
|
|
28
|
+
const baseForwardRouteOptions: RouteOptions = options.baseForwardRouteOptions ?? {};
|
|
29
|
+
|
|
30
|
+
// Axios instance to perform connections
|
|
31
|
+
const axiosInstance = axios.create({
|
|
32
|
+
...options.axiosConfig,
|
|
33
|
+
baseURL: options.docboxBaseURL,
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Accept: 'application/json',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Add interceptor to handle axios errors
|
|
41
|
+
axiosInstance.interceptors.response.use(
|
|
42
|
+
(res) => res,
|
|
43
|
+
createHandleAxiosError(options.docboxBaseURL)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const getRequestUser: PluginOptions['getRequestUser'] = options.getRequestUser ?? (() => null);
|
|
47
|
+
|
|
48
|
+
// Base route for creating document boxes
|
|
49
|
+
const createBoxRoute: ServerRoute = {
|
|
50
|
+
path: `${basePath}/box`,
|
|
51
|
+
method: 'POST',
|
|
52
|
+
handler: async function (request, h) {
|
|
53
|
+
const payload = request.payload;
|
|
54
|
+
if (typeof payload !== 'object') {
|
|
55
|
+
return badRequest('request payload must be an object');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const scope = (payload as any).scope;
|
|
59
|
+
if (scope === undefined || typeof scope !== 'string') {
|
|
60
|
+
return badRequest('scope must be defined and a string');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const path = request.path.substring(basePath.length);
|
|
64
|
+
const isAllowed = await options.isAllowedWrite(request, scope, path);
|
|
65
|
+
if (!isAllowed) {
|
|
66
|
+
return forbidden("you don't have permission to access this resource");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tenant = await options.getRequestTenant(request);
|
|
70
|
+
const user = await getRequestUser(request);
|
|
71
|
+
return forwardRequest(axiosInstance, request, tenant, user, h, path);
|
|
72
|
+
},
|
|
73
|
+
options: baseRouteOptions,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Docbox read forwarding endpoint
|
|
77
|
+
const forwardReadRoute: ServerRoute = {
|
|
78
|
+
path: `${basePath}/box/{scope}/{path*}`,
|
|
79
|
+
method: ['GET'],
|
|
80
|
+
handler: async function (request, h) {
|
|
81
|
+
const scope = request.params.scope;
|
|
82
|
+
if (scope === undefined || typeof scope !== 'string') {
|
|
83
|
+
return badRequest('scope must be defined and a string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const path = request.path.substring(basePath.length);
|
|
87
|
+
const isAllowed = await options.isAllowedRead(request, scope, path);
|
|
88
|
+
if (!isAllowed) {
|
|
89
|
+
return forbidden("you don't have permission to access this resource");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tenant = await options.getRequestTenant(request);
|
|
93
|
+
const user = await getRequestUser(request);
|
|
94
|
+
return forwardRequest(axiosInstance, request, tenant, user, h, path);
|
|
95
|
+
},
|
|
96
|
+
options: { ...baseRouteOptions, ...baseForwardRouteOptions },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Docbox write forwarding endpoint
|
|
100
|
+
const forwardWriteRoute: ServerRoute = {
|
|
101
|
+
path: `${basePath}/box/{scope}/{path*}`,
|
|
102
|
+
method: ['POST', 'PATCH', 'PUT', 'DELETE'],
|
|
103
|
+
handler: async function (request, h) {
|
|
104
|
+
const scope = request.params.scope;
|
|
105
|
+
if (scope === undefined || typeof scope !== 'string') {
|
|
106
|
+
return badRequest('scope must be defined and a string');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const path = request.path.substring(basePath.length);
|
|
110
|
+
|
|
111
|
+
// Searching uses the POST method but only requires read access
|
|
112
|
+
const isWriting = isWriteRequest(request, path);
|
|
113
|
+
|
|
114
|
+
const accessHandler = isWriting ? options.isAllowedWrite : options.isAllowedRead;
|
|
115
|
+
|
|
116
|
+
const isAllowed = await accessHandler(request, scope, path);
|
|
117
|
+
if (!isAllowed) {
|
|
118
|
+
return forbidden("you don't have permission to access this resource");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tenant = await options.getRequestTenant(request);
|
|
122
|
+
const user = await getRequestUser(request);
|
|
123
|
+
return forwardRequest(axiosInstance, request, tenant, user, h, path);
|
|
124
|
+
},
|
|
125
|
+
options: {
|
|
126
|
+
...baseRouteOptions,
|
|
127
|
+
...baseForwardRouteOptions,
|
|
128
|
+
payload: {
|
|
129
|
+
...baseRouteOptions.payload,
|
|
130
|
+
...baseForwardRouteOptions.payload,
|
|
131
|
+
|
|
132
|
+
// Payload must not be parsed so that files are properly sent to
|
|
133
|
+
// the docbox server
|
|
134
|
+
parse: false,
|
|
135
|
+
|
|
136
|
+
// Ensure we get our data as full buffers to forward onward, not
|
|
137
|
+
// preprocessed by HAPI
|
|
138
|
+
output: 'data',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return [createBoxRoute, forwardReadRoute, forwardWriteRoute];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Additional check for write endpoints to see if the
|
|
148
|
+
* request is actually a write, as some endpoints such
|
|
149
|
+
* as search use the POST method
|
|
150
|
+
*
|
|
151
|
+
* @param request The HTTP request
|
|
152
|
+
* @param path The request path
|
|
153
|
+
* @returns Whether the request is a write request
|
|
154
|
+
*/
|
|
155
|
+
function isWriteRequest(request: Request, path: string) {
|
|
156
|
+
if (request.method === 'post') {
|
|
157
|
+
const parts = path.substring(1).split('/');
|
|
158
|
+
|
|
159
|
+
// Request is a search request (/box/{scope}/search)
|
|
160
|
+
if (parts.length === 3 && parts[0] === 'box' && parts[2] === 'search') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Request is a file search request (/box/{scope}/file/{file_id}/search)
|
|
165
|
+
if (parts.length === 5 && parts[0] === 'box' && parts[2] === 'file' && parts[4] === 'search') {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Forwards the provided request onto the docbox server
|
|
175
|
+
*
|
|
176
|
+
* @param axios Axios instance to forward the request through
|
|
177
|
+
* @param request The request itself
|
|
178
|
+
* @param user User performing the request
|
|
179
|
+
* @param h Response toolkit for creating the response
|
|
180
|
+
* @param path Path requested from the server
|
|
181
|
+
*/
|
|
182
|
+
async function forwardRequest(
|
|
183
|
+
axios: AxiosInstance,
|
|
184
|
+
request: Request,
|
|
185
|
+
tenant: DocboxRequestTenant,
|
|
186
|
+
user: DocboxRequestUser | null,
|
|
187
|
+
h: ResponseToolkit,
|
|
188
|
+
path: string
|
|
189
|
+
) {
|
|
190
|
+
const userHeaders: Record<string, any> = request.headers;
|
|
191
|
+
const forwardHeaders: Record<string, any> = {};
|
|
192
|
+
|
|
193
|
+
// Forward content related headers
|
|
194
|
+
if (userHeaders['accept'] !== undefined) {
|
|
195
|
+
forwardHeaders['accept'] = userHeaders['accept'];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (userHeaders['content-type'] !== undefined) {
|
|
199
|
+
forwardHeaders['content-type'] = userHeaders['content-type'];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (userHeaders['content-length'] !== undefined) {
|
|
203
|
+
forwardHeaders['content-length'] = userHeaders['content-length'];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Set tenant headers
|
|
207
|
+
forwardHeaders['x-tenant-env'] = tenant.env;
|
|
208
|
+
forwardHeaders['x-tenant-id'] = tenant.id;
|
|
209
|
+
|
|
210
|
+
// Set user headers
|
|
211
|
+
if (user !== null && user.id !== undefined) {
|
|
212
|
+
forwardHeaders['x-user-id'] = encodeURIComponent(user.id);
|
|
213
|
+
|
|
214
|
+
if (user.name) {
|
|
215
|
+
forwardHeaders['x-user-name'] = encodeURIComponent(user.name);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (user.imageId) {
|
|
219
|
+
forwardHeaders['x-user-image-id'] = encodeURIComponent(user.imageId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Forward the request to docbox
|
|
224
|
+
const docboxResponse = await axios.request({
|
|
225
|
+
method: request.method.toUpperCase(),
|
|
226
|
+
url: path,
|
|
227
|
+
headers: forwardHeaders,
|
|
228
|
+
data: request.payload,
|
|
229
|
+
params: request.query,
|
|
230
|
+
responseType: 'stream',
|
|
231
|
+
// Don't throw if the status code is not an 2xx status code
|
|
232
|
+
// (we pass error statuses onto the consumer)
|
|
233
|
+
validateStatus: () => true,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
let response = h.response(docboxResponse.data).code(docboxResponse.status);
|
|
237
|
+
|
|
238
|
+
// Set the response headers
|
|
239
|
+
Object.entries(docboxResponse.headers).forEach(
|
|
240
|
+
([name, value]) => (response = response.header(name, value))
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return response;
|
|
244
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Request, RouteOptions } from '@hapi/hapi';
|
|
2
|
+
import type { CreateAxiosDefaults } from 'axios';
|
|
3
|
+
|
|
4
|
+
export interface DocboxRequestUser {
|
|
5
|
+
/** Internal unique user ID for the user (Specific to your app) */
|
|
6
|
+
id?: string;
|
|
7
|
+
|
|
8
|
+
/** Username of the user */
|
|
9
|
+
name?: string;
|
|
10
|
+
|
|
11
|
+
/** Some internal field used to identify the profile image of the user */
|
|
12
|
+
imageId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DocboxRequestTenant {
|
|
16
|
+
/** ID of the tenant */
|
|
17
|
+
id: string;
|
|
18
|
+
|
|
19
|
+
/** Environment the tenant is within */
|
|
20
|
+
env: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PluginOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Base URL of the docbox server
|
|
26
|
+
*/
|
|
27
|
+
docboxBaseURL: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base path to serve docbox endpoints from. Must begin
|
|
31
|
+
* with a slash
|
|
32
|
+
*
|
|
33
|
+
* Docbox endpoints will be added in a nested /box path
|
|
34
|
+
*
|
|
35
|
+
* @default /
|
|
36
|
+
*/
|
|
37
|
+
basePath?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Base set of route options applied to any routes created
|
|
41
|
+
* by docbox
|
|
42
|
+
*/
|
|
43
|
+
baseRouteOptions?: RouteOptions;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Base set of route options applied only to forwarding
|
|
47
|
+
* routes created by docbox
|
|
48
|
+
*/
|
|
49
|
+
baseForwardRouteOptions?: RouteOptions;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Additional configuration options for axios
|
|
53
|
+
*/
|
|
54
|
+
axiosConfig?: CreateAxiosDefaults;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write access check, will be invoked to check whether the request
|
|
58
|
+
* has authorization to perform a write action against docbox
|
|
59
|
+
*
|
|
60
|
+
* @param request The HTTP request
|
|
61
|
+
* @param scope The requested docbox scope
|
|
62
|
+
* @param path The requested path
|
|
63
|
+
* @returns Whether the user is allowed to write
|
|
64
|
+
*/
|
|
65
|
+
isAllowedWrite(request: Request, scope: string, path: string): Promise<boolean> | boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read access check, will be invoked to check whether the request
|
|
69
|
+
* has authorization to perform a read action against docbox
|
|
70
|
+
*
|
|
71
|
+
* @param request The HTTP request
|
|
72
|
+
* @param scope The requested docbox scope
|
|
73
|
+
* @param path The requested path
|
|
74
|
+
* @returns Whether the user is allowed to write
|
|
75
|
+
*/
|
|
76
|
+
isAllowedRead(request: Request, scope: string, path: string): Promise<boolean> | boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract the user details from the provided request
|
|
80
|
+
*
|
|
81
|
+
* @param request The HTTP request
|
|
82
|
+
* @returns The user details if they were obtainable
|
|
83
|
+
*/
|
|
84
|
+
getRequestUser?: (
|
|
85
|
+
request: Request
|
|
86
|
+
) => Promise<DocboxRequestUser | null> | (DocboxRequestUser | null);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the docbox tenant from the request
|
|
90
|
+
*
|
|
91
|
+
* @param request The HTTP request
|
|
92
|
+
* @returns The current tenant
|
|
93
|
+
*/
|
|
94
|
+
getRequestTenant(request: Request): Promise<DocboxRequestTenant> | DocboxRequestTenant;
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationDir": "dist/types",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"typeRoots": ["./node_modules/@types"],
|
|
12
|
+
"moduleResolution": "node"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|