music-assistant-server

10.7 KBHTML
oauth_callback.html
10.7 KB331 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>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