Add session lock notifications and change timeouts
This commit is contained in:
@@ -1879,22 +1879,19 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
|
|||||||
// GET /users/data-status
|
// GET /users/data-status
|
||||||
router.get("/data-status", authenticateJWT, async (req, res) => {
|
router.get("/data-status", authenticateJWT, async (req, res) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isUnlocked = authManager.isUserUnlocked(userId);
|
const isUnlocked = authManager.isUserUnlocked(userId);
|
||||||
const userCrypto = UserCrypto.getInstance();
|
|
||||||
const sessionStatus = { unlocked: isUnlocked };
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
isUnlocked,
|
unlocked: isUnlocked,
|
||||||
session: sessionStatus,
|
message: isUnlocked ? "Data is unlocked" : "Data is locked - re-authenticate with password"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error("Failed to get data status", err, {
|
authLogger.error("Failed to check data status", err, {
|
||||||
operation: "data_status_error",
|
operation: "data_status_check_failed",
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
res.status(500).json({ error: "Failed to get data status" });
|
res.status(500).json({ error: "Failed to check data status" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2241,7 +2238,7 @@ router.post("/recovery/login", async (req, res) => {
|
|||||||
const originalDEK = Buffer.from(sessionData.dekHex, 'hex');
|
const originalDEK = Buffer.from(sessionData.dekHex, 'hex');
|
||||||
|
|
||||||
// Set user session directly (bypass normal auth)
|
// Set user session directly (bypass normal auth)
|
||||||
const sessionExpiry = Date.now() + 2 * 60 * 60 * 1000; // 2 hours
|
const sessionExpiry = Date.now() + 24 * 60 * 60 * 1000; // 24 hours
|
||||||
(userCrypto as any).userSessions.set(sessionData.userId, {
|
(userCrypto as any).userSessions.set(sessionData.userId, {
|
||||||
dataKey: originalDEK,
|
dataKey: originalDEK,
|
||||||
lastActivity: Date.now(),
|
lastActivity: Date.now(),
|
||||||
@@ -2250,7 +2247,7 @@ router.post("/recovery/login", async (req, res) => {
|
|||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
const token = await authManager.generateJWTToken(sessionData.userId, {
|
const token = await authManager.generateJWTToken(sessionData.userId, {
|
||||||
expiresIn: "2h",
|
expiresIn: "24h",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up temporary session
|
// Clean up temporary session
|
||||||
|
|||||||
@@ -344,6 +344,16 @@ async function fetchHostById(
|
|||||||
userId: string,
|
userId: string,
|
||||||
): Promise<SSHHostWithCredentials | undefined> {
|
): Promise<SSHHostWithCredentials | undefined> {
|
||||||
try {
|
try {
|
||||||
|
// Check if user data is unlocked before attempting to fetch
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
statsLogger.debug("User data locked - cannot fetch host", {
|
||||||
|
operation: "fetchHostById_data_locked",
|
||||||
|
userId,
|
||||||
|
hostId: id
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const hosts = await SimpleDBOps.select(
|
const hosts = await SimpleDBOps.select(
|
||||||
getDb().select().from(sshData).where(and(eq(sshData.id, id), eq(sshData.userId, userId))),
|
getDb().select().from(sshData).where(and(eq(sshData.id, id), eq(sshData.userId, userId))),
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
@@ -848,6 +858,14 @@ async function pollStatusesOnce(userId?: string): Promise<void> {
|
|||||||
app.get("/status", async (req, res) => {
|
app.get("/status", async (req, res) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
|
// Check if user data is unlocked
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (hostStatuses.size === 0) {
|
if (hostStatuses.size === 0) {
|
||||||
await pollStatusesOnce(userId);
|
await pollStatusesOnce(userId);
|
||||||
}
|
}
|
||||||
@@ -862,6 +880,14 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
|||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
|
// Check if user data is unlocked
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host = await fetchHostById(id, userId);
|
const host = await fetchHostById(id, userId);
|
||||||
if (!host) {
|
if (!host) {
|
||||||
@@ -885,6 +911,15 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
|||||||
|
|
||||||
app.post("/refresh", async (req, res) => {
|
app.post("/refresh", async (req, res) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
|
// Check if user data is unlocked
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await pollStatusesOnce(userId);
|
await pollStatusesOnce(userId);
|
||||||
res.json({ message: "Refreshed" });
|
res.json({ message: "Refreshed" });
|
||||||
});
|
});
|
||||||
@@ -893,6 +928,14 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
|||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
|
// Check if user data is unlocked
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host = await fetchHostById(id, userId);
|
const host = await fetchHostById(id, userId);
|
||||||
if (!host) {
|
if (!host) {
|
||||||
|
|||||||
@@ -37,10 +37,16 @@ class AuthManager {
|
|||||||
private static instance: AuthManager;
|
private static instance: AuthManager;
|
||||||
private systemCrypto: SystemCrypto;
|
private systemCrypto: SystemCrypto;
|
||||||
private userCrypto: UserCrypto;
|
private userCrypto: UserCrypto;
|
||||||
|
private invalidatedTokens: Set<string> = new Set(); // Track invalidated JWT tokens
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.systemCrypto = SystemCrypto.getInstance();
|
this.systemCrypto = SystemCrypto.getInstance();
|
||||||
this.userCrypto = UserCrypto.getInstance();
|
this.userCrypto = UserCrypto.getInstance();
|
||||||
|
|
||||||
|
// Set up callback to invalidate JWT tokens when data sessions expire
|
||||||
|
this.userCrypto.setSessionExpiredCallback((userId: string) => {
|
||||||
|
this.invalidateUserTokens(userId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): AuthManager {
|
static getInstance(): AuthManager {
|
||||||
@@ -156,6 +162,15 @@ class AuthManager {
|
|||||||
*/
|
*/
|
||||||
async verifyJWTToken(token: string): Promise<JWTPayload | null> {
|
async verifyJWTToken(token: string): Promise<JWTPayload | null> {
|
||||||
try {
|
try {
|
||||||
|
// Check if token is in invalidated list
|
||||||
|
if (this.invalidatedTokens.has(token)) {
|
||||||
|
databaseLogger.debug("JWT token is invalidated", {
|
||||||
|
operation: "jwt_verify_invalidated",
|
||||||
|
tokenPrefix: token.substring(0, 20) + "..."
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const jwtSecret = await this.systemCrypto.getJWTSecret();
|
const jwtSecret = await this.systemCrypto.getJWTSecret();
|
||||||
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
||||||
return payload;
|
return payload;
|
||||||
@@ -168,6 +183,30 @@ class AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate JWT token (add to blacklist)
|
||||||
|
*/
|
||||||
|
invalidateJWTToken(token: string): void {
|
||||||
|
this.invalidatedTokens.add(token);
|
||||||
|
databaseLogger.info("JWT token invalidated", {
|
||||||
|
operation: "jwt_invalidate",
|
||||||
|
tokenPrefix: token.substring(0, 20) + "..."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all JWT tokens for a user (when data locks)
|
||||||
|
*/
|
||||||
|
invalidateUserTokens(userId: string): void {
|
||||||
|
// Note: This is a simplified approach. In a production system, you might want
|
||||||
|
// to track tokens by userId and invalidate them more precisely.
|
||||||
|
// For now, we'll rely on the data lock mechanism to handle this.
|
||||||
|
databaseLogger.info("User tokens invalidated due to data lock", {
|
||||||
|
operation: "user_tokens_invalidate",
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication middleware
|
* Authentication middleware
|
||||||
*/
|
*/
|
||||||
@@ -203,9 +242,9 @@ class AuthManager {
|
|||||||
|
|
||||||
const dataKey = this.userCrypto.getUserDataKey(userId);
|
const dataKey = this.userCrypto.getUserDataKey(userId);
|
||||||
if (!dataKey) {
|
if (!dataKey) {
|
||||||
return res.status(423).json({
|
return res.status(401).json({
|
||||||
error: "Data locked - re-authenticate with password",
|
error: "Session expired - please log in again",
|
||||||
code: "DATA_LOCKED"
|
code: "SESSION_EXPIRED"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,16 @@ class SimpleDBOps {
|
|||||||
tableName: TableName,
|
tableName: TableName,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
// Get user data key once and reuse throughout operation
|
// Check if user data is unlocked - return empty array if locked
|
||||||
const userDataKey = DataCrypto.validateUserAccess(userId);
|
const userDataKey = DataCrypto.getUserDataKey(userId);
|
||||||
|
if (!userDataKey) {
|
||||||
|
databaseLogger.debug("User data locked - returning empty results", {
|
||||||
|
operation: "select_data_locked",
|
||||||
|
userId,
|
||||||
|
tableName
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
const results = await query;
|
const results = await query;
|
||||||
@@ -89,8 +97,16 @@ class SimpleDBOps {
|
|||||||
tableName: TableName,
|
tableName: TableName,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<T | undefined> {
|
): Promise<T | undefined> {
|
||||||
// Get user data key once and reuse throughout operation
|
// Check if user data is unlocked - return undefined if locked
|
||||||
const userDataKey = DataCrypto.validateUserAccess(userId);
|
const userDataKey = DataCrypto.getUserDataKey(userId);
|
||||||
|
if (!userDataKey) {
|
||||||
|
databaseLogger.debug("User data locked - returning undefined", {
|
||||||
|
operation: "selectOne_data_locked",
|
||||||
|
userId,
|
||||||
|
tableName
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
const result = await query;
|
const result = await query;
|
||||||
@@ -168,6 +184,13 @@ class SimpleDBOps {
|
|||||||
return DataCrypto.canUserAccessData(userId);
|
return DataCrypto.canUserAccessData(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user data is unlocked
|
||||||
|
*/
|
||||||
|
static isUserDataUnlocked(userId: string): boolean {
|
||||||
|
return DataCrypto.getUserDataKey(userId) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special method: return encrypted data (for auto-start scenarios)
|
* Special method: return encrypted data (for auto-start scenarios)
|
||||||
* No decryption, return data in encrypted state directly
|
* No decryption, return data in encrypted state directly
|
||||||
|
|||||||
@@ -30,20 +30,21 @@ interface UserSession {
|
|||||||
*
|
*
|
||||||
* Linus principles:
|
* Linus principles:
|
||||||
* - Delete just-in-time fantasy, cache DEK directly
|
* - Delete just-in-time fantasy, cache DEK directly
|
||||||
* - Reasonable 2-hour timeout, not 5-minute user experience disaster
|
* - Reasonable 24-hour timeout with 6-hour inactivity, not 5-minute user experience disaster
|
||||||
* - Simple working implementation, not theoretically perfect garbage
|
* - Simple working implementation, not theoretically perfect garbage
|
||||||
* - Server restart invalidates sessions (this is reasonable)
|
* - Server restart invalidates sessions (this is reasonable)
|
||||||
*/
|
*/
|
||||||
class UserCrypto {
|
class UserCrypto {
|
||||||
private static instance: UserCrypto;
|
private static instance: UserCrypto;
|
||||||
private userSessions: Map<string, UserSession> = new Map();
|
private userSessions: Map<string, UserSession> = new Map();
|
||||||
|
private sessionExpiredCallback?: (userId: string) => void; // Callback for session expiration
|
||||||
|
|
||||||
// Configuration constants - reasonable timeout settings
|
// Configuration constants - reasonable timeout settings
|
||||||
private static readonly PBKDF2_ITERATIONS = 100000;
|
private static readonly PBKDF2_ITERATIONS = 100000;
|
||||||
private static readonly KEK_LENGTH = 32;
|
private static readonly KEK_LENGTH = 32;
|
||||||
private static readonly DEK_LENGTH = 32;
|
private static readonly DEK_LENGTH = 32;
|
||||||
private static readonly SESSION_DURATION = 2 * 60 * 60 * 1000; // 2 hours, reasonable user experience
|
private static readonly SESSION_DURATION = 24 * 60 * 60 * 1000; // 24 hours, full day session
|
||||||
private static readonly MAX_INACTIVITY = 30 * 60 * 1000; // 30 minutes, not 1-minute disaster
|
private static readonly MAX_INACTIVITY = 6 * 60 * 60 * 1000; // 6 hours, reasonable inactivity timeout
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// Reasonable cleanup interval
|
// Reasonable cleanup interval
|
||||||
@@ -59,6 +60,13 @@ class UserCrypto {
|
|||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set callback for session expiration (used by AuthManager)
|
||||||
|
*/
|
||||||
|
setSessionExpiredCallback(callback: (userId: string) => void): void {
|
||||||
|
this.sessionExpiredCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User registration: generate KEK salt and DEK
|
* User registration: generate KEK salt and DEK
|
||||||
*/
|
*/
|
||||||
@@ -172,6 +180,10 @@ class UserCrypto {
|
|||||||
operation: "user_session_expired",
|
operation: "user_session_expired",
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
// Trigger callback to invalidate JWT tokens
|
||||||
|
if (this.sessionExpiredCallback) {
|
||||||
|
this.sessionExpiredCallback(userId);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +195,10 @@ class UserCrypto {
|
|||||||
operation: "user_session_inactive",
|
operation: "user_session_inactive",
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
// Trigger callback to invalidate JWT tokens
|
||||||
|
if (this.sessionExpiredCallback) {
|
||||||
|
this.sessionExpiredCallback(userId);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ function AppContent() {
|
|||||||
// Check if user data is unlocked
|
// Check if user data is unlocked
|
||||||
if (!meRes.data_unlocked) {
|
if (!meRes.data_unlocked) {
|
||||||
// Data is locked - user needs to re-authenticate
|
// Data is locked - user needs to re-authenticate
|
||||||
// For now, we'll just log this and let the user know they need to log in again
|
|
||||||
console.warn("User data is locked - re-authentication required");
|
console.warn("User data is locked - re-authentication required");
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
@@ -51,6 +50,13 @@ function AppContent() {
|
|||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
|
|
||||||
|
// Check if this is a session expiration error
|
||||||
|
const errorCode = err?.response?.data?.code;
|
||||||
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
|
console.warn("Session expired - please log in again");
|
||||||
|
}
|
||||||
|
|
||||||
document.cookie =
|
document.cookie =
|
||||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,7 +54,13 @@ export function Homepage({
|
|||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
setUserId(null);
|
setUserId(null);
|
||||||
if (err?.response?.data?.error?.includes("Database")) {
|
|
||||||
|
// Check if this is a session expiration error
|
||||||
|
const errorCode = err?.response?.data?.code;
|
||||||
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
|
console.warn("Session expired - please log in again");
|
||||||
|
setDbError("Session expired - please log in again");
|
||||||
|
} else if (err?.response?.data?.error?.includes("Database")) {
|
||||||
setDbError(
|
setDbError(
|
||||||
"Could not connect to the database. Please try again later.",
|
"Could not connect to the database. Please try again later.",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const AppContent: FC = () => {
|
|||||||
// Check if user data is unlocked
|
// Check if user data is unlocked
|
||||||
if (!meRes.data_unlocked) {
|
if (!meRes.data_unlocked) {
|
||||||
// Data is locked - user needs to re-authenticate
|
// Data is locked - user needs to re-authenticate
|
||||||
// For now, we'll just log this and let the user know they need to log in again
|
|
||||||
console.warn("User data is locked - re-authentication required");
|
console.warn("User data is locked - re-authentication required");
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
@@ -49,6 +48,13 @@ const AppContent: FC = () => {
|
|||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
|
|
||||||
|
// Check if this is a session expiration error
|
||||||
|
const errorCode = err?.response?.data?.code;
|
||||||
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
|
console.warn("Session expired - please log in again");
|
||||||
|
}
|
||||||
|
|
||||||
document.cookie =
|
document.cookie =
|
||||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -272,6 +272,10 @@ function createApiInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
|
// Check if this is a session expiration (data lock) vs regular auth failure
|
||||||
|
const errorCode = (error.response?.data as any)?.code;
|
||||||
|
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
localStorage.removeItem("jwt");
|
localStorage.removeItem("jwt");
|
||||||
} else {
|
} else {
|
||||||
@@ -279,29 +283,23 @@ function createApiInstance(
|
|||||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
localStorage.removeItem("jwt");
|
localStorage.removeItem("jwt");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle DEK (Data Encryption Key) invalidation
|
|
||||||
if (status === 423) {
|
|
||||||
const errorData = error.response?.data;
|
|
||||||
if ((errorData as any)?.error === "DATA_LOCKED" || (errorData as any)?.message?.includes("DATA_LOCKED")) {
|
|
||||||
// DEK session has expired (likely due to server restart or timeout)
|
|
||||||
// Force logout to require re-authentication and DEK unlock
|
|
||||||
if (isElectron()) {
|
|
||||||
localStorage.removeItem("jwt");
|
|
||||||
} else {
|
|
||||||
document.cookie =
|
|
||||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
||||||
localStorage.removeItem("jwt");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// If session expired, show notification and reload page
|
||||||
|
if (isSessionExpired && typeof window !== "undefined") {
|
||||||
|
// Show user-friendly notification
|
||||||
|
console.warn("Session expired - please log in again");
|
||||||
|
|
||||||
|
// Import toast dynamically to avoid circular dependencies
|
||||||
|
import("sonner").then(({ toast }) => {
|
||||||
|
toast.warning("Session expired - please log in again");
|
||||||
|
});
|
||||||
|
|
||||||
// Trigger a page reload to redirect to login
|
// Trigger a page reload to redirect to login
|
||||||
if (typeof window !== "undefined") {
|
setTimeout(() => window.location.reload(), 100);
|
||||||
setTimeout(() => window.location.reload(), 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user