@constructive-io/graphql-server 2.10.5
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/LICENSE +23 -0
- package/README.md +89 -0
- package/errors/404-message.d.ts +2 -0
- package/errors/404-message.js +232 -0
- package/errors/404.d.ts +2 -0
- package/errors/404.js +218 -0
- package/errors/50x.d.ts +2 -0
- package/errors/50x.js +216 -0
- package/esm/errors/404-message.js +230 -0
- package/esm/errors/404.js +216 -0
- package/esm/errors/50x.js +214 -0
- package/esm/index.js +2 -0
- package/esm/middleware/api.js +337 -0
- package/esm/middleware/auth.js +68 -0
- package/esm/middleware/cors.js +63 -0
- package/esm/middleware/flush.js +49 -0
- package/esm/middleware/gql.js +125 -0
- package/esm/middleware/graphile.js +84 -0
- package/esm/middleware/types.js +1 -0
- package/esm/plugins/PublicKeySignature.js +114 -0
- package/esm/run.js +8 -0
- package/esm/schema.js +86 -0
- package/esm/scripts/create-bucket.js +32 -0
- package/esm/server.js +95 -0
- package/esm/types.js +1 -0
- package/index.d.ts +2 -0
- package/index.js +18 -0
- package/middleware/api.d.ts +6 -0
- package/middleware/api.js +346 -0
- package/middleware/auth.d.ts +4 -0
- package/middleware/auth.js +75 -0
- package/middleware/cors.d.ts +14 -0
- package/middleware/cors.js +70 -0
- package/middleware/flush.d.ts +5 -0
- package/middleware/flush.js +54 -0
- package/middleware/gql.d.ts +6 -0
- package/middleware/gql.js +131 -0
- package/middleware/graphile.d.ts +4 -0
- package/middleware/graphile.js +91 -0
- package/middleware/types.d.ts +33 -0
- package/middleware/types.js +2 -0
- package/package.json +88 -0
- package/plugins/PublicKeySignature.d.ts +11 -0
- package/plugins/PublicKeySignature.js +121 -0
- package/run.d.ts +2 -0
- package/run.js +10 -0
- package/schema.d.ts +12 -0
- package/schema.js +123 -0
- package/scripts/create-bucket.d.ts +1 -0
- package/scripts/create-bucket.js +34 -0
- package/server.d.ts +17 -0
- package/server.js +102 -0
- package/types.d.ts +85 -0
- package/types.js +2 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export default `
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
|
|
6
|
+
<META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW">
|
|
7
|
+
<META NAME="GOOGLEBOT" CONTENT="NOARCHIVE">
|
|
8
|
+
<title>Uh Oh</title>
|
|
9
|
+
|
|
10
|
+
<link href='//fonts.googleapis.com/css2?family=Fjalla+One&display=swap' rel='stylesheet' type='text/css'>
|
|
11
|
+
|
|
12
|
+
<style type="text/css">
|
|
13
|
+
.fade-in-cls {
|
|
14
|
+
-webkit-animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
15
|
+
-moz-animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
16
|
+
animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
17
|
+
-webkit-transform: translateY(10px);
|
|
18
|
+
-moz-transform: translateY(10px);
|
|
19
|
+
-o-transform: translateY(10px);
|
|
20
|
+
transform: translateY(10px);
|
|
21
|
+
-webkit-opacity: 0;
|
|
22
|
+
-moz-opacity: 0;
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@-webkit-keyframes fade-in {
|
|
27
|
+
100% {
|
|
28
|
+
-webkit-transform: translateY(0px);
|
|
29
|
+
-moz-transform: translateY(0px);
|
|
30
|
+
-o-transform: translateY(0px);
|
|
31
|
+
transform: translateY(0px);
|
|
32
|
+
-webkit-opacity: 1;
|
|
33
|
+
-moz-opacity: 1;
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@-moz-keyframes fade-in {
|
|
39
|
+
100% {
|
|
40
|
+
-webkit-transform: translateY(0px);
|
|
41
|
+
-moz-transform: translateY(0px);
|
|
42
|
+
-o-transform: translateY(0px);
|
|
43
|
+
transform: translateY(0px);
|
|
44
|
+
-webkit-opacity: 1;
|
|
45
|
+
-moz-opacity: 1;
|
|
46
|
+
opacity: 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes fade-in {
|
|
51
|
+
100% {
|
|
52
|
+
-webkit-transform: translateY(0px);
|
|
53
|
+
-moz-transform: translateY(0px);
|
|
54
|
+
-o-transform: translateY(0px);
|
|
55
|
+
transform: translateY(0px);
|
|
56
|
+
-webkit-opacity: 1;
|
|
57
|
+
-moz-opacity: 1;
|
|
58
|
+
opacity: 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.time {
|
|
63
|
+
|
|
64
|
+
-webkit-animation: ckw 15s infinite;
|
|
65
|
+
/* Safari 4+ */
|
|
66
|
+
-moz-animation: ckw 15s infinite;
|
|
67
|
+
/* Fx 5+ */
|
|
68
|
+
-o-animation: ckw 15s infinite;
|
|
69
|
+
/* Opera 12+ */
|
|
70
|
+
animation: ckw 15s infinite;
|
|
71
|
+
/* IE 10+, Fx 29+ */
|
|
72
|
+
-webkit-animation-timing-function: linear;
|
|
73
|
+
/* Chrome, Safari, Opera */
|
|
74
|
+
animation-timing-function: linear;
|
|
75
|
+
transform-origin: 50% 50%;
|
|
76
|
+
display: inline-block;
|
|
77
|
+
/* <--- */
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@keyframes ckw {
|
|
81
|
+
0% {
|
|
82
|
+
transform: rotate(0deg);
|
|
83
|
+
-webkit-transform: rotate(0deg);
|
|
84
|
+
-moz-transform: rotate(0deg);
|
|
85
|
+
-o-transform: rotate(0deg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
100% {
|
|
89
|
+
transform: rotate(360deg);
|
|
90
|
+
-webkit-transform: rotate(360deg);
|
|
91
|
+
-moz-transform: rotate(360deg);
|
|
92
|
+
-o-transform: rotate(360deg);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@-webkit-keyframes ckw {
|
|
97
|
+
0% {
|
|
98
|
+
transform: rotate(0deg);
|
|
99
|
+
-webkit-transform: rotate(0deg);
|
|
100
|
+
-moz-transform: rotate(0deg);
|
|
101
|
+
-o-transform: rotate(0deg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
100% {
|
|
105
|
+
transform: rotate(360deg);
|
|
106
|
+
-webkit-transform: rotate(360deg);
|
|
107
|
+
-moz-transform: rotate(360deg);
|
|
108
|
+
-o-transform: rotate(360deg);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@-moz-keyframes ckw {
|
|
113
|
+
0% {
|
|
114
|
+
transform: rotate(0deg);
|
|
115
|
+
-webkit-transform: rotate(0deg);
|
|
116
|
+
-moz-transform: rotate(0deg);
|
|
117
|
+
-o-transform: rotate(0deg);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
100% {
|
|
121
|
+
transform: rotate(360deg);
|
|
122
|
+
-webkit-transform: rotate(360deg);
|
|
123
|
+
-moz-transform: rotate(360deg);
|
|
124
|
+
-o-transform: rotate(360deg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
body {
|
|
129
|
+
background-color: #dde7e9;
|
|
130
|
+
color: #01A1FF;
|
|
131
|
+
font-family: 'Fjalla One', sans-serif;
|
|
132
|
+
position: relative;
|
|
133
|
+
margin: 0px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
section {
|
|
137
|
+
width: 100%;
|
|
138
|
+
height: 100%;
|
|
139
|
+
position: absolute;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
article {
|
|
143
|
+
display: table;
|
|
144
|
+
width: 100%;
|
|
145
|
+
height: 100%;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* .border-top {
|
|
149
|
+
width: 95px;
|
|
150
|
+
background-color: #fff;
|
|
151
|
+
height: 2px;
|
|
152
|
+
display: inline-block;
|
|
153
|
+
margin: 0px auto;
|
|
154
|
+
} */
|
|
155
|
+
|
|
156
|
+
.vcntr {
|
|
157
|
+
display: table-cell;
|
|
158
|
+
height: 100%;
|
|
159
|
+
width: 100%;
|
|
160
|
+
vertical-align: middle;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.logo {
|
|
164
|
+
width: 100px;
|
|
165
|
+
margin: 10px auto;
|
|
166
|
+
display: block;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
h1 {
|
|
170
|
+
font-size: 21px;
|
|
171
|
+
line-height: 32px;
|
|
172
|
+
margin-bottom: 0px;
|
|
173
|
+
margin-top: 28px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
letter-spacing: 2px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
p {
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
line-height: 23px;
|
|
182
|
+
margin-bottom: 8px;
|
|
183
|
+
margin-top: 6px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.textc {
|
|
187
|
+
text-align: center;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@media only screen and (max-width:480px) {
|
|
191
|
+
.logo {
|
|
192
|
+
width: 100px;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
|
|
198
|
+
<body class="">
|
|
199
|
+
<section>
|
|
200
|
+
<article>
|
|
201
|
+
<div class="vcntr">
|
|
202
|
+
<div class="logo fade-in-cls">
|
|
203
|
+
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64.95 61.24"><path d="M16.36 43.47a14.56 14.56 0 01-8.79 0A11.16 11.16 0 011 38.34a8.83 8.83 0 01-.45-1A8.09 8.09 0 01.76 31a10.08 10.08 0 013.68-4.17 6.23 6.23 0 01-.24-.65A8.17 8.17 0 014.68 20a10.47 10.47 0 014.53-4.53 13.25 13.25 0 012.12-.9 12.35 12.35 0 012.5-7.45A17.35 17.35 0 0122.7 1a22.38 22.38 0 0113.48 0 17.07 17.07 0 019.42 6.9 18.75 18.75 0 019.8.4 14 14 0 018.29 6.46 10.17 10.17 0 01.78 1.83 10 10 0 01-.59 7.56 13.05 13.05 0 01-5.64 5.64A17.05 17.05 0 0155.4 31l-39 12.45zM64.92 36q-1.05 4.48-10.54 7.63l-.36.12L15 56.22a9.87 9.87 0 01-1.91.41C6.88 57.55 2.16 54.78 0 51.24a8.2 8.2 0 00.56 3.2 7.45 7.45 0 00.45 1 11.09 11.09 0 006.56 5.13 14.64 14.64 0 008.79 0l39-12.45a17.07 17.07 0 002.84-1.2 13 13 0 005.64-5.63 10.19 10.19 0 001-5.27zm0-8.59q-1.05 4.49-10.54 7.64l-.36.12L15 47.64a9.84 9.84 0 01-1.91.4C6.88 49 2.16 46.19 0 42.65a8.28 8.28 0 00.56 3.21 8.83 8.83 0 00.45 1A11.16 11.16 0 007.57 52a14.64 14.64 0 008.79 0l39-12.45a17.05 17.05 0 002.84-1.19 13.05 13.05 0 005.64-5.64 10.17 10.17 0 001-5.27zM9 39.13a10.11 10.11 0 006 0l39-12.45a12.25 12.25 0 002.06-.87 8.52 8.52 0 003.7-3.62 5.52 5.52 0 00.37-4.19 6.6 6.6 0 00-.46-1A9.57 9.57 0 0054 12.68a14.17 14.17 0 00-8.48 0l-.16.05-.2.07-2 .69-.84-2c-.07-.16-.12-.27-.14-.33l-.18-.27a12.26 12.26 0 00-7.2-5.51 17.85 17.85 0 00-10.73 0 12.81 12.81 0 00-6.56 4.44 7.58 7.58 0 00-1.47 6l.45 2.29-2.31.4c-.27 0-.5.09-.68.13s-.46.12-.66.19a8.31 8.31 0 00-1.47.62A5.88 5.88 0 008.78 22a3.71 3.71 0 00-.24 2.79 3.77 3.77 0 00.31.7c.07.13.15.26.24.4.1.14.19.27.29.39l2 2.44-3 1.13a9.92 9.92 0 00-.94.44 5.79 5.79 0 00-2.59 2.57 3.57 3.57 0 00-.1 2.83 4.9 4.9 0 00.22.48 6.69 6.69 0 003.92 3z" fill="#01a1ff"/></svg>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="textc">
|
|
206
|
+
<h1>Uh Oh!</h1>
|
|
207
|
+
<p>We’re really sorry about that. Please contact support of the issue persists.</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</article>
|
|
211
|
+
</section>
|
|
212
|
+
</body>
|
|
213
|
+
|
|
214
|
+
</html>`;
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { getNodeEnv } from '@constructive-io/graphql-env';
|
|
2
|
+
import { svcCache } from '@pgpmjs/server-utils';
|
|
3
|
+
import { getSchema, GraphileQuery } from 'graphile-query';
|
|
4
|
+
import { getGraphileSettings } from 'graphile-settings';
|
|
5
|
+
import { getPgPool } from 'pg-cache';
|
|
6
|
+
import errorPage50x from '../errors/50x';
|
|
7
|
+
import errorPage404Message from '../errors/404-message';
|
|
8
|
+
import { ApiByNameQuery, ApiQuery, ListOfAllDomainsOfDb } from './gql';
|
|
9
|
+
import './types'; // for Request type
|
|
10
|
+
const transformServiceToApi = (svc) => {
|
|
11
|
+
const api = svc.data.api;
|
|
12
|
+
const schemaNames = api.schemaNamesFromExt?.nodes?.map((n) => n.schemaName) || [];
|
|
13
|
+
const additionalSchemas = api.schemaNames?.nodes?.map((n) => n.schemaName) || [];
|
|
14
|
+
let domains = [];
|
|
15
|
+
if (api.database?.sites?.nodes) {
|
|
16
|
+
domains = api.database.sites.nodes.reduce((acc, site) => {
|
|
17
|
+
if (site.domains?.nodes && site.domains.nodes.length) {
|
|
18
|
+
const siteUrls = site.domains.nodes.map((domain) => {
|
|
19
|
+
const hostname = domain.subdomain
|
|
20
|
+
? `${domain.subdomain}.${domain.domain}`
|
|
21
|
+
: domain.domain;
|
|
22
|
+
const protocol = domain.domain === 'localhost' ? 'http://' : 'https://';
|
|
23
|
+
return protocol + hostname;
|
|
24
|
+
});
|
|
25
|
+
return [...acc, ...siteUrls];
|
|
26
|
+
}
|
|
27
|
+
return acc;
|
|
28
|
+
}, []);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
dbname: api.dbname,
|
|
32
|
+
anonRole: api.anonRole,
|
|
33
|
+
roleName: api.roleName,
|
|
34
|
+
schema: [...schemaNames, ...additionalSchemas],
|
|
35
|
+
apiModules: api.apiModules?.nodes?.map((node) => ({
|
|
36
|
+
name: node.name,
|
|
37
|
+
data: node.data,
|
|
38
|
+
})) || [],
|
|
39
|
+
rlsModule: api.rlsModule,
|
|
40
|
+
domains,
|
|
41
|
+
databaseId: api.databaseId,
|
|
42
|
+
isPublic: api.isPublic,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const getPortFromRequest = (req) => {
|
|
46
|
+
const host = req.headers.host;
|
|
47
|
+
if (!host)
|
|
48
|
+
return null;
|
|
49
|
+
const parts = host.split(':');
|
|
50
|
+
return parts.length === 2 ? `:${parts[1]}` : null;
|
|
51
|
+
};
|
|
52
|
+
export const getSubdomain = (reqDomains) => {
|
|
53
|
+
const names = reqDomains.filter((name) => !['www'].includes(name));
|
|
54
|
+
return !names.length ? null : names.join('.');
|
|
55
|
+
};
|
|
56
|
+
export const createApiMiddleware = (opts) => {
|
|
57
|
+
return async (req, res, next) => {
|
|
58
|
+
if (opts.api?.enableMetaApi === false) {
|
|
59
|
+
const schemas = opts.api.exposedSchemas;
|
|
60
|
+
const anonRole = opts.api.anonRole;
|
|
61
|
+
const roleName = opts.api.roleName;
|
|
62
|
+
const databaseId = opts.api.defaultDatabaseId;
|
|
63
|
+
const api = {
|
|
64
|
+
dbname: opts.pg?.database ?? '',
|
|
65
|
+
anonRole,
|
|
66
|
+
roleName,
|
|
67
|
+
schema: schemas,
|
|
68
|
+
apiModules: [],
|
|
69
|
+
domains: [],
|
|
70
|
+
databaseId,
|
|
71
|
+
isPublic: false,
|
|
72
|
+
};
|
|
73
|
+
req.api = api;
|
|
74
|
+
req.databaseId = databaseId;
|
|
75
|
+
return next();
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const svc = await getApiConfig(opts, req);
|
|
79
|
+
if (svc?.errorHtml) {
|
|
80
|
+
res
|
|
81
|
+
.status(404)
|
|
82
|
+
.send(errorPage404Message('API not found', svc.errorHtml));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
else if (!svc) {
|
|
86
|
+
res
|
|
87
|
+
.status(404)
|
|
88
|
+
.send(errorPage404Message('API service not found for the given domain/subdomain.'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const api = transformServiceToApi(svc);
|
|
92
|
+
req.api = api;
|
|
93
|
+
req.databaseId = api.databaseId;
|
|
94
|
+
next();
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (e.code === 'NO_VALID_SCHEMAS') {
|
|
98
|
+
res.status(404).send(errorPage404Message(e.message));
|
|
99
|
+
}
|
|
100
|
+
else if (e.message.match(/does not exist/)) {
|
|
101
|
+
res
|
|
102
|
+
.status(404)
|
|
103
|
+
.send(errorPage404Message("The resource you're looking for does not exist."));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(e);
|
|
107
|
+
res.status(500).send(errorPage50x);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const getHardCodedSchemata = ({ opts, schemata, databaseId, key, }) => {
|
|
113
|
+
const svc = {
|
|
114
|
+
data: {
|
|
115
|
+
api: {
|
|
116
|
+
databaseId,
|
|
117
|
+
isPublic: false,
|
|
118
|
+
dbname: opts.pg.database,
|
|
119
|
+
anonRole: 'administrator',
|
|
120
|
+
roleName: 'administrator',
|
|
121
|
+
schemaNamesFromExt: {
|
|
122
|
+
nodes: schemata
|
|
123
|
+
.split(',')
|
|
124
|
+
.map((schema) => schema.trim())
|
|
125
|
+
.map((schemaName) => ({ schemaName })),
|
|
126
|
+
},
|
|
127
|
+
schemaNames: { nodes: [] },
|
|
128
|
+
apiModules: [],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
svcCache.set(key, svc);
|
|
133
|
+
return svc;
|
|
134
|
+
};
|
|
135
|
+
const getMetaSchema = ({ opts, key, databaseId, }) => {
|
|
136
|
+
const apiOpts = opts.api || {};
|
|
137
|
+
const schemata = apiOpts.metaSchemas || [];
|
|
138
|
+
const svc = {
|
|
139
|
+
data: {
|
|
140
|
+
api: {
|
|
141
|
+
databaseId,
|
|
142
|
+
isPublic: false,
|
|
143
|
+
dbname: opts.pg.database,
|
|
144
|
+
anonRole: 'administrator',
|
|
145
|
+
roleName: 'administrator',
|
|
146
|
+
schemaNamesFromExt: {
|
|
147
|
+
nodes: schemata.map((schemaName) => ({ schemaName })),
|
|
148
|
+
},
|
|
149
|
+
schemaNames: { nodes: [] },
|
|
150
|
+
apiModules: [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
svcCache.set(key, svc);
|
|
155
|
+
return svc;
|
|
156
|
+
};
|
|
157
|
+
const queryServiceByDomainAndSubdomain = async ({ opts, key, client, domain, subdomain, }) => {
|
|
158
|
+
const result = await client.query({
|
|
159
|
+
role: 'administrator',
|
|
160
|
+
query: ApiQuery,
|
|
161
|
+
variables: { domain, subdomain },
|
|
162
|
+
});
|
|
163
|
+
if (result.errors?.length) {
|
|
164
|
+
console.error(result.errors);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const nodes = result?.data?.domains?.nodes;
|
|
168
|
+
if (nodes?.length) {
|
|
169
|
+
const data = nodes[0];
|
|
170
|
+
const apiPublic = opts.api?.isPublic;
|
|
171
|
+
if (!data.api || data.api.isPublic !== apiPublic)
|
|
172
|
+
return null;
|
|
173
|
+
const svc = { data };
|
|
174
|
+
svcCache.set(key, svc);
|
|
175
|
+
return svc;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
const queryServiceByApiName = async ({ opts, key, client, databaseId, name, }) => {
|
|
180
|
+
const result = await client.query({
|
|
181
|
+
role: 'administrator',
|
|
182
|
+
query: ApiByNameQuery,
|
|
183
|
+
variables: { databaseId, name },
|
|
184
|
+
});
|
|
185
|
+
if (result.errors?.length) {
|
|
186
|
+
console.error(result.errors);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const data = result?.data;
|
|
190
|
+
const apiPublic = opts.api?.isPublic;
|
|
191
|
+
if (data?.api && data.api.isPublic === apiPublic) {
|
|
192
|
+
const svc = { data };
|
|
193
|
+
svcCache.set(key, svc);
|
|
194
|
+
return svc;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
};
|
|
198
|
+
const getSvcKey = (opts, req) => {
|
|
199
|
+
const domain = req.urlDomains.domain;
|
|
200
|
+
const key = req.urlDomains.subdomains
|
|
201
|
+
.filter((name) => !['www'].includes(name))
|
|
202
|
+
.concat(domain)
|
|
203
|
+
.join('.');
|
|
204
|
+
const apiPublic = opts.api?.isPublic;
|
|
205
|
+
if (apiPublic === false) {
|
|
206
|
+
if (req.get('X-Api-Name')) {
|
|
207
|
+
return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name');
|
|
208
|
+
}
|
|
209
|
+
if (req.get('X-Schemata')) {
|
|
210
|
+
return ('schemata:' + req.get('X-Database-Id') + ':' + req.get('X-Schemata'));
|
|
211
|
+
}
|
|
212
|
+
if (req.get('X-Meta-Schema')) {
|
|
213
|
+
return 'metaschema:api:' + req.get('X-Database-Id');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return key;
|
|
217
|
+
};
|
|
218
|
+
const validateSchemata = async (pool, schemata) => {
|
|
219
|
+
const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemata]);
|
|
220
|
+
return result.rows.map((row) => row.schema_name);
|
|
221
|
+
};
|
|
222
|
+
export const getApiConfig = async (opts, req) => {
|
|
223
|
+
const rootPgPool = getPgPool(opts.pg);
|
|
224
|
+
// @ts-ignore
|
|
225
|
+
const subdomain = getSubdomain(req.urlDomains.subdomains);
|
|
226
|
+
const domain = req.urlDomains.domain;
|
|
227
|
+
const key = getSvcKey(opts, req);
|
|
228
|
+
req.svc_key = key;
|
|
229
|
+
let svc;
|
|
230
|
+
if (svcCache.has(key)) {
|
|
231
|
+
svc = svcCache.get(key);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const apiOpts = opts.api || {};
|
|
235
|
+
const allSchemata = apiOpts.metaSchemas || [];
|
|
236
|
+
const validatedSchemata = await validateSchemata(rootPgPool, allSchemata);
|
|
237
|
+
if (validatedSchemata.length === 0) {
|
|
238
|
+
const message = `No valid schemas found for domain: ${domain}, subdomain: ${subdomain}`;
|
|
239
|
+
const error = new Error(message);
|
|
240
|
+
error.code = 'NO_VALID_SCHEMAS';
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const settings = getGraphileSettings({
|
|
244
|
+
graphile: {
|
|
245
|
+
schema: validatedSchemata,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
// @ts-ignore
|
|
249
|
+
const schema = await getSchema(rootPgPool, settings);
|
|
250
|
+
// @ts-ignore
|
|
251
|
+
const client = new GraphileQuery({ schema, pool: rootPgPool, settings });
|
|
252
|
+
const apiPublic = opts.api?.isPublic;
|
|
253
|
+
if (apiPublic === false) {
|
|
254
|
+
if (req.get('X-Schemata')) {
|
|
255
|
+
svc = getHardCodedSchemata({
|
|
256
|
+
opts,
|
|
257
|
+
key,
|
|
258
|
+
schemata: req.get('X-Schemata'),
|
|
259
|
+
databaseId: req.get('X-Database-Id'),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else if (req.get('X-Api-Name')) {
|
|
263
|
+
svc = await queryServiceByApiName({
|
|
264
|
+
opts,
|
|
265
|
+
key,
|
|
266
|
+
client,
|
|
267
|
+
name: req.get('X-Api-Name'),
|
|
268
|
+
databaseId: req.get('X-Database-Id'),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else if (req.get('X-Meta-Schema')) {
|
|
272
|
+
svc = getMetaSchema({
|
|
273
|
+
opts,
|
|
274
|
+
key,
|
|
275
|
+
databaseId: req.get('X-Database-Id'),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
svc = await queryServiceByDomainAndSubdomain({
|
|
280
|
+
opts,
|
|
281
|
+
key,
|
|
282
|
+
client,
|
|
283
|
+
domain,
|
|
284
|
+
subdomain,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
svc = await queryServiceByDomainAndSubdomain({
|
|
290
|
+
opts,
|
|
291
|
+
key,
|
|
292
|
+
client,
|
|
293
|
+
domain,
|
|
294
|
+
subdomain,
|
|
295
|
+
});
|
|
296
|
+
if (!svc) {
|
|
297
|
+
if (getNodeEnv() === 'development') {
|
|
298
|
+
// TODO ONLY DO THIS IN DEV MODE
|
|
299
|
+
const fallbackResult = await client.query({
|
|
300
|
+
role: 'administrator',
|
|
301
|
+
// @ts-ignore
|
|
302
|
+
query: ListOfAllDomainsOfDb,
|
|
303
|
+
// variables: { databaseId }
|
|
304
|
+
});
|
|
305
|
+
if (!fallbackResult.errors?.length &&
|
|
306
|
+
fallbackResult.data?.apis?.nodes?.length) {
|
|
307
|
+
const port = getPortFromRequest(req);
|
|
308
|
+
const allDomains = fallbackResult.data.apis.nodes.flatMap((api) => api.domains.nodes.map((d) => ({
|
|
309
|
+
domain: d.domain,
|
|
310
|
+
subdomain: d.subdomain,
|
|
311
|
+
href: d.subdomain
|
|
312
|
+
? `http://${d.subdomain}.${d.domain}${port}/graphiql`
|
|
313
|
+
: `http://${d.domain}${port}/graphiql`,
|
|
314
|
+
})));
|
|
315
|
+
const linksHtml = allDomains.length
|
|
316
|
+
? `<ul class="mt-4 pl-5 list-disc space-y-1">` +
|
|
317
|
+
allDomains
|
|
318
|
+
.map((d) => `<li><a href="${d.href}" class="text-brand hover:underline">${d.href}</a></li>`)
|
|
319
|
+
.join('') +
|
|
320
|
+
`</ul>`
|
|
321
|
+
: `<p class="text-gray-600">No APIs are currently registered for this database.</p>`;
|
|
322
|
+
const errorHtml = `
|
|
323
|
+
<p class="text-sm text-gray-700">Try some of these:</p>
|
|
324
|
+
<div class="mt-4">
|
|
325
|
+
${linksHtml}
|
|
326
|
+
</div>
|
|
327
|
+
`.trim();
|
|
328
|
+
return {
|
|
329
|
+
errorHtml,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return svc;
|
|
337
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getPgPool } from 'pg-cache';
|
|
2
|
+
import pgQueryContext from 'pg-query-context';
|
|
3
|
+
import './types'; // for Request type
|
|
4
|
+
export const createAuthenticateMiddleware = (opts) => {
|
|
5
|
+
return async (req, res, next) => {
|
|
6
|
+
const api = req.api;
|
|
7
|
+
if (!api) {
|
|
8
|
+
res.status(500).send('Missing API info');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const pool = getPgPool({
|
|
12
|
+
...opts.pg,
|
|
13
|
+
database: api.dbname,
|
|
14
|
+
});
|
|
15
|
+
const rlsModule = api.rlsModule;
|
|
16
|
+
if (!rlsModule)
|
|
17
|
+
return next();
|
|
18
|
+
const authFn = opts.server.strictAuth
|
|
19
|
+
? rlsModule.authenticateStrict
|
|
20
|
+
: rlsModule.authenticate;
|
|
21
|
+
if (authFn && rlsModule.privateSchema.schemaName) {
|
|
22
|
+
const { authorization = '' } = req.headers;
|
|
23
|
+
const [authType, authToken] = authorization.split(' ');
|
|
24
|
+
let token = {};
|
|
25
|
+
if (authType?.toLowerCase() === 'bearer' && authToken) {
|
|
26
|
+
const context = {
|
|
27
|
+
'jwt.claims.ip_address': req.clientIp,
|
|
28
|
+
};
|
|
29
|
+
if (req.get('origin')) {
|
|
30
|
+
context['jwt.claims.origin'] = req.get('origin');
|
|
31
|
+
}
|
|
32
|
+
if (req.get('User-Agent')) {
|
|
33
|
+
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const result = await pgQueryContext({
|
|
37
|
+
client: pool,
|
|
38
|
+
context,
|
|
39
|
+
query: `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`,
|
|
40
|
+
variables: [authToken],
|
|
41
|
+
});
|
|
42
|
+
if (result?.rowCount === 0) {
|
|
43
|
+
res.status(200).json({
|
|
44
|
+
errors: [{ extensions: { code: 'UNAUTHENTICATED' } }],
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
token = result.rows[0];
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
res.status(200).json({
|
|
52
|
+
errors: [
|
|
53
|
+
{
|
|
54
|
+
extensions: {
|
|
55
|
+
code: 'BAD_TOKEN_DEFINITION',
|
|
56
|
+
message: e.message,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
req.token = token;
|
|
65
|
+
}
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parseUrl } from '@constructive-io/url-domains';
|
|
2
|
+
import corsPlugin from 'cors';
|
|
3
|
+
import './types'; // for Request type
|
|
4
|
+
/**
|
|
5
|
+
* Unified CORS middleware for Constructive API
|
|
6
|
+
*
|
|
7
|
+
* Feature parity + compatibility:
|
|
8
|
+
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
9
|
+
* - Preserves multi-tenant, per-API CORS via meta schema ('cors' module + domains).
|
|
10
|
+
* - Always allows localhost to ease development.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* app.use(cors(fallbackOrigin));
|
|
14
|
+
*/
|
|
15
|
+
export const cors = (fallbackOrigin) => {
|
|
16
|
+
// Use the cors library's dynamic origin function to decide per request
|
|
17
|
+
const dynamicOrigin = (origin, callback, req) => {
|
|
18
|
+
// 1) Global fallback (fast path)
|
|
19
|
+
if (fallbackOrigin && fallbackOrigin.trim().length) {
|
|
20
|
+
if (fallbackOrigin.trim() === '*') {
|
|
21
|
+
// Reflect whatever Origin the caller sent
|
|
22
|
+
return callback(null, true);
|
|
23
|
+
}
|
|
24
|
+
if (origin && origin.trim() === fallbackOrigin.trim()) {
|
|
25
|
+
return callback(null, true);
|
|
26
|
+
}
|
|
27
|
+
// If a strict fallback origin is provided and does not match,
|
|
28
|
+
// continue to per-API checks below (do not immediately deny).
|
|
29
|
+
}
|
|
30
|
+
// 2) Per-API allowlist sourced from req.api (if available)
|
|
31
|
+
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
32
|
+
const api = req.api;
|
|
33
|
+
if (api) {
|
|
34
|
+
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
35
|
+
const siteUrls = api.domains || [];
|
|
36
|
+
const listOfDomains = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], siteUrls);
|
|
37
|
+
if (origin && listOfDomains.includes(origin)) {
|
|
38
|
+
return callback(null, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 3) Localhost is always allowed
|
|
42
|
+
if (origin) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = parseUrl(new URL(origin));
|
|
45
|
+
if (parsed.domain === 'localhost') {
|
|
46
|
+
return callback(null, true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// ignore invalid origin
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Default: not allowed
|
|
54
|
+
return callback(null, false);
|
|
55
|
+
};
|
|
56
|
+
// Wrap in the cors plugin with our dynamic origin resolver
|
|
57
|
+
const handler = (req, res, next) => corsPlugin({
|
|
58
|
+
origin: (reqOrigin, cb) => dynamicOrigin(reqOrigin, cb, req),
|
|
59
|
+
credentials: true,
|
|
60
|
+
optionsSuccessStatus: 200,
|
|
61
|
+
})(req, res, next);
|
|
62
|
+
return handler;
|
|
63
|
+
};
|