fix: File cleanup
This commit is contained in:
@@ -54,7 +54,6 @@ class AuthManager {
|
||||
this.invalidateUserTokens(userId);
|
||||
});
|
||||
|
||||
// Run session cleanup every 5 minutes
|
||||
setInterval(
|
||||
() => {
|
||||
this.cleanupExpiredSessions().catch((error) => {
|
||||
@@ -162,16 +161,15 @@ class AuthManager {
|
||||
): Promise<string> {
|
||||
const jwtSecret = await this.systemCrypto.getJWTSecret();
|
||||
|
||||
// Determine expiration based on device type
|
||||
let expiresIn = options.expiresIn;
|
||||
if (!expiresIn && !options.pendingTOTP) {
|
||||
if (options.deviceType === "desktop" || options.deviceType === "mobile") {
|
||||
expiresIn = "30d"; // 30 days for desktop and mobile
|
||||
expiresIn = "30d";
|
||||
} else {
|
||||
expiresIn = "7d"; // 7 days for web
|
||||
expiresIn = "7d";
|
||||
}
|
||||
} else if (!expiresIn) {
|
||||
expiresIn = "7d"; // Default
|
||||
expiresIn = "7d";
|
||||
}
|
||||
|
||||
const payload: JWTPayload = { userId };
|
||||
@@ -179,23 +177,19 @@ class AuthManager {
|
||||
payload.pendingTOTP = true;
|
||||
}
|
||||
|
||||
// Create session in database if not a temporary TOTP token
|
||||
if (!options.pendingTOTP && options.deviceType && options.deviceInfo) {
|
||||
const sessionId = nanoid();
|
||||
payload.sessionId = sessionId;
|
||||
|
||||
// Generate the token first to get it for storage
|
||||
const token = jwt.sign(payload, jwtSecret, {
|
||||
expiresIn,
|
||||
} as jwt.SignOptions);
|
||||
|
||||
// Calculate expiration timestamp
|
||||
const expirationMs = this.parseExpiresIn(expiresIn);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + expirationMs).toISOString();
|
||||
const createdAt = now.toISOString();
|
||||
|
||||
// Store session in database
|
||||
try {
|
||||
await db.insert(sessions).values({
|
||||
id: sessionId,
|
||||
@@ -208,27 +202,11 @@ class AuthManager {
|
||||
lastActiveAt: createdAt,
|
||||
});
|
||||
|
||||
databaseLogger.info("Session created", {
|
||||
operation: "session_create",
|
||||
userId,
|
||||
sessionId,
|
||||
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",
|
||||
@@ -245,7 +223,6 @@ class AuthManager {
|
||||
userId,
|
||||
sessionId,
|
||||
});
|
||||
// Continue anyway - session tracking is non-critical
|
||||
}
|
||||
|
||||
return token;
|
||||
@@ -259,7 +236,7 @@ class AuthManager {
|
||||
*/
|
||||
private parseExpiresIn(expiresIn: string): number {
|
||||
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
||||
if (!match) return 7 * 24 * 60 * 60 * 1000; // Default 7 days
|
||||
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
@@ -282,26 +259,8 @@ class AuthManager {
|
||||
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) {
|
||||
try {
|
||||
const sessionRecords = await db
|
||||
@@ -322,13 +281,6 @@ class AuthManager {
|
||||
);
|
||||
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",
|
||||
@@ -338,15 +290,8 @@ class AuthManager {
|
||||
sessionId: payload.sessionId,
|
||||
},
|
||||
);
|
||||
// Continue anyway - database errors shouldn't block valid JWTs
|
||||
}
|
||||
}
|
||||
|
||||
databaseLogger.info("JWT verification successful", {
|
||||
operation: "jwt_verify_success",
|
||||
userId: payload.userId,
|
||||
sessionId: payload.sessionId,
|
||||
});
|
||||
return payload;
|
||||
} catch (error) {
|
||||
databaseLogger.warn("JWT verification failed", {
|
||||
@@ -358,35 +303,14 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
invalidateJWTToken(token: string): void {
|
||||
// No-op: Token invalidation is now handled through database session deletion
|
||||
databaseLogger.info(
|
||||
"Token invalidation requested (handled via session deletion)",
|
||||
{
|
||||
operation: "token_invalidate",
|
||||
},
|
||||
);
|
||||
}
|
||||
invalidateJWTToken(token: string): void {}
|
||||
|
||||
invalidateUserTokens(userId: string): void {
|
||||
databaseLogger.info("User tokens invalidation requested due to data lock", {
|
||||
operation: "user_tokens_invalidate",
|
||||
userId,
|
||||
});
|
||||
// Session cleanup will happen through revokeAllUserSessions if needed
|
||||
}
|
||||
invalidateUserTokens(userId: string): void {}
|
||||
|
||||
async revokeSession(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
// Delete the session from database
|
||||
// The JWT will be invalidated because verifyJWTToken checks for session existence
|
||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||
|
||||
databaseLogger.info("Session deleted", {
|
||||
operation: "session_delete",
|
||||
sessionId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete session", error, {
|
||||
@@ -402,7 +326,6 @@ class AuthManager {
|
||||
exceptSessionId?: string,
|
||||
): Promise<number> {
|
||||
try {
|
||||
// Get session count before deletion
|
||||
const userSessions = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
@@ -412,8 +335,6 @@ class AuthManager {
|
||||
(s) => !exceptSessionId || s.id !== exceptSessionId,
|
||||
).length;
|
||||
|
||||
// Delete sessions from database
|
||||
// JWTs will be invalidated because verifyJWTToken checks for session existence
|
||||
if (exceptSessionId) {
|
||||
await db
|
||||
.delete(sessions)
|
||||
@@ -427,13 +348,6 @@ class AuthManager {
|
||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||
}
|
||||
|
||||
databaseLogger.info("User sessions deleted", {
|
||||
operation: "user_sessions_delete",
|
||||
userId,
|
||||
exceptSessionId,
|
||||
deletedCount,
|
||||
});
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete user sessions", error, {
|
||||
@@ -446,7 +360,6 @@ class AuthManager {
|
||||
|
||||
async cleanupExpiredSessions(): Promise<number> {
|
||||
try {
|
||||
// Get expired sessions count
|
||||
const expiredSessions = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
@@ -454,19 +367,10 @@ class AuthManager {
|
||||
|
||||
const expiredCount = expiredSessions.length;
|
||||
|
||||
// Delete expired sessions
|
||||
// JWTs will be invalidated because verifyJWTToken checks for session existence
|
||||
await db
|
||||
.delete(sessions)
|
||||
.where(sql`${sessions.expiresAt} < datetime('now')`);
|
||||
|
||||
if (expiredCount > 0) {
|
||||
databaseLogger.info("Expired sessions cleaned up", {
|
||||
operation: "sessions_cleanup",
|
||||
count: expiredCount,
|
||||
});
|
||||
}
|
||||
|
||||
return expiredCount;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to cleanup expired sessions", error, {
|
||||
@@ -539,7 +443,6 @@ class AuthManager {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
|
||||
// Check session status if sessionId is present
|
||||
if (payload.sessionId) {
|
||||
try {
|
||||
const sessionRecords = await db
|
||||
@@ -557,9 +460,6 @@ class AuthManager {
|
||||
|
||||
const session = sessionRecords[0];
|
||||
|
||||
// Session exists, no need to check isRevoked since we delete sessions instead
|
||||
|
||||
// Check if session has expired by comparing timestamps
|
||||
const sessionExpiryTime = new Date(session.expiresAt).getTime();
|
||||
const currentTime = Date.now();
|
||||
const isExpired = sessionExpiryTime < currentTime;
|
||||
@@ -579,7 +479,6 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Update lastActiveAt timestamp (async, non-blocking)
|
||||
db.update(sessions)
|
||||
.set({ lastActiveAt: new Date().toISOString() })
|
||||
.where(eq(sessions.id, payload.sessionId))
|
||||
@@ -596,7 +495,6 @@ class AuthManager {
|
||||
operation: "session_check_failed",
|
||||
sessionId: payload.sessionId,
|
||||
});
|
||||
// Continue anyway - session tracking failures shouldn't block auth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,14 +512,8 @@ class AuthManager {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
|
||||
// Try to get data key if available (may be null after restart)
|
||||
const dataKey = this.userCrypto.getUserDataKey(userId);
|
||||
authReq.dataKey = dataKey || undefined;
|
||||
|
||||
// Note: Data key will be null after backend restart until user performs
|
||||
// an operation that requires decryption. This is expected behavior.
|
||||
// Individual routes that need encryption should check dataKey explicitly.
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -688,15 +580,9 @@ class AuthManager {
|
||||
async logoutUser(userId: string, sessionId?: string): Promise<void> {
|
||||
this.userCrypto.logoutUser(userId);
|
||||
|
||||
// Delete the specific session from database if sessionId provided
|
||||
if (sessionId) {
|
||||
try {
|
||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||
databaseLogger.info("Session deleted on logout", {
|
||||
operation: "session_delete_logout",
|
||||
userId,
|
||||
sessionId,
|
||||
});
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete session on logout", error, {
|
||||
operation: "session_delete_logout_failed",
|
||||
@@ -705,13 +591,8 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If no sessionId, delete all sessions for this user
|
||||
try {
|
||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||
databaseLogger.info("All user sessions deleted on logout", {
|
||||
operation: "sessions_delete_logout",
|
||||
userId,
|
||||
});
|
||||
} catch (error) {
|
||||
databaseLogger.error(
|
||||
"Failed to delete user sessions on logout",
|
||||
|
||||
@@ -233,9 +233,7 @@ IP.3 = 0.0.0.0
|
||||
let envContent = "";
|
||||
try {
|
||||
envContent = await fs.readFile(this.ENV_FILE, "utf8");
|
||||
} catch {
|
||||
// File doesn't exist yet, will create with SSL config
|
||||
}
|
||||
} catch {}
|
||||
|
||||
let updatedContent = envContent;
|
||||
let hasChanges = false;
|
||||
|
||||
@@ -393,18 +393,6 @@ class DataCrypto {
|
||||
|
||||
result.success = result.errors.length === 0;
|
||||
|
||||
databaseLogger.info(
|
||||
"User data re-encryption completed after password reset",
|
||||
{
|
||||
operation: "password_reset_reencrypt_completed",
|
||||
userId,
|
||||
success: result.success,
|
||||
reencryptedTables: result.reencryptedTables,
|
||||
reencryptedFieldsCount: result.reencryptedFieldsCount,
|
||||
errorsCount: result.errors.length,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
databaseLogger.error(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Database from "better-sqlite3";
|
||||
/import Database from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
@@ -62,10 +62,6 @@ export class DatabaseMigration {
|
||||
"Empty unencrypted database found alongside encrypted database. Removing empty file.";
|
||||
try {
|
||||
fs.unlinkSync(this.unencryptedDbPath);
|
||||
databaseLogger.info("Removed empty unencrypted database file", {
|
||||
operation: "migration_cleanup_empty",
|
||||
path: this.unencryptedDbPath,
|
||||
});
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Failed to remove empty unencrypted database", {
|
||||
operation: "migration_cleanup_empty_failed",
|
||||
|
||||
@@ -71,11 +71,6 @@ export class DatabaseSaveTrigger {
|
||||
this.pendingSave = true;
|
||||
|
||||
try {
|
||||
databaseLogger.info("Force saving database", {
|
||||
operation: "db_save_trigger_force_start",
|
||||
reason,
|
||||
});
|
||||
|
||||
await this.saveFunction();
|
||||
} catch (error) {
|
||||
databaseLogger.error("Database force save failed", error, {
|
||||
@@ -110,9 +105,5 @@ export class DatabaseSaveTrigger {
|
||||
this.pendingSave = false;
|
||||
this.isInitialized = false;
|
||||
this.saveFunction = null;
|
||||
|
||||
databaseLogger.info("Database save trigger cleaned up", {
|
||||
operation: "db_save_trigger_cleanup",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,7 @@ export class LazyFieldEncryption {
|
||||
legacyFieldName,
|
||||
);
|
||||
return decrypted;
|
||||
} catch {
|
||||
// Ignore legacy format errors
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
@@ -176,9 +174,7 @@ export class LazyFieldEncryption {
|
||||
wasPlaintext: false,
|
||||
wasLegacyEncryption: true,
|
||||
};
|
||||
} catch {
|
||||
// Ignore legacy format errors
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
encrypted: fieldValue,
|
||||
|
||||
@@ -6,7 +6,6 @@ type TableName = "users" | "ssh_data" | "ssh_credentials" | "recent_activity";
|
||||
|
||||
class SimpleDBOps {
|
||||
static async insert<T extends Record<string, unknown>>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
data: T,
|
||||
@@ -91,7 +90,6 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async update<T extends Record<string, unknown>>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
where: unknown,
|
||||
@@ -110,7 +108,6 @@ class SimpleDBOps {
|
||||
const result = await getDb()
|
||||
.update(table)
|
||||
.set(encryptedData)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.where(where as any)
|
||||
.returning();
|
||||
|
||||
@@ -127,14 +124,12 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async delete(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
where: unknown,
|
||||
): Promise<unknown[]> {
|
||||
const result = await getDb()
|
||||
.delete(table)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.where(where as any)
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -84,9 +84,7 @@ function detectKeyTypeFromContent(keyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch {
|
||||
// Cannot decode key, fallback to length-based detection
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (content.length < 800) {
|
||||
return "ssh-ed25519";
|
||||
@@ -142,9 +140,7 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch {
|
||||
// Cannot decode key, fallback to length-based detection
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (content.length < 400) {
|
||||
return "ssh-ed25519";
|
||||
@@ -246,9 +242,7 @@ export function parseSSHKey(
|
||||
|
||||
useSSH2 = true;
|
||||
}
|
||||
} catch {
|
||||
// SSH2 parsing failed, will use fallback method
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!useSSH2) {
|
||||
@@ -274,9 +268,7 @@ export function parseSSHKey(
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fallback parsing also failed
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
privateKey: privateKeyData,
|
||||
|
||||
@@ -107,9 +107,7 @@ class SystemCrypto {
|
||||
process.env.DATABASE_KEY = dbKeyMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new key
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await this.generateAndGuideDatabaseKey();
|
||||
} catch (error) {
|
||||
@@ -146,9 +144,7 @@ class SystemCrypto {
|
||||
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new token
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await this.generateAndGuideInternalAuthToken();
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,59 +7,43 @@ export interface DeviceInfo {
|
||||
browser: string;
|
||||
version: string;
|
||||
os: string;
|
||||
deviceInfo: string; // Formatted string like "Chrome 120 on Windows 11"
|
||||
deviceInfo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the platform type based on request headers
|
||||
*/
|
||||
export function detectPlatform(req: Request): DeviceType {
|
||||
const userAgent = req.headers["user-agent"] || "";
|
||||
const electronHeader = req.headers["x-electron-app"];
|
||||
|
||||
// Electron app detection
|
||||
if (electronHeader === "true") {
|
||||
return "desktop";
|
||||
}
|
||||
|
||||
// Mobile app detection
|
||||
if (userAgent.includes("Termix-Mobile")) {
|
||||
return "mobile";
|
||||
}
|
||||
|
||||
// Default to web
|
||||
return "web";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse User-Agent string to extract device information
|
||||
*/
|
||||
export function parseUserAgent(req: Request): DeviceInfo {
|
||||
const userAgent = req.headers["user-agent"] || "Unknown";
|
||||
const platform = detectPlatform(req);
|
||||
|
||||
// For Electron
|
||||
if (platform === "desktop") {
|
||||
return parseElectronUserAgent(userAgent);
|
||||
}
|
||||
|
||||
// For Mobile app
|
||||
if (platform === "mobile") {
|
||||
return parseMobileUserAgent(userAgent);
|
||||
}
|
||||
|
||||
// For web browsers
|
||||
return parseWebUserAgent(userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Electron app user agent
|
||||
*/
|
||||
function parseElectronUserAgent(userAgent: string): DeviceInfo {
|
||||
let os = "Unknown OS";
|
||||
let version = "Unknown";
|
||||
|
||||
// Detect OS
|
||||
if (userAgent.includes("Windows")) {
|
||||
os = parseWindowsVersion(userAgent);
|
||||
} else if (userAgent.includes("Mac OS X")) {
|
||||
@@ -68,7 +52,6 @@ function parseElectronUserAgent(userAgent: string): DeviceInfo {
|
||||
os = "Linux";
|
||||
}
|
||||
|
||||
// Try to extract Electron version
|
||||
const electronMatch = userAgent.match(/Electron\/([\d.]+)/);
|
||||
if (electronMatch) {
|
||||
version = electronMatch[1];
|
||||
@@ -83,23 +66,17 @@ function parseElectronUserAgent(userAgent: string): DeviceInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse mobile app user agent
|
||||
*/
|
||||
function parseMobileUserAgent(userAgent: string): DeviceInfo {
|
||||
let os = "Unknown OS";
|
||||
let version = "Unknown";
|
||||
|
||||
// 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, ".");
|
||||
@@ -109,7 +86,6 @@ function parseMobileUserAgent(userAgent: string): DeviceInfo {
|
||||
}
|
||||
}
|
||||
} 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";
|
||||
@@ -128,8 +104,6 @@ function parseMobileUserAgent(userAgent: string): DeviceInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract app version (if included in UA)
|
||||
// Match patterns like "Termix-Mobile/1.0.0" or just "Termix-Mobile"
|
||||
const versionMatch = userAgent.match(
|
||||
/Termix-Mobile\/(?:Android|iOS|)([\d.]+)/i,
|
||||
);
|
||||
@@ -146,15 +120,11 @@ function parseMobileUserAgent(userAgent: string): DeviceInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse web browser user agent
|
||||
*/
|
||||
function parseWebUserAgent(userAgent: string): DeviceInfo {
|
||||
let browser = "Unknown Browser";
|
||||
let version = "Unknown";
|
||||
let os = "Unknown OS";
|
||||
|
||||
// Detect browser
|
||||
if (userAgent.includes("Edg/")) {
|
||||
const match = userAgent.match(/Edg\/([\d.]+)/);
|
||||
browser = "Edge";
|
||||
@@ -177,7 +147,6 @@ function parseWebUserAgent(userAgent: string): DeviceInfo {
|
||||
version = match ? match[1] : "Unknown";
|
||||
}
|
||||
|
||||
// Detect OS
|
||||
if (userAgent.includes("Windows")) {
|
||||
os = parseWindowsVersion(userAgent);
|
||||
} else if (userAgent.includes("Mac OS X")) {
|
||||
@@ -201,7 +170,6 @@ function parseWebUserAgent(userAgent: string): DeviceInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Shorten version to major.minor
|
||||
if (version !== "Unknown") {
|
||||
const versionParts = version.split(".");
|
||||
version = versionParts.slice(0, 2).join(".");
|
||||
@@ -216,9 +184,6 @@ function parseWebUserAgent(userAgent: string): DeviceInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Windows version from user agent
|
||||
*/
|
||||
function parseWindowsVersion(userAgent: string): string {
|
||||
if (userAgent.includes("Windows NT 10.0")) {
|
||||
return "Windows 10/11";
|
||||
@@ -239,9 +204,6 @@ function parseWindowsVersion(userAgent: string): string {
|
||||
return "Windows";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse macOS version from user agent
|
||||
*/
|
||||
function parseMacVersion(userAgent: string): string {
|
||||
const match = userAgent.match(/Mac OS X ([\d_]+)/);
|
||||
if (match) {
|
||||
@@ -250,7 +212,6 @@ function parseMacVersion(userAgent: string): string {
|
||||
const major = parseInt(parts[0]);
|
||||
const minor = parseInt(parts[1]);
|
||||
|
||||
// macOS naming
|
||||
if (major === 10) {
|
||||
if (minor >= 15) return `macOS ${major}.${minor}`;
|
||||
if (minor === 14) return "macOS Mojave";
|
||||
|
||||
Reference in New Issue
Block a user