feat: remove sessions after reboot
This commit is contained in:
@@ -191,7 +191,9 @@ class AuthManager {
|
||||
|
||||
// Calculate expiration timestamp
|
||||
const expirationMs = this.parseExpiresIn(expiresIn);
|
||||
const expiresAt = new Date(Date.now() + expirationMs).toISOString();
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + expirationMs).toISOString();
|
||||
const createdAt = now.toISOString();
|
||||
|
||||
// Store session in database
|
||||
try {
|
||||
@@ -201,7 +203,9 @@ class AuthManager {
|
||||
jwtToken: token,
|
||||
deviceType: options.deviceType,
|
||||
deviceInfo: options.deviceInfo,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
lastActiveAt: createdAt,
|
||||
});
|
||||
|
||||
databaseLogger.info("Session created", {
|
||||
@@ -211,6 +215,30 @@ class AuthManager {
|
||||
deviceType: options.deviceType,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
// Immediately save database to disk to ensure session persists across restarts
|
||||
try {
|
||||
const { saveMemoryDatabaseToFile } = await import(
|
||||
"../database/db/index.js"
|
||||
);
|
||||
await saveMemoryDatabaseToFile();
|
||||
databaseLogger.info(
|
||||
"Database saved immediately after session creation",
|
||||
{
|
||||
operation: "session_create_db_save",
|
||||
sessionId,
|
||||
},
|
||||
);
|
||||
} catch (saveError) {
|
||||
databaseLogger.error(
|
||||
"Failed to save database after session creation",
|
||||
saveError,
|
||||
{
|
||||
operation: "session_create_db_save_failed",
|
||||
sessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to create session", error, {
|
||||
operation: "session_create_failed",
|
||||
@@ -253,8 +281,25 @@ class AuthManager {
|
||||
async verifyJWTToken(token: string): Promise<JWTPayload | null> {
|
||||
try {
|
||||
const jwtSecret = await this.systemCrypto.getJWTSecret();
|
||||
|
||||
databaseLogger.info("Attempting JWT verification", {
|
||||
operation: "jwt_verify_attempt",
|
||||
tokenLength: token.length,
|
||||
secretLength: jwtSecret.length,
|
||||
});
|
||||
|
||||
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
||||
|
||||
databaseLogger.info("JWT signature verified successfully", {
|
||||
operation: "jwt_signature_verified",
|
||||
userId: payload.userId,
|
||||
sessionId: payload.sessionId,
|
||||
hasExpiration: !!payload.exp,
|
||||
expiresAt: payload.exp
|
||||
? new Date(payload.exp * 1000).toISOString()
|
||||
: "N/A",
|
||||
});
|
||||
|
||||
// For tokens with sessionId, verify the session exists in database
|
||||
// This ensures revoked sessions are rejected even after backend restart
|
||||
if (payload.sessionId) {
|
||||
@@ -272,10 +317,18 @@ class AuthManager {
|
||||
operation: "jwt_verify_failed",
|
||||
reason: "session_not_found",
|
||||
sessionId: payload.sessionId,
|
||||
userId: payload.userId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
databaseLogger.info("Session found in database", {
|
||||
operation: "jwt_session_found",
|
||||
sessionId: payload.sessionId,
|
||||
userId: payload.userId,
|
||||
sessionExpiresAt: sessionRecords[0].expiresAt,
|
||||
});
|
||||
} catch (dbError) {
|
||||
databaseLogger.error(
|
||||
"Failed to check session in database during JWT verification",
|
||||
|
||||
@@ -23,24 +23,55 @@ class SystemCrypto {
|
||||
const envSecret = process.env.JWT_SECRET;
|
||||
if (envSecret && envSecret.length >= 64) {
|
||||
this.jwtSecret = envSecret;
|
||||
databaseLogger.info("JWT secret loaded from environment variable", {
|
||||
operation: "jwt_init_from_env",
|
||||
secretLength: envSecret.length,
|
||||
secretPrefix: envSecret.substring(0, 8) + "...",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||
const envPath = path.join(dataDir, ".env");
|
||||
|
||||
databaseLogger.info("Attempting to load JWT secret from .env file", {
|
||||
operation: "jwt_init_from_file",
|
||||
envPath,
|
||||
});
|
||||
|
||||
try {
|
||||
const envContent = await fs.readFile(envPath, "utf8");
|
||||
const jwtMatch = envContent.match(/^JWT_SECRET=(.+)$/m);
|
||||
if (jwtMatch && jwtMatch[1] && jwtMatch[1].length >= 64) {
|
||||
this.jwtSecret = jwtMatch[1];
|
||||
process.env.JWT_SECRET = jwtMatch[1];
|
||||
databaseLogger.success("JWT secret loaded from .env file", {
|
||||
operation: "jwt_init_from_file_success",
|
||||
secretLength: jwtMatch[1].length,
|
||||
secretPrefix: jwtMatch[1].substring(0, 8) + "...",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
databaseLogger.warn(
|
||||
"JWT_SECRET in .env file is invalid or too short",
|
||||
{
|
||||
operation: "jwt_init_invalid_secret",
|
||||
hasMatch: !!jwtMatch,
|
||||
secretLength: jwtMatch?.[1]?.length || 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new secret
|
||||
} catch (fileError) {
|
||||
databaseLogger.warn("Failed to read .env file for JWT secret", {
|
||||
operation: "jwt_init_file_read_failed",
|
||||
error:
|
||||
fileError instanceof Error ? fileError.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
|
||||
databaseLogger.warn("Generating new JWT secret", {
|
||||
operation: "jwt_generating_new_secret",
|
||||
});
|
||||
await this.generateAndGuideUser();
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to initialize JWT secret", error, {
|
||||
|
||||
@@ -90,26 +90,49 @@ function parseMobileUserAgent(userAgent: string): DeviceInfo {
|
||||
let os = "Unknown OS";
|
||||
let version = "Unknown";
|
||||
|
||||
// Detect mobile OS
|
||||
if (userAgent.includes("Android")) {
|
||||
const androidMatch = userAgent.match(/Android ([\d.]+)/);
|
||||
os = androidMatch ? `Android ${androidMatch[1]}` : "Android";
|
||||
} else if (
|
||||
userAgent.includes("iOS") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("iPad")
|
||||
) {
|
||||
const iosMatch = userAgent.match(/OS ([\d_]+)/);
|
||||
if (iosMatch) {
|
||||
const iosVersion = iosMatch[1].replace(/_/g, ".");
|
||||
os = `iOS ${iosVersion}`;
|
||||
} else {
|
||||
os = "iOS";
|
||||
// Check for Termix-Mobile/Platform format first (e.g., "Termix-Mobile/Android" or "Termix-Mobile/iOS")
|
||||
const termixPlatformMatch = userAgent.match(/Termix-Mobile\/(Android|iOS)/i);
|
||||
if (termixPlatformMatch) {
|
||||
const platform = termixPlatformMatch[1];
|
||||
if (platform.toLowerCase() === "android") {
|
||||
// Try to get Android version from full UA string
|
||||
const androidMatch = userAgent.match(/Android ([\d.]+)/);
|
||||
os = androidMatch ? `Android ${androidMatch[1]}` : "Android";
|
||||
} else if (platform.toLowerCase() === "ios") {
|
||||
// Try to get iOS version from full UA string
|
||||
const iosMatch = userAgent.match(/OS ([\d_]+)/);
|
||||
if (iosMatch) {
|
||||
const iosVersion = iosMatch[1].replace(/_/g, ".");
|
||||
os = `iOS ${iosVersion}`;
|
||||
} else {
|
||||
os = "iOS";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: Check for standard Android/iOS patterns in the user agent
|
||||
if (userAgent.includes("Android")) {
|
||||
const androidMatch = userAgent.match(/Android ([\d.]+)/);
|
||||
os = androidMatch ? `Android ${androidMatch[1]}` : "Android";
|
||||
} else if (
|
||||
userAgent.includes("iOS") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("iPad")
|
||||
) {
|
||||
const iosMatch = userAgent.match(/OS ([\d_]+)/);
|
||||
if (iosMatch) {
|
||||
const iosVersion = iosMatch[1].replace(/_/g, ".");
|
||||
os = `iOS ${iosVersion}`;
|
||||
} else {
|
||||
os = "iOS";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract app version (if included in UA)
|
||||
const versionMatch = userAgent.match(/Termix-Mobile\/([\d.]+)/);
|
||||
// Match patterns like "Termix-Mobile/1.0.0" or just "Termix-Mobile"
|
||||
const versionMatch = userAgent.match(
|
||||
/Termix-Mobile\/(?:Android|iOS|)([\d.]+)/i,
|
||||
);
|
||||
if (versionMatch) {
|
||||
version = versionMatch[1];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user