@mjasano/devtunnel 1.2.0 → 1.4.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/.claude/settings.local.json +5 -1
- package/.prettierignore +2 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +34 -0
- package/README.md +138 -0
- package/bin/cli.js +40 -11
- package/eslint.config.js +32 -0
- package/package.json +12 -3
- package/public/app.js +839 -0
- package/public/index.html +10 -1501
- package/public/login.html +242 -0
- package/public/styles.css +857 -0
- package/server.js +276 -7
- package/test/server.test.js +204 -0
|
@@ -0,0 +1,242 @@
|
|
|
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>DevTunnel - Login</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
background-color: #0d1117;
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
color: #c9d1d9;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.login-container {
|
|
25
|
+
background-color: #161b22;
|
|
26
|
+
border: 1px solid #30363d;
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
padding: 40px;
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: 400px;
|
|
31
|
+
text-align: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo {
|
|
35
|
+
width: 64px;
|
|
36
|
+
height: 64px;
|
|
37
|
+
background: linear-gradient(135deg, #58a6ff, #8b5cf6);
|
|
38
|
+
border-radius: 16px;
|
|
39
|
+
margin: 0 auto 24px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1 {
|
|
43
|
+
font-size: 24px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
margin-bottom: 8px;
|
|
46
|
+
color: #fff;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.subtitle {
|
|
50
|
+
color: #8b949e;
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
margin-bottom: 32px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.form-group {
|
|
56
|
+
margin-bottom: 24px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.form-group label {
|
|
60
|
+
display: block;
|
|
61
|
+
text-align: left;
|
|
62
|
+
font-size: 14px;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
margin-bottom: 8px;
|
|
65
|
+
color: #c9d1d9;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-group input {
|
|
69
|
+
width: 100%;
|
|
70
|
+
padding: 12px 16px;
|
|
71
|
+
background-color: #0d1117;
|
|
72
|
+
border: 1px solid #30363d;
|
|
73
|
+
border-radius: 8px;
|
|
74
|
+
color: #c9d1d9;
|
|
75
|
+
font-size: 16px;
|
|
76
|
+
text-align: center;
|
|
77
|
+
letter-spacing: 4px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.form-group input:focus {
|
|
81
|
+
outline: none;
|
|
82
|
+
border-color: #58a6ff;
|
|
83
|
+
box-shadow: 0 0 0 3px rgba(56, 139, 253, 0.2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.form-group input::placeholder {
|
|
87
|
+
color: #6e7681;
|
|
88
|
+
letter-spacing: normal;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.btn {
|
|
92
|
+
width: 100%;
|
|
93
|
+
padding: 12px 24px;
|
|
94
|
+
background-color: #238636;
|
|
95
|
+
color: #fff;
|
|
96
|
+
border: none;
|
|
97
|
+
border-radius: 8px;
|
|
98
|
+
font-size: 16px;
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: background-color 0.15s ease;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.btn:hover {
|
|
105
|
+
background-color: #2ea043;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.btn:disabled {
|
|
109
|
+
background-color: #21262d;
|
|
110
|
+
color: #484f58;
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.error-message {
|
|
115
|
+
background-color: rgba(248, 81, 73, 0.1);
|
|
116
|
+
border: 1px solid #f85149;
|
|
117
|
+
color: #f85149;
|
|
118
|
+
padding: 12px;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
margin-bottom: 24px;
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
display: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.error-message.show {
|
|
126
|
+
display: block;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.loading {
|
|
130
|
+
display: inline-block;
|
|
131
|
+
width: 16px;
|
|
132
|
+
height: 16px;
|
|
133
|
+
border: 2px solid #fff;
|
|
134
|
+
border-radius: 50%;
|
|
135
|
+
border-top-color: transparent;
|
|
136
|
+
animation: spin 0.8s linear infinite;
|
|
137
|
+
margin-right: 8px;
|
|
138
|
+
vertical-align: middle;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes spin {
|
|
142
|
+
to { transform: rotate(360deg); }
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<div class="login-container">
|
|
148
|
+
<div class="logo"></div>
|
|
149
|
+
<h1>DevTunnel</h1>
|
|
150
|
+
<p class="subtitle">Enter passcode to continue</p>
|
|
151
|
+
|
|
152
|
+
<div class="error-message" id="error-message"></div>
|
|
153
|
+
|
|
154
|
+
<form id="login-form">
|
|
155
|
+
<div class="form-group">
|
|
156
|
+
<label for="passcode">Passcode</label>
|
|
157
|
+
<input
|
|
158
|
+
type="password"
|
|
159
|
+
id="passcode"
|
|
160
|
+
name="passcode"
|
|
161
|
+
placeholder="Enter passcode"
|
|
162
|
+
autocomplete="off"
|
|
163
|
+
autofocus
|
|
164
|
+
required
|
|
165
|
+
>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<button type="submit" class="btn" id="submit-btn">
|
|
169
|
+
Login
|
|
170
|
+
</button>
|
|
171
|
+
</form>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<script>
|
|
175
|
+
const form = document.getElementById('login-form');
|
|
176
|
+
const passcodeInput = document.getElementById('passcode');
|
|
177
|
+
const submitBtn = document.getElementById('submit-btn');
|
|
178
|
+
const errorMessage = document.getElementById('error-message');
|
|
179
|
+
|
|
180
|
+
// Check if already authenticated
|
|
181
|
+
fetch('/api/auth/status')
|
|
182
|
+
.then(res => res.json())
|
|
183
|
+
.then(data => {
|
|
184
|
+
if (data.authenticated) {
|
|
185
|
+
window.location.href = '/';
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
form.addEventListener('submit', async (e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
|
|
192
|
+
const passcode = passcodeInput.value;
|
|
193
|
+
|
|
194
|
+
if (!passcode) {
|
|
195
|
+
showError('Please enter a passcode');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setLoading(true);
|
|
200
|
+
hideError();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch('/api/auth/login', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
body: JSON.stringify({ passcode })
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const data = await res.json();
|
|
210
|
+
|
|
211
|
+
if (data.success) {
|
|
212
|
+
window.location.href = '/';
|
|
213
|
+
} else {
|
|
214
|
+
showError(data.error || 'Invalid passcode');
|
|
215
|
+
passcodeInput.value = '';
|
|
216
|
+
passcodeInput.focus();
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
showError('Connection error. Please try again.');
|
|
220
|
+
} finally {
|
|
221
|
+
setLoading(false);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function showError(message) {
|
|
226
|
+
errorMessage.textContent = message;
|
|
227
|
+
errorMessage.classList.add('show');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function hideError() {
|
|
231
|
+
errorMessage.classList.remove('show');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function setLoading(loading) {
|
|
235
|
+
submitBtn.disabled = loading;
|
|
236
|
+
submitBtn.innerHTML = loading
|
|
237
|
+
? '<span class="loading"></span>Logging in...'
|
|
238
|
+
: 'Login';
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
</body>
|
|
242
|
+
</html>
|