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.
Files changed (60) hide show
  1. package/build-prod/index.html +1 -1
  2. package/build-prod/offline.html +1024 -0
  3. package/build-prod/service-worker.js +1 -0
  4. package/build-prod/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
  5. package/build-prod/static/js/3-b7f6c4256e55d2c173ac.js +1 -0
  6. package/build-prod/static/js/41-d729186c867a4a7f5f49.js +1 -0
  7. package/build-prod/static/js/525-506bbc7da1b98d130181.js +1 -0
  8. package/build-prod/static/js/573-8fcda455788ea4aa45cd.js +1 -0
  9. package/build-prod/static/js/99-ccf46a26c15904ecb674.js +1 -0
  10. package/build-prod/static/js/main-34c866ebbc6fbc291b85.js +1 -0
  11. package/build-prod-ssr/3.ssr.prod.js +1 -1
  12. package/build-prod-ssr/41.ssr.prod.js +1 -1
  13. package/build-prod-ssr/525.ssr.prod.js +1 -0
  14. package/build-prod-ssr/573.ssr.prod.js +1 -1
  15. package/build-prod-ssr/ssr.prod.js +1 -1
  16. package/build-prod-ssr/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
  17. package/build-prod-static/index.html +1 -1
  18. package/build-prod-static/offline.html +1024 -0
  19. package/build-prod-static/service-worker.js +1 -0
  20. package/build-prod-static/static/css/main-b7c0f6b3ea505263e9a2.css +1 -0
  21. package/build-prod-static/static/js/23-c9124d3cac34021e3406.js +1 -0
  22. package/build-prod-static/static/js/303-0d29064b1a912a7441ef.js +1 -0
  23. package/build-prod-static/static/js/636-adf7ca5d4e262a755df3.js +1 -0
  24. package/build-prod-static/static/js/863-2c35feb0663baeecb6f8.js +1 -0
  25. package/build-prod-static/static/js/main-02c8c189d5bec6c0a7f1.js +1 -0
  26. package/install.js +190 -20
  27. package/package.json +5 -4
  28. package/server.js +9 -0
  29. package/src/ARoutes/AFRoutes.js +22 -5
  30. package/src/Api/api.config.js +266 -0
  31. package/src/Api/login.service.js +44 -0
  32. package/src/App.js +10 -9
  33. package/src/Components/Loading/LoadingIndicator.js +12 -0
  34. package/src/Components/Loading/LoadingIndicator.module.css +34 -0
  35. package/src/Components/Loading/LoadingSpinner.js +27 -0
  36. package/src/Components/Loading/LoadingSpinner.module.css +100 -0
  37. package/src/Components/RequireAuth.js +30 -0
  38. package/src/PageNotFound.js +1 -1
  39. package/src/Pages/Signup.js +230 -0
  40. package/src/Routes/ARoutes.js +47 -6
  41. package/src/Routes/ARoutesStatic.js +83 -0
  42. package/src/Static/appStatic.js +10 -20
  43. package/src/Static/indexStatic.js +3 -0
  44. package/src/Utils/LoadingContext.js +5 -0
  45. package/src/index.js +0 -4
  46. package/webpack.build-prod.js +17 -4
  47. package/webpack.dev.js +19 -1
  48. package/webpack.prod.js +15 -6
  49. package/webpack.ssr.prod.js +4 -0
  50. package/build-prod/static/css/main-7f7c4e72ce002df48179.css +0 -1
  51. package/build-prod/static/js/3-985f8801e97b3b2025e2.js +0 -1
  52. package/build-prod/static/js/41-a66fae84fd39cfb1e349.js +0 -1
  53. package/build-prod/static/js/573-f7791eb491c75b0ccfb7.js +0 -1
  54. package/build-prod/static/js/main-1cbe42ef1eb3d942625c.js +0 -1
  55. package/build-prod-ssr/static/css/main-7f7c4e72ce002df48179.css +0 -1
  56. package/build-prod-static/static/css/main-7f7c4e72ce002df48179.css +0 -1
  57. package/build-prod-static/static/js/23-9e69ffb20f982ca1ba75.js +0 -1
  58. package/build-prod-static/static/js/303-1b6136e5efb4925c5f98.js +0 -1
  59. package/build-prod-static/static/js/636-7f7bf68f9765bc200b86.js +0 -1
  60. 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.19.zip'; // Updated URL
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
- fs.unlink(destination, () => reject(err));
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
- fs.rm(dirPath, { recursive: true, force: true }, (err) => {
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
- fs.readdir(srcPath, (err, files) => {
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
- if (SKIP_FILES.includes(file)) {
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
- const srcFile = path.join(srcPath, file);
167
- const destFile = path.join(destPath, file);
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(destFile)
174
- .then(() => moveFiles(srcFile, destFile))
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
- fs.rename(srcFile, destFile, (err) => {
181
- if (err) {
182
- return reject(err);
183
- }
184
- if (!--pending) resolve();
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.19",
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.19",
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
@@ -1,11 +1,28 @@
1
1
  import { lazy } from "react";
2
2
 
3
-
4
3
  const routes = [
5
- { path: "/", element: lazy(() => import("../Pages/Home.js")) },
6
- { path: "/support", element: lazy(() => import("../Pages/Support.js")) },
7
- { path: "/*", element: lazy(() => import("../PageNotFound.js")) }, // Page Not Found route
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 };