coffeeinabit 0.0.45 → 0.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/helpers/auto_start.js +165 -0
- package/helpers/token_sync.js +77 -0
- package/helpers/version_check.js +172 -0
- package/middleware/auth.js +38 -0
- package/package.json +1 -1
- package/routes/auth.js +110 -0
- package/routes/automation.js +199 -0
- package/server.js +56 -157
- package/socket/handlers.js +48 -0
- package/state/app_state.js +214 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Auto-Start Helper
|
|
6
|
+
*
|
|
7
|
+
* Handles automatic startup of LinkedIn automation when the server starts
|
|
8
|
+
* if there are existing authenticated sessions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Attempt to automatically start automation if user is authenticated
|
|
13
|
+
* Note: Auto-start uses legacy single instance for backward compatibility.
|
|
14
|
+
* New implementations should wait for WebSocket connections.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} sessionStore - Express session store
|
|
17
|
+
* @param {string} contextDir - Context directory path
|
|
18
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
19
|
+
* @param {object} linkedinAutomation - LinkedInAutomation instance
|
|
20
|
+
*/
|
|
21
|
+
export async function tryAutoStartAutomation(sessionStore, contextDir, cloudAuth, linkedinAutomation) {
|
|
22
|
+
try {
|
|
23
|
+
// First, try using sessionStore.all if available
|
|
24
|
+
if (typeof sessionStore.all === 'function') {
|
|
25
|
+
sessionStore.all(async (err, sessions) => {
|
|
26
|
+
if (err) {
|
|
27
|
+
console.log('[AutoStart] Could not retrieve sessions for auto-start:', err.message);
|
|
28
|
+
// Fall through to file-based enumeration
|
|
29
|
+
await tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!sessions || Object.keys(sessions).length === 0) {
|
|
34
|
+
console.log('[AutoStart] No existing sessions found, skipping auto-start');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try to find an authenticated session
|
|
39
|
+
for (const sessionId in sessions) {
|
|
40
|
+
const sessionData = sessions[sessionId];
|
|
41
|
+
if (sessionData && sessionData.tokens && sessionData.tokens.idToken && sessionData.tokens.accessToken) {
|
|
42
|
+
const success = await tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation);
|
|
43
|
+
if (success) return; // Successfully started, exit function
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log('[AutoStart] No valid authenticated session found, skipping auto-start');
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
// Fall back to file-based enumeration
|
|
51
|
+
await tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('[AutoStart] Error during auto-start check:', error.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Try to start automation by reading session files directly from the filesystem
|
|
60
|
+
*/
|
|
61
|
+
async function tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation) {
|
|
62
|
+
try {
|
|
63
|
+
// session-file-store may store sessions in a 'sessions' subdirectory or directly in contextDir
|
|
64
|
+
let sessionsDir = path.join(contextDir, 'sessions');
|
|
65
|
+
let sessionFiles = [];
|
|
66
|
+
|
|
67
|
+
// First try the sessions subdirectory
|
|
68
|
+
if (fs.existsSync(sessionsDir)) {
|
|
69
|
+
sessionFiles = fs.readdirSync(sessionsDir)
|
|
70
|
+
.filter(file => file.endsWith('.json'))
|
|
71
|
+
.map(file => path.join(sessionsDir, file));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If no files found in subdirectory, check the context directory directly
|
|
75
|
+
if (sessionFiles.length === 0 && fs.existsSync(contextDir)) {
|
|
76
|
+
const allFiles = fs.readdirSync(contextDir).filter(file => file.endsWith('.json'));
|
|
77
|
+
// Filter out non-session files (like storage_state files)
|
|
78
|
+
sessionFiles = allFiles
|
|
79
|
+
.filter(file => {
|
|
80
|
+
const filePath = path.join(contextDir, file);
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
const data = JSON.parse(content);
|
|
84
|
+
// Session files have tokens and cookie structure
|
|
85
|
+
return data.tokens && data.cookie;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.map(file => path.join(contextDir, file));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (sessionFiles.length === 0) {
|
|
94
|
+
console.log('[AutoStart] No session files found, skipping auto-start');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`[AutoStart] Found ${sessionFiles.length} session file(s), checking for authenticated sessions...`);
|
|
99
|
+
|
|
100
|
+
// Try each session file
|
|
101
|
+
for (const sessionPath of sessionFiles) {
|
|
102
|
+
try {
|
|
103
|
+
const fileContent = fs.readFileSync(sessionPath, 'utf8');
|
|
104
|
+
const sessionData = JSON.parse(fileContent);
|
|
105
|
+
|
|
106
|
+
if (sessionData && sessionData.tokens && sessionData.tokens.idToken && sessionData.tokens.accessToken) {
|
|
107
|
+
const success = await tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation);
|
|
108
|
+
if (success) return; // Successfully started, exit function
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Skip invalid session files
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('[AutoStart] No valid authenticated session found in session files, skipping auto-start');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.log('[AutoStart] Error reading session files:', error.message);
|
|
119
|
+
console.log('[AutoStart] Automation will start when clients connect via WebSocket');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Try to start automation with a session object
|
|
125
|
+
* @param {object} sessionData - Session data object with tokens and user info
|
|
126
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
127
|
+
* @param {object} linkedinAutomation - LinkedInAutomation instance
|
|
128
|
+
* @returns {Promise<boolean>} true if automation started successfully
|
|
129
|
+
*/
|
|
130
|
+
export async function tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation) {
|
|
131
|
+
try {
|
|
132
|
+
// Check if automation is already running
|
|
133
|
+
if (linkedinAutomation.isRunning) {
|
|
134
|
+
console.log('[AutoStart] Automation is already running, skipping auto-start');
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create a session-like object that works with cloudAuth methods
|
|
139
|
+
const sessionObj = {
|
|
140
|
+
tokens: sessionData.tokens,
|
|
141
|
+
user: sessionData.user
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Validate and refresh token if needed
|
|
145
|
+
const tokenCheck = await cloudAuth.ensureValidToken(sessionObj);
|
|
146
|
+
if (!tokenCheck.valid) {
|
|
147
|
+
console.log('[AutoStart] Session token invalid, skipping auto-start');
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Start automation with the authenticated session (legacy mode)
|
|
152
|
+
// Note: This uses the single instance for backward compatibility
|
|
153
|
+
// New implementations should wait for WebSocket connections
|
|
154
|
+
console.log('[AutoStart] Auto-starting automation with authenticated session (legacy mode)...');
|
|
155
|
+
linkedinAutomation._currentAccessToken = sessionObj.tokens.idToken;
|
|
156
|
+
linkedinAutomation._currentRefreshToken = sessionObj.tokens.refreshToken;
|
|
157
|
+
linkedinAutomation._currentUserEmail = sessionObj.user?.email || 'default_user';
|
|
158
|
+
await linkedinAutomation.startAutomation(true); // Start in headless mode by default
|
|
159
|
+
console.log('[AutoStart] ✅ Automation auto-started successfully (legacy mode)');
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('[AutoStart] Error during auto-start:', error.message);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Synchronization Helper
|
|
3
|
+
*
|
|
4
|
+
* Handles synchronization of authentication tokens between sessions
|
|
5
|
+
* and LinkedIn automation processes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sync tokens from session to LinkedInAutomation
|
|
10
|
+
* Supports both legacy single instance and new multi-process state
|
|
11
|
+
*
|
|
12
|
+
* @param {object} session - Express session object
|
|
13
|
+
* @param {string|null} socketId - Optional socket ID for process-specific sync
|
|
14
|
+
* @param {object} appState - AppState instance (optional, for multi-process)
|
|
15
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
16
|
+
* @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
|
|
17
|
+
*/
|
|
18
|
+
export function syncTokensToAutomation(session, socketId, appState, cloudAuth, linkedinAutomation = null) {
|
|
19
|
+
if (!session?.tokens || !session?.user) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If socketId provided, sync to specific process
|
|
24
|
+
if (socketId && appState) {
|
|
25
|
+
const process = appState.getProcess(socketId);
|
|
26
|
+
if (process && process.automation && process.automation.isRunning) {
|
|
27
|
+
const hadExpiredToken = process.automation._currentAccessToken &&
|
|
28
|
+
cloudAuth.isTokenExpired(process.automation._currentAccessToken);
|
|
29
|
+
|
|
30
|
+
process.automation._currentAccessToken = session.tokens.idToken;
|
|
31
|
+
process.automation._currentRefreshToken = session.tokens.refreshToken;
|
|
32
|
+
|
|
33
|
+
if (hadExpiredToken) {
|
|
34
|
+
console.log(`[TokenSync] Synced refreshed tokens to process ${socketId} for user:`, session.user.email);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else if (linkedinAutomation) {
|
|
38
|
+
// Legacy: sync to single automation instance
|
|
39
|
+
if (linkedinAutomation.isRunning &&
|
|
40
|
+
linkedinAutomation._currentUserEmail === session.user.email) {
|
|
41
|
+
const hadExpiredToken = linkedinAutomation._currentAccessToken &&
|
|
42
|
+
cloudAuth.isTokenExpired(linkedinAutomation._currentAccessToken);
|
|
43
|
+
|
|
44
|
+
linkedinAutomation._currentAccessToken = session.tokens.idToken;
|
|
45
|
+
linkedinAutomation._currentRefreshToken = session.tokens.refreshToken;
|
|
46
|
+
|
|
47
|
+
if (hadExpiredToken) {
|
|
48
|
+
console.log('[TokenSync] Synced refreshed tokens to LinkedInAutomation for user:', session.user.email);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sync tokens to all processes for a specific user
|
|
56
|
+
*
|
|
57
|
+
* @param {object} session - Express session object
|
|
58
|
+
* @param {string} userEmail - User email address
|
|
59
|
+
* @param {object} appState - AppState instance
|
|
60
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
61
|
+
* @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
|
|
62
|
+
*/
|
|
63
|
+
export function syncTokensToAllUserProcesses(session, userEmail, appState, cloudAuth, linkedinAutomation = null) {
|
|
64
|
+
if (!userEmail || !appState) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const userProcesses = appState.getProcessesByUser(userEmail);
|
|
69
|
+
userProcesses.forEach(process => {
|
|
70
|
+
syncTokensToAutomation(session, process.socketId, appState, cloudAuth);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Also sync to legacy instance if provided
|
|
74
|
+
if (linkedinAutomation) {
|
|
75
|
+
syncTokensToAutomation(session, null, null, cloudAuth, linkedinAutomation);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Version Check Helper
|
|
15
|
+
*
|
|
16
|
+
* Checks npm registry for newer version and automatically installs it if available.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get current version from package.json
|
|
21
|
+
* @returns {string} Current version
|
|
22
|
+
*/
|
|
23
|
+
function getCurrentVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
26
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
27
|
+
return packageJson.version;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('[VersionCheck] Error reading package.json:', error.message);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Query npm registry for latest version
|
|
36
|
+
* @param {string} packageName - Package name to check
|
|
37
|
+
* @returns {Promise<string|null>} Latest version or null if error
|
|
38
|
+
*/
|
|
39
|
+
async function getLatestVersion(packageName) {
|
|
40
|
+
try {
|
|
41
|
+
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
|
42
|
+
const response = await axios.get(registryUrl, {
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
headers: {
|
|
45
|
+
'Accept': 'application/json'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return response.data.version;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[VersionCheck] Error querying npm registry:', error.message);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compare two semantic versions
|
|
57
|
+
* @param {string} current - Current version
|
|
58
|
+
* @param {string} latest - Latest version
|
|
59
|
+
* @returns {boolean} True if latest is newer than current
|
|
60
|
+
*/
|
|
61
|
+
function isNewerVersion(current, latest) {
|
|
62
|
+
if (!current || !latest) return false;
|
|
63
|
+
|
|
64
|
+
const currentParts = current.split('.').map(Number);
|
|
65
|
+
const latestParts = latest.split('.').map(Number);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
68
|
+
const currentPart = currentParts[i] || 0;
|
|
69
|
+
const latestPart = latestParts[i] || 0;
|
|
70
|
+
|
|
71
|
+
if (latestPart > currentPart) return true;
|
|
72
|
+
if (latestPart < currentPart) return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install the latest version globally
|
|
80
|
+
* @param {string} packageName - Package name to install
|
|
81
|
+
* @returns {Promise<boolean>} True if installation succeeded
|
|
82
|
+
*/
|
|
83
|
+
async function installLatestVersion(packageName) {
|
|
84
|
+
try {
|
|
85
|
+
console.log(`[VersionCheck] Installing ${packageName}@latest...`);
|
|
86
|
+
const { stdout, stderr } = await execAsync(`npm install -g ${packageName}@latest`, {
|
|
87
|
+
timeout: 120000 // 2 minutes timeout
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (stderr && !stderr.includes('npm WARN')) {
|
|
91
|
+
console.error('[VersionCheck] Installation warnings:', stderr);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('[VersionCheck] ✅ Successfully installed latest version');
|
|
95
|
+
console.log('[VersionCheck] Please restart the application to use the new version');
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[VersionCheck] ❌ Failed to install latest version:', error.message);
|
|
99
|
+
if (error.stdout) console.log('[VersionCheck] stdout:', error.stdout);
|
|
100
|
+
if (error.stderr) console.error('[VersionCheck] stderr:', error.stderr);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check for updates and install if newer version is available
|
|
107
|
+
* @param {string} packageName - Package name to check (default: 'coffeeinabit')
|
|
108
|
+
* @param {boolean} autoInstall - Whether to automatically install (default: true)
|
|
109
|
+
* @returns {Promise<object>} Update check result
|
|
110
|
+
*/
|
|
111
|
+
export async function checkAndUpdateVersion(packageName = 'coffeeinabit', autoInstall = true) {
|
|
112
|
+
try {
|
|
113
|
+
console.log('[VersionCheck] Checking for updates...');
|
|
114
|
+
|
|
115
|
+
const currentVersion = getCurrentVersion();
|
|
116
|
+
if (!currentVersion) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Could not read current version'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`[VersionCheck] Current version: ${currentVersion}`);
|
|
124
|
+
|
|
125
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
126
|
+
if (!latestVersion) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: 'Could not query npm registry'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`[VersionCheck] Latest version: ${latestVersion}`);
|
|
134
|
+
|
|
135
|
+
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
136
|
+
console.log('[VersionCheck] ✅ You are running the latest version');
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
currentVersion,
|
|
140
|
+
latestVersion,
|
|
141
|
+
updateAvailable: false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`[VersionCheck] ⚠️ New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
146
|
+
|
|
147
|
+
if (autoInstall) {
|
|
148
|
+
const installed = await installLatestVersion(packageName);
|
|
149
|
+
return {
|
|
150
|
+
success: installed,
|
|
151
|
+
currentVersion,
|
|
152
|
+
latestVersion,
|
|
153
|
+
updateAvailable: true,
|
|
154
|
+
installed
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
currentVersion,
|
|
161
|
+
latestVersion,
|
|
162
|
+
updateAvailable: true,
|
|
163
|
+
installed: false
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[VersionCheck] Error during version check:', error.message);
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: error.message
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { syncTokensToAllUserProcesses } from '../helpers/token_sync.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication Middleware
|
|
5
|
+
*
|
|
6
|
+
* Handles token validation and refresh for authenticated requests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Middleware to automatically refresh tokens if expired
|
|
11
|
+
* and sync them to all user processes
|
|
12
|
+
*
|
|
13
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
14
|
+
* @param {object} appState - AppState instance
|
|
15
|
+
* @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
|
|
16
|
+
* @returns {Function} Express middleware function
|
|
17
|
+
*/
|
|
18
|
+
export function createAutoRefreshTokenMiddleware(cloudAuth, appState, linkedinAutomation = null) {
|
|
19
|
+
return async (req, res, next) => {
|
|
20
|
+
if (cloudAuth.isAuthenticated(req.session)) {
|
|
21
|
+
const tokenWasExpired = req.session.tokens?.idToken &&
|
|
22
|
+
cloudAuth.isTokenExpired(req.session.tokens.idToken);
|
|
23
|
+
|
|
24
|
+
const tokenCheck = await cloudAuth.ensureValidToken(req.session);
|
|
25
|
+
if (!tokenCheck.valid) {
|
|
26
|
+
console.log('[AuthMiddleware] Token validation failed:', tokenCheck.error);
|
|
27
|
+
cloudAuth.clearSession(req.session);
|
|
28
|
+
} else if (tokenWasExpired) {
|
|
29
|
+
// Tokens were refreshed, sync to all processes for this user
|
|
30
|
+
const userEmail = req.session.user?.email;
|
|
31
|
+
if (userEmail) {
|
|
32
|
+
syncTokensToAllUserProcesses(req.session, userEmail, appState, cloudAuth, linkedinAutomation);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
}
|
package/package.json
CHANGED
package/routes/auth.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { syncTokensToAllUserProcesses } from '../helpers/token_sync.js';
|
|
3
|
+
import { tryStartAutomationWithSession } from '../helpers/auto_start.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication Routes
|
|
7
|
+
*
|
|
8
|
+
* Handles authentication endpoints including login, callback, logout, and token refresh.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create authentication routes
|
|
13
|
+
*
|
|
14
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
15
|
+
* @param {number} port - Server port number
|
|
16
|
+
* @param {object} appState - AppState instance
|
|
17
|
+
* @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
|
|
18
|
+
* @returns {Router} Express router
|
|
19
|
+
*/
|
|
20
|
+
export function createAuthRoutes(cloudAuth, port, appState, linkedinAutomation = null) {
|
|
21
|
+
const router = express.Router();
|
|
22
|
+
|
|
23
|
+
router.get('/status', (req, res) => {
|
|
24
|
+
res.json(cloudAuth.getAuthStatus(req.session));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.get('/login', async (req, res) => {
|
|
28
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
29
|
+
const result = await cloudAuth.getLoginUrl(redirectUri);
|
|
30
|
+
|
|
31
|
+
if (result.success) {
|
|
32
|
+
console.log('[Auth] Redirecting to login URL:', result.loginUrl);
|
|
33
|
+
res.redirect(result.loginUrl);
|
|
34
|
+
} else {
|
|
35
|
+
console.error('[Auth] Failed to get login URL:', result.error);
|
|
36
|
+
res.redirect('/?error=' + encodeURIComponent('Failed to get login URL: ' + result.error));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
router.get('/callback', async (req, res) => {
|
|
41
|
+
const { code } = req.query;
|
|
42
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
43
|
+
|
|
44
|
+
if (!code) {
|
|
45
|
+
console.error('[Auth] No authorization code received');
|
|
46
|
+
return res.redirect('/?error=no_code');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await cloudAuth.exchangeCodeForTokens(code, redirectUri);
|
|
50
|
+
|
|
51
|
+
if (result.success) {
|
|
52
|
+
cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
|
|
53
|
+
|
|
54
|
+
// Auto-start automation after successful authentication
|
|
55
|
+
if (linkedinAutomation) {
|
|
56
|
+
const sessionData = {
|
|
57
|
+
tokens: result.tokens,
|
|
58
|
+
user: result.user
|
|
59
|
+
};
|
|
60
|
+
// Start automation asynchronously (don't block the redirect)
|
|
61
|
+
tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation).catch(err => {
|
|
62
|
+
console.error('[Auth] Auto-start automation failed after login:', err.message);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.redirect('/dashboard?login=success');
|
|
67
|
+
} else {
|
|
68
|
+
console.error('[Auth] Token exchange failed:', result.error);
|
|
69
|
+
res.redirect('/login?error=' + encodeURIComponent(result.error));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
router.post('/refresh', async (req, res) => {
|
|
74
|
+
if (!req.session.tokens?.refreshToken) {
|
|
75
|
+
return res.status(401).json({ error: 'No refresh token available' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await cloudAuth.refreshTokens(req.session.tokens.refreshToken);
|
|
79
|
+
|
|
80
|
+
if (result.success) {
|
|
81
|
+
cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
|
|
82
|
+
console.log('[Auth] Token refreshed successfully');
|
|
83
|
+
|
|
84
|
+
// Sync tokens to all processes for this user
|
|
85
|
+
const userEmail = req.session.user?.email;
|
|
86
|
+
if (userEmail) {
|
|
87
|
+
syncTokensToAllUserProcesses(req.session, userEmail, appState, cloudAuth, linkedinAutomation);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
res.json({ success: true });
|
|
91
|
+
} else {
|
|
92
|
+
console.error('[Auth] Token refresh failed:', result.error);
|
|
93
|
+
res.status(401).json({ error: result.error });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
router.post('/logout', (req, res) => {
|
|
98
|
+
cloudAuth.clearSession(req.session);
|
|
99
|
+
req.session.destroy((err) => {
|
|
100
|
+
if (err) {
|
|
101
|
+
console.error('[Auth] Error during logout:', err);
|
|
102
|
+
return res.status(500).json({ error: 'Logout failed' });
|
|
103
|
+
}
|
|
104
|
+
console.log('[Auth] User logged out');
|
|
105
|
+
res.json({ success: true });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return router;
|
|
110
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automation Routes
|
|
5
|
+
*
|
|
6
|
+
* Handles automation control endpoints including start, stop, status, and settings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create automation routes
|
|
11
|
+
*
|
|
12
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
13
|
+
* @param {object} appState - AppState instance
|
|
14
|
+
* @param {object} linkedinAutomation - Legacy LinkedInAutomation instance
|
|
15
|
+
* @param {Server} io - Socket.IO server instance
|
|
16
|
+
* @returns {Router} Express router
|
|
17
|
+
*/
|
|
18
|
+
export function createAutomationRoutes(cloudAuth, appState, linkedinAutomation, io) {
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
router.post('/start', async (req, res) => {
|
|
22
|
+
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
23
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { headless = false, socketId = null } = req.body;
|
|
28
|
+
|
|
29
|
+
// If socketId provided, use process-specific automation
|
|
30
|
+
// Otherwise, use legacy single instance (for backward compatibility)
|
|
31
|
+
let automation;
|
|
32
|
+
let processSocketId = socketId;
|
|
33
|
+
|
|
34
|
+
if (socketId) {
|
|
35
|
+
let process = appState.getProcess(socketId);
|
|
36
|
+
if (!process) {
|
|
37
|
+
// Create process if it doesn't exist
|
|
38
|
+
process = appState.createProcess(socketId, req.session, io);
|
|
39
|
+
}
|
|
40
|
+
automation = process.automation;
|
|
41
|
+
processSocketId = socketId;
|
|
42
|
+
} else {
|
|
43
|
+
// Legacy: use single automation instance
|
|
44
|
+
automation = linkedinAutomation;
|
|
45
|
+
console.warn('[Automation] Starting automation without socketId - using legacy single instance');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
automation._currentAccessToken = req.session.tokens.idToken;
|
|
49
|
+
automation._currentRefreshToken = req.session.tokens.refreshToken;
|
|
50
|
+
automation._currentUserEmail = req.session.user.email;
|
|
51
|
+
|
|
52
|
+
await automation.startAutomation(headless);
|
|
53
|
+
|
|
54
|
+
// Update process status
|
|
55
|
+
if (processSocketId) {
|
|
56
|
+
appState.updateProcessStatus(processSocketId, {
|
|
57
|
+
phase: automation.currentPhase || 'running',
|
|
58
|
+
isRunning: automation.isRunning,
|
|
59
|
+
startedAt: new Date().toISOString()
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.json({
|
|
64
|
+
success: true,
|
|
65
|
+
message: 'Automation started',
|
|
66
|
+
socketId: processSocketId,
|
|
67
|
+
processId: processSocketId
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('[Automation] Failed to start automation:', error);
|
|
71
|
+
res.status(500).json({ error: error.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
router.post('/stop', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const { socketId = null } = req.body;
|
|
78
|
+
|
|
79
|
+
// If socketId provided, stop specific process
|
|
80
|
+
// Otherwise, stop legacy single instance
|
|
81
|
+
let automation;
|
|
82
|
+
|
|
83
|
+
if (socketId) {
|
|
84
|
+
const process = appState.getProcess(socketId);
|
|
85
|
+
if (!process) {
|
|
86
|
+
return res.status(404).json({ error: 'Process not found' });
|
|
87
|
+
}
|
|
88
|
+
automation = process.automation;
|
|
89
|
+
|
|
90
|
+
await automation.stopAutomation();
|
|
91
|
+
|
|
92
|
+
// Update process status
|
|
93
|
+
appState.updateProcessStatus(socketId, {
|
|
94
|
+
phase: 'idle',
|
|
95
|
+
isRunning: false
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// Legacy: stop single automation instance
|
|
99
|
+
automation = linkedinAutomation;
|
|
100
|
+
await automation.stopAutomation();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
res.json({ success: true, message: 'Automation stopped' });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('[Automation] Failed to stop automation:', error);
|
|
106
|
+
res.status(500).json({ error: error.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
router.get('/status', (req, res) => {
|
|
111
|
+
const { socketId = null } = req.query;
|
|
112
|
+
|
|
113
|
+
// If socketId provided, get status for specific process
|
|
114
|
+
// Otherwise, return legacy single instance status + global summary
|
|
115
|
+
if (socketId) {
|
|
116
|
+
const process = appState.getProcess(socketId);
|
|
117
|
+
if (!process) {
|
|
118
|
+
return res.status(404).json({ error: 'Process not found' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const status = process.automation.getStatus();
|
|
122
|
+
res.json({
|
|
123
|
+
...status,
|
|
124
|
+
socketId: process.socketId,
|
|
125
|
+
userId: process.session.userId,
|
|
126
|
+
metadata: process.metadata
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
// Legacy: return single instance status + global state summary
|
|
130
|
+
const legacyStatus = linkedinAutomation.getStatus();
|
|
131
|
+
res.json({
|
|
132
|
+
...legacyStatus,
|
|
133
|
+
globalState: appState.getSummary(),
|
|
134
|
+
stats: appState.getStats()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
router.post('/settings', async (req, res) => {
|
|
140
|
+
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
141
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const { keepBrowser, socketId = null } = req.body;
|
|
146
|
+
|
|
147
|
+
if (typeof keepBrowser !== 'boolean') {
|
|
148
|
+
return res.status(400).json({ error: 'keepBrowser must be a boolean' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If socketId provided, update specific process
|
|
152
|
+
// Otherwise, update legacy single instance
|
|
153
|
+
let automation;
|
|
154
|
+
|
|
155
|
+
if (socketId) {
|
|
156
|
+
const process = appState.getProcess(socketId);
|
|
157
|
+
if (!process) {
|
|
158
|
+
return res.status(404).json({ error: 'Process not found' });
|
|
159
|
+
}
|
|
160
|
+
automation = process.automation;
|
|
161
|
+
} else {
|
|
162
|
+
// Legacy: update single automation instance
|
|
163
|
+
automation = linkedinAutomation;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await automation.updateBrowserVisibility(keepBrowser);
|
|
167
|
+
res.json({ success: true, message: 'Browser visibility setting updated' });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('[Automation] Failed to update browser settings:', error);
|
|
170
|
+
res.status(500).json({ error: error.message });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return router;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create state routes
|
|
179
|
+
*
|
|
180
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
181
|
+
* @param {object} appState - AppState instance
|
|
182
|
+
* @returns {Router} Express router
|
|
183
|
+
*/
|
|
184
|
+
export function createStateRoutes(cloudAuth, appState) {
|
|
185
|
+
const router = express.Router();
|
|
186
|
+
|
|
187
|
+
router.get('/', (req, res) => {
|
|
188
|
+
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
189
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
res.json({
|
|
193
|
+
summary: appState.getSummary(),
|
|
194
|
+
stats: appState.getStats()
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return router;
|
|
199
|
+
}
|
package/server.js
CHANGED
|
@@ -7,39 +7,55 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import dotenv from 'dotenv';
|
|
8
8
|
import { createServer } from 'http';
|
|
9
9
|
import { Server } from 'socket.io';
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
import { CloudAuth } from './cloud_auth.js';
|
|
12
12
|
import { LinkedInAutomation } from './linkedin_automation.js';
|
|
13
13
|
import { getContextDirectory } from './tools/context_paths.js';
|
|
14
|
+
import { AppState } from './state/app_state.js';
|
|
15
|
+
import { createAutoRefreshTokenMiddleware } from './middleware/auth.js';
|
|
16
|
+
import { createAuthRoutes } from './routes/auth.js';
|
|
17
|
+
import { createAutomationRoutes, createStateRoutes } from './routes/automation.js';
|
|
18
|
+
import { setupSocketHandlers } from './socket/handlers.js';
|
|
19
|
+
import { tryAutoStartAutomation } from './helpers/auto_start.js';
|
|
20
|
+
import { checkAndUpdateVersion } from './helpers/version_check.js';
|
|
14
21
|
|
|
15
22
|
dotenv.config();
|
|
16
23
|
|
|
17
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
25
|
const __dirname = path.dirname(__filename);
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
// Initialize Express app and HTTP server
|
|
22
28
|
const app = express();
|
|
23
29
|
const server = createServer(app);
|
|
24
30
|
const io = new Server(server);
|
|
25
31
|
const PORT = process.env.PORT || 323;
|
|
32
|
+
|
|
33
|
+
// Initialize core services
|
|
26
34
|
const cloudAuth = new CloudAuth();
|
|
27
|
-
const
|
|
28
|
-
const contextDir = getContextDirectory();
|
|
35
|
+
const appState = new AppState();
|
|
29
36
|
|
|
37
|
+
// Legacy single automation instance (for backward compatibility during migration)
|
|
38
|
+
// TODO: Remove after full migration to multi-process state
|
|
39
|
+
const linkedinAutomation = new LinkedInAutomation(io);
|
|
30
40
|
linkedinAutomation.getAccessToken = function() {
|
|
31
41
|
return this._currentAccessToken || null;
|
|
32
42
|
};
|
|
33
43
|
|
|
44
|
+
// Initialize session store
|
|
45
|
+
const FileStoreSession = FileStore(session);
|
|
46
|
+
const contextDir = getContextDirectory();
|
|
47
|
+
const sessionStore = new FileStoreSession({
|
|
48
|
+
path: contextDir,
|
|
49
|
+
ttl: 30 * 24 * 60 * 60,
|
|
50
|
+
retries: 0
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Configure Express middleware
|
|
34
54
|
app.use(express.json());
|
|
35
55
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
36
56
|
|
|
37
57
|
app.use(session({
|
|
38
|
-
store:
|
|
39
|
-
path: contextDir,
|
|
40
|
-
ttl: 30 * 24 * 60 * 60,
|
|
41
|
-
retries: 0
|
|
42
|
-
}),
|
|
58
|
+
store: sessionStore,
|
|
43
59
|
secret: cloudAuth.sessionSecret,
|
|
44
60
|
resave: false,
|
|
45
61
|
saveUninitialized: false,
|
|
@@ -50,169 +66,52 @@ app.use(session({
|
|
|
50
66
|
}
|
|
51
67
|
}));
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const tokenCheck = await cloudAuth.ensureValidToken(req.session);
|
|
56
|
-
if (!tokenCheck.valid) {
|
|
57
|
-
console.log('[Server] Token validation failed:', tokenCheck.error);
|
|
58
|
-
cloudAuth.clearSession(req.session);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
next();
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
app.use(autoRefreshTokenMiddleware);
|
|
69
|
+
// Apply authentication middleware
|
|
70
|
+
app.use(createAutoRefreshTokenMiddleware(cloudAuth, appState, linkedinAutomation));
|
|
65
71
|
|
|
72
|
+
// Basic routes
|
|
66
73
|
app.get('/', (req, res) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
app.get('/login', (req, res) => {
|
|
75
|
-
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
app.get('/dashboard', (req, res) => {
|
|
79
|
-
if (cloudAuth.isAuthenticated(req.session)) {
|
|
80
|
-
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
|
|
81
|
-
} else {
|
|
82
|
-
res.redirect('/login');
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
app.get('/auth/status', (req, res) => {
|
|
87
|
-
res.json(cloudAuth.getAuthStatus(req.session));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
app.get('/auth/login', async (req, res) => {
|
|
91
|
-
const redirectUri = `http://localhost:${PORT}/auth/callback`;
|
|
92
|
-
const result = await cloudAuth.getLoginUrl(redirectUri);
|
|
93
|
-
|
|
94
|
-
if (result.success) {
|
|
95
|
-
console.log('[Server] Redirecting to login URL:', result.loginUrl);
|
|
96
|
-
res.redirect(result.loginUrl);
|
|
74
|
+
if (cloudAuth.isAuthenticated(req.session)) {
|
|
75
|
+
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
|
|
97
76
|
} else {
|
|
98
|
-
|
|
99
|
-
res.redirect('/?error=' + encodeURIComponent('Failed to get login URL: ' + result.error));
|
|
77
|
+
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
100
78
|
}
|
|
101
79
|
});
|
|
102
80
|
|
|
103
|
-
app.get('/
|
|
104
|
-
|
|
105
|
-
const redirectUri = `http://localhost:${PORT}/auth/callback`;
|
|
106
|
-
|
|
107
|
-
if (!code) {
|
|
108
|
-
console.error('[Server] No authorization code received');
|
|
109
|
-
return res.redirect('/?error=no_code');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const result = await cloudAuth.exchangeCodeForTokens(code, redirectUri);
|
|
113
|
-
|
|
114
|
-
if (result.success) {
|
|
115
|
-
cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
|
|
116
|
-
res.redirect('/dashboard?login=success');
|
|
117
|
-
} else {
|
|
118
|
-
console.error('[Server] Token exchange failed:', result.error);
|
|
119
|
-
res.redirect('/login?error=' + encodeURIComponent(result.error));
|
|
120
|
-
}
|
|
81
|
+
app.get('/login', (req, res) => {
|
|
82
|
+
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
121
83
|
});
|
|
122
84
|
|
|
123
|
-
app.
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const result = await cloudAuth.refreshTokens(req.session.tokens.refreshToken);
|
|
129
|
-
|
|
130
|
-
if (result.success) {
|
|
131
|
-
cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
|
|
132
|
-
console.log('[Server] Token refreshed successfully');
|
|
133
|
-
res.json({ success: true });
|
|
85
|
+
app.get('/dashboard', (req, res) => {
|
|
86
|
+
if (cloudAuth.isAuthenticated(req.session)) {
|
|
87
|
+
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
|
|
134
88
|
} else {
|
|
135
|
-
|
|
136
|
-
res.status(401).json({ error: result.error });
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
app.post('/api/automation/start', async (req, res) => {
|
|
141
|
-
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
142
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const { headless = false } = req.body;
|
|
147
|
-
linkedinAutomation._currentAccessToken = req.session.tokens.idToken;
|
|
148
|
-
linkedinAutomation._currentRefreshToken = req.session.tokens.refreshToken;
|
|
149
|
-
linkedinAutomation._currentUserEmail = req.session.user.email;
|
|
150
|
-
await linkedinAutomation.startAutomation(headless);
|
|
151
|
-
res.json({ success: true, message: 'Automation started' });
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error('[Server] Failed to start automation:', error);
|
|
154
|
-
res.status(500).json({ error: error.message });
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
app.post('/api/automation/stop', async (req, res) => {
|
|
159
|
-
try {
|
|
160
|
-
await linkedinAutomation.stopAutomation();
|
|
161
|
-
res.json({ success: true, message: 'Automation stopped' });
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error('[Server] Failed to stop automation:', error);
|
|
164
|
-
res.status(500).json({ error: error.message });
|
|
89
|
+
res.redirect('/login');
|
|
165
90
|
}
|
|
166
91
|
});
|
|
167
92
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
app.post('/api/automation/settings', async (req, res) => {
|
|
173
|
-
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
174
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const { keepBrowser } = req.body;
|
|
179
|
-
|
|
180
|
-
if (typeof keepBrowser !== 'boolean') {
|
|
181
|
-
return res.status(400).json({ error: 'keepBrowser must be a boolean' });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await linkedinAutomation.updateBrowserVisibility(keepBrowser);
|
|
185
|
-
res.json({ success: true, message: 'Browser visibility setting updated' });
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error('[Server] Failed to update browser settings:', error);
|
|
188
|
-
res.status(500).json({ error: error.message });
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
io.on('connection', (socket) => {
|
|
193
|
-
console.log('[Server] Client connected:', socket.id);
|
|
194
|
-
|
|
195
|
-
socket.on('disconnect', () => {
|
|
196
|
-
console.log('[Server] Client disconnected:', socket.id);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
93
|
+
// API routes
|
|
94
|
+
app.use('/auth', createAuthRoutes(cloudAuth, PORT, appState, linkedinAutomation));
|
|
95
|
+
app.use('/api/automation', createAutomationRoutes(cloudAuth, appState, linkedinAutomation, io));
|
|
96
|
+
app.use('/api/state', createStateRoutes(cloudAuth, appState));
|
|
199
97
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
req.session.destroy((err) => {
|
|
203
|
-
if (err) {
|
|
204
|
-
console.error('[Server] Error during logout:', err);
|
|
205
|
-
return res.status(500).json({ error: 'Logout failed' });
|
|
206
|
-
}
|
|
207
|
-
console.log('[Server] User logged out');
|
|
208
|
-
res.json({ success: true });
|
|
209
|
-
});
|
|
210
|
-
});
|
|
98
|
+
// Setup WebSocket handlers
|
|
99
|
+
setupSocketHandlers(io, appState);
|
|
211
100
|
|
|
212
|
-
|
|
101
|
+
// Start server
|
|
102
|
+
server.listen(PORT, async () => {
|
|
213
103
|
console.log('');
|
|
214
104
|
console.log(` Server running: http://localhost:${PORT}`);
|
|
215
105
|
console.log(' Press Ctrl+C to stop');
|
|
216
106
|
console.log('');
|
|
107
|
+
|
|
108
|
+
// Check for updates on startup (non-blocking)
|
|
109
|
+
checkAndUpdateVersion('coffeeinabit', true).catch(err => {
|
|
110
|
+
console.error('[Server] Version check failed:', err.message);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Wait a bit for everything to initialize, then try to auto-start automation
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
tryAutoStartAutomation(sessionStore, contextDir, cloudAuth, linkedinAutomation);
|
|
116
|
+
}, 1000);
|
|
217
117
|
});
|
|
218
|
-
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket connections and manages process state for each connection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Setup WebSocket connection handlers
|
|
9
|
+
*
|
|
10
|
+
* @param {Server} io - Socket.IO server instance
|
|
11
|
+
* @param {object} appState - AppState instance
|
|
12
|
+
*/
|
|
13
|
+
export function setupSocketHandlers(io, appState) {
|
|
14
|
+
io.on('connection', (socket) => {
|
|
15
|
+
console.log('[Socket] Client connected:', socket.id);
|
|
16
|
+
|
|
17
|
+
// Get session from socket handshake (if available)
|
|
18
|
+
const session = socket.request?.session;
|
|
19
|
+
|
|
20
|
+
// Create process state for this connection
|
|
21
|
+
const processState = appState.createProcess(socket.id, session || {}, io);
|
|
22
|
+
|
|
23
|
+
// Update metadata with connection info
|
|
24
|
+
appState.updateProcessMetadata(socket.id, {
|
|
25
|
+
userAgent: socket.request?.headers['user-agent'] || null,
|
|
26
|
+
ipAddress: socket.request?.connection?.remoteAddress || null
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Handle session updates
|
|
30
|
+
socket.on('session_update', (sessionData) => {
|
|
31
|
+
const process = appState.getProcess(socket.id);
|
|
32
|
+
if (process) {
|
|
33
|
+
process.session = {
|
|
34
|
+
userId: sessionData?.user?.email || null,
|
|
35
|
+
sessionId: sessionData?.id || null,
|
|
36
|
+
authenticated: sessionData?.tokens ? true : false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
socket.on('disconnect', async () => {
|
|
42
|
+
console.log('[Socket] Client disconnected:', socket.id);
|
|
43
|
+
|
|
44
|
+
// Cleanup process state
|
|
45
|
+
await appState.removeProcess(socket.id);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { LinkedInAutomation } from '../linkedin_automation.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global Application State Manager
|
|
5
|
+
*
|
|
6
|
+
* Manages state for multiple concurrent LinkedIn automation processes.
|
|
7
|
+
* Each process is associated with a WebSocket connection (tab) and has its own
|
|
8
|
+
* LinkedInAutomation instance, user session, and status.
|
|
9
|
+
*
|
|
10
|
+
* Design Principles:
|
|
11
|
+
* - Single Responsibility: Manages only state tracking
|
|
12
|
+
* - Encapsulation: Private state with controlled access methods
|
|
13
|
+
* - Scalability: Supports multiple concurrent processes
|
|
14
|
+
* - Cleanup: Automatic cleanup on disconnect
|
|
15
|
+
*/
|
|
16
|
+
export class AppState {
|
|
17
|
+
constructor() {
|
|
18
|
+
// Private state: Map of socketId -> process state
|
|
19
|
+
this._processes = new Map();
|
|
20
|
+
|
|
21
|
+
// Statistics
|
|
22
|
+
this._stats = {
|
|
23
|
+
totalProcessesCreated: 0,
|
|
24
|
+
totalProcessesDestroyed: 0,
|
|
25
|
+
activeProcesses: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new automation process for a socket connection
|
|
31
|
+
* @param {string} socketId - WebSocket connection ID
|
|
32
|
+
* @param {object} session - Express session object
|
|
33
|
+
* @param {Server} io - Socket.IO server instance
|
|
34
|
+
* @returns {object} Process state object
|
|
35
|
+
*/
|
|
36
|
+
createProcess(socketId, session, io) {
|
|
37
|
+
if (this._processes.has(socketId)) {
|
|
38
|
+
console.warn(`[AppState] Process already exists for socket ${socketId}`);
|
|
39
|
+
return this._processes.get(socketId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const automation = new LinkedInAutomation(io);
|
|
43
|
+
automation.getAccessToken = function() {
|
|
44
|
+
return this._currentAccessToken || null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const processState = {
|
|
48
|
+
socketId,
|
|
49
|
+
automation,
|
|
50
|
+
session: {
|
|
51
|
+
userId: session?.user?.email || null,
|
|
52
|
+
sessionId: session?.id || null,
|
|
53
|
+
authenticated: session?.tokens ? true : false
|
|
54
|
+
},
|
|
55
|
+
status: {
|
|
56
|
+
phase: 'idle',
|
|
57
|
+
isRunning: false,
|
|
58
|
+
startedAt: null,
|
|
59
|
+
lastActivityAt: new Date().toISOString()
|
|
60
|
+
},
|
|
61
|
+
metadata: {
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
userAgent: null,
|
|
64
|
+
ipAddress: null
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this._processes.set(socketId, processState);
|
|
69
|
+
this._stats.totalProcessesCreated++;
|
|
70
|
+
this._stats.activeProcesses = this._processes.size;
|
|
71
|
+
|
|
72
|
+
console.log(`[AppState] Created process for socket ${socketId} (total active: ${this._stats.activeProcesses})`);
|
|
73
|
+
return processState;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get process state by socket ID
|
|
78
|
+
* @param {string} socketId - WebSocket connection ID
|
|
79
|
+
* @returns {object|null} Process state or null if not found
|
|
80
|
+
*/
|
|
81
|
+
getProcess(socketId) {
|
|
82
|
+
return this._processes.get(socketId) || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all active processes
|
|
87
|
+
* @returns {Array} Array of process state objects
|
|
88
|
+
*/
|
|
89
|
+
getAllProcesses() {
|
|
90
|
+
return Array.from(this._processes.values());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get processes by user email
|
|
95
|
+
* @param {string} userEmail - User email address
|
|
96
|
+
* @returns {Array} Array of process state objects for the user
|
|
97
|
+
*/
|
|
98
|
+
getProcessesByUser(userEmail) {
|
|
99
|
+
return this.getAllProcesses().filter(
|
|
100
|
+
process => process.session.userId === userEmail
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update process status
|
|
106
|
+
* @param {string} socketId - WebSocket connection ID
|
|
107
|
+
* @param {object} updates - Status updates
|
|
108
|
+
*/
|
|
109
|
+
updateProcessStatus(socketId, updates) {
|
|
110
|
+
const process = this._processes.get(socketId);
|
|
111
|
+
if (!process) {
|
|
112
|
+
console.warn(`[AppState] Cannot update status - process not found for socket ${socketId}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Object.assign(process.status, updates);
|
|
117
|
+
process.status.lastActivityAt = new Date().toISOString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update process metadata
|
|
122
|
+
* @param {string} socketId - WebSocket connection ID
|
|
123
|
+
* @param {object} metadata - Metadata updates
|
|
124
|
+
*/
|
|
125
|
+
updateProcessMetadata(socketId, metadata) {
|
|
126
|
+
const process = this._processes.get(socketId);
|
|
127
|
+
if (!process) {
|
|
128
|
+
console.warn(`[AppState] Cannot update metadata - process not found for socket ${socketId}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Object.assign(process.metadata, metadata);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove and cleanup a process
|
|
137
|
+
* @param {string} socketId - WebSocket connection ID
|
|
138
|
+
* @returns {boolean} True if process was removed, false if not found
|
|
139
|
+
*/
|
|
140
|
+
async removeProcess(socketId) {
|
|
141
|
+
const process = this._processes.get(socketId);
|
|
142
|
+
if (!process) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Cleanup automation if running
|
|
147
|
+
if (process.automation && process.automation.isRunning) {
|
|
148
|
+
try {
|
|
149
|
+
console.log(`[AppState] Stopping automation for socket ${socketId} before cleanup`);
|
|
150
|
+
await process.automation.stopAutomation();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`[AppState] Error stopping automation during cleanup:`, error.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this._processes.delete(socketId);
|
|
157
|
+
this._stats.totalProcessesDestroyed++;
|
|
158
|
+
this._stats.activeProcesses = this._processes.size;
|
|
159
|
+
|
|
160
|
+
console.log(`[AppState] Removed process for socket ${socketId} (total active: ${this._stats.activeProcesses})`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get application-wide statistics
|
|
166
|
+
* @returns {object} Statistics object
|
|
167
|
+
*/
|
|
168
|
+
getStats() {
|
|
169
|
+
return {
|
|
170
|
+
...this._stats,
|
|
171
|
+
activeProcesses: this._processes.size,
|
|
172
|
+
processesByUser: this._getProcessesByUserCount()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get count of processes per user
|
|
178
|
+
* @private
|
|
179
|
+
* @returns {object} Map of userEmail -> count
|
|
180
|
+
*/
|
|
181
|
+
_getProcessesByUserCount() {
|
|
182
|
+
const counts = {};
|
|
183
|
+
this._processes.forEach(process => {
|
|
184
|
+
const userId = process.session.userId || 'anonymous';
|
|
185
|
+
counts[userId] = (counts[userId] || 0) + 1;
|
|
186
|
+
});
|
|
187
|
+
return counts;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get summary of all processes (for debugging/monitoring)
|
|
192
|
+
* @returns {object} Summary object
|
|
193
|
+
*/
|
|
194
|
+
getSummary() {
|
|
195
|
+
const processes = this.getAllProcesses();
|
|
196
|
+
return {
|
|
197
|
+
total: processes.length,
|
|
198
|
+
byStatus: processes.reduce((acc, p) => {
|
|
199
|
+
const status = p.status.phase || 'unknown';
|
|
200
|
+
acc[status] = (acc[status] || 0) + 1;
|
|
201
|
+
return acc;
|
|
202
|
+
}, {}),
|
|
203
|
+
byUser: this._getProcessesByUserCount(),
|
|
204
|
+
processes: processes.map(p => ({
|
|
205
|
+
socketId: p.socketId,
|
|
206
|
+
userId: p.session.userId,
|
|
207
|
+
status: p.status.phase,
|
|
208
|
+
isRunning: p.status.isRunning,
|
|
209
|
+
startedAt: p.status.startedAt,
|
|
210
|
+
createdAt: p.metadata.createdAt
|
|
211
|
+
}))
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|