@constructive-io/graphql-server 4.36.0 → 4.36.2

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.
@@ -1,348 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.uploadRoute = exports.createUploadAuthenticateMiddleware = void 0;
7
- const logger_1 = require("@pgpmjs/logger");
8
- const fs_1 = __importDefault(require("fs"));
9
- const multer_1 = __importDefault(require("multer"));
10
- const os_1 = __importDefault(require("os"));
11
- const quotes_1 = require("@pgsql/quotes");
12
- const pg_cache_1 = require("pg-cache");
13
- const pg_query_context_1 = __importDefault(require("pg-query-context"));
14
- const graphile_settings_1 = require("graphile-settings");
15
- require("./types");
16
- const uploadLog = new logger_1.Logger('upload');
17
- const authLog = new logger_1.Logger('upload-auth');
18
- const envFileSize = process.env.MAX_UPLOAD_FILE_SIZE
19
- ? parseInt(process.env.MAX_UPLOAD_FILE_SIZE, 10)
20
- : NaN;
21
- const MAX_FILE_SIZE = envFileSize > 0 ? envFileSize : 10 * 1024 * 1024;
22
- const BLOCKED_MIME_TYPES = new Set([
23
- 'application/x-executable',
24
- 'application/x-sharedlib',
25
- 'application/x-mach-binary',
26
- 'application/x-dosexec',
27
- 'text/html',
28
- 'application/xhtml+xml',
29
- 'application/javascript',
30
- 'text/javascript'
31
- ]);
32
- const parseFile = (0, multer_1.default)({
33
- storage: multer_1.default.diskStorage({
34
- destination: os_1.default.tmpdir(),
35
- filename: (_req, _file, cb) => {
36
- const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
37
- cb(null, `upload-${uniqueSuffix}.tmp`);
38
- },
39
- }),
40
- limits: { fileSize: MAX_FILE_SIZE },
41
- }).single('file');
42
- const parseFileWithErrors = (req, res, next) => {
43
- parseFile(req, res, (err) => {
44
- if (!err)
45
- return next();
46
- if (err.code === 'LIMIT_FILE_SIZE') {
47
- return res.status(413).json({ error: `File exceeds maximum size of ${MAX_FILE_SIZE / (1024 * 1024)} MB` });
48
- }
49
- if (err.code === 'LIMIT_UNEXPECTED_FILE') {
50
- return res.status(400).json({ error: 'Unexpected file field. Send a single file as "file".' });
51
- }
52
- return res.status(400).json({ error: 'File upload failed' });
53
- });
54
- };
55
- const RLS_MODULE_BY_DATABASE_ID_SQL = `
56
- SELECT am.data
57
- FROM services_public.api_modules am
58
- JOIN services_public.apis a ON am.api_id = a.id
59
- WHERE am.name = 'rls_module' AND a.database_id = $1
60
- ORDER BY a.id
61
- LIMIT 1
62
- `;
63
- const RLS_MODULE_BY_API_ID_SQL = `
64
- SELECT data
65
- FROM services_public.api_modules
66
- WHERE api_id = $1 AND name = 'rls_module'
67
- LIMIT 1
68
- `;
69
- const RLS_MODULE_BY_DBNAME_SQL = `
70
- SELECT am.data
71
- FROM services_public.api_modules am
72
- JOIN services_public.apis a ON am.api_id = a.id
73
- WHERE am.name = 'rls_module' AND a.dbname = $1
74
- ORDER BY a.id
75
- LIMIT 1
76
- `;
77
- const RLS_SETTINGS_BY_DATABASE_ID_SQL = `
78
- SELECT
79
- auth_schema.schema_name AS authenticate_schema,
80
- role_schema.schema_name AS role_schema,
81
- auth_fn.name AS authenticate,
82
- auth_strict_fn.name AS authenticate_strict,
83
- role_fn.name AS current_role,
84
- role_id_fn.name AS current_role_id,
85
- ua_fn.name AS current_user_agent,
86
- ip_fn.name AS current_ip_address
87
- FROM services_public.rls_settings rs
88
- LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
89
- LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
90
- LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
91
- LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
92
- LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
93
- LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
94
- LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
95
- LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
96
- WHERE rs.database_id = $1
97
- LIMIT 1
98
- `;
99
- const RLS_SETTINGS_BY_DBNAME_SQL = `
100
- SELECT
101
- auth_schema.schema_name AS authenticate_schema,
102
- role_schema.schema_name AS role_schema,
103
- auth_fn.name AS authenticate,
104
- auth_strict_fn.name AS authenticate_strict,
105
- role_fn.name AS current_role,
106
- role_id_fn.name AS current_role_id,
107
- ua_fn.name AS current_user_agent,
108
- ip_fn.name AS current_ip_address
109
- FROM services_public.rls_settings rs
110
- JOIN services_public.apis a ON rs.database_id = a.database_id
111
- LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
112
- LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
113
- LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
114
- LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
115
- LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
116
- LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
117
- LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
118
- LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
119
- WHERE a.dbname = $1
120
- LIMIT 1
121
- `;
122
- const toRlsModule = (row) => {
123
- if (!row?.data)
124
- return undefined;
125
- const d = row.data;
126
- return {
127
- authenticate: d.authenticate,
128
- authenticateStrict: d.authenticate_strict,
129
- privateSchema: { schemaName: d.authenticate_schema },
130
- publicSchema: { schemaName: d.role_schema },
131
- currentRole: d.current_role,
132
- currentRoleId: d.current_role_id,
133
- currentIpAddress: d.current_ip_address,
134
- currentUserAgent: d.current_user_agent,
135
- };
136
- };
137
- const toRlsModuleFromSettings = (row) => {
138
- if (!row)
139
- return undefined;
140
- return {
141
- authenticate: row.authenticate,
142
- authenticateStrict: row.authenticate_strict,
143
- privateSchema: { schemaName: row.authenticate_schema },
144
- publicSchema: { schemaName: row.role_schema },
145
- currentRole: row.current_role,
146
- currentRoleId: row.current_role_id,
147
- currentIpAddress: row.current_ip_address,
148
- currentUserAgent: row.current_user_agent,
149
- };
150
- };
151
- const getBearerToken = (authorization) => {
152
- if (!authorization)
153
- return null;
154
- const [authType, authToken] = authorization.split(' ');
155
- if (authType?.toLowerCase() !== 'bearer' || !authToken) {
156
- return null;
157
- }
158
- return authToken;
159
- };
160
- const queryRlsSettingsByDatabaseId = async (pool, databaseId) => {
161
- try {
162
- const result = await pool.query(RLS_SETTINGS_BY_DATABASE_ID_SQL, [databaseId]);
163
- return toRlsModuleFromSettings(result.rows[0] ?? null);
164
- }
165
- catch {
166
- return undefined;
167
- }
168
- };
169
- const queryRlsSettingsByDbname = async (pool, dbname) => {
170
- try {
171
- const result = await pool.query(RLS_SETTINGS_BY_DBNAME_SQL, [dbname]);
172
- return toRlsModuleFromSettings(result.rows[0] ?? null);
173
- }
174
- catch {
175
- return undefined;
176
- }
177
- };
178
- const queryRlsModuleByDatabaseId = async (pool, databaseId) => {
179
- const fromSettings = await queryRlsSettingsByDatabaseId(pool, databaseId);
180
- if (fromSettings)
181
- return fromSettings;
182
- const result = await pool.query(RLS_MODULE_BY_DATABASE_ID_SQL, [databaseId]);
183
- return toRlsModule(result.rows[0] ?? null);
184
- };
185
- const queryRlsModuleByApiId = async (pool, apiId) => {
186
- const result = await pool.query(RLS_MODULE_BY_API_ID_SQL, [apiId]);
187
- return toRlsModule(result.rows[0] ?? null);
188
- };
189
- const queryRlsModuleByDbname = async (pool, dbname) => {
190
- const fromSettings = await queryRlsSettingsByDbname(pool, dbname);
191
- if (fromSettings)
192
- return fromSettings;
193
- const result = await pool.query(RLS_MODULE_BY_DBNAME_SQL, [dbname]);
194
- return toRlsModule(result.rows[0] ?? null);
195
- };
196
- const resolveUploadRlsModule = async (opts, req) => {
197
- const api = req.api;
198
- if (!api)
199
- return undefined;
200
- // Use API-scoped RLS module when available (e.g., meta API).
201
- if (api.rlsModule) {
202
- return api.rlsModule;
203
- }
204
- const pool = (0, pg_cache_1.getPgPool)(opts.pg);
205
- if (api.apiId) {
206
- const byApiId = await queryRlsModuleByApiId(pool, api.apiId);
207
- if (byApiId)
208
- return byApiId;
209
- }
210
- if (api.databaseId) {
211
- const byDatabaseId = await queryRlsModuleByDatabaseId(pool, api.databaseId);
212
- if (byDatabaseId)
213
- return byDatabaseId;
214
- }
215
- if (api.dbname) {
216
- return queryRlsModuleByDbname(pool, api.dbname);
217
- }
218
- return undefined;
219
- };
220
- const authError = (res) => res.status(401).json({ error: 'Authentication required' });
221
- /**
222
- * Upload-specific authentication middleware.
223
- *
224
- * This middleware enforces strict auth semantics for `POST /upload` while
225
- * preserving existing GraphQL auth behavior for other routes.
226
- */
227
- const createUploadAuthenticateMiddleware = (opts) => {
228
- return async (req, res, next) => {
229
- const api = req.api;
230
- if (!api) {
231
- res.status(500).send('Missing API info');
232
- return;
233
- }
234
- if (req.token?.user_id) {
235
- next();
236
- return;
237
- }
238
- const authToken = getBearerToken(req.headers.authorization);
239
- if (!authToken) {
240
- authError(res);
241
- return;
242
- }
243
- let rlsModule;
244
- try {
245
- rlsModule = await resolveUploadRlsModule(opts, req);
246
- }
247
- catch (error) {
248
- authLog.error('[upload-auth] Failed to resolve RLS module for upload route', error);
249
- authError(res);
250
- return;
251
- }
252
- if (!rlsModule) {
253
- authLog.info(`[upload-auth] No RLS module found for db=${api.dbname} databaseId=${api.databaseId ?? 'none'}`);
254
- authError(res);
255
- return;
256
- }
257
- const authFn = opts.server?.strictAuth ? rlsModule.authenticateStrict : rlsModule.authenticate;
258
- const privateSchema = rlsModule.privateSchema?.schemaName;
259
- if (!authFn || !privateSchema) {
260
- authLog.warn(`[upload-auth] Missing auth function or private schema for db=${api.dbname}; strictAuth=${opts.server?.strictAuth ?? false}`);
261
- authError(res);
262
- return;
263
- }
264
- const pool = (0, pg_cache_1.getPgPool)({
265
- ...opts.pg,
266
- database: api.dbname,
267
- });
268
- const context = {};
269
- if (req.clientIp) {
270
- context['jwt.claims.ip_address'] = req.clientIp;
271
- }
272
- if (req.get('origin')) {
273
- context['jwt.claims.origin'] = req.get('origin');
274
- }
275
- if (req.get('User-Agent')) {
276
- context['jwt.claims.user_agent'] = req.get('User-Agent');
277
- }
278
- try {
279
- const result = await (0, pg_query_context_1.default)({
280
- client: pool,
281
- context,
282
- query: `SELECT * FROM ${quotes_1.QuoteUtils.quoteQualifiedIdentifier(privateSchema, authFn)}($1)`,
283
- variables: [authToken],
284
- });
285
- if (!result?.rowCount) {
286
- authError(res);
287
- return;
288
- }
289
- req.token = result.rows[0];
290
- if (!req.token?.user_id) {
291
- authError(res);
292
- return;
293
- }
294
- next();
295
- }
296
- catch (error) {
297
- authLog.warn('[upload-auth] Upload authentication failed', error);
298
- authError(res);
299
- }
300
- };
301
- };
302
- exports.createUploadAuthenticateMiddleware = createUploadAuthenticateMiddleware;
303
- /**
304
- * REST file upload endpoint.
305
- *
306
- * Accepts a single file via multipart/form-data, streams it to S3/MinIO,
307
- * and returns file metadata. The frontend uses this in a two-step flow:
308
- *
309
- * 1. POST /upload -> { url, filename, mime, size }
310
- * 2. GraphQL mutation -> patch row with the returned metadata
311
- */
312
- exports.uploadRoute = [
313
- parseFileWithErrors,
314
- (async (req, res, next) => {
315
- if (!req.token?.user_id) {
316
- return res.status(401).json({ error: 'Authentication required' });
317
- }
318
- if (!req.file) {
319
- return res.status(400).json({ error: 'No file provided. Send a "file" field.' });
320
- }
321
- if (req.file.mimetype && BLOCKED_MIME_TYPES.has(req.file.mimetype)) {
322
- fs_1.default.unlink(req.file.path, () => { });
323
- return res.status(415).json({ error: 'File type not allowed' });
324
- }
325
- try {
326
- const readStream = fs_1.default.createReadStream(req.file.path);
327
- const result = await (0, graphile_settings_1.streamToStorage)(readStream, req.file.originalname);
328
- uploadLog.debug(`[upload] Uploaded file for user=${req.token.user_id} filename=${req.file.originalname} mime=${result.mime} size=${req.file.size}`);
329
- res.json({
330
- url: result.url,
331
- filename: result.filename,
332
- mime: result.mime,
333
- size: req.file.size,
334
- });
335
- }
336
- catch (error) {
337
- uploadLog.error('[upload] Upload processing failed', error);
338
- if (!res.headersSent) {
339
- res.status(500).json({ error: 'Upload processing failed' });
340
- }
341
- }
342
- finally {
343
- if (req.file?.path) {
344
- fs_1.default.unlink(req.file.path, () => { });
345
- }
346
- }
347
- }),
348
- ];