lobstakit-cloud 1.0.0
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/bin/lobstakit.js +2 -0
- package/lib/config.js +176 -0
- package/lib/gateway.js +104 -0
- package/lib/proxy.js +33 -0
- package/package.json +16 -0
- package/public/css/styles.css +579 -0
- package/public/index.html +507 -0
- package/public/js/app.js +198 -0
- package/public/js/login.js +93 -0
- package/public/js/manage.js +1274 -0
- package/public/js/setup.js +755 -0
- package/public/login.html +73 -0
- package/public/manage.html +734 -0
- package/server.js +1357 -0
package/public/js/app.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LobstaKit Cloud — Common JavaScript utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Show a toast notification
|
|
7
|
+
* @param {string} message - Message to display
|
|
8
|
+
* @param {string} type - Type: 'success', 'error', 'info', 'warning'
|
|
9
|
+
* @param {number} duration - Duration in ms (default 3000)
|
|
10
|
+
*/
|
|
11
|
+
function showToast(message, type = 'info', duration = 3000) {
|
|
12
|
+
const container = document.getElementById('toast-container');
|
|
13
|
+
if (!container) return;
|
|
14
|
+
|
|
15
|
+
const toast = document.createElement('div');
|
|
16
|
+
|
|
17
|
+
const colors = {
|
|
18
|
+
success: 'bg-green-500',
|
|
19
|
+
error: 'bg-red-500',
|
|
20
|
+
warning: 'bg-yellow-500',
|
|
21
|
+
info: 'bg-blue-500'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const icons = {
|
|
25
|
+
success: '✓',
|
|
26
|
+
error: '✗',
|
|
27
|
+
warning: '⚠',
|
|
28
|
+
info: 'ℹ'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
toast.className = `toast flex items-center space-x-3 px-4 py-3 rounded-lg shadow-lg ${colors[type] || colors.info} text-white min-w-64`;
|
|
32
|
+
toast.innerHTML = `
|
|
33
|
+
<span class="font-bold">${icons[type] || icons.info}</span>
|
|
34
|
+
<span>${escapeHtml(message)}</span>
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
container.appendChild(toast);
|
|
38
|
+
|
|
39
|
+
// Auto-remove
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
toast.classList.add('hiding');
|
|
42
|
+
setTimeout(() => toast.remove(), 300);
|
|
43
|
+
}, duration);
|
|
44
|
+
|
|
45
|
+
// Click to dismiss
|
|
46
|
+
toast.addEventListener('click', () => {
|
|
47
|
+
toast.classList.add('hiding');
|
|
48
|
+
setTimeout(() => toast.remove(), 300);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Escape HTML to prevent XSS
|
|
54
|
+
* @param {string} text - Raw text
|
|
55
|
+
* @returns {string} Escaped text
|
|
56
|
+
*/
|
|
57
|
+
function escapeHtml(text) {
|
|
58
|
+
const div = document.createElement('div');
|
|
59
|
+
div.textContent = text;
|
|
60
|
+
return div.innerHTML;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format a timestamp
|
|
65
|
+
* @param {string|number} timestamp - ISO string or unix timestamp
|
|
66
|
+
* @returns {string} Formatted string
|
|
67
|
+
*/
|
|
68
|
+
function formatTime(timestamp) {
|
|
69
|
+
const date = new Date(timestamp);
|
|
70
|
+
return date.toLocaleString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Debounce a function
|
|
75
|
+
* @param {Function} func - Function to debounce
|
|
76
|
+
* @param {number} wait - Wait time in ms
|
|
77
|
+
* @returns {Function} Debounced function
|
|
78
|
+
*/
|
|
79
|
+
function debounce(func, wait) {
|
|
80
|
+
let timeout;
|
|
81
|
+
return function executedFunction(...args) {
|
|
82
|
+
const later = () => {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
func(...args);
|
|
85
|
+
};
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
timeout = setTimeout(later, wait);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get CSRF token — not used in Express cloud version
|
|
93
|
+
* @returns {null}
|
|
94
|
+
*/
|
|
95
|
+
function getCsrfToken() {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Make an API call with error handling
|
|
101
|
+
* @param {string} url - API endpoint
|
|
102
|
+
* @param {object} options - Fetch options
|
|
103
|
+
* @returns {Promise<object>} Response data
|
|
104
|
+
*/
|
|
105
|
+
async function apiCall(url, options = {}) {
|
|
106
|
+
try {
|
|
107
|
+
const headers = {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
...options.headers
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
headers,
|
|
114
|
+
...options
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(data.error || `HTTP ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return data;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`API Error (${url}):`, error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Copy text to clipboard
|
|
132
|
+
* @param {string} text - Text to copy
|
|
133
|
+
*/
|
|
134
|
+
async function copyToClipboard(text) {
|
|
135
|
+
try {
|
|
136
|
+
await navigator.clipboard.writeText(text);
|
|
137
|
+
showToast('Copied to clipboard', 'success');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Fallback for older browsers
|
|
140
|
+
const textarea = document.createElement('textarea');
|
|
141
|
+
textarea.value = text;
|
|
142
|
+
document.body.appendChild(textarea);
|
|
143
|
+
textarea.select();
|
|
144
|
+
document.execCommand('copy');
|
|
145
|
+
document.body.removeChild(textarea);
|
|
146
|
+
showToast('Copied to clipboard', 'success');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format bytes to human readable
|
|
152
|
+
* @param {number} bytes - Number of bytes
|
|
153
|
+
* @returns {string} Formatted string
|
|
154
|
+
*/
|
|
155
|
+
function formatBytes(bytes) {
|
|
156
|
+
if (bytes === 0) return '0 B';
|
|
157
|
+
const k = 1024;
|
|
158
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
159
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
160
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sleep/delay utility
|
|
165
|
+
* @param {number} ms - Milliseconds to wait
|
|
166
|
+
* @returns {Promise} Resolves after delay
|
|
167
|
+
*/
|
|
168
|
+
function sleep(ms) {
|
|
169
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Toggle password field visibility
|
|
174
|
+
* @param {string} inputId - ID of the input field
|
|
175
|
+
* @param {HTMLElement} btn - The toggle button
|
|
176
|
+
*/
|
|
177
|
+
function toggleVisibility(inputId, btn) {
|
|
178
|
+
const input = document.getElementById(inputId);
|
|
179
|
+
if (input.type === 'password') {
|
|
180
|
+
input.type = 'text';
|
|
181
|
+
btn.textContent = 'Hide';
|
|
182
|
+
} else {
|
|
183
|
+
input.type = 'password';
|
|
184
|
+
btn.textContent = 'Show';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Global error handler for unhandled promise rejections
|
|
189
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
190
|
+
console.error('Unhandled promise rejection:', event.reason);
|
|
191
|
+
// Don't show toast for network errors during polling
|
|
192
|
+
if (!event.reason?.message?.includes('fetch')) {
|
|
193
|
+
showToast('An error occurred', 'error');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Log LobstaKit initialization
|
|
198
|
+
console.log('%c🛠️ LobstaKit Cloud', 'font-size: 16px; font-weight: bold; color: #DC2626;');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LobstaKit Cloud — Login Page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Check if already authenticated
|
|
6
|
+
async function checkAuth() {
|
|
7
|
+
const token = localStorage.getItem('lobstakit_token');
|
|
8
|
+
if (token) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch('/api/auth/status', {
|
|
11
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
12
|
+
});
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
if (data.authenticated) {
|
|
15
|
+
window.location.href = '/manage.html';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Token check failed — continue to login
|
|
20
|
+
}
|
|
21
|
+
// Token expired/invalid
|
|
22
|
+
localStorage.removeItem('lobstakit_token');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if setup is complete
|
|
26
|
+
try {
|
|
27
|
+
const statusRes = await fetch('/api/auth/status');
|
|
28
|
+
const status = await statusRes.json();
|
|
29
|
+
if (!status.passwordSet) {
|
|
30
|
+
window.location.href = '/index.html'; // Go to setup
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Pre-fill email if stored
|
|
34
|
+
if (status.email) {
|
|
35
|
+
const emailEl = document.getElementById('email');
|
|
36
|
+
if (emailEl) {
|
|
37
|
+
emailEl.value = status.email;
|
|
38
|
+
// Focus password field instead since email is pre-filled
|
|
39
|
+
const pwEl = document.getElementById('password');
|
|
40
|
+
if (pwEl) pwEl.focus();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Can't reach server — stay on login page
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function login() {
|
|
49
|
+
const email = document.getElementById('email').value.trim();
|
|
50
|
+
const password = document.getElementById('password').value;
|
|
51
|
+
const errorEl = document.getElementById('login-error');
|
|
52
|
+
const btn = document.getElementById('login-btn');
|
|
53
|
+
errorEl.classList.add('hidden');
|
|
54
|
+
|
|
55
|
+
if (!email || !email.includes('@')) {
|
|
56
|
+
errorEl.textContent = 'Please enter a valid email address';
|
|
57
|
+
errorEl.classList.remove('hidden');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!password) {
|
|
62
|
+
errorEl.textContent = 'Password is required';
|
|
63
|
+
errorEl.classList.remove('hidden');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
btn.disabled = true;
|
|
68
|
+
btn.textContent = 'Signing in...';
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch('/api/auth/login', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ email, password })
|
|
75
|
+
});
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
if (data.status === 'ok') {
|
|
78
|
+
localStorage.setItem('lobstakit_token', data.token);
|
|
79
|
+
window.location.href = '/manage.html';
|
|
80
|
+
} else {
|
|
81
|
+
errorEl.textContent = data.error || 'Login failed';
|
|
82
|
+
errorEl.classList.remove('hidden');
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
errorEl.textContent = 'Connection error';
|
|
86
|
+
errorEl.classList.remove('hidden');
|
|
87
|
+
} finally {
|
|
88
|
+
btn.disabled = false;
|
|
89
|
+
btn.textContent = 'Sign In';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
document.addEventListener('DOMContentLoaded', checkAuth);
|