coffeeinabit 0.0.45 → 0.0.46
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/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 +50 -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,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,54 @@ 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';
|
|
14
20
|
|
|
15
21
|
dotenv.config();
|
|
16
22
|
|
|
17
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
24
|
const __dirname = path.dirname(__filename);
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
// Initialize Express app and HTTP server
|
|
22
27
|
const app = express();
|
|
23
28
|
const server = createServer(app);
|
|
24
29
|
const io = new Server(server);
|
|
25
30
|
const PORT = process.env.PORT || 323;
|
|
31
|
+
|
|
32
|
+
// Initialize core services
|
|
26
33
|
const cloudAuth = new CloudAuth();
|
|
27
|
-
const
|
|
28
|
-
const contextDir = getContextDirectory();
|
|
34
|
+
const appState = new AppState();
|
|
29
35
|
|
|
36
|
+
// Legacy single automation instance (for backward compatibility during migration)
|
|
37
|
+
// TODO: Remove after full migration to multi-process state
|
|
38
|
+
const linkedinAutomation = new LinkedInAutomation(io);
|
|
30
39
|
linkedinAutomation.getAccessToken = function() {
|
|
31
40
|
return this._currentAccessToken || null;
|
|
32
41
|
};
|
|
33
42
|
|
|
43
|
+
// Initialize session store
|
|
44
|
+
const FileStoreSession = FileStore(session);
|
|
45
|
+
const contextDir = getContextDirectory();
|
|
46
|
+
const sessionStore = new FileStoreSession({
|
|
47
|
+
path: contextDir,
|
|
48
|
+
ttl: 30 * 24 * 60 * 60,
|
|
49
|
+
retries: 0
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Configure Express middleware
|
|
34
53
|
app.use(express.json());
|
|
35
54
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
36
55
|
|
|
37
56
|
app.use(session({
|
|
38
|
-
store:
|
|
39
|
-
path: contextDir,
|
|
40
|
-
ttl: 30 * 24 * 60 * 60,
|
|
41
|
-
retries: 0
|
|
42
|
-
}),
|
|
57
|
+
store: sessionStore,
|
|
43
58
|
secret: cloudAuth.sessionSecret,
|
|
44
59
|
resave: false,
|
|
45
60
|
saveUninitialized: false,
|
|
@@ -50,169 +65,47 @@ app.use(session({
|
|
|
50
65
|
}
|
|
51
66
|
}));
|
|
52
67
|
|
|
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);
|
|
68
|
+
// Apply authentication middleware
|
|
69
|
+
app.use(createAutoRefreshTokenMiddleware(cloudAuth, appState, linkedinAutomation));
|
|
65
70
|
|
|
71
|
+
// Basic routes
|
|
66
72
|
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);
|
|
73
|
+
if (cloudAuth.isAuthenticated(req.session)) {
|
|
74
|
+
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
|
|
97
75
|
} else {
|
|
98
|
-
|
|
99
|
-
res.redirect('/?error=' + encodeURIComponent('Failed to get login URL: ' + result.error));
|
|
76
|
+
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
100
77
|
}
|
|
101
78
|
});
|
|
102
79
|
|
|
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
|
-
}
|
|
80
|
+
app.get('/login', (req, res) => {
|
|
81
|
+
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
121
82
|
});
|
|
122
83
|
|
|
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 });
|
|
84
|
+
app.get('/dashboard', (req, res) => {
|
|
85
|
+
if (cloudAuth.isAuthenticated(req.session)) {
|
|
86
|
+
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
|
|
134
87
|
} 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 });
|
|
88
|
+
res.redirect('/login');
|
|
155
89
|
}
|
|
156
90
|
});
|
|
157
91
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error('[Server] Failed to stop automation:', error);
|
|
164
|
-
res.status(500).json({ error: error.message });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
app.get('/api/automation/status', (req, res) => {
|
|
169
|
-
res.json(linkedinAutomation.getStatus());
|
|
170
|
-
});
|
|
92
|
+
// API routes
|
|
93
|
+
app.use('/auth', createAuthRoutes(cloudAuth, PORT, appState, linkedinAutomation));
|
|
94
|
+
app.use('/api/automation', createAutomationRoutes(cloudAuth, appState, linkedinAutomation, io));
|
|
95
|
+
app.use('/api/state', createStateRoutes(cloudAuth, appState));
|
|
171
96
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
});
|
|
97
|
+
// Setup WebSocket handlers
|
|
98
|
+
setupSocketHandlers(io, appState);
|
|
191
99
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
socket.on('disconnect', () => {
|
|
196
|
-
console.log('[Server] Client disconnected:', socket.id);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
app.post('/auth/logout', (req, res) => {
|
|
201
|
-
cloudAuth.clearSession(req.session);
|
|
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
|
-
});
|
|
211
|
-
|
|
212
|
-
server.listen(PORT, () => {
|
|
100
|
+
// Start server
|
|
101
|
+
server.listen(PORT, async () => {
|
|
213
102
|
console.log('');
|
|
214
103
|
console.log(` Server running: http://localhost:${PORT}`);
|
|
215
104
|
console.log(' Press Ctrl+C to stop');
|
|
216
105
|
console.log('');
|
|
106
|
+
|
|
107
|
+
// Wait a bit for everything to initialize, then try to auto-start automation
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
tryAutoStartAutomation(sessionStore, contextDir, cloudAuth, linkedinAutomation);
|
|
110
|
+
}, 1000);
|
|
217
111
|
});
|
|
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
|
+
}
|