diff --git a/package.json b/package.json index fd966458..48d61678 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "ioredis": "^5.4.1", "joi": "^17.6.0", "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", "maxun-core": "^0.0.3", @@ -46,7 +47,7 @@ "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", "pg": "^8.13.0", - "playwright": "^1.20.1", + "playwright": "^1.48.2", "playwright-extra": "^4.3.6", "posthog-node": "^4.2.1", "prismjs": "^1.28.0", diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 4f99043d..0cae9f59 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -341,12 +341,14 @@ router.get('/google/callback', requireSignIn, async (req: AuthenticatedRequest, const jwtToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' }); res.cookie('token', jwtToken, { httpOnly: true }); - res.json({ - message: 'Google authentication successful', - google_sheet_email: robot.google_sheet_email, - jwtToken, - files - }); + // res.json({ + // message: 'Google authentication successful', + // google_sheet_email: robot.google_sheet_email, + // jwtToken, + // files + // }); + + res.redirect(`http://localhost:5173`); } catch (error: any) { res.status(500).json({ message: `Google OAuth error: ${error.message}` }); } diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index f8313df7..ddab4bd4 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -6,29 +6,37 @@ export const hashPassword = (password: string): Promise => { return new Promise((resolve, reject) => { bcrypt.genSalt(12, (err, salt) => { if (err) { - reject(err) + reject(err); } bcrypt.hash(password, salt, (err, hash) => { if (err) { - reject(err) + reject(err); } - resolve(hash) - }) - }) - }) -} + resolve(hash); + }); + }); + }); +}; // password from frontend and hash from database export const comparePassword = (password: string, hash: string): Promise => { - return bcrypt.compare(password, hash) -} + return bcrypt.compare(password, hash); +}; export const encrypt = (text: string): string => { const ivLength = 16; const iv = crypto.randomBytes(ivLength); const algorithm = 'aes-256-cbc'; - const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex'); - const cipher = crypto.createCipheriv(algorithm, key, iv); + + // Retrieve the encryption key or generate a new one if invalid or empty + let key = getEnvVariable('ENCRYPTION_KEY'); + if (!key || key.length !== 64) { // aes-256-cbc requires a 256-bit key, which is 64 hex characters + console.warn('Invalid or missing ENCRYPTION_KEY, generating a new one.'); + key = crypto.randomBytes(32).toString('hex'); // Generate a new 256-bit (32-byte) key + } + const keyBuffer = Buffer.from(key, 'hex'); + + const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return `${iv.toString('hex')}:${encrypted}`; @@ -37,9 +45,17 @@ export const encrypt = (text: string): string => { export const decrypt = (encryptedText: string): string => { const [iv, encrypted] = encryptedText.split(':'); const algorithm = "aes-256-cbc"; - const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex'); - const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex')); + + // Retrieve the encryption key or generate a new one if invalid or empty + let key = getEnvVariable('ENCRYPTION_KEY'); + if (!key || key.length !== 64) { // aes-256-cbc requires a 256-bit key, which is 64 hex characters + console.warn('Invalid or missing ENCRYPTION_KEY, generating a new one.'); + key = crypto.randomBytes(32).toString('hex'); // Generate a new 256-bit (32-byte) key + } + const keyBuffer = Buffer.from(key, 'hex'); + + const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(iv, 'hex')); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; -}; \ No newline at end of file +}; diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 1b99686a..10b1039c 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -1,6 +1,7 @@ import { useReducer, createContext, useEffect } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; +import { jwtDecode } from "jwt-decode"; import { apiUrl } from "../apiConfig"; interface AuthProviderProps { @@ -50,10 +51,33 @@ const AuthProvider = ({ children }: AuthProviderProps) => { const navigate = useNavigate(); axios.defaults.withCredentials = true; + // Function to handle logout + const logoutUser = () => { + dispatch({ type: 'LOGOUT' }); + window.localStorage.removeItem('user'); + navigate('/login'); + }; + + // Function to check token expiration + const checkTokenExpiration = (token: string) => { + const decodedToken: any = jwtDecode(token); + const currentTime = Date.now(); + const tokenExpiryTime = decodedToken.exp * 1000; // Convert to milliseconds + const timeUntilExpiry = tokenExpiryTime - currentTime; + + if (timeUntilExpiry > 0) { + setTimeout(logoutUser, timeUntilExpiry); // Auto-logout when token expires + } else { + logoutUser(); // Immediately logout if token is expired + } + }; + useEffect(() => { const storedUser = window.localStorage.getItem('user'); if (storedUser) { - dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) }); + const userData = JSON.parse(storedUser); + dispatch({ type: 'LOGIN', payload: userData }); + checkTokenExpiration(userData.token); // Check if token is still valid } }, []); @@ -69,9 +93,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => { .get(`${apiUrl}/auth/logout`) .then(() => { console.log('/401 error > logout'); - dispatch({ type: 'LOGOUT' }); - window.localStorage.removeItem('user'); - navigate('/login'); + logoutUser(); }) .catch((err) => { console.error('AXIOS INTERCEPTORS ERROR:', err);