afront 1.0.19 → 1.0.21
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/build-prod/index.html +1 -1
- package/build-prod/offline.html +1024 -0
- package/build-prod/service-worker.js +1 -0
- package/build-prod/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
- package/build-prod/static/js/3-b7f6c4256e55d2c173ac.js +1 -0
- package/build-prod/static/js/41-d729186c867a4a7f5f49.js +1 -0
- package/build-prod/static/js/525-506bbc7da1b98d130181.js +1 -0
- package/build-prod/static/js/573-8fcda455788ea4aa45cd.js +1 -0
- package/build-prod/static/js/99-ccf46a26c15904ecb674.js +1 -0
- package/build-prod/static/js/main-34c866ebbc6fbc291b85.js +1 -0
- package/build-prod-ssr/3.ssr.prod.js +1 -1
- package/build-prod-ssr/41.ssr.prod.js +1 -1
- package/build-prod-ssr/525.ssr.prod.js +1 -0
- package/build-prod-ssr/573.ssr.prod.js +1 -1
- package/build-prod-ssr/ssr.prod.js +1 -1
- package/build-prod-ssr/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
- package/build-prod-static/index.html +1 -1
- package/build-prod-static/offline.html +1024 -0
- package/build-prod-static/service-worker.js +1 -0
- package/build-prod-static/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
- package/build-prod-static/static/js/23-c9124d3cac34021e3406.js +1 -0
- package/build-prod-static/static/js/303-0d29064b1a912a7441ef.js +1 -0
- package/build-prod-static/static/js/636-adf7ca5d4e262a755df3.js +1 -0
- package/build-prod-static/static/js/863-2c35feb0663baeecb6f8.js +1 -0
- package/build-prod-static/static/js/main-02c8c189d5bec6c0a7f1.js +1 -0
- package/install.js +190 -20
- package/package.json +5 -4
- package/server.js +9 -0
- package/src/ARoutes/AFRoutes.js +22 -5
- package/src/Api/api.config.js +266 -0
- package/src/Api/login.service.js +44 -0
- package/src/App.js +10 -9
- package/src/Components/Loading/LoadingIndicator.js +12 -0
- package/src/Components/Loading/LoadingIndicator.module.css +34 -0
- package/src/Components/Loading/LoadingSpinner.js +27 -0
- package/src/Components/Loading/LoadingSpinner.module.css +100 -0
- package/src/Components/RequireAuth.js +30 -0
- package/src/PageNotFound.js +1 -1
- package/src/Pages/Signup.js +230 -0
- package/src/Routes/ARoutes.js +47 -6
- package/src/Routes/ARoutesStatic.js +83 -0
- package/src/Static/appStatic.js +10 -20
- package/src/Static/indexStatic.js +3 -0
- package/src/Utils/LoadingContext.js +5 -0
- package/src/index.js +0 -4
- package/webpack.build-prod.js +17 -4
- package/webpack.dev.js +19 -1
- package/webpack.prod.js +15 -6
- package/webpack.ssr.prod.js +4 -0
- package/build-prod/static/css/main-7f7c4e72ce002df48179.css +0 -1
- package/build-prod/static/js/3-985f8801e97b3b2025e2.js +0 -1
- package/build-prod/static/js/41-a66fae84fd39cfb1e349.js +0 -1
- package/build-prod/static/js/573-f7791eb491c75b0ccfb7.js +0 -1
- package/build-prod/static/js/main-1cbe42ef1eb3d942625c.js +0 -1
- package/build-prod-ssr/static/css/main-7f7c4e72ce002df48179.css +0 -1
- package/build-prod-static/static/css/main-7f7c4e72ce002df48179.css +0 -1
- package/build-prod-static/static/js/23-9e69ffb20f982ca1ba75.js +0 -1
- package/build-prod-static/static/js/303-1b6136e5efb4925c5f98.js +0 -1
- package/build-prod-static/static/js/636-7f7bf68f9765bc200b86.js +0 -1
- package/build-prod-static/static/js/main-6c6f40c44813930620ed.js +0 -1
package/install.js
CHANGED
|
@@ -3,12 +3,13 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { https } = require('follow-redirects');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
|
+
const os = require('os');
|
|
6
7
|
const tmp = require('tmp');
|
|
7
8
|
const AdmZip = require('adm-zip');
|
|
8
9
|
const readline = require('readline');
|
|
9
10
|
|
|
10
11
|
// Configuration
|
|
11
|
-
const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.
|
|
12
|
+
const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.21.zip'; // Updated URL
|
|
12
13
|
|
|
13
14
|
// Define files to skip
|
|
14
15
|
const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
|
|
@@ -69,7 +70,8 @@ const downloadFile = (url, destination) => {
|
|
|
69
70
|
});
|
|
70
71
|
});
|
|
71
72
|
}).on('error', (err) => {
|
|
72
|
-
|
|
73
|
+
// attempt to remove partial download only if it's inside allowed temp locations
|
|
74
|
+
safeUnlink(destination).finally(() => reject(err));
|
|
73
75
|
});
|
|
74
76
|
});
|
|
75
77
|
};
|
|
@@ -101,7 +103,7 @@ const extractZip = (zipPath, extractTo) => {
|
|
|
101
103
|
const runNpmInstall = (directory) => {
|
|
102
104
|
return new Promise((resolve, reject) => {
|
|
103
105
|
const stopSpinner = spinner('Running npm install');
|
|
104
|
-
exec('npm install', { cwd: directory }, (err, stdout, stderr) => {
|
|
106
|
+
exec('npm install --legacy-peer-deps --no-audit --no-fund', { cwd: directory }, (err, stdout, stderr) => {
|
|
105
107
|
if (err) {
|
|
106
108
|
stopSpinner();
|
|
107
109
|
console.error('Error running npm install:', stderr);
|
|
@@ -127,6 +129,97 @@ const createDirIfNotExists = (dirPath) => {
|
|
|
127
129
|
});
|
|
128
130
|
};
|
|
129
131
|
|
|
132
|
+
const isPathInside = (root, target) => {
|
|
133
|
+
const r = path.resolve(root);
|
|
134
|
+
const t = path.resolve(target);
|
|
135
|
+
return t === r || t.startsWith(r + path.sep);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
// Security: path validated against SAFE_UNLINK_ROOT before unlink
|
|
140
|
+
const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
|
|
141
|
+
|
|
142
|
+
const safeUnlink = async (targetPath) => {
|
|
143
|
+
const resolved = path.resolve(targetPath);
|
|
144
|
+
|
|
145
|
+
if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
|
|
146
|
+
throw new Error(`Blocked unlink outside safe root: ${resolved}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stats = await fs.promises.lstat(resolved);
|
|
150
|
+
|
|
151
|
+
if (!stats.isFile()) {
|
|
152
|
+
throw new Error(`Refusing to unlink non-file: ${resolved}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await fs.promises.unlink(resolved);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
const safeRm = (dirPath, cb) => {
|
|
160
|
+
const resolved = path.resolve(dirPath);
|
|
161
|
+
if (!isPathInside(process.cwd(), resolved)) {
|
|
162
|
+
return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
|
|
163
|
+
}
|
|
164
|
+
fs.rm(resolved, { recursive: true, force: true }, cb);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
|
|
168
|
+
const safeMoveFile = async (resolvedSrc, resolvedDest) => {
|
|
169
|
+
const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
170
|
+
let srcHandle;
|
|
171
|
+
let destHandle;
|
|
172
|
+
const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
|
|
173
|
+
try {
|
|
174
|
+
srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const stats = await srcHandle.stat();
|
|
181
|
+
if (stats.isDirectory()) {
|
|
182
|
+
await srcHandle.close();
|
|
183
|
+
throw new Error('Source is a directory');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await createDirIfNotExists(path.dirname(resolvedDest));
|
|
187
|
+
|
|
188
|
+
destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
|
|
189
|
+
|
|
190
|
+
const bufferSize = 64 * 1024;
|
|
191
|
+
const buffer = Buffer.allocUnsafe(bufferSize);
|
|
192
|
+
let position = 0;
|
|
193
|
+
while (true) {
|
|
194
|
+
const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
|
|
195
|
+
if (!bytesRead) break;
|
|
196
|
+
let written = 0;
|
|
197
|
+
while (written < bytesRead) {
|
|
198
|
+
const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
|
|
199
|
+
written += bytesWritten;
|
|
200
|
+
}
|
|
201
|
+
position += bytesRead;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await destHandle.sync();
|
|
205
|
+
await destHandle.close();
|
|
206
|
+
await srcHandle.close();
|
|
207
|
+
|
|
208
|
+
await fs.promises.rename(tmpName, resolvedDest);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
try {
|
|
211
|
+
if (destHandle) await destHandle.close();
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
try {
|
|
214
|
+
await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
|
|
215
|
+
} catch (e) {}
|
|
216
|
+
try {
|
|
217
|
+
if (srcHandle) await srcHandle.close();
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
130
223
|
const promptForFolderName = async () => {
|
|
131
224
|
const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
|
|
132
225
|
return answer;
|
|
@@ -140,49 +233,94 @@ const promptForReplace = async (dirPath) => {
|
|
|
140
233
|
const removeDir = (dirPath) => {
|
|
141
234
|
return new Promise((resolve, reject) => {
|
|
142
235
|
console.log(`Removing existing directory: ${dirPath}`);
|
|
143
|
-
|
|
144
|
-
if (err)
|
|
145
|
-
return reject(err);
|
|
146
|
-
}
|
|
236
|
+
safeRm(dirPath, (err) => {
|
|
237
|
+
if (err) return reject(err);
|
|
147
238
|
resolve();
|
|
148
239
|
});
|
|
149
240
|
});
|
|
150
241
|
};
|
|
151
242
|
|
|
243
|
+
const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
|
|
244
|
+
|
|
245
|
+
const safeReaddir = (dirPath, cb) => {
|
|
246
|
+
const resolved = path.resolve(dirPath);
|
|
247
|
+
|
|
248
|
+
if (!resolved.startsWith(SAFE_READDIR_ROOT + path.sep)) {
|
|
249
|
+
return cb(new Error(`Blocked readdir outside safe root: ${resolved}`));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fs.lstat(resolved, (err, stats) => {
|
|
253
|
+
if (err) return cb(err);
|
|
254
|
+
if (!stats.isDirectory()) {
|
|
255
|
+
return cb(new Error(`Not a directory: ${resolved}`));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.readdir(resolved, cb);
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
|
|
152
263
|
const moveFiles = (srcPath, destPath) => {
|
|
153
264
|
return new Promise((resolve, reject) => {
|
|
154
|
-
|
|
265
|
+
safeReaddir(srcPath, (err, files) => {
|
|
155
266
|
if (err) {
|
|
156
267
|
return reject(err);
|
|
157
268
|
}
|
|
158
269
|
let pending = files.length;
|
|
159
270
|
if (!pending) return resolve();
|
|
160
271
|
files.forEach((file) => {
|
|
161
|
-
|
|
272
|
+
// Normalize and validate file name to avoid path traversal
|
|
273
|
+
const fileName = path.basename(file);
|
|
274
|
+
if (SKIP_FILES.includes(fileName)) {
|
|
275
|
+
if (!--pending) resolve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const srcRoot = path.resolve(srcPath);
|
|
280
|
+
const destRoot = path.resolve(destPath);
|
|
281
|
+
const resolvedSrc = path.resolve(srcPath, file);
|
|
282
|
+
const resolvedDest = path.resolve(destPath, file);
|
|
283
|
+
|
|
284
|
+
// Ensure resolved paths are inside their respective roots
|
|
285
|
+
if (
|
|
286
|
+
!(resolvedSrc === srcRoot || resolvedSrc.startsWith(srcRoot + path.sep)) ||
|
|
287
|
+
!(resolvedDest === destRoot || resolvedDest.startsWith(destRoot + path.sep))
|
|
288
|
+
) {
|
|
289
|
+
console.warn(`Skipping suspicious path: ${file}`);
|
|
162
290
|
if (!--pending) resolve();
|
|
163
291
|
return;
|
|
164
292
|
}
|
|
165
293
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
fs.stat(srcFile, (err, stats) => {
|
|
294
|
+
// Use lstat to detect symlinks and refuse to follow them
|
|
295
|
+
fs.lstat(resolvedSrc, (err, stats) => {
|
|
169
296
|
if (err) {
|
|
170
297
|
return reject(err);
|
|
171
298
|
}
|
|
299
|
+
|
|
300
|
+
if (stats.isSymbolicLink()) {
|
|
301
|
+
console.warn(`Skipping symbolic link for safety: ${resolvedSrc}`);
|
|
302
|
+
if (!--pending) resolve();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
172
306
|
if (stats.isDirectory()) {
|
|
173
|
-
createDirIfNotExists(
|
|
174
|
-
.then(() => moveFiles(
|
|
307
|
+
createDirIfNotExists(resolvedDest)
|
|
308
|
+
.then(() => moveFiles(resolvedSrc, resolvedDest))
|
|
175
309
|
.then(() => {
|
|
176
310
|
if (!--pending) resolve();
|
|
177
311
|
})
|
|
178
312
|
.catch(reject);
|
|
179
313
|
} else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
314
|
+
// Use atomic-safe move: copy from a non-following FD to temp file, then rename
|
|
315
|
+
createDirIfNotExists(path.dirname(resolvedDest))
|
|
316
|
+
.then(() => {
|
|
317
|
+
safeMoveFile(resolvedSrc, resolvedDest)
|
|
318
|
+
.then(() => {
|
|
319
|
+
if (!--pending) resolve();
|
|
320
|
+
})
|
|
321
|
+
.catch(reject);
|
|
322
|
+
})
|
|
323
|
+
.catch(reject);
|
|
186
324
|
}
|
|
187
325
|
});
|
|
188
326
|
});
|
|
@@ -202,6 +340,29 @@ const main = async () => {
|
|
|
202
340
|
folderName = await promptForFolderName();
|
|
203
341
|
}
|
|
204
342
|
|
|
343
|
+
// Sanitize the provided folder name to prevent path traversal or absolute paths
|
|
344
|
+
const sanitizeFolderName = (name) => {
|
|
345
|
+
if (!name) return '';
|
|
346
|
+
// If user provided '.', use current dir basename
|
|
347
|
+
if (name === '.') return path.basename(process.cwd());
|
|
348
|
+
// Disallow absolute paths
|
|
349
|
+
if (path.isAbsolute(name)) {
|
|
350
|
+
console.warn('Absolute paths are not allowed for destination; using basename.');
|
|
351
|
+
name = path.basename(name);
|
|
352
|
+
}
|
|
353
|
+
// Disallow parent traversal and nested paths
|
|
354
|
+
if (name === '..' || name.includes(path.sep) || name.includes('..')) {
|
|
355
|
+
console.warn('Path traversal detected in destination name; using basename of input.');
|
|
356
|
+
name = path.basename(name);
|
|
357
|
+
}
|
|
358
|
+
// Finally, ensure it's a single path segment
|
|
359
|
+
name = path.basename(name);
|
|
360
|
+
if (!name) throw new Error('Invalid destination folder name');
|
|
361
|
+
return name;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
folderName = sanitizeFolderName(folderName);
|
|
365
|
+
|
|
205
366
|
const destDir = path.join(process.cwd(), folderName);
|
|
206
367
|
|
|
207
368
|
if (fs.existsSync(destDir)) {
|
|
@@ -230,6 +391,15 @@ const main = async () => {
|
|
|
230
391
|
|
|
231
392
|
await moveFiles(extractedFolderPath, destDir);
|
|
232
393
|
|
|
394
|
+
// Remove any bundled lockfiles from the extracted archive to avoid integrity
|
|
395
|
+
// checksum mismatches originating from the release zip's lockfile.
|
|
396
|
+
try {
|
|
397
|
+
await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
|
|
398
|
+
await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
|
|
399
|
+
} catch (e) {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
|
|
233
403
|
await runNpmInstall(destDir);
|
|
234
404
|
|
|
235
405
|
rl.close();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afront",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "AFront is a front-end JavaScript library designed to create seamless server-side rendered (SSSR) websites.",
|
|
5
5
|
"main": "webpack.dev.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "webpack --config webpack.prod.js",
|
|
7
|
+
"build": "webpack --config webpack.prod.js && webpack --config webpack.post-prod.js",
|
|
8
8
|
"build:ssr": "webpack --config webpack.ssr.prod.js",
|
|
9
|
-
"build:alt": "webpack --config webpack.build-prod.js",
|
|
9
|
+
"build:alt": "webpack --config webpack.build-prod.js && webpack --config webpack.post-prod-static.js --stats-error-details",
|
|
10
10
|
"prod:ssr": "node build-prod-ssr/ssr.prod.js",
|
|
11
11
|
"start": "webpack serve --config webpack.dev.js",
|
|
12
12
|
"static": "node server.js"
|
|
@@ -38,10 +38,11 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"adm-zip": "^0.5.16",
|
|
41
|
-
"afront": "^1.0.
|
|
41
|
+
"afront": "^1.0.15",
|
|
42
42
|
"asggen-headtags": "^1.0.3",
|
|
43
43
|
"babel-plugin-css-modules-transform": "^1.6.2",
|
|
44
44
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
45
|
+
"banner-webpack-after-content": "^1.0.3",
|
|
45
46
|
"copy-webpack-plugin": "^12.0.2",
|
|
46
47
|
"dotenv": "^16.4.5",
|
|
47
48
|
"express-rate-limit": "^7.4.1",
|
package/server.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const RateLimit = require("express-rate-limit");
|
|
4
|
+
const helmet = require("helmet");
|
|
4
5
|
require("dotenv").config();
|
|
5
6
|
|
|
6
7
|
const app = express();
|
|
7
8
|
const PORT = process.env.PORT;
|
|
8
9
|
const HOST = process.env.HOST;
|
|
9
10
|
|
|
11
|
+
app.set("trust proxy", 1);
|
|
12
|
+
|
|
13
|
+
// Disable the X-Powered-By header to avoid exposing Express
|
|
14
|
+
app.disable("x-powered-by");
|
|
15
|
+
|
|
16
|
+
// Use Helmet to set various HTTP headers for better security
|
|
17
|
+
app.use(helmet());
|
|
18
|
+
|
|
10
19
|
// Set up rate limiter: maximum of 100 requests per 15 minutes
|
|
11
20
|
const limiter = RateLimit({
|
|
12
21
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
package/src/ARoutes/AFRoutes.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { lazy } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
const routes = [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
{
|
|
5
|
+
path: "/",
|
|
6
|
+
element: lazy(() => import("../Pages/Home.js")),
|
|
7
|
+
withHeaderFooter: true,
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
path: "/signup",
|
|
11
|
+
element: lazy(() => import("../Pages/Signup.js")),
|
|
12
|
+
withHeaderFooter: false,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: "/support/s",
|
|
16
|
+
element: lazy(() => import("../Pages/Support.js")),
|
|
17
|
+
protected: true,
|
|
18
|
+
withHeaderFooter: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: "/*",
|
|
22
|
+
element: lazy(() => import("../PageNotFound.js")),
|
|
23
|
+
withHeaderFooter: true,
|
|
24
|
+
}, // Page Not Found route
|
|
8
25
|
// Add More Pages
|
|
9
26
|
];
|
|
10
27
|
|
|
11
|
-
export default routes;
|
|
28
|
+
export default routes;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Lightweight fetch-based API instance with interceptors and retry helper
|
|
2
|
+
const apiBase =
|
|
3
|
+
(typeof process !== "undefined" &&
|
|
4
|
+
process.env &&
|
|
5
|
+
process.env.REACT_APP_API_BASE) ||
|
|
6
|
+
(typeof window !== "undefined" &&
|
|
7
|
+
window.__env &&
|
|
8
|
+
window.__env.REACT_APP_API_BASE) ||
|
|
9
|
+
"http://localhost:4000/api/v1";
|
|
10
|
+
|
|
11
|
+
function isAbsoluteUrl(u) {
|
|
12
|
+
return /^https?:\/\//i.test(u);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildUrl(base, url, params) {
|
|
16
|
+
const full = isAbsoluteUrl(url) ? url : `${base}${url}`;
|
|
17
|
+
if (!params) return full;
|
|
18
|
+
const search = new URLSearchParams(params).toString();
|
|
19
|
+
return search ? `${full}${full.includes("?") ? "&" : "?"}${search}` : full;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createInstance({
|
|
23
|
+
baseURL = apiBase,
|
|
24
|
+
timeout = 60000,
|
|
25
|
+
headers = { "Content-Type": "application/json" },
|
|
26
|
+
withCredentials = true,
|
|
27
|
+
} = {}) {
|
|
28
|
+
const requestHandlers = [];
|
|
29
|
+
const responseHandlers = [];
|
|
30
|
+
|
|
31
|
+
const interceptors = {
|
|
32
|
+
request: {
|
|
33
|
+
use: (fn) => {
|
|
34
|
+
requestHandlers.push(fn);
|
|
35
|
+
return requestHandlers.length - 1;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
response: {
|
|
39
|
+
use: (onFulfilled, onRejected) => {
|
|
40
|
+
responseHandlers.push({ onFulfilled, onRejected });
|
|
41
|
+
return responseHandlers.length - 1;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function runRequestHandlers(cfg) {
|
|
47
|
+
let c = { ...cfg };
|
|
48
|
+
for (const h of requestHandlers) {
|
|
49
|
+
// handlers may be sync or async
|
|
50
|
+
// allow them to modify/return the config
|
|
51
|
+
// if they return undefined, assume they mutated in place
|
|
52
|
+
// otherwise use returned value
|
|
53
|
+
// eslint-disable-next-line no-await-in-loop
|
|
54
|
+
const out = await h(c);
|
|
55
|
+
if (typeof out !== "undefined") c = out;
|
|
56
|
+
}
|
|
57
|
+
return c;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runResponseHandlers(resOrErr, success = true) {
|
|
61
|
+
let acc = resOrErr;
|
|
62
|
+
for (const h of responseHandlers) {
|
|
63
|
+
try {
|
|
64
|
+
if (success && typeof h.onFulfilled === "function") {
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop
|
|
66
|
+
const out = await h.onFulfilled(acc);
|
|
67
|
+
if (typeof out !== "undefined") acc = out;
|
|
68
|
+
} else if (!success && typeof h.onRejected === "function") {
|
|
69
|
+
// eslint-disable-next-line no-await-in-loop
|
|
70
|
+
const out = await h.onRejected(acc);
|
|
71
|
+
if (typeof out !== "undefined") acc = out;
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// if a handler throws, treat as rejection
|
|
75
|
+
acc = e;
|
|
76
|
+
success = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (success) return acc;
|
|
80
|
+
throw acc;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function request(cfg) {
|
|
84
|
+
const finalCfg = await runRequestHandlers({
|
|
85
|
+
baseURL,
|
|
86
|
+
timeout,
|
|
87
|
+
headers: { ...headers },
|
|
88
|
+
withCredentials,
|
|
89
|
+
...cfg,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const url = buildUrl(
|
|
93
|
+
finalCfg.baseURL || baseURL,
|
|
94
|
+
finalCfg.url || finalCfg.path || "/",
|
|
95
|
+
finalCfg.params
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const to = finalCfg.timeout || timeout;
|
|
100
|
+
const timer = setTimeout(() => controller.abort(), to);
|
|
101
|
+
|
|
102
|
+
const fetchOpts = {
|
|
103
|
+
method: (finalCfg.method || "GET").toUpperCase(),
|
|
104
|
+
headers: finalCfg.headers || {},
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
};
|
|
107
|
+
if (finalCfg.withCredentials || finalCfg.withCredentials === undefined)
|
|
108
|
+
fetchOpts.credentials = "include";
|
|
109
|
+
if (finalCfg.body !== undefined && finalCfg.body !== null) {
|
|
110
|
+
fetchOpts.body = finalCfg.body;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// log request for convenience
|
|
115
|
+
// allow user interceptors to already log as requested
|
|
116
|
+
const raw = await fetch(url, fetchOpts);
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
|
|
119
|
+
// try to parse JSON, fallback to text
|
|
120
|
+
let data = null;
|
|
121
|
+
const ct = raw.headers.get("content-type") || "";
|
|
122
|
+
if (ct.includes("application/json")) {
|
|
123
|
+
try {
|
|
124
|
+
data = await raw.json();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
data = null;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
try {
|
|
130
|
+
data = await raw.text();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
data = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const response = { ok: raw.ok, status: raw.status, data, raw };
|
|
137
|
+
return await runResponseHandlers(response, true);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
// network or abort
|
|
141
|
+
return await runResponseHandlers(err, false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// convenience methods
|
|
146
|
+
const instance = {
|
|
147
|
+
interceptors,
|
|
148
|
+
request,
|
|
149
|
+
get: (url, cfg = {}) => request({ method: "GET", url, ...cfg }),
|
|
150
|
+
post: (url, body, cfg = {}) =>
|
|
151
|
+
request({
|
|
152
|
+
method: "POST",
|
|
153
|
+
url,
|
|
154
|
+
body:
|
|
155
|
+
typeof body === "object" && !(body instanceof FormData)
|
|
156
|
+
? JSON.stringify(body)
|
|
157
|
+
: body,
|
|
158
|
+
headers: {
|
|
159
|
+
...(cfg.headers || {}),
|
|
160
|
+
"Content-Type":
|
|
161
|
+
body instanceof FormData ? undefined : "application/json",
|
|
162
|
+
},
|
|
163
|
+
...cfg,
|
|
164
|
+
}),
|
|
165
|
+
put: (url, body, cfg = {}) =>
|
|
166
|
+
request({
|
|
167
|
+
method: "PUT",
|
|
168
|
+
url,
|
|
169
|
+
body:
|
|
170
|
+
typeof body === "object" && !(body instanceof FormData)
|
|
171
|
+
? JSON.stringify(body)
|
|
172
|
+
: body,
|
|
173
|
+
headers: {
|
|
174
|
+
...(cfg.headers || {}),
|
|
175
|
+
"Content-Type":
|
|
176
|
+
body instanceof FormData ? undefined : "application/json",
|
|
177
|
+
},
|
|
178
|
+
...cfg,
|
|
179
|
+
}),
|
|
180
|
+
patch: (url, body, cfg = {}) =>
|
|
181
|
+
request({
|
|
182
|
+
method: "PATCH",
|
|
183
|
+
url,
|
|
184
|
+
body:
|
|
185
|
+
typeof body === "object" && !(body instanceof FormData)
|
|
186
|
+
? JSON.stringify(body)
|
|
187
|
+
: body,
|
|
188
|
+
headers: {
|
|
189
|
+
...(cfg.headers || {}),
|
|
190
|
+
"Content-Type":
|
|
191
|
+
body instanceof FormData ? undefined : "application/json",
|
|
192
|
+
},
|
|
193
|
+
...cfg,
|
|
194
|
+
}),
|
|
195
|
+
delete: (url, cfg = {}) => request({ method: "DELETE", url, ...cfg }),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return instance;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function requestWithRetry(
|
|
202
|
+
fn,
|
|
203
|
+
{ retries = 3, retryDelay = 1000, retryOn = (err) => true } = {}
|
|
204
|
+
) {
|
|
205
|
+
return new Promise(async (resolve, reject) => {
|
|
206
|
+
let attempt = 0;
|
|
207
|
+
while (attempt < retries) {
|
|
208
|
+
try {
|
|
209
|
+
// fn should be a function returning a promise
|
|
210
|
+
const res = await fn();
|
|
211
|
+
return resolve(res);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
attempt += 1;
|
|
214
|
+
if (attempt >= retries || !retryOn(err)) return reject(err);
|
|
215
|
+
// exponential backoff
|
|
216
|
+
// eslint-disable-next-line no-await-in-loop
|
|
217
|
+
await new Promise((r) => setTimeout(r, retryDelay * attempt));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
reject(new Error("Retries exhausted"));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const instance = createInstance({
|
|
225
|
+
baseURL: apiBase,
|
|
226
|
+
timeout: 60000,
|
|
227
|
+
headers: { "Content-Type": "application/json" },
|
|
228
|
+
withCredentials: true,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// expose defaults for logging/compatibility
|
|
232
|
+
instance.defaults = { baseURL: apiBase, timeout: 60000 };
|
|
233
|
+
|
|
234
|
+
// request logging interceptor
|
|
235
|
+
instance.interceptors.request.use((config) => {
|
|
236
|
+
try {
|
|
237
|
+
console.log(
|
|
238
|
+
`API Request -> ${(
|
|
239
|
+
(config.method || "").toString() || "GET"
|
|
240
|
+
).toUpperCase()} ${config.url || config.path} (base: ${
|
|
241
|
+
instance.defaults.baseURL
|
|
242
|
+
}) (timeout: ${config.timeout || instance.defaults.timeout}ms)`
|
|
243
|
+
);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
/* ignore */
|
|
246
|
+
}
|
|
247
|
+
return config;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// response / error logging interceptor
|
|
251
|
+
instance.interceptors.response.use(
|
|
252
|
+
(response) => response,
|
|
253
|
+
(error) => {
|
|
254
|
+
if (!error || !error.status) {
|
|
255
|
+
console.error(
|
|
256
|
+
"Network or CORS error when calling API:",
|
|
257
|
+
error && error.message ? error.message : error
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
console.warn("API response error:", error.status, error.data);
|
|
261
|
+
}
|
|
262
|
+
return Promise.reject(error);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
export default instance;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import api from "./api.config.js";
|
|
2
|
+
|
|
3
|
+
const BASE = "/auth";
|
|
4
|
+
|
|
5
|
+
export async function login({ email, password }) {
|
|
6
|
+
return api.post(
|
|
7
|
+
`${BASE}/login`,
|
|
8
|
+
{ email, password },
|
|
9
|
+
{ withCredentials: true }
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function signup({
|
|
14
|
+
email,
|
|
15
|
+
password,
|
|
16
|
+
name,
|
|
17
|
+
accountType,
|
|
18
|
+
companyName,
|
|
19
|
+
}) {
|
|
20
|
+
return api.post(
|
|
21
|
+
`${BASE}/signup`,
|
|
22
|
+
{ email, password, name, accountType, companyName },
|
|
23
|
+
{ withCredentials: true }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function checkSignup({ email }) {
|
|
28
|
+
return api.post(`${BASE}/check`, { email }, { withCredentials: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function logout() {
|
|
32
|
+
try {
|
|
33
|
+
await api.post(`${BASE}/logout`, null, { withCredentials: true });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
/* ignore */
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem("auth-event", String(Date.now()));
|
|
39
|
+
} catch (e) {}
|
|
40
|
+
window.dispatchEvent(new Event("auth-changed"));
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default { login, signup, logout, checkSignup };
|