music-assistant-server

9.1 KBHTML
setup.html
9.1 KB305 lines • xml
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