/
/
/
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>Music Assistant - Create Admin Account</title>
7 <link rel="stylesheet" href="resources/common.css">
8 <style>
9 body {
10 min-height: 100vh;
11 display: flex;
12 justify-content: center;
13 align-items: center;
14 padding: 20px;
15 }
16
17 .setup-container {
18 background: var(--panel);
19 border-radius: 16px;
20 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12),
21 0 0 0 1px var(--border);
22 padding: 48px 40px;
23 width: 100%;
24 max-width: 520px;
25 }
26
27 .logo {
28 text-align: center;
29 margin-bottom: 36px;
30 }
31
32 .logo-icon {
33 width: 72px;
34 height: 72px;
35 margin: 0 auto 16px;
36 }
37
38 .logo-icon img {
39 width: 100%;
40 height: 100%;
41 object-fit: contain;
42 }
43
44 .logo h1 {
45 color: var(--fg);
46 font-size: 24px;
47 font-weight: 600;
48 letter-spacing: -0.5px;
49 margin-bottom: 6px;
50 }
51
52 .logo p {
53 color: var(--text-tertiary);
54 font-size: 14px;
55 font-weight: 400;
56 }
57
58 .header {
59 margin-bottom: 24px;
60 }
61
62 .header h2 {
63 color: var(--fg);
64 font-size: 20px;
65 font-weight: 600;
66 margin-bottom: 8px;
67 }
68
69 .header p {
70 color: var(--text-secondary);
71 font-size: 14px;
72 line-height: 1.6;
73 }
74
75 .password-requirements {
76 margin-top: 8px;
77 font-size: 12px;
78 color: var(--text-tertiary);
79 }
80
81 .form-actions {
82 margin-top: 24px;
83 }
84
85 .btn {
86 width: 100%;
87 padding: 15px;
88 border: none;
89 border-radius: 10px;
90 font-size: 15px;
91 font-weight: 600;
92 cursor: pointer;
93 transition: all 0.2s ease;
94 letter-spacing: 0.3px;
95 }
96
97 .btn-primary {
98 background: var(--primary);
99 color: white;
100 }
101
102 .btn-primary:hover {
103 filter: brightness(1.1);
104 box-shadow: 0 8px 24px var(--primary-glow);
105 transform: translateY(-1px);
106 }
107
108 .btn-primary:active {
109 transform: translateY(0);
110 filter: brightness(0.95);
111 }
112
113 .btn-primary:disabled {
114 opacity: 0.5;
115 cursor: not-allowed;
116 transform: none;
117 box-shadow: none;
118 filter: none;
119 }
120
121 </style>
122</head>
123<body>
124 <div class="setup-container">
125 <div class="logo">
126 <div class="logo-icon">
127 <img src="logo.png" alt="Music Assistant">
128 </div>
129 <h1>Music Assistant</h1>
130 <p>Create Admin Account</p>
131 </div>
132
133 <div class="header">
134 <h2>Welcome!</h2>
135 <p>Create an administrator account to get started with Music Assistant.</p>
136 </div>
137
138 <div class="error-message" id="errorMessage"></div>
139
140 <form id="setupForm">
141 <div class="form-group">
142 <label for="username">Username</label>
143 <input
144 type="text"
145 id="username"
146 name="username"
147 required
148 autocomplete="username"
149 placeholder="Enter your username"
150 minlength="2"
151 >
152 </div>
153
154 <div class="form-group">
155 <label for="password">Password</label>
156 <input
157 type="password"
158 id="password"
159 name="password"
160 required
161 autocomplete="new-password"
162 placeholder="Enter a secure password"
163 minlength="8"
164 >
165 <div class="password-requirements">
166 Minimum 8 characters
167 </div>
168 </div>
169
170 <div class="form-group">
171 <label for="confirmPassword">Confirm Password</label>
172 <input
173 type="password"
174 id="confirmPassword"
175 name="confirmPassword"
176 required
177 autocomplete="new-password"
178 placeholder="Re-enter your password"
179 >
180 </div>
181
182 <div class="form-actions">
183 <button type="submit" class="btn btn-primary" id="createAccountBtn">Create Account</button>
184 </div>
185 </form>
186
187 </div>
188
189 <script>
190 const urlParams = new URLSearchParams(window.location.search);
191 const deviceName = urlParams.get('device_name');
192 const returnUrl = urlParams.get('return_url');
193
194 function isValidRedirectUrl(url) {
195 if (!url) return false;
196 try {
197 const parsed = new URL(url, window.location.origin);
198 const allowedProtocols = ['http:', 'https:', 'musicassistant:'];
199 return allowedProtocols.includes(parsed.protocol);
200 } catch {
201 return false;
202 }
203 }
204
205 function showError(message) {
206 const errorMessage = document.getElementById('errorMessage');
207 errorMessage.textContent = message;
208 errorMessage.classList.add('show');
209 }
210
211 function hideError() {
212 const errorMessage = document.getElementById('errorMessage');
213 errorMessage.classList.remove('show');
214 }
215
216 function redirectWithToken(token) {
217 if (returnUrl && isValidRedirectUrl(returnUrl)) {
218 let finalUrl = returnUrl;
219
220 if (returnUrl.includes('#')) {
221 const parts = returnUrl.split('#', 2);
222 const basePart = parts[0];
223 const hashPart = parts[1];
224 const separator = basePart.includes('?') ? '&' : '?';
225 finalUrl = `${basePart}${separator}code=${encodeURIComponent(token)}&onboard=true#${hashPart}`;
226 } else {
227 const separator = returnUrl.includes('?') ? '&' : '?';
228 finalUrl = `${returnUrl}${separator}code=${encodeURIComponent(token)}&onboard=true`;
229 }
230
231 window.location.href = finalUrl;
232 } else {
233 window.location.href = `./?code=${encodeURIComponent(token)}&onboard=true`;
234 }
235 }
236
237 document.getElementById('setupForm').addEventListener('submit', async (e) => {
238 e.preventDefault();
239 hideError();
240
241 const username = document.getElementById('username').value.trim();
242 const password = document.getElementById('password').value;
243 const confirmPassword = document.getElementById('confirmPassword').value;
244
245 if (username.length < 2) {
246 showError('Username must be at least 2 characters long');
247 return;
248 }
249
250 if (password.length < 8) {
251 showError('Password must be at least 8 characters long');
252 return;
253 }
254
255 if (password !== confirmPassword) {
256 showError('Passwords do not match');
257 return;
258 }
259
260 const submitBtn = document.getElementById('createAccountBtn');
261 submitBtn.disabled = true;
262 submitBtn.textContent = 'Creating Account...';
263
264 try {
265 const requestBody = {
266 username: username,
267 password: password,
268 };
269
270 if (deviceName) {
271 requestBody.device_name = deviceName;
272 }
273
274 const response = await fetch('setup', {
275 method: 'POST',
276 headers: {
277 'Content-Type': 'application/json',
278 },
279 body: JSON.stringify(requestBody),
280 });
281
282 const data = await response.json();
283
284 if (response.ok && data.success) {
285 redirectWithToken(data.token);
286 } else {
287 submitBtn.disabled = false;
288 submitBtn.textContent = 'Create Account';
289 showError(data.error || 'Setup failed. Please try again.');
290 }
291 } catch (error) {
292 submitBtn.disabled = false;
293 submitBtn.textContent = 'Create Account';
294 showError('Network error. Please check your connection and try again.');
295 console.error('Setup error:', error);
296 }
297 });
298
299 document.querySelectorAll('input').forEach(input => {
300 input.addEventListener('input', hideError);
301 });
302 </script>
303</body>
304</html>
305