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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",
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
- import { execSync } from 'child_process';
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
- const FileStoreSession = FileStore(session);
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 linkedinAutomation = new LinkedInAutomation(io);
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: new FileStoreSession({
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
- const autoRefreshTokenMiddleware = async (req, res, next) => {
54
- if (cloudAuth.isAuthenticated(req.session)) {
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
- if (cloudAuth.isAuthenticated(req.session)) {
68
- res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
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
- console.error('[Server] Failed to get login URL:', result.error);
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('/auth/callback', async (req, res) => {
104
- const { code } = req.query;
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.post('/auth/refresh', async (req, res) => {
124
- if (!req.session.tokens?.refreshToken) {
125
- return res.status(401).json({ error: 'No refresh token available' });
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
- console.error('[Server] Token refresh failed:', result.error);
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
- 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 });
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
- 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
- });
97
+ // Setup WebSocket handlers
98
+ setupSocketHandlers(io, appState);
191
99
 
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
- });
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
+ }