/
/
/
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 Successful</title>
7 <link rel="stylesheet" href="../resources/common.css">
8 <style>
9 body {
10 display: flex;
11 flex-direction: column;
12 align-items: center;
13 justify-content: center;
14 min-height: 100vh;
15 padding: 20px;
16 }
17
18 .consent-banner {
19 display: none;
20 background: var(--error-bg);
21 border: 1px solid var(--error-border);
22 border-left: 4px solid var(--error-text);
23 padding: 20px;
24 margin-bottom: 20px;
25 border-radius: 10px;
26 max-width: 500px;
27 width: 100%;
28 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--border);
29 }
30
31 .consent-banner.show {
32 display: block;
33 }
34
35 .consent-banner h2 {
36 color: var(--error-text);
37 font-size: 16px;
38 font-weight: 600;
39 margin: 0 0 10px 0;
40 }
41
42 .consent-banner p {
43 color: var(--fg);
44 font-size: 14px;
45 margin: 0 0 8px 0;
46 line-height: 1.5;
47 }
48
49 .consent-banner .domain {
50 font-weight: 600;
51 font-family: 'Monaco', 'Menlo', monospace;
52 background: var(--input-bg);
53 padding: 2px 6px;
54 border-radius: 4px;
55 }
56
57 .consent-banner .buttons {
58 margin-top: 16px;
59 display: flex;
60 gap: 10px;
61 }
62
63 .consent-banner button {
64 flex: 1;
65 padding: 10px;
66 border: none;
67 border-radius: 10px;
68 font-size: 14px;
69 font-weight: 600;
70 cursor: pointer;
71 transition: all 0.2s ease;
72 }
73
74 .consent-banner .btn-approve {
75 background: var(--primary);
76 color: white;
77 }
78
79 .consent-banner .btn-approve:hover {
80 filter: brightness(1.1);
81 }
82
83 .consent-banner .btn-cancel {
84 background: var(--input-bg);
85 color: var(--fg);
86 border: 1px solid var(--border);
87 }
88
89 .consent-banner .btn-cancel:hover {
90 background: var(--input-focus-bg);
91 }
92
93 .callback-container {
94 background: var(--panel);
95 padding: 48px 40px;
96 border-radius: 16px;
97 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--border);
98 text-align: center;
99 max-width: 500px;
100 width: 100%;
101 }
102
103 h1 {
104 color: var(--fg);
105 font-size: 24px;
106 font-weight: 600;
107 letter-spacing: -0.5px;
108 margin-bottom: 12px;
109 }
110
111 p {
112 color: var(--text-secondary);
113 font-size: 15px;
114 }
115
116 .close-button {
117 display: none;
118 margin-top: 24px;
119 padding: 12px 24px;
120 background: var(--primary);
121 color: white;
122 border: none;
123 border-radius: 10px;
124 font-size: 15px;
125 font-weight: 600;
126 cursor: pointer;
127 transition: all 0.2s ease;
128 }
129
130 .close-button:hover {
131 filter: brightness(1.1);
132 transform: translateY(-1px);
133 }
134
135 .close-button:active {
136 transform: translateY(0);
137 }
138
139 .close-button.show {
140 display: inline-block;
141 }
142 </style>
143</head>
144<body>
145 <div class="consent-banner" id="consentBanner">
146 <h2>â ï¸ External Application Authorization</h2>
147 <p>The application at <span class="domain" id="redirectDomain"></span> is requesting access to your Music Assistant instance.</p>
148 <p>Only authorize if you trust this application.</p>
149 <div class="buttons">
150 <button class="btn-cancel" onclick="denyRedirect()">Cancel</button>
151 <button class="btn-approve" onclick="approveRedirect()">Authorize & Continue</button>
152 </div>
153 </div>
154 <div class="callback-container">
155 <h1 id="status">Login Successful!</h1>
156 <p id="message">Redirecting...</p>
157 <button class="close-button" id="closeButton" onclick="handleManualClose()">Close Window</button>
158 </div>
159 <script>
160 const statusEl = document.getElementById('status');
161 const messageEl = document.getElementById('message');
162 const token = '{TOKEN}';
163 const redirectUrl = '{REDIRECT_URL}';
164 const requiresConsent = {REQUIRES_CONSENT};
165
166 const isPWA = (window.navigator.standalone === true) ||
167 window.matchMedia('(display-mode: standalone)').matches ||
168 window.matchMedia('(display-mode: minimal-ui)').matches;
169
170 function isValidRedirectUrl(url) {
171 if (!url) return false;
172 try {
173 const parsed = new URL(url, window.location.origin);
174 const allowedProtocols = ['http:', 'https:', 'musicassistant:'];
175 return allowedProtocols.includes(parsed.protocol);
176 } catch {
177 return false;
178 }
179 }
180
181 function attemptAutoClose() {
182 window.close();
183 if (window.self !== window.top) {
184 window.self.close();
185 }
186 if (window.opener) {
187 try {
188 window.opener.focus();
189 window.close();
190 } catch (e) {
191 // Silently fail
192 }
193 }
194 }
195
196 function showManualCloseButton() {
197 const closeButton = document.getElementById('closeButton');
198 if (closeButton) {
199 closeButton.classList.add('show');
200 }
201 messageEl.textContent = 'Authentication successful! You can close this window.';
202 }
203
204 function handleManualClose() {
205 attemptAutoClose();
206 setTimeout(() => {
207 if (window.opener && !window.opener.closed) {
208 window.blur();
209 }
210 messageEl.textContent = 'Please close this window manually.';
211 statusEl.textContent = 'Ready to Close';
212 }, 500);
213 }
214
215 function performRedirect() {
216 const isPopup = window.opener !== null;
217 const isRemoteAuth = redirectUrl === 'about:blank';
218
219 if (isRemoteAuth) {
220 statusEl.textContent = 'Authentication Successful';
221 messageEl.textContent = 'Closing window...';
222 setTimeout(() => {
223 attemptAutoClose();
224 setTimeout(() => {
225 showManualCloseButton();
226 }, 300);
227 }, 100);
228 return;
229 }
230
231 // PWA mode: OAuth flow on iOS PWAs opens in Safari, then redirects back to PWA
232 // window.opener is null and localStorage is not shared between contexts
233 if (isPWA) {
234 statusEl.textContent = 'Login Complete!';
235 messageEl.textContent = 'Returning to app...';
236 if (isValidRedirectUrl(redirectUrl)) {
237 window.location.href = redirectUrl;
238 } else {
239 window.location.href = '/';
240 }
241 return;
242 }
243
244 if (isPopup) {
245 const isExternalRedirect = redirectUrl && !redirectUrl.startsWith(window.location.origin);
246
247 if (isExternalRedirect) {
248 // External redirect - must redirect to complete OAuth flow
249 statusEl.textContent = 'Authentication Successful';
250 messageEl.textContent = 'Redirecting...';
251
252 if (isValidRedirectUrl(redirectUrl)) {
253 setTimeout(() => {
254 window.location.href = redirectUrl;
255 }, 500);
256 } else {
257 messageEl.textContent = 'Redirect failed. Please close this window.';
258 showManualCloseButton();
259 }
260 } else {
261 // Internal redirect - close popup and post message
262 statusEl.textContent = 'Login Complete!';
263 messageEl.textContent = 'Closing popup...';
264
265 if (window.opener && !window.opener.closed) {
266 try {
267 window.opener.postMessage({
268 type: 'oauth_success',
269 token: token,
270 redirectUrl: redirectUrl
271 }, window.location.origin);
272 } catch (e) {
273 // Silently fail
274 }
275 }
276
277 setTimeout(() => {
278 attemptAutoClose();
279 setTimeout(() => {
280 showManualCloseButton();
281 }, 300);
282 }, 100);
283 }
284 } else {
285 statusEl.textContent = 'Authentication Successful';
286 messageEl.textContent = 'Redirecting...';
287 localStorage.setItem('auth_token', token);
288
289 if (isValidRedirectUrl(redirectUrl)) {
290 setTimeout(() => {
291 window.location.href = redirectUrl;
292 }, 500);
293 } else {
294 setTimeout(() => {
295 messageEl.textContent = 'Redirect failed. Please close this window.';
296 showManualCloseButton();
297 }, 1000);
298 }
299
300 setTimeout(() => {
301 showManualCloseButton();
302 }, 2000);
303 }
304 }
305
306 function approveRedirect() {
307 document.getElementById('consentBanner').classList.remove('show');
308 performRedirect();
309 }
310
311 function denyRedirect() {
312 window.location.href = '/';
313 }
314
315 if (requiresConsent) {
316 try {
317 const parsed = new URL(redirectUrl);
318 document.getElementById('redirectDomain').textContent = parsed.origin;
319 } catch {
320 document.getElementById('redirectDomain').textContent = 'unknown';
321 }
322 document.getElementById('consentBanner').classList.add('show');
323 statusEl.textContent = 'Authorization Required';
324 messageEl.textContent = 'Please review the authorization request above.';
325 } else {
326 performRedirect();
327 }
328 </script>
329</body>
330</html>
331