coffeeinabit 0.0.1

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,163 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CoffeeInABit - Login</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
9
+ <link href="/styles.css" rel="stylesheet">
10
+ </head>
11
+ <body class="bg-light">
12
+ <div class="dark-mode-toggle-container">
13
+ <div class="dark-mode-toggle-button" onclick="toggleDarkMode()">
14
+ <i class="bi bi-moon-stars"></i>
15
+ <span>Dark Mode</span>
16
+ <div class="form-check form-switch mb-0">
17
+ <input class="form-check-input" type="checkbox" role="switch" id="darkModeToggle">
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="container-fluid vh-100 d-flex align-items-center justify-content-center">
23
+ <div class="row w-100 justify-content-center">
24
+ <div class="col-md-6 col-lg-4">
25
+ <div class="card shadow-lg border-0">
26
+ <div class="card-body p-5">
27
+ <div class="text-center mb-4">
28
+ <h1 class="h3 mb-3 fw-bold text-primary">
29
+ <i class="bi bi-cup-hot-fill me-2"></i>
30
+ CoffeeInABit
31
+ </h1>
32
+ <p class="text-muted">Sign in to your account to continue</p>
33
+ </div>
34
+
35
+ <div class="d-grid gap-2">
36
+ <button id="loginButton" class="btn btn-primary btn-lg" onclick="loginToCloud()">
37
+ <i class="bi bi-cloud-arrow-up me-2"></i>
38
+ Login to CoffeeInABit Cloud
39
+ </button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div id="toastContainer" class="toast-container"></div>
48
+
49
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
50
+ <script>
51
+ const loginButton = document.getElementById('loginButton');
52
+ const toastContainer = document.getElementById('toastContainer');
53
+
54
+ function initializeDarkMode() {
55
+ const darkModeEnabled = localStorage.getItem('darkMode') === 'true';
56
+ const darkModeToggle = document.getElementById('darkModeToggle');
57
+
58
+ if (darkModeEnabled) {
59
+ document.body.classList.add('dark-mode');
60
+ if (darkModeToggle) {
61
+ darkModeToggle.checked = true;
62
+ }
63
+ }
64
+ }
65
+
66
+ function toggleDarkMode() {
67
+ const darkModeToggle = document.getElementById('darkModeToggle');
68
+ const isDarkMode = darkModeToggle.checked;
69
+
70
+ if (isDarkMode) {
71
+ document.body.classList.add('dark-mode');
72
+ localStorage.setItem('darkMode', 'true');
73
+ } else {
74
+ document.body.classList.remove('dark-mode');
75
+ localStorage.setItem('darkMode', 'false');
76
+ }
77
+ }
78
+
79
+ async function checkAuthStatus() {
80
+ try {
81
+ const response = await fetch('/auth/status');
82
+ const data = await response.json();
83
+
84
+ if (data.authenticated) {
85
+ window.location.href = '/dashboard';
86
+ }
87
+ } catch (error) {
88
+ console.error('Error checking auth status:', error);
89
+ }
90
+ }
91
+
92
+ function loginToCloud() {
93
+ loginButton.disabled = true;
94
+ loginButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Redirecting...';
95
+ window.location.href = '/auth/login';
96
+ }
97
+
98
+ function showAlert(type, message) {
99
+ const icons = {
100
+ success: 'bi-check-circle-fill',
101
+ error: 'bi-exclamation-circle-fill',
102
+ info: 'bi-info-circle-fill',
103
+ warning: 'bi-exclamation-triangle-fill'
104
+ };
105
+
106
+ const titles = {
107
+ success: 'Success',
108
+ error: 'Error',
109
+ info: 'Info',
110
+ warning: 'Warning'
111
+ };
112
+
113
+ const toastId = 'toast-' + Date.now();
114
+ const toast = document.createElement('div');
115
+ toast.className = `toast-notification ${type}`;
116
+ toast.id = toastId;
117
+ toast.innerHTML = `
118
+ <i class="bi ${icons[type] || icons.info} toast-icon"></i>
119
+ <div class="toast-content">
120
+ <strong>${titles[type] || titles.info}</strong>
121
+ <p>${message}</p>
122
+ </div>
123
+ <button class="toast-close" onclick="removeToast('${toastId}')">
124
+ <i class="bi bi-x"></i>
125
+ </button>
126
+ `;
127
+
128
+ toastContainer.appendChild(toast);
129
+
130
+ setTimeout(() => {
131
+ removeToast(toastId);
132
+ }, 6000);
133
+ }
134
+
135
+ function removeToast(toastId) {
136
+ const toast = document.getElementById(toastId);
137
+ if (toast) {
138
+ toast.classList.add('removing');
139
+ setTimeout(() => {
140
+ toast.remove();
141
+ }, 300);
142
+ }
143
+ }
144
+
145
+ const urlParams = new URLSearchParams(window.location.search);
146
+ const loginStatus = urlParams.get('login');
147
+ const errorMessage = urlParams.get('error');
148
+
149
+ if (loginStatus === 'success') {
150
+ showAlert('success', 'Successfully logged in! Redirecting to dashboard...');
151
+ setTimeout(() => {
152
+ window.location.href = '/dashboard';
153
+ }, 2000);
154
+ } else if (errorMessage) {
155
+ showAlert('error', `Login failed: ${decodeURIComponent(errorMessage)}`);
156
+ history.replaceState({}, document.title, window.location.pathname);
157
+ }
158
+
159
+ initializeDarkMode();
160
+ checkAuthStatus();
161
+ </script>
162
+ </body>
163
+ </html>
@@ -0,0 +1,298 @@
1
+ body {
2
+ transition: background-color 0.3s ease, color 0.3s ease;
3
+ }
4
+
5
+ body.dark-mode {
6
+ background-color: #1a1a1a !important;
7
+ color: #e0e0e0;
8
+ }
9
+
10
+ body.dark-mode .navbar {
11
+ background-color: #2d2d2d !important;
12
+ }
13
+
14
+ body.dark-mode .card {
15
+ background-color: #2d2d2d;
16
+ color: #e0e0e0;
17
+ border-color: #404040 !important;
18
+ }
19
+
20
+ body.dark-mode .card-header {
21
+ background-color: #252525 !important;
22
+ border-bottom-color: #404040 !important;
23
+ }
24
+
25
+ body.dark-mode .bg-light {
26
+ background-color: #1a1a1a !important;
27
+ }
28
+
29
+ body.dark-mode .bg-white {
30
+ background-color: #252525 !important;
31
+ }
32
+
33
+ body.dark-mode .text-muted {
34
+ color: #a0a0a0 !important;
35
+ }
36
+
37
+ body.dark-mode .text-primary {
38
+ color: #6db3f2 !important;
39
+ }
40
+
41
+ body.dark-mode .dropdown-menu {
42
+ background-color: #2d2d2d;
43
+ border-color: #404040;
44
+ color: #e0e0e0;
45
+ }
46
+
47
+ body.dark-mode .dropdown-menu strong {
48
+ color: #e0e0e0;
49
+ }
50
+
51
+ body.dark-mode .dropdown-menu small {
52
+ color: #a0a0a0;
53
+ }
54
+
55
+ body.dark-mode .dropdown-item {
56
+ color: #e0e0e0;
57
+ }
58
+
59
+ body.dark-mode .dropdown-item:hover {
60
+ background-color: #404040;
61
+ color: #ffffff;
62
+ }
63
+
64
+ body.dark-mode .dropdown-divider {
65
+ border-color: #404040;
66
+ }
67
+
68
+ body.dark-mode .border {
69
+ border-color: #404040 !important;
70
+ }
71
+
72
+ body.dark-mode .alert-success {
73
+ background-color: #1e4620;
74
+ border-color: #2d6930;
75
+ color: #a3d9a5;
76
+ }
77
+
78
+ body.dark-mode .alert-danger {
79
+ background-color: #4a1f1f;
80
+ border-color: #6b2c2c;
81
+ color: #f0a3a3;
82
+ }
83
+
84
+ body.dark-mode .badge {
85
+ border: 1px solid #404040;
86
+ }
87
+
88
+ body.dark-mode a:not(.btn):not(.dropdown-item) {
89
+ color: #6db3f2;
90
+ }
91
+
92
+ body.dark-mode .shadow-sm {
93
+ box-shadow: 0 .125rem .25rem rgba(0,0,0,.5) !important;
94
+ }
95
+
96
+ body.dark-mode .shadow-lg {
97
+ box-shadow: 0 1rem 3rem rgba(0,0,0,.5) !important;
98
+ }
99
+
100
+ body.dark-mode .form-check-input {
101
+ background-color: #404040;
102
+ border-color: #606060;
103
+ }
104
+
105
+ body.dark-mode .form-check-input:checked {
106
+ background-color: #0d6efd;
107
+ border-color: #0d6efd;
108
+ }
109
+
110
+ .dark-mode-toggle-container {
111
+ position: fixed;
112
+ top: 20px;
113
+ right: 20px;
114
+ z-index: 1000;
115
+ }
116
+
117
+ .dark-mode-toggle-button {
118
+ background-color: rgba(255, 255, 255, 0.9);
119
+ border: 1px solid #dee2e6;
120
+ border-radius: 50px;
121
+ padding: 8px 16px;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ cursor: pointer;
126
+ transition: all 0.3s ease;
127
+ }
128
+
129
+ body.dark-mode .dark-mode-toggle-button {
130
+ background-color: rgba(45, 45, 45, 0.9);
131
+ border-color: #404040;
132
+ }
133
+
134
+ .dark-mode-toggle-button:hover {
135
+ box-shadow: 0 .125rem .25rem rgba(0,0,0,.2);
136
+ }
137
+
138
+ .toast-container {
139
+ position: fixed;
140
+ bottom: 20px;
141
+ right: 20px;
142
+ z-index: 9999;
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 10px;
146
+ max-width: 400px;
147
+ }
148
+
149
+ .toast-notification {
150
+ background-color: #ffffff;
151
+ border-radius: 8px;
152
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
153
+ padding: 16px 20px;
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 12px;
157
+ min-width: 300px;
158
+ opacity: 0;
159
+ transform: translateX(400px);
160
+ animation: slideIn 0.3s ease-out forwards;
161
+ transition: all 0.3s ease;
162
+ }
163
+
164
+ .toast-notification.removing {
165
+ animation: slideOut 0.3s ease-in forwards;
166
+ }
167
+
168
+ .toast-notification.success {
169
+ border-left: 4px solid #198754;
170
+ }
171
+
172
+ .toast-notification.error {
173
+ border-left: 4px solid #dc3545;
174
+ }
175
+
176
+ .toast-notification.info {
177
+ border-left: 4px solid #0dcaf0;
178
+ }
179
+
180
+ .toast-notification.warning {
181
+ border-left: 4px solid #ffc107;
182
+ }
183
+
184
+ .toast-icon {
185
+ font-size: 24px;
186
+ flex-shrink: 0;
187
+ }
188
+
189
+ .toast-notification.success .toast-icon {
190
+ color: #198754;
191
+ }
192
+
193
+ .toast-notification.error .toast-icon {
194
+ color: #dc3545;
195
+ }
196
+
197
+ .toast-notification.info .toast-icon {
198
+ color: #0dcaf0;
199
+ }
200
+
201
+ .toast-notification.warning .toast-icon {
202
+ color: #ffc107;
203
+ }
204
+
205
+ .toast-content {
206
+ flex: 1;
207
+ color: #333;
208
+ }
209
+
210
+ .toast-content strong {
211
+ display: block;
212
+ margin-bottom: 4px;
213
+ font-size: 14px;
214
+ font-weight: 600;
215
+ }
216
+
217
+ .toast-content p {
218
+ margin: 0;
219
+ font-size: 13px;
220
+ color: #666;
221
+ }
222
+
223
+ .toast-close {
224
+ background: none;
225
+ border: none;
226
+ font-size: 20px;
227
+ color: #999;
228
+ cursor: pointer;
229
+ padding: 0;
230
+ width: 24px;
231
+ height: 24px;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ flex-shrink: 0;
236
+ transition: color 0.2s;
237
+ }
238
+
239
+ .toast-close:hover {
240
+ color: #333;
241
+ }
242
+
243
+ body.dark-mode .toast-notification {
244
+ background-color: #2d2d2d;
245
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
246
+ }
247
+
248
+ body.dark-mode .toast-content {
249
+ color: #e0e0e0;
250
+ }
251
+
252
+ body.dark-mode .toast-content p {
253
+ color: #a0a0a0;
254
+ }
255
+
256
+ body.dark-mode .toast-close {
257
+ color: #a0a0a0;
258
+ }
259
+
260
+ body.dark-mode .toast-close:hover {
261
+ color: #e0e0e0;
262
+ }
263
+
264
+ @keyframes slideIn {
265
+ to {
266
+ opacity: 1;
267
+ transform: translateX(0);
268
+ }
269
+ }
270
+
271
+ @keyframes slideOut {
272
+ to {
273
+ opacity: 0;
274
+ transform: translateX(400px);
275
+ }
276
+ }
277
+
278
+ #browserViewToggle {
279
+ color: inherit;
280
+ transition: all 0.2s ease;
281
+ }
282
+
283
+ #browserViewToggle:hover {
284
+ color: #0d6efd;
285
+ }
286
+
287
+ #browserViewChevron {
288
+ transition: transform 0.2s ease;
289
+ }
290
+
291
+ body.dark-mode #browserViewToggle {
292
+ color: #e0e0e0;
293
+ }
294
+
295
+ body.dark-mode #browserViewToggle:hover {
296
+ color: #6db3f2;
297
+ }
298
+
package/server.js ADDED
@@ -0,0 +1,201 @@
1
+ import express from 'express';
2
+ import session from 'express-session';
3
+ import FileStore from 'session-file-store';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import dotenv from 'dotenv';
7
+ import { createServer } from 'http';
8
+ import { Server } from 'socket.io';
9
+ import { execSync } from 'child_process';
10
+ import { CloudAuth } from './cloud_auth.js';
11
+ import { LinkedInAutomation } from './linkedin_automation.js';
12
+
13
+ dotenv.config();
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ try {
19
+ execSync('npx playwright install firefox', { stdio: 'inherit' });
20
+ console.log('[Server] Playwright Firefox browser installed/verified');
21
+ } catch (error) {
22
+ console.warn('[Server] Warning: Could not install Playwright Firefox browser:', error.message);
23
+ }
24
+
25
+ const FileStoreSession = FileStore(session);
26
+
27
+ const app = express();
28
+ const server = createServer(app);
29
+ const io = new Server(server);
30
+ const PORT = process.env.PORT || 3000;
31
+ const cloudAuth = new CloudAuth();
32
+ const linkedinAutomation = new LinkedInAutomation(io);
33
+
34
+ linkedinAutomation.getAccessToken = function() {
35
+ return this._currentAccessToken || null;
36
+ };
37
+
38
+ app.use(express.json());
39
+ app.use(express.static('public'));
40
+
41
+ app.use(session({
42
+ store: new FileStoreSession({
43
+ path: path.join(__dirname, 'context'),
44
+ ttl: 30 * 24 * 60 * 60,
45
+ retries: 0
46
+ }),
47
+ secret: cloudAuth.sessionSecret,
48
+ resave: false,
49
+ saveUninitialized: false,
50
+ cookie: {
51
+ secure: false,
52
+ httpOnly: true,
53
+ maxAge: 30 * 24 * 60 * 60 * 1000
54
+ }
55
+ }));
56
+
57
+ const autoRefreshTokenMiddleware = async (req, res, next) => {
58
+ if (cloudAuth.isAuthenticated(req.session)) {
59
+ const tokenCheck = await cloudAuth.ensureValidToken(req.session);
60
+ if (!tokenCheck.valid) {
61
+ console.log('[Server] Token validation failed:', tokenCheck.error);
62
+ cloudAuth.clearSession(req.session);
63
+ }
64
+ }
65
+ next();
66
+ };
67
+
68
+ app.use(autoRefreshTokenMiddleware);
69
+
70
+ app.get('/', (req, res) => {
71
+ if (cloudAuth.isAuthenticated(req.session)) {
72
+ res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
73
+ } else {
74
+ res.sendFile(path.join(__dirname, 'public', 'login.html'));
75
+ }
76
+ });
77
+
78
+ app.get('/login', (req, res) => {
79
+ res.sendFile(path.join(__dirname, 'public', 'login.html'));
80
+ });
81
+
82
+ app.get('/dashboard', (req, res) => {
83
+ if (cloudAuth.isAuthenticated(req.session)) {
84
+ res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
85
+ } else {
86
+ res.redirect('/login');
87
+ }
88
+ });
89
+
90
+ app.get('/auth/status', (req, res) => {
91
+ res.json(cloudAuth.getAuthStatus(req.session));
92
+ });
93
+
94
+ app.get('/auth/login', async (req, res) => {
95
+ const redirectUri = `http://localhost:${PORT}/auth/callback`;
96
+ const result = await cloudAuth.getLoginUrl(redirectUri);
97
+
98
+ if (result.success) {
99
+ console.log('[Server] Redirecting to login URL:', result.loginUrl);
100
+ res.redirect(result.loginUrl);
101
+ } else {
102
+ console.error('[Server] Failed to get login URL:', result.error);
103
+ res.redirect('/?error=' + encodeURIComponent('Failed to get login URL: ' + result.error));
104
+ }
105
+ });
106
+
107
+ app.get('/auth/callback', async (req, res) => {
108
+ const { code } = req.query;
109
+ const redirectUri = `http://localhost:${PORT}/auth/callback`;
110
+
111
+ if (!code) {
112
+ console.error('[Server] No authorization code received');
113
+ return res.redirect('/?error=no_code');
114
+ }
115
+
116
+ const result = await cloudAuth.exchangeCodeForTokens(code, redirectUri);
117
+
118
+ if (result.success) {
119
+ cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
120
+ res.redirect('/dashboard?login=success');
121
+ } else {
122
+ console.error('[Server] Token exchange failed:', result.error);
123
+ res.redirect('/login?error=' + encodeURIComponent(result.error));
124
+ }
125
+ });
126
+
127
+ app.post('/auth/refresh', async (req, res) => {
128
+ if (!req.session.tokens?.refreshToken) {
129
+ return res.status(401).json({ error: 'No refresh token available' });
130
+ }
131
+
132
+ const result = await cloudAuth.refreshTokens(req.session.tokens.refreshToken);
133
+
134
+ if (result.success) {
135
+ cloudAuth.storeTokensInSession(req.session, result.tokens, result.user);
136
+ console.log('[Server] Token refreshed successfully');
137
+ res.json({ success: true });
138
+ } else {
139
+ console.error('[Server] Token refresh failed:', result.error);
140
+ res.status(401).json({ error: result.error });
141
+ }
142
+ });
143
+
144
+ app.post('/api/automation/start', async (req, res) => {
145
+ if (!cloudAuth.isAuthenticated(req.session)) {
146
+ return res.status(401).json({ error: 'Authentication required' });
147
+ }
148
+
149
+ try {
150
+ const { headless = false } = req.body;
151
+ linkedinAutomation._currentAccessToken = req.session.tokens.idToken;
152
+ linkedinAutomation._currentUserEmail = req.session.user.email;
153
+ await linkedinAutomation.startAutomation(headless);
154
+ res.json({ success: true, message: 'Automation started' });
155
+ } catch (error) {
156
+ console.error('[Server] Failed to start automation:', error);
157
+ res.status(500).json({ error: error.message });
158
+ }
159
+ });
160
+
161
+ app.post('/api/automation/stop', async (req, res) => {
162
+ try {
163
+ await linkedinAutomation.stopAutomation();
164
+ res.json({ success: true, message: 'Automation stopped' });
165
+ } catch (error) {
166
+ console.error('[Server] Failed to stop automation:', error);
167
+ res.status(500).json({ error: error.message });
168
+ }
169
+ });
170
+
171
+ app.get('/api/automation/status', (req, res) => {
172
+ res.json(linkedinAutomation.getStatus());
173
+ });
174
+
175
+ io.on('connection', (socket) => {
176
+ console.log('[Server] Client connected:', socket.id);
177
+
178
+ socket.on('disconnect', () => {
179
+ console.log('[Server] Client disconnected:', socket.id);
180
+ });
181
+ });
182
+
183
+ app.post('/auth/logout', (req, res) => {
184
+ cloudAuth.clearSession(req.session);
185
+ req.session.destroy((err) => {
186
+ if (err) {
187
+ console.error('[Server] Error during logout:', err);
188
+ return res.status(500).json({ error: 'Logout failed' });
189
+ }
190
+ console.log('[Server] User logged out');
191
+ res.json({ success: true });
192
+ });
193
+ });
194
+
195
+ server.listen(PORT, () => {
196
+ console.log('');
197
+ console.log(` Server running: http://localhost:${PORT}`);
198
+ console.log(' Press Ctrl+C to stop');
199
+ console.log('');
200
+ });
201
+
@@ -0,0 +1,32 @@
1
+ export async function executeCheckConnectionStatus(page, action) {
2
+ const username = action.parameters?.username || action.username;
3
+
4
+ if (!username) {
5
+ throw new Error('Missing username for check_connection_status');
6
+ }
7
+
8
+ const profileUrl = `https://www.linkedin.com/in/${username}`;
9
+ await page.goto(profileUrl, {
10
+ waitUntil: 'load',
11
+ timeout: Math.floor(Math.random() * 20000) + 40000
12
+ });
13
+
14
+ await page.waitForLoadState('domcontentloaded');
15
+ await new Promise(resolve => setTimeout(resolve, 2000));
16
+
17
+ const connection = await page.evaluate(() => {
18
+ const dist = document.querySelector('.dist-value');
19
+ if (!dist) return 'unknown';
20
+ const value = dist.innerText.trim();
21
+ if (value === '1st') return '1st';
22
+ if (value === '2nd') return '2nd';
23
+ if (value === '3rd') return '3rd';
24
+ return 'unknown';
25
+ });
26
+
27
+ return {
28
+ action: 'check_connection_status',
29
+ result: { status: 'success', connection },
30
+ status: 'success'
31
+ };
32
+ }