/
/
/
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>Login - Music Assistant</title>
7 <link rel="stylesheet" href="resources/common.css">
8 <style>
9 body {
10 min-height: 100vh;
11 display: flex;
12 align-items: center;
13 justify-content: center;
14 padding: 20px;
15 }
16
17 .login-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 width: 100%;
23 max-width: 400px;
24 padding: 48px 40px;
25 }
26
27 h1 {
28 text-align: center;
29 color: var(--fg);
30 font-size: 24px;
31 font-weight: 600;
32 letter-spacing: -0.5px;
33 margin-bottom: 8px;
34 }
35
36 .subtitle {
37 text-align: center;
38 color: var(--text-tertiary);
39 font-size: 14px;
40 margin-bottom: 32px;
41 }
42
43 .oauth-providers {
44 margin-top: 8px;
45 }
46 </style>
47</head>
48<body>
49 <div class="login-container">
50 <div class="logo">
51 <img src="logo.png" alt="Music Assistant">
52 </div>
53
54 <h1>Music Assistant</h1>
55 <p class="subtitle">External Client Authentication</p>
56
57 <div id="error" class="error"></div>
58
59 <form id="loginForm">
60 <div class="form-group">
61 <label for="username">Username</label>
62 <input type="text" id="username" name="username" required autofocus placeholder="Enter your username">
63 </div>
64
65 <div class="form-group">
66 <label for="password">Password</label>
67 <input type="password" id="password" name="password" required placeholder="Enter your password">
68 </div>
69
70 <button type="submit" class="btn btn-primary" id="loginBtn">
71 <span id="loginText">Sign In</span>
72 <span id="loginLoading" class="loading" style="display: none;"></span>
73 </button>
74 </form>
75
76 <div id="oauthProviders" class="oauth-providers">
77 <!-- OAuth providers will be inserted here -->
78 </div>
79 </div>
80
81 <script>
82 const API_BASE = window.location.origin;
83
84 // Get return_url and device_name from query string
85 const urlParams = new URLSearchParams(window.location.search);
86 const returnUrl = urlParams.get('return_url');
87 const deviceName = urlParams.get('device_name');
88
89 // Show error message
90 function showError(message) {
91 const errorEl = document.getElementById('error');
92 errorEl.textContent = message;
93 errorEl.classList.add('show');
94 }
95
96 // Hide error message
97 function hideError() {
98 document.getElementById('error').classList.remove('show');
99 }
100
101 // Set loading state
102 function setLoading(loading) {
103 const btn = document.getElementById('loginBtn');
104 const text = document.getElementById('loginText');
105 const loadingEl = document.getElementById('loginLoading');
106
107 btn.disabled = loading;
108 text.style.display = loading ? 'none' : 'inline';
109 loadingEl.style.display = loading ? 'inline-block' : 'none';
110 }
111
112 // Load OAuth providers
113 async function loadProviders() {
114 try {
115 const response = await fetch(`${API_BASE}/auth/providers`);
116 const providers = await response.json();
117
118 const oauthProviders = providers.filter(p => p.requires_redirect && p.provider_type !== 'builtin');
119
120 if (oauthProviders.length > 0) {
121 const container = document.getElementById('oauthProviders');
122
123 // Add divider
124 const divider = document.createElement('div');
125 divider.className = 'divider';
126 divider.innerHTML = '<span>Or continue with</span>';
127 container.appendChild(divider);
128
129 // Add OAuth buttons
130 oauthProviders.forEach(provider => {
131 const btn = document.createElement('button');
132 btn.className = 'btn btn-secondary';
133 btn.type = 'button';
134
135 let providerName = provider.provider_type;
136 if (provider.provider_type === 'homeassistant') {
137 providerName = 'Home Assistant';
138 } else if (provider.provider_type === 'google') {
139 providerName = 'Google';
140 }
141
142 btn.innerHTML = `<span>Sign in with ${providerName}</span>`;
143 btn.onclick = () => initiateOAuth(provider.provider_id);
144
145 container.appendChild(btn);
146 });
147 }
148 } catch (error) {
149 console.error('Failed to load providers:', error);
150 }
151 }
152
153 // Handle form submission
154 document.getElementById('loginForm').addEventListener('submit', async (e) => {
155 e.preventDefault();
156 hideError();
157 setLoading(true);
158
159 const username = document.getElementById('username').value;
160 const password = document.getElementById('password').value;
161
162 try {
163 const requestBody = {
164 provider_id: 'builtin',
165 credentials: { username, password }
166 };
167
168 // Include device_name if provided via query parameter
169 if (deviceName) {
170 requestBody.device_name = deviceName;
171 }
172
173 // Include return_url if present
174 if (returnUrl) {
175 requestBody.return_url = returnUrl;
176 }
177
178 const response = await fetch(`${API_BASE}/auth/login`, {
179 method: 'POST',
180 headers: {
181 'Content-Type': 'application/json'
182 },
183 body: JSON.stringify(requestBody)
184 });
185
186 const data = await response.json();
187
188 if (data.success) {
189 if (data.redirect_to) {
190 window.location.href = data.redirect_to;
191 } else {
192 window.location.href = `/?code=${encodeURIComponent(data.token)}`;
193 }
194 } else {
195 showError(data.error || 'Login failed');
196 }
197 } catch (error) {
198 showError('Network error. Please try again.');
199 } finally {
200 setLoading(false);
201 }
202 });
203
204 // Initiate OAuth flow
205 async function initiateOAuth(providerId) {
206 try {
207 let authorizeUrl = `${API_BASE}/auth/authorize?provider_id=${providerId}`;
208
209 // Pass return_url to authorize endpoint if present
210 if (returnUrl) {
211 authorizeUrl += `&return_url=${encodeURIComponent(returnUrl)}`;
212 }
213
214 const response = await fetch(authorizeUrl);
215 const data = await response.json();
216
217 if (data.authorization_url) {
218 // Redirect directly to OAuth provider
219 window.location.href = data.authorization_url;
220 } else {
221 showError('Failed to initiate OAuth flow');
222 }
223 } catch (error) {
224 showError('Network error. Please try again.');
225 }
226 }
227
228 // Clear error on input
229 document.querySelectorAll('input').forEach(input => {
230 input.addEventListener('input', hideError);
231 });
232
233 // Load providers on page load
234 loadProviders();
235 </script>
236</body>
237</html>
238