skopix 2.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/.dockerignore +65 -0
- package/.github/workflows/docker.yml +78 -0
- package/cli/commands/agent.js +378 -0
- package/cli/commands/config.js +67 -0
- package/cli/commands/dashboard.js +3524 -0
- package/cli/commands/init.js +190 -0
- package/cli/commands/report.js +41 -0
- package/cli/commands/run.js +350 -0
- package/cli/index.js +85 -0
- package/cli/ui.js +126 -0
- package/core/auth.js +148 -0
- package/core/browser.js +1049 -0
- package/core/credentials.js +47 -0
- package/core/db.js +503 -0
- package/core/llm.js +641 -0
- package/core/recorder.js +653 -0
- package/core/reporter.js +282 -0
- package/core/tracker.js +768 -0
- package/package.json +54 -0
- package/web/app/index.html +5937 -0
- package/web/index.html +644 -0
- package/web/invite.html +244 -0
- package/web/login.html +271 -0
- package/web/reset.html +222 -0
- package/web/setup.html +300 -0
package/web/reset.html
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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>Skopix · Reset password</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@500;700&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #080810; --surface: #0d0d1a; --surface2: #12121f;
|
|
13
|
+
--border: rgba(255,255,255,0.06);
|
|
14
|
+
--cyan: #00d4ff; --cyan-dim: rgba(0,212,255,0.12);
|
|
15
|
+
--red: #ef4444; --green: #10b981; --yellow: #f59e0b;
|
|
16
|
+
--text: #e8eaf0; --muted: #5a6180; --muted2: #3a3f5c;
|
|
17
|
+
--white: #ffffff;
|
|
18
|
+
--mono: 'DM Mono', monospace; --display: 'Syne', sans-serif;
|
|
19
|
+
}
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
background: var(--bg); color: var(--text); font-family: var(--display);
|
|
23
|
+
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
24
|
+
padding: 24px; position: relative; overflow-x: hidden;
|
|
25
|
+
}
|
|
26
|
+
body::before {
|
|
27
|
+
content: ''; position: fixed; inset: 0;
|
|
28
|
+
background-image:
|
|
29
|
+
linear-gradient(rgba(0,212,255,0.03) 1px, transparent 1px),
|
|
30
|
+
linear-gradient(90deg, rgba(0,212,255,0.03) 1px, transparent 1px);
|
|
31
|
+
background-size: 60px 60px; pointer-events: none; z-index: 0;
|
|
32
|
+
}
|
|
33
|
+
body::after {
|
|
34
|
+
content: ''; position: fixed; inset: 0;
|
|
35
|
+
background: radial-gradient(circle at 50% 30%, rgba(0,212,255,0.08) 0%, transparent 60%);
|
|
36
|
+
pointer-events: none; z-index: 0;
|
|
37
|
+
}
|
|
38
|
+
.wrap { position: relative; z-index: 1; max-width: 480px; width: 100%; }
|
|
39
|
+
.brand {
|
|
40
|
+
font-family: var(--mono); font-size: 13px; font-weight: 500;
|
|
41
|
+
color: var(--cyan); letter-spacing: 0.2em; text-align: center; margin-bottom: 12px;
|
|
42
|
+
}
|
|
43
|
+
.title {
|
|
44
|
+
font-family: var(--display); font-weight: 700; font-size: 36px;
|
|
45
|
+
color: var(--white); text-align: center; margin-bottom: 12px; line-height: 1.1;
|
|
46
|
+
}
|
|
47
|
+
.sub {
|
|
48
|
+
font-family: var(--mono); font-size: 13px; color: var(--muted);
|
|
49
|
+
text-align: center; margin-bottom: 36px; line-height: 1.6;
|
|
50
|
+
}
|
|
51
|
+
.card {
|
|
52
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
53
|
+
border-radius: 16px; padding: 32px; box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
54
|
+
}
|
|
55
|
+
.field { margin-bottom: 18px; }
|
|
56
|
+
.label {
|
|
57
|
+
display: block; font-family: var(--mono); font-size: 11px;
|
|
58
|
+
letter-spacing: 0.1em; color: var(--muted); text-transform: uppercase; margin-bottom: 8px;
|
|
59
|
+
}
|
|
60
|
+
.input {
|
|
61
|
+
width: 100%; padding: 12px 14px;
|
|
62
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
63
|
+
border-radius: 8px; color: var(--white); font-family: var(--mono);
|
|
64
|
+
font-size: 14px; outline: none; transition: border-color 0.2s;
|
|
65
|
+
}
|
|
66
|
+
.input:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px var(--cyan-dim); }
|
|
67
|
+
.help { font-family: var(--mono); font-size: 11px; color: var(--muted2); margin-top: 6px; }
|
|
68
|
+
.btn {
|
|
69
|
+
width: 100%; padding: 14px 24px;
|
|
70
|
+
background: var(--cyan); color: var(--bg); border: none;
|
|
71
|
+
border-radius: 8px; font-family: var(--mono); font-size: 13px;
|
|
72
|
+
font-weight: 500; letter-spacing: 0.05em; cursor: pointer;
|
|
73
|
+
transition: all 0.2s; margin-top: 8px; text-transform: uppercase;
|
|
74
|
+
}
|
|
75
|
+
.btn:hover { background: #00b8e0; box-shadow: 0 0 30px rgba(0,212,255,0.3); }
|
|
76
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
77
|
+
.alert {
|
|
78
|
+
padding: 12px 14px; border-radius: 8px;
|
|
79
|
+
font-family: var(--mono); font-size: 12px;
|
|
80
|
+
margin-bottom: 16px; line-height: 1.5;
|
|
81
|
+
}
|
|
82
|
+
.alert.error {
|
|
83
|
+
background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: var(--red);
|
|
84
|
+
}
|
|
85
|
+
.alert.success {
|
|
86
|
+
background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); color: var(--green);
|
|
87
|
+
}
|
|
88
|
+
.note {
|
|
89
|
+
font-family: var(--mono); font-size: 11px; color: var(--muted);
|
|
90
|
+
text-align: center; margin-top: 24px; line-height: 1.6;
|
|
91
|
+
}
|
|
92
|
+
.hidden { display: none; }
|
|
93
|
+
.loading {
|
|
94
|
+
text-align: center; padding: 40px;
|
|
95
|
+
font-family: var(--mono); font-size: 13px; color: var(--muted);
|
|
96
|
+
}
|
|
97
|
+
.context-pill {
|
|
98
|
+
display: inline-flex; gap: 6px; align-items: center;
|
|
99
|
+
padding: 4px 10px; border-radius: 999px;
|
|
100
|
+
background: rgba(255,255,255,0.04); border: 1px solid var(--border);
|
|
101
|
+
font-family: var(--mono); font-size: 11px; color: var(--text);
|
|
102
|
+
letter-spacing: 0.05em; margin-bottom: 18px;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<div class="wrap">
|
|
108
|
+
<div class="brand">SKOPIX · RESET PASSWORD</div>
|
|
109
|
+
<h1 class="title">Set a new<br>password.</h1>
|
|
110
|
+
<p class="sub" id="sub">Loading reset link...</p>
|
|
111
|
+
|
|
112
|
+
<div id="loading-card" class="card loading">Verifying link...</div>
|
|
113
|
+
|
|
114
|
+
<div id="form-card" class="card hidden">
|
|
115
|
+
<div style="text-align:center">
|
|
116
|
+
<div class="context-pill" id="email-pill">EMAIL</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div id="alert" class="alert hidden"></div>
|
|
120
|
+
|
|
121
|
+
<form id="reset-form">
|
|
122
|
+
<div class="field">
|
|
123
|
+
<label class="label" for="password">New password</label>
|
|
124
|
+
<input class="input" id="password" name="password" type="password" autocomplete="new-password" required minlength="8" placeholder="At least 8 characters">
|
|
125
|
+
<div class="help">Make it strong — at least 8 characters.</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="field">
|
|
129
|
+
<label class="label" for="password2">Confirm new password</label>
|
|
130
|
+
<input class="input" id="password2" name="password2" type="password" autocomplete="new-password" required minlength="8" placeholder="Type it again">
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<button id="submit-btn" class="btn" type="submit">Set new password →</button>
|
|
134
|
+
</form>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div id="error-card" class="card hidden">
|
|
138
|
+
<div class="alert error" id="error-message" style="margin-bottom:0">This reset link is no longer valid.</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<p class="note">Need a fresh link? Ask your Skopix admin.</p>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
const token = window.location.pathname.replace(/^\/reset\//, '').replace(/\/$/, '');
|
|
146
|
+
const loadingCard = document.getElementById('loading-card');
|
|
147
|
+
const formCard = document.getElementById('form-card');
|
|
148
|
+
const errorCard = document.getElementById('error-card');
|
|
149
|
+
const errorMsg = document.getElementById('error-message');
|
|
150
|
+
const sub = document.getElementById('sub');
|
|
151
|
+
const alertEl = document.getElementById('alert');
|
|
152
|
+
const submitBtn = document.getElementById('submit-btn');
|
|
153
|
+
|
|
154
|
+
function showAlert(msg, kind = 'error') {
|
|
155
|
+
alertEl.textContent = msg;
|
|
156
|
+
alertEl.className = 'alert ' + kind;
|
|
157
|
+
alertEl.classList.remove('hidden');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function loadReset() {
|
|
161
|
+
if (!token || token.length < 8) {
|
|
162
|
+
loadingCard.classList.add('hidden');
|
|
163
|
+
errorCard.classList.remove('hidden');
|
|
164
|
+
errorMsg.textContent = 'No reset token provided.';
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch('/api/password-reset/' + encodeURIComponent(token));
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
loadingCard.classList.add('hidden');
|
|
172
|
+
errorCard.classList.remove('hidden');
|
|
173
|
+
errorMsg.textContent = data.error || 'Invalid or expired link.';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
loadingCard.classList.add('hidden');
|
|
177
|
+
formCard.classList.remove('hidden');
|
|
178
|
+
sub.textContent = `Setting a new password for ${data.name}.`;
|
|
179
|
+
document.getElementById('email-pill').textContent = data.email;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
loadingCard.classList.add('hidden');
|
|
182
|
+
errorCard.classList.remove('hidden');
|
|
183
|
+
errorMsg.textContent = 'Network error: ' + err.message;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
loadReset();
|
|
187
|
+
|
|
188
|
+
document.getElementById('reset-form').addEventListener('submit', async (e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
alertEl.classList.add('hidden');
|
|
191
|
+
const password = document.getElementById('password').value;
|
|
192
|
+
const password2 = document.getElementById('password2').value;
|
|
193
|
+
if (password !== password2) { showAlert("Passwords don't match."); return; }
|
|
194
|
+
if (password.length < 8) { showAlert('Password must be at least 8 characters.'); return; }
|
|
195
|
+
|
|
196
|
+
submitBtn.disabled = true;
|
|
197
|
+
submitBtn.textContent = 'Setting password...';
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch('/api/password-reset/' + encodeURIComponent(token), {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({ newPassword: password }),
|
|
204
|
+
});
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
showAlert(data.error || 'Failed to reset password.');
|
|
208
|
+
submitBtn.disabled = false;
|
|
209
|
+
submitBtn.textContent = 'Set new password →';
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
showAlert('Password set. Redirecting to login...', 'success');
|
|
213
|
+
setTimeout(() => { window.location.href = '/login'; }, 1000);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
showAlert('Network error: ' + err.message);
|
|
216
|
+
submitBtn.disabled = false;
|
|
217
|
+
submitBtn.textContent = 'Set new password →';
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
</script>
|
|
221
|
+
</body>
|
|
222
|
+
</html>
|
package/web/setup.html
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
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>Skopix · Setup</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@500;700&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #080810;
|
|
13
|
+
--surface: #0d0d1a;
|
|
14
|
+
--surface2: #12121f;
|
|
15
|
+
--border: rgba(255,255,255,0.06);
|
|
16
|
+
--border-bright: rgba(0,212,255,0.2);
|
|
17
|
+
--cyan: #00d4ff;
|
|
18
|
+
--cyan-dim: rgba(0,212,255,0.12);
|
|
19
|
+
--red: #ef4444;
|
|
20
|
+
--green: #10b981;
|
|
21
|
+
--yellow: #f59e0b;
|
|
22
|
+
--text: #e8eaf0;
|
|
23
|
+
--muted: #5a6180;
|
|
24
|
+
--muted2: #3a3f5c;
|
|
25
|
+
--white: #ffffff;
|
|
26
|
+
--mono: 'DM Mono', monospace;
|
|
27
|
+
--display: 'Syne', sans-serif;
|
|
28
|
+
}
|
|
29
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
|
+
body {
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
font-family: var(--display);
|
|
34
|
+
min-height: 100vh;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
padding: 24px;
|
|
39
|
+
position: relative;
|
|
40
|
+
overflow-x: hidden;
|
|
41
|
+
}
|
|
42
|
+
body::before {
|
|
43
|
+
content: '';
|
|
44
|
+
position: fixed;
|
|
45
|
+
inset: 0;
|
|
46
|
+
background-image:
|
|
47
|
+
linear-gradient(rgba(0,212,255,0.03) 1px, transparent 1px),
|
|
48
|
+
linear-gradient(90deg, rgba(0,212,255,0.03) 1px, transparent 1px);
|
|
49
|
+
background-size: 60px 60px;
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
z-index: 0;
|
|
52
|
+
}
|
|
53
|
+
body::after {
|
|
54
|
+
content: '';
|
|
55
|
+
position: fixed;
|
|
56
|
+
inset: 0;
|
|
57
|
+
background: radial-gradient(circle at 50% 30%, rgba(0,212,255,0.08) 0%, transparent 60%);
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
z-index: 0;
|
|
60
|
+
}
|
|
61
|
+
.wrap {
|
|
62
|
+
position: relative;
|
|
63
|
+
z-index: 1;
|
|
64
|
+
max-width: 480px;
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
.brand {
|
|
68
|
+
font-family: var(--mono);
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
color: var(--cyan);
|
|
72
|
+
letter-spacing: 0.2em;
|
|
73
|
+
text-align: center;
|
|
74
|
+
margin-bottom: 12px;
|
|
75
|
+
}
|
|
76
|
+
.title {
|
|
77
|
+
font-family: var(--display);
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
font-size: 36px;
|
|
80
|
+
color: var(--white);
|
|
81
|
+
text-align: center;
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
line-height: 1.1;
|
|
84
|
+
}
|
|
85
|
+
.sub {
|
|
86
|
+
font-family: var(--mono);
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
text-align: center;
|
|
90
|
+
margin-bottom: 36px;
|
|
91
|
+
line-height: 1.6;
|
|
92
|
+
}
|
|
93
|
+
.card {
|
|
94
|
+
background: var(--surface);
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
border-radius: 16px;
|
|
97
|
+
padding: 32px;
|
|
98
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
99
|
+
}
|
|
100
|
+
.field {
|
|
101
|
+
margin-bottom: 18px;
|
|
102
|
+
}
|
|
103
|
+
.label {
|
|
104
|
+
display: block;
|
|
105
|
+
font-family: var(--mono);
|
|
106
|
+
font-size: 11px;
|
|
107
|
+
letter-spacing: 0.1em;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
text-transform: uppercase;
|
|
110
|
+
margin-bottom: 8px;
|
|
111
|
+
}
|
|
112
|
+
.input {
|
|
113
|
+
width: 100%;
|
|
114
|
+
padding: 12px 14px;
|
|
115
|
+
background: var(--surface2);
|
|
116
|
+
border: 1px solid var(--border);
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
color: var(--white);
|
|
119
|
+
font-family: var(--mono);
|
|
120
|
+
font-size: 14px;
|
|
121
|
+
outline: none;
|
|
122
|
+
transition: border-color 0.2s;
|
|
123
|
+
}
|
|
124
|
+
.input:focus {
|
|
125
|
+
border-color: var(--cyan);
|
|
126
|
+
box-shadow: 0 0 0 3px var(--cyan-dim);
|
|
127
|
+
}
|
|
128
|
+
.help {
|
|
129
|
+
font-family: var(--mono);
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
color: var(--muted2);
|
|
132
|
+
margin-top: 6px;
|
|
133
|
+
}
|
|
134
|
+
.btn {
|
|
135
|
+
width: 100%;
|
|
136
|
+
padding: 14px 24px;
|
|
137
|
+
background: var(--cyan);
|
|
138
|
+
color: var(--bg);
|
|
139
|
+
border: none;
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
font-family: var(--mono);
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
letter-spacing: 0.05em;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
transition: all 0.2s;
|
|
147
|
+
margin-top: 8px;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
}
|
|
150
|
+
.btn:hover { background: #00b8e0; box-shadow: 0 0 30px rgba(0,212,255,0.3); }
|
|
151
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
152
|
+
.alert {
|
|
153
|
+
padding: 12px 14px;
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
font-family: var(--mono);
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
margin-bottom: 16px;
|
|
158
|
+
line-height: 1.5;
|
|
159
|
+
}
|
|
160
|
+
.alert.error {
|
|
161
|
+
background: rgba(239,68,68,0.1);
|
|
162
|
+
border: 1px solid rgba(239,68,68,0.3);
|
|
163
|
+
color: var(--red);
|
|
164
|
+
}
|
|
165
|
+
.alert.success {
|
|
166
|
+
background: rgba(16,185,129,0.1);
|
|
167
|
+
border: 1px solid rgba(16,185,129,0.3);
|
|
168
|
+
color: var(--green);
|
|
169
|
+
}
|
|
170
|
+
.note {
|
|
171
|
+
font-family: var(--mono);
|
|
172
|
+
font-size: 11px;
|
|
173
|
+
color: var(--muted);
|
|
174
|
+
text-align: center;
|
|
175
|
+
margin-top: 24px;
|
|
176
|
+
line-height: 1.6;
|
|
177
|
+
}
|
|
178
|
+
.hidden { display: none; }
|
|
179
|
+
</style>
|
|
180
|
+
</head>
|
|
181
|
+
<body>
|
|
182
|
+
<div class="wrap">
|
|
183
|
+
<div class="brand">SKOPIX · TEAM SETUP</div>
|
|
184
|
+
<h1 class="title">Create the<br>first admin.</h1>
|
|
185
|
+
<p class="sub">
|
|
186
|
+
This account will manage your Skopix workspace.<br>
|
|
187
|
+
Choose carefully — admins can invite others and edit anything.
|
|
188
|
+
</p>
|
|
189
|
+
|
|
190
|
+
<div class="card">
|
|
191
|
+
<div id="alert" class="alert hidden"></div>
|
|
192
|
+
|
|
193
|
+
<form id="setup-form">
|
|
194
|
+
<div class="field">
|
|
195
|
+
<label class="label" for="name">Full name</label>
|
|
196
|
+
<input class="input" id="name" name="name" type="text" autocomplete="name" required minlength="1" placeholder="Adil Khan">
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="field">
|
|
200
|
+
<label class="label" for="email">Email address</label>
|
|
201
|
+
<input class="input" id="email" name="email" type="email" autocomplete="email" required placeholder="adil@yourcompany.com">
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="field">
|
|
205
|
+
<label class="label" for="password">Password</label>
|
|
206
|
+
<input class="input" id="password" name="password" type="password" autocomplete="new-password" required minlength="8" placeholder="At least 8 characters">
|
|
207
|
+
<div class="help">Use a strong password — this account has full control.</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="field">
|
|
211
|
+
<label class="label" for="password2">Confirm password</label>
|
|
212
|
+
<input class="input" id="password2" name="password2" type="password" autocomplete="new-password" required minlength="8" placeholder="Type it again">
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<button id="submit-btn" class="btn" type="submit">Create admin account →</button>
|
|
216
|
+
</form>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<p class="note">
|
|
220
|
+
Skopix runs on your own infrastructure. Your data never leaves this server.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
const form = document.getElementById('setup-form');
|
|
226
|
+
const alertEl = document.getElementById('alert');
|
|
227
|
+
const submitBtn = document.getElementById('submit-btn');
|
|
228
|
+
|
|
229
|
+
function showAlert(message, kind = 'error') {
|
|
230
|
+
alertEl.textContent = message;
|
|
231
|
+
alertEl.className = 'alert ' + kind;
|
|
232
|
+
alertEl.classList.remove('hidden');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hideAlert() {
|
|
236
|
+
alertEl.classList.add('hidden');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check on load — if setup is already done, redirect away
|
|
240
|
+
async function checkStatus() {
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch('/api/team/status');
|
|
243
|
+
if (res.ok) {
|
|
244
|
+
const data = await res.json();
|
|
245
|
+
if (!data.needsSetup) {
|
|
246
|
+
showAlert('Setup is already complete. Redirecting to login...', 'success');
|
|
247
|
+
setTimeout(() => { window.location.href = '/app/'; }, 1500);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
checkStatus();
|
|
253
|
+
|
|
254
|
+
form.addEventListener('submit', async (e) => {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
hideAlert();
|
|
257
|
+
|
|
258
|
+
const name = document.getElementById('name').value.trim();
|
|
259
|
+
const email = document.getElementById('email').value.trim();
|
|
260
|
+
const password = document.getElementById('password').value;
|
|
261
|
+
const password2 = document.getElementById('password2').value;
|
|
262
|
+
|
|
263
|
+
if (password !== password2) {
|
|
264
|
+
showAlert("Passwords don't match.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (password.length < 8) {
|
|
268
|
+
showAlert('Password must be at least 8 characters.');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
submitBtn.disabled = true;
|
|
273
|
+
submitBtn.textContent = 'Creating account...';
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const res = await fetch('/api/setup', {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
279
|
+
body: JSON.stringify({ name, email, password }),
|
|
280
|
+
});
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
showAlert(data.error || 'Setup failed. Please try again.');
|
|
285
|
+
submitBtn.disabled = false;
|
|
286
|
+
submitBtn.textContent = 'Create admin account →';
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
showAlert('Admin account created. Redirecting...', 'success');
|
|
291
|
+
setTimeout(() => { window.location.href = '/app/'; }, 1500);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
showAlert('Network error: ' + err.message);
|
|
294
|
+
submitBtn.disabled = false;
|
|
295
|
+
submitBtn.textContent = 'Create admin account →';
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
</script>
|
|
299
|
+
</body>
|
|
300
|
+
</html>
|