refactor(build): remove Gruntfile and transition to npm scripts for build process
- Deleted Gruntfile.js to streamline the build process. - Updated package.json to include new npm scripts for build, clean, and watch tasks. - Added dependencies for concurrent execution and CSRF token management. - Integrated csrf-sync for improved CSRF protection in the application. - Refactored app and API client to utilize the new CSRF token management approach.
This commit is contained in:
@@ -1,131 +0,0 @@
|
|||||||
module.exports = function (grunt) {
|
|
||||||
|
|
||||||
// Project configuration.
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON("package.json"),
|
|
||||||
clean: {
|
|
||||||
dist: "build"
|
|
||||||
},
|
|
||||||
compress: require("./grunt/grunt-compress"),
|
|
||||||
copy: {
|
|
||||||
main: {
|
|
||||||
files: [
|
|
||||||
{expand: true, cwd: "src", src: ["public/**"], dest: "build"},
|
|
||||||
{expand: true, cwd: "src", src: ["views/**"], dest: "build"},
|
|
||||||
{expand: true, cwd: "landing-page-assets", src: ["**"], dest: "build/public/assets"},
|
|
||||||
{expand: true, cwd: "src", src: ["shared/sample-data.json"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "src", src: ["shared/templates/**"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "src", src: ["shared/postgresql-error-codes.json"], dest: "build", filter: "isFile"},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
packages: {
|
|
||||||
files: [
|
|
||||||
{expand: true, cwd: "", src: [".env"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: [".gitignore"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: ["release"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: ["jest.config.js"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: ["package.json"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: ["package-lock.json"], dest: "build", filter: "isFile"},
|
|
||||||
{expand: true, cwd: "", src: ["common_modules/**"], dest: "build"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sync: {
|
|
||||||
main: {
|
|
||||||
files: [
|
|
||||||
{cwd: "src", src: ["views/**", "public/**"], dest: "build/"}, // makes all src relative to cwd
|
|
||||||
],
|
|
||||||
verbose: true,
|
|
||||||
failOnError: true,
|
|
||||||
compareUsing: "md5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uglify: {
|
|
||||||
all: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: "build",
|
|
||||||
src: "**/*.js",
|
|
||||||
dest: "build"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
controllers: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: "build",
|
|
||||||
src: "controllers/*.js",
|
|
||||||
dest: "build"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
routes: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: "build",
|
|
||||||
src: "routes/**/*.js",
|
|
||||||
dest: "build"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: "build",
|
|
||||||
src: "public/assets/**/*.js",
|
|
||||||
dest: "build"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shell: {
|
|
||||||
tsc: {
|
|
||||||
command: "tsc --build tsconfig.prod.json"
|
|
||||||
},
|
|
||||||
esbuild: {
|
|
||||||
// command: "esbuild `find src -type f -name '*.ts'` --platform=node --minify=false --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=build"
|
|
||||||
command: "node esbuild && node cli/esbuild-patch"
|
|
||||||
},
|
|
||||||
tsc_dev: {
|
|
||||||
command: "tsc --build tsconfig.json"
|
|
||||||
},
|
|
||||||
swagger: {
|
|
||||||
command: "node ./cli/swagger"
|
|
||||||
},
|
|
||||||
inline_queries: {
|
|
||||||
command: "node ./cli/inline-queries"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
scripts: {
|
|
||||||
files: ["src/**/*.ts"],
|
|
||||||
tasks: ["shell:tsc_dev"],
|
|
||||||
options: {
|
|
||||||
debounceDelay: 250,
|
|
||||||
spawn: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
files: ["src/**/*.pug", "landing-page-assets/**"],
|
|
||||||
tasks: ["sync"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.registerTask("clean", ["clean"]);
|
|
||||||
grunt.registerTask("copy", ["copy:main"]);
|
|
||||||
grunt.registerTask("swagger", ["shell:swagger"]);
|
|
||||||
grunt.registerTask("build:tsc", ["shell:tsc"]);
|
|
||||||
grunt.registerTask("build", ["clean", "shell:tsc", "copy:main", "compress"]);
|
|
||||||
grunt.registerTask("build:es", ["clean", "shell:esbuild", "copy:main", "uglify:assets", "compress"]);
|
|
||||||
grunt.registerTask("build:strict", ["clean", "shell:tsc", "copy:packages", "uglify:all", "copy:main", "compress"]);
|
|
||||||
grunt.registerTask("dev", ["clean", "copy:main", "shell:tsc_dev", "shell:inline_queries", "watch"]);
|
|
||||||
|
|
||||||
// Load the plugin that provides the "uglify" task.
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-clean");
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-copy");
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-uglify");
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-compress");
|
|
||||||
grunt.loadNpmTasks("grunt-shell");
|
|
||||||
grunt.loadNpmTasks("grunt-sync");
|
|
||||||
|
|
||||||
// Default task(s).
|
|
||||||
grunt.registerTask("default", []);
|
|
||||||
};
|
|
||||||
2443
worklenz-backend/package-lock.json
generated
2443
worklenz-backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,16 +11,30 @@
|
|||||||
"repository": "GITHUB_REPO_HERE",
|
"repository": "GITHUB_REPO_HERE",
|
||||||
"author": "worklenz.com",
|
"author": "worklenz.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./build/bin/www",
|
"test": "jest",
|
||||||
"tcs": "grunt build:tsc",
|
"start": "node build/bin/www.js",
|
||||||
"build": "grunt build",
|
"dev": "npm run build:dev && npm run watch",
|
||||||
"watch": "grunt watch",
|
"build": "npm run clean && npm run compile && npm run copy && npm run compress",
|
||||||
"dev": "grunt dev",
|
"build:dev": "npm run clean && npm run compile:dev && npm run copy",
|
||||||
"es": "esbuild `find src -type f -name '*.ts'` --platform=node --minify=true --watch=true --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=dist",
|
"build:prod": "npm run clean && npm run compile:prod && npm run copy && npm run minify && npm run compress",
|
||||||
"copy": "grunt copy",
|
"clean": "rimraf build",
|
||||||
|
"compile": "tsc --build tsconfig.prod.json",
|
||||||
|
"compile:dev": "tsc --build tsconfig.json",
|
||||||
|
"compile:prod": "tsc --build tsconfig.prod.json",
|
||||||
|
"copy": "npm run copy:assets && npm run copy:views && npm run copy:config && npm run copy:shared",
|
||||||
|
"copy:assets": "npx cpx2 \"src/public/**\" build/public",
|
||||||
|
"copy:views": "npx cpx2 \"src/views/**\" build/views",
|
||||||
|
"copy:config": "npx cpx2 \".env\" build && npx cpx2 \"package.json\" build",
|
||||||
|
"copy:shared": "npx cpx2 \"src/shared/postgresql-error-codes.json\" build/shared && npx cpx2 \"src/shared/sample-data.json\" build/shared && npx cpx2 \"src/shared/templates/**\" build/shared/templates",
|
||||||
|
"watch": "concurrently \"npm run watch:ts\" \"npm run watch:assets\"",
|
||||||
|
"watch:ts": "tsc --build tsconfig.json --watch",
|
||||||
|
"watch:assets": "npx cpx2 \"src/{public,views}/**\" build --watch",
|
||||||
|
"minify": "terser build/**/*.js --compress --mangle --output-dir build",
|
||||||
|
"compress": "node scripts/compress.js",
|
||||||
|
"swagger": "node ./cli/swagger",
|
||||||
|
"inline-queries": "node ./cli/inline-queries",
|
||||||
"sonar": "sonar-scanner -Dproject.settings=sonar-project-dev.properties",
|
"sonar": "sonar-scanner -Dproject.settings=sonar-project-dev.properties",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"test": "jest --setupFiles dotenv/config",
|
|
||||||
"test:watch": "jest --watch --setupFiles dotenv/config"
|
"test:watch": "jest --watch --setupFiles dotenv/config"
|
||||||
},
|
},
|
||||||
"jestSonar": {
|
"jestSonar": {
|
||||||
@@ -45,6 +59,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron": "^2.4.0",
|
"cron": "^2.4.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
"csrf-sync": "^4.2.1",
|
||||||
"csurf": "^1.11.0",
|
"csurf": "^1.11.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@@ -120,26 +135,22 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
|
"cpx2": "^8.0.0",
|
||||||
"esbuild": "^0.17.19",
|
"esbuild": "^0.17.19",
|
||||||
"esbuild-envfile-plugin": "^1.0.5",
|
"esbuild-envfile-plugin": "^1.0.5",
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.8.0",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-security": "^1.7.1",
|
"eslint-plugin-security": "^1.7.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"grunt": "^1.6.1",
|
|
||||||
"grunt-contrib-clean": "^2.0.1",
|
|
||||||
"grunt-contrib-compress": "^2.0.0",
|
|
||||||
"grunt-contrib-copy": "^1.0.0",
|
|
||||||
"grunt-contrib-uglify": "^5.2.2",
|
|
||||||
"grunt-contrib-watch": "^1.1.0",
|
|
||||||
"grunt-shell": "^4.0.0",
|
|
||||||
"grunt-sync": "^0.8.2",
|
|
||||||
"highcharts": "^11.1.0",
|
"highcharts": "^11.1.0",
|
||||||
"jest": "^28.1.3",
|
"jest": "^28.1.3",
|
||||||
"jest-sonar-reporter": "^2.0.0",
|
"jest-sonar-reporter": "^2.0.0",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"nodeman": "^1.1.2",
|
"nodeman": "^1.1.2",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"terser": "^5.40.0",
|
||||||
"ts-jest": "^28.0.8",
|
"ts-jest": "^28.0.8",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
|
|||||||
53
worklenz-backend/scripts/compress.js
Normal file
53
worklenz-backend/scripts/compress.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { createGzip } = require('zlib');
|
||||||
|
const { pipeline } = require('stream');
|
||||||
|
|
||||||
|
async function compressFile(inputPath, outputPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const gzip = createGzip();
|
||||||
|
const source = fs.createReadStream(inputPath);
|
||||||
|
const destination = fs.createWriteStream(outputPath);
|
||||||
|
|
||||||
|
pipeline(source, gzip, destination, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compressDirectory(dir) {
|
||||||
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
await compressDirectory(fullPath);
|
||||||
|
} else if (file.name.endsWith('.js') || file.name.endsWith('.css')) {
|
||||||
|
const gzPath = fullPath + '.gz';
|
||||||
|
await compressFile(fullPath, gzPath);
|
||||||
|
console.log(`Compressed: ${fullPath} -> ${gzPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const buildDir = path.join(__dirname, '../build');
|
||||||
|
if (fs.existsSync(buildDir)) {
|
||||||
|
await compressDirectory(buildDir);
|
||||||
|
console.log('Compression complete!');
|
||||||
|
} else {
|
||||||
|
console.log('Build directory not found. Run build first.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Compression failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -6,7 +6,7 @@ import logger from "morgan";
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import csurf from "csurf";
|
import { csrfSync } from "csrf-sync";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import flash from "connect-flash";
|
import flash from "connect-flash";
|
||||||
@@ -112,17 +112,13 @@ function isLoggedIn(req: Request, _res: Response, next: NextFunction) {
|
|||||||
return req.user ? next() : next(createError(401));
|
return req.user ? next() : next(createError(401));
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF configuration
|
// CSRF configuration using csrf-sync for session-based authentication
|
||||||
const csrfProtection = csurf({
|
const {
|
||||||
cookie: {
|
invalidCsrfTokenError,
|
||||||
key: "XSRF-TOKEN",
|
generateToken,
|
||||||
path: "/",
|
csrfSynchronisedProtection,
|
||||||
httpOnly: false,
|
} = csrfSync({
|
||||||
secure: isProduction(), // Only secure in production
|
getTokenFromRequest: (req: Request) => req.headers["x-csrf-token"] as string || (req.body && req.body["_csrf"])
|
||||||
sameSite: isProduction() ? "none" : "lax", // Different settings for dev vs prod
|
|
||||||
domain: isProduction() ? ".worklenz.com" : undefined // Only set domain in production
|
|
||||||
},
|
|
||||||
ignoreMethods: ["HEAD", "OPTIONS"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply CSRF selectively (exclude webhooks and public routes)
|
// Apply CSRF selectively (exclude webhooks and public routes)
|
||||||
@@ -135,38 +131,25 @@ app.use((req, res, next) => {
|
|||||||
) {
|
) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
csrfProtection(req, res, next);
|
csrfSynchronisedProtection(req, res, next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set CSRF token cookie
|
// Set CSRF token method on request object for compatibility
|
||||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
if (req.csrfToken) {
|
// Add csrfToken method to request object for compatibility
|
||||||
const token = req.csrfToken();
|
if (!req.csrfToken && generateToken) {
|
||||||
res.cookie("XSRF-TOKEN", token, {
|
req.csrfToken = (overwrite?: boolean) => generateToken(req, overwrite);
|
||||||
httpOnly: false,
|
|
||||||
secure: isProduction(),
|
|
||||||
sameSite: isProduction() ? "none" : "lax",
|
|
||||||
domain: isProduction() ? ".worklenz.com" : undefined,
|
|
||||||
path: "/"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSRF token refresh endpoint
|
// CSRF token refresh endpoint
|
||||||
app.get("/csrf-token", (req: Request, res: Response) => {
|
app.get("/csrf-token", (req: Request, res: Response) => {
|
||||||
if (req.csrfToken) {
|
try {
|
||||||
const token = req.csrfToken();
|
const token = generateToken(req);
|
||||||
res.cookie("XSRF-TOKEN", token, {
|
res.status(200).json({ done: true, message: "CSRF token refreshed", token });
|
||||||
httpOnly: false,
|
} catch (error) {
|
||||||
secure: isProduction(),
|
|
||||||
sameSite: isProduction() ? "none" : "lax",
|
|
||||||
domain: isProduction() ? ".worklenz.com" : undefined,
|
|
||||||
path: "/"
|
|
||||||
});
|
|
||||||
res.status(200).json({ done: true, message: "CSRF token refreshed" });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ done: false, message: "Failed to generate CSRF token" });
|
res.status(500).json({ done: false, message: "Failed to generate CSRF token" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -219,7 +202,7 @@ if (isInternalServer()) {
|
|||||||
|
|
||||||
// CSRF error handler
|
// CSRF error handler
|
||||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
||||||
if (err.code === "EBADCSRFTOKEN") {
|
if (err === invalidCsrfTokenError) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
done: false,
|
done: false,
|
||||||
message: "Invalid CSRF token",
|
message: "Invalid CSRF token",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import router from './app/routes';
|
|||||||
// Hooks & Utils
|
// Hooks & Utils
|
||||||
import { useAppSelector } from './hooks/useAppSelector';
|
import { useAppSelector } from './hooks/useAppSelector';
|
||||||
import { initMixpanel } from './utils/mixpanelInit';
|
import { initMixpanel } from './utils/mixpanelInit';
|
||||||
|
import { initializeCsrfToken } from './api/api-client';
|
||||||
|
|
||||||
// Types & Constants
|
// Types & Constants
|
||||||
import { Language } from './features/i18n/localesSlice';
|
import { Language } from './features/i18n/localesSlice';
|
||||||
@@ -35,6 +36,13 @@ const App: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
});
|
});
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
// Initialize CSRF token on app startup
|
||||||
|
useEffect(() => {
|
||||||
|
initializeCsrfToken().catch(error => {
|
||||||
|
logger.error('Failed to initialize CSRF token:', error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
|
|||||||
@@ -4,27 +4,36 @@ import alertService from '@/services/alerts/alertService';
|
|||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import config from '@/config/env';
|
import config from '@/config/env';
|
||||||
|
|
||||||
export const getCsrfToken = (): string | null => {
|
// Store CSRF token in memory (since csrf-sync uses session-based tokens)
|
||||||
const match = document.cookie.split('; ').find(cookie => cookie.startsWith('XSRF-TOKEN='));
|
let csrfToken: string | null = null;
|
||||||
|
|
||||||
if (!match) {
|
export const getCsrfToken = (): string | null => {
|
||||||
return null;
|
return csrfToken;
|
||||||
}
|
|
||||||
return decodeURIComponent(match.split('=')[1]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to refresh CSRF token if needed
|
// Function to refresh CSRF token from server
|
||||||
export const refreshCsrfToken = async (): Promise<string | null> => {
|
export const refreshCsrfToken = async (): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
// Make a GET request to the server to get a fresh CSRF token
|
// Make a GET request to the server to get a fresh CSRF token
|
||||||
await axios.get(`${config.apiUrl}/csrf-token`, { withCredentials: true });
|
const response = await axios.get(`${config.apiUrl}/csrf-token`, { withCredentials: true });
|
||||||
return getCsrfToken();
|
if (response.data && response.data.token) {
|
||||||
|
csrfToken = response.data.token;
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh CSRF token:', error);
|
console.error('Failed to refresh CSRF token:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize CSRF token on app load
|
||||||
|
export const initializeCsrfToken = async (): Promise<void> => {
|
||||||
|
if (!csrfToken) {
|
||||||
|
await refreshCsrfToken();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -36,12 +45,16 @@ const apiClient = axios.create({
|
|||||||
|
|
||||||
// Request interceptor
|
// Request interceptor
|
||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
config => {
|
async config => {
|
||||||
const token = getCsrfToken();
|
// Ensure we have a CSRF token before making requests
|
||||||
if (token) {
|
if (!csrfToken) {
|
||||||
config.headers['X-CSRF-Token'] = token;
|
await refreshCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (csrfToken) {
|
||||||
|
config.headers['X-CSRF-Token'] = csrfToken;
|
||||||
} else {
|
} else {
|
||||||
console.warn('No CSRF token found');
|
console.warn('No CSRF token available');
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
@@ -84,7 +97,7 @@ apiClient.interceptors.response.use(
|
|||||||
(typeof errorResponse.data === 'object' &&
|
(typeof errorResponse.data === 'object' &&
|
||||||
errorResponse.data !== null &&
|
errorResponse.data !== null &&
|
||||||
'message' in errorResponse.data &&
|
'message' in errorResponse.data &&
|
||||||
errorResponse.data.message === 'Invalid CSRF token' ||
|
(errorResponse.data.message === 'invalid csrf token' || errorResponse.data.message === 'Invalid CSRF token') ||
|
||||||
(error as any).code === 'EBADCSRFTOKEN')) {
|
(error as any).code === 'EBADCSRFTOKEN')) {
|
||||||
alertService.error('Security Error', 'Invalid security token. Refreshing your session...');
|
alertService.error('Security Error', 'Invalid security token. Refreshing your session...');
|
||||||
|
|
||||||
@@ -94,7 +107,7 @@ apiClient.interceptors.response.use(
|
|||||||
// Update the token in the failed request
|
// Update the token in the failed request
|
||||||
error.config.headers['X-CSRF-Token'] = newToken;
|
error.config.headers['X-CSRF-Token'] = newToken;
|
||||||
// Retry the original request with the new token
|
// Retry the original request with the new token
|
||||||
return axios(error.config);
|
return apiClient(error.config);
|
||||||
} else {
|
} else {
|
||||||
// If token refresh failed, redirect to login
|
// If token refresh failed, redirect to login
|
||||||
window.location.href = '/auth/login';
|
window.location.href = '/auth/login';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { toQueryString } from '@/utils/toQueryString';
|
|||||||
import { IHomeTasksModel, IHomeTasksConfig } from '@/types/home/home-page.types';
|
import { IHomeTasksModel, IHomeTasksConfig } from '@/types/home/home-page.types';
|
||||||
import { IMyTask } from '@/types/home/my-tasks.types';
|
import { IMyTask } from '@/types/home/my-tasks.types';
|
||||||
import { IProject } from '@/types/project/project.types';
|
import { IProject } from '@/types/project/project.types';
|
||||||
import { getCsrfToken } from '../api-client';
|
import { getCsrfToken, refreshCsrfToken } from '../api-client';
|
||||||
import config from '@/config/env';
|
import config from '@/config/env';
|
||||||
|
|
||||||
const rootUrl = '/home';
|
const rootUrl = '/home';
|
||||||
@@ -14,9 +14,18 @@ const api = createApi({
|
|||||||
reducerPath: 'homePageApi',
|
reducerPath: 'homePageApi',
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: fetchBaseQuery({
|
||||||
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
|
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
|
||||||
prepareHeaders: headers => {
|
prepareHeaders: async headers => {
|
||||||
headers.set('X-CSRF-Token', getCsrfToken() || '');
|
// Get CSRF token, refresh if needed
|
||||||
|
let token = getCsrfToken();
|
||||||
|
if (!token) {
|
||||||
|
token = await refreshCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.set('X-CSRF-Token', token);
|
||||||
|
}
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
|
return headers;
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IProjectCategory } from '@/types/project/projectCategory.types';
|
|||||||
import { IProjectsViewModel } from '@/types/project/projectsViewModel.types';
|
import { IProjectsViewModel } from '@/types/project/projectsViewModel.types';
|
||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IProjectMembersViewModel } from '@/types/projectMember.types';
|
import { IProjectMembersViewModel } from '@/types/projectMember.types';
|
||||||
import { getCsrfToken } from '../api-client';
|
import { getCsrfToken, refreshCsrfToken } from '../api-client';
|
||||||
import config from '@/config/env';
|
import config from '@/config/env';
|
||||||
|
|
||||||
const rootUrl = '/projects';
|
const rootUrl = '/projects';
|
||||||
@@ -14,9 +14,18 @@ export const projectsApi = createApi({
|
|||||||
reducerPath: 'projectsApi',
|
reducerPath: 'projectsApi',
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: fetchBaseQuery({
|
||||||
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
|
baseUrl: `${config.apiUrl}${API_BASE_URL}`,
|
||||||
prepareHeaders: headers => {
|
prepareHeaders: async headers => {
|
||||||
headers.set('X-CSRF-Token', getCsrfToken() || '');
|
// Get CSRF token, refresh if needed
|
||||||
|
let token = getCsrfToken();
|
||||||
|
if (!token) {
|
||||||
|
token = await refreshCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.set('X-CSRF-Token', token);
|
||||||
|
}
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
|
return headers;
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user