@lumiapassport/ui-kit 1.10.1 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iframe/index.html +17 -1
- package/dist/iframe/main.js +44 -25
- package/dist/iframe/main.js.map +1 -1
- package/dist/iframe/oauth/x.html +160 -0
- package/dist/iframe/oauth/x.js +378 -0
- package/dist/index.cjs +6576 -5402
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -4
- package/dist/index.d.ts +76 -4
- package/dist/index.js +6154 -4980
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
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>X (Twitter) Login - Lumia Passport</title>
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
17
|
+
background: linear-gradient(135deg, #1DA1F2 0%, #14171A 100%);
|
|
18
|
+
color: #333;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 2rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.container {
|
|
27
|
+
background: white;
|
|
28
|
+
border-radius: 16px;
|
|
29
|
+
padding: 3rem 2rem;
|
|
30
|
+
max-width: 450px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.logo {
|
|
37
|
+
width: 80px;
|
|
38
|
+
height: 80px;
|
|
39
|
+
margin: 0 auto 1.5rem;
|
|
40
|
+
background: linear-gradient(135deg, #1DA1F2 0%, #14171A 100%);
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
font-size: 2.5rem;
|
|
46
|
+
color: white;
|
|
47
|
+
font-weight: bold;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
h1 {
|
|
51
|
+
font-size: 1.75rem;
|
|
52
|
+
margin-bottom: 0.5rem;
|
|
53
|
+
color: #333;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p {
|
|
57
|
+
color: #666;
|
|
58
|
+
margin-bottom: 2rem;
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#content {
|
|
63
|
+
min-height: 120px;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 1rem;
|
|
69
|
+
margin: 2rem 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.loading {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 1rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.spinner {
|
|
80
|
+
width: 40px;
|
|
81
|
+
height: 40px;
|
|
82
|
+
border: 4px solid rgba(29, 161, 242, 0.3);
|
|
83
|
+
border-top-color: #1DA1F2;
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
animation: spin 1s linear infinite;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes spin {
|
|
89
|
+
to { transform: rotate(360deg); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.error {
|
|
93
|
+
background: #fee;
|
|
94
|
+
border: 1px solid #fcc;
|
|
95
|
+
border-radius: 8px;
|
|
96
|
+
padding: 1rem;
|
|
97
|
+
color: #c33;
|
|
98
|
+
margin-top: 1rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.success {
|
|
102
|
+
background: #d1fae5;
|
|
103
|
+
border: 1px solid #6ee7b7;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
padding: 1rem;
|
|
106
|
+
color: #065f46;
|
|
107
|
+
margin-top: 1rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.footer {
|
|
111
|
+
margin-top: 2rem;
|
|
112
|
+
padding-top: 1.5rem;
|
|
113
|
+
border-top: 1px solid #e0e0e0;
|
|
114
|
+
font-size: 0.875rem;
|
|
115
|
+
color: #999;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.retry-button {
|
|
119
|
+
background: #1DA1F2;
|
|
120
|
+
color: white;
|
|
121
|
+
border: none;
|
|
122
|
+
padding: 0.75rem 1.5rem;
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
font-size: 1rem;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
margin-top: 1rem;
|
|
127
|
+
transition: background 0.2s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.retry-button:hover {
|
|
131
|
+
background: #1991DB;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.retry-button:disabled {
|
|
135
|
+
background: #ccc;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
<div class="container">
|
|
142
|
+
<div class="logo">𝕏</div>
|
|
143
|
+
<h1>Sign in with X</h1>
|
|
144
|
+
<p>Redirecting to X (Twitter) for authentication...</p>
|
|
145
|
+
|
|
146
|
+
<div id="content">
|
|
147
|
+
<div class="loading">
|
|
148
|
+
<div class="spinner"></div>
|
|
149
|
+
<p>Initializing OAuth flow...</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="footer">
|
|
154
|
+
<p>You can close this window after authentication</p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<script src="./x.js"></script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X (Twitter) OAuth Popup Script
|
|
3
|
+
* Handles OAuth 2.0 with PKCE flow for X authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Get configuration from URL parameters
|
|
7
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
8
|
+
|
|
9
|
+
// TSS_URL and PROJECT_ID are build-time constants, passed via URL on initial load
|
|
10
|
+
// After backend redirect, we use stored values from localStorage
|
|
11
|
+
const TSS_URL = urlParams.get('tssUrl') || localStorage.getItem('x_oauth_tssUrl') || (typeof __LUMIA_TSS_URL__ !== 'undefined' ? __LUMIA_TSS_URL__ : null);
|
|
12
|
+
const MODE = urlParams.get('mode') || localStorage.getItem('x_oauth_mode') || 'login';
|
|
13
|
+
const PROJECT_ID = urlParams.get('projectId') || localStorage.getItem('x_oauth_projectId') || (typeof window !== 'undefined' && window.__LUMIA_PROJECT_ID__) || null;
|
|
14
|
+
|
|
15
|
+
const STATE = urlParams.get('state');
|
|
16
|
+
const CODE = urlParams.get('code');
|
|
17
|
+
const ERROR_PARAM = urlParams.get('error');
|
|
18
|
+
const SUCCESS = urlParams.get('success'); // Backend redirected with success=true
|
|
19
|
+
|
|
20
|
+
console.log('[X OAuth] Initializing with:', { TSS_URL, MODE, PROJECT_ID, STATE, CODE, ERROR_PARAM, SUCCESS });
|
|
21
|
+
|
|
22
|
+
const contentEl = document.getElementById('content');
|
|
23
|
+
|
|
24
|
+
// Track if we've successfully sent auth result to parent
|
|
25
|
+
let authResultSent = false;
|
|
26
|
+
// Track if we're redirecting to X OAuth (to prevent cancellation on redirect)
|
|
27
|
+
let isRedirectingToProvider = false;
|
|
28
|
+
|
|
29
|
+
function showLoading(message) {
|
|
30
|
+
if (contentEl) {
|
|
31
|
+
contentEl.innerHTML = `
|
|
32
|
+
<div class="loading">
|
|
33
|
+
<div class="spinner"></div>
|
|
34
|
+
<p>${message}</p>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showError(message, allowRetry = true) {
|
|
41
|
+
console.error('[X OAuth] Error:', message);
|
|
42
|
+
|
|
43
|
+
if (contentEl) {
|
|
44
|
+
contentEl.innerHTML = `
|
|
45
|
+
<div class="error">
|
|
46
|
+
<strong>Error:</strong> ${message}
|
|
47
|
+
</div>
|
|
48
|
+
${allowRetry ? '<button class="retry-button" onclick="window.location.reload()">Retry</button>' : ''}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Send error to opener
|
|
53
|
+
if (window.opener) {
|
|
54
|
+
window.opener.postMessage({
|
|
55
|
+
type: 'X_AUTH_ERROR',
|
|
56
|
+
provider: 'x',
|
|
57
|
+
error: message
|
|
58
|
+
}, '*');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showSuccess(message) {
|
|
63
|
+
if (contentEl) {
|
|
64
|
+
contentEl.innerHTML = `
|
|
65
|
+
<div class="success">
|
|
66
|
+
✓ ${message}<br>
|
|
67
|
+
<small style="color: #666;">Closing window...</small>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function startOAuthFlow() {
|
|
74
|
+
if (!TSS_URL) {
|
|
75
|
+
showError('TSS URL not configured. Missing tssUrl parameter.', false);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
showLoading('Starting X OAuth flow...');
|
|
81
|
+
|
|
82
|
+
// Determine the correct endpoint based on mode
|
|
83
|
+
const endpoint = MODE === 'link'
|
|
84
|
+
? `${TSS_URL}/api/auth/link/x/start`
|
|
85
|
+
: `${TSS_URL}/api/auth/x/start`;
|
|
86
|
+
|
|
87
|
+
// Add projectId to endpoint if available
|
|
88
|
+
const fullEndpoint = PROJECT_ID
|
|
89
|
+
? `${endpoint}?projectId=${encodeURIComponent(PROJECT_ID)}`
|
|
90
|
+
: endpoint;
|
|
91
|
+
|
|
92
|
+
console.log('[X OAuth] Starting flow with config:', { TSS_URL, MODE, PROJECT_ID, endpoint: fullEndpoint });
|
|
93
|
+
console.log('[X OAuth] Making request to:', fullEndpoint);
|
|
94
|
+
|
|
95
|
+
// Call backend to get authorization URL
|
|
96
|
+
const response = await fetch(fullEndpoint, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
...(MODE === 'link' ? {
|
|
101
|
+
'Authorization': `Bearer ${urlParams.get('token')}`
|
|
102
|
+
} : {})
|
|
103
|
+
},
|
|
104
|
+
credentials: 'include'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const errorText = await response.text().catch(() => '');
|
|
109
|
+
let errorData;
|
|
110
|
+
try {
|
|
111
|
+
errorData = JSON.parse(errorText);
|
|
112
|
+
} catch {
|
|
113
|
+
errorData = { message: errorText };
|
|
114
|
+
}
|
|
115
|
+
console.error('[X OAuth] Backend error response:', { status: response.status, errorData, errorText });
|
|
116
|
+
throw new Error(errorData.message || errorText || `Failed to start OAuth: ${response.statusText}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
console.log('[X OAuth] Backend response:', data);
|
|
121
|
+
|
|
122
|
+
// Backend may return 'url' or 'authorizationUrl'
|
|
123
|
+
const authUrl = data.authorizationUrl || data.url;
|
|
124
|
+
console.log('[X OAuth] Authorization URL:', authUrl);
|
|
125
|
+
|
|
126
|
+
if (!authUrl) {
|
|
127
|
+
throw new Error('No authorization URL returned from server');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Store state and config in localStorage (persists across redirects)
|
|
131
|
+
if (data.state) {
|
|
132
|
+
localStorage.setItem('x_oauth_state', data.state);
|
|
133
|
+
}
|
|
134
|
+
localStorage.setItem('x_oauth_mode', MODE);
|
|
135
|
+
localStorage.setItem('x_oauth_tssUrl', TSS_URL);
|
|
136
|
+
if (PROJECT_ID) {
|
|
137
|
+
localStorage.setItem('x_oauth_projectId', PROJECT_ID);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Redirect to X OAuth page
|
|
141
|
+
showLoading('Redirecting to X...');
|
|
142
|
+
isRedirectingToProvider = true; // Prevent cancellation message on redirect
|
|
143
|
+
window.location.href = authUrl;
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('[X OAuth] Start flow error:', error);
|
|
147
|
+
const errorMessage = error.message || 'Failed to start OAuth flow';
|
|
148
|
+
console.error('[X OAuth] Error details:', { error, MODE, TSS_URL, PROJECT_ID });
|
|
149
|
+
showError(errorMessage);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle successful OAuth after backend redirect
|
|
155
|
+
* Backend processes callback and redirects back with success=true
|
|
156
|
+
*/
|
|
157
|
+
async function handleBackendSuccess() {
|
|
158
|
+
try {
|
|
159
|
+
showLoading('Completing authentication...');
|
|
160
|
+
|
|
161
|
+
console.log('[X OAuth] Backend redirected with success, verifying session...');
|
|
162
|
+
console.log('[X OAuth] Using config:', { TSS_URL, PROJECT_ID, MODE });
|
|
163
|
+
|
|
164
|
+
// Validate required parameters
|
|
165
|
+
if (!TSS_URL) {
|
|
166
|
+
throw new Error('Missing TSS URL. Check build-time configuration.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Verify the session was created by checking auth endpoint
|
|
170
|
+
const verifyEndpoint = PROJECT_ID
|
|
171
|
+
? `${TSS_URL}/api/auth/verify?projectId=${encodeURIComponent(PROJECT_ID)}`
|
|
172
|
+
: `${TSS_URL}/api/auth/verify`;
|
|
173
|
+
|
|
174
|
+
const verifyResponse = await fetch(verifyEndpoint, {
|
|
175
|
+
method: 'GET',
|
|
176
|
+
credentials: 'include',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!verifyResponse.ok) {
|
|
180
|
+
console.error('[X OAuth] Verify failed:', verifyResponse.status);
|
|
181
|
+
throw new Error('Failed to verify authentication. Session may not be created.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const userData = await verifyResponse.json();
|
|
185
|
+
console.log('[X OAuth] Authentication verified:', userData);
|
|
186
|
+
|
|
187
|
+
// Send success to opener
|
|
188
|
+
if (window.opener) {
|
|
189
|
+
window.opener.postMessage({
|
|
190
|
+
type: 'X_AUTH_SUCCESS',
|
|
191
|
+
provider: 'x',
|
|
192
|
+
user: userData,
|
|
193
|
+
mode: MODE
|
|
194
|
+
}, '*');
|
|
195
|
+
|
|
196
|
+
// Mark that we've sent the auth result
|
|
197
|
+
authResultSent = true;
|
|
198
|
+
|
|
199
|
+
showSuccess(MODE === 'link' ? 'Account linked successfully!' : 'Authentication successful!');
|
|
200
|
+
|
|
201
|
+
// Clean up localStorage
|
|
202
|
+
localStorage.removeItem('x_oauth_state');
|
|
203
|
+
localStorage.removeItem('x_oauth_mode');
|
|
204
|
+
localStorage.removeItem('x_oauth_tssUrl');
|
|
205
|
+
localStorage.removeItem('x_oauth_projectId');
|
|
206
|
+
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
window.close();
|
|
209
|
+
}, 1500);
|
|
210
|
+
} else {
|
|
211
|
+
console.error('[X OAuth] No opener window found');
|
|
212
|
+
showError('No opener window found. Please close this window manually.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('[X OAuth] Backend success handler error:', error);
|
|
217
|
+
showError(error.message || 'Failed to complete authentication');
|
|
218
|
+
|
|
219
|
+
// Clean up localStorage on error
|
|
220
|
+
localStorage.removeItem('x_oauth_state');
|
|
221
|
+
localStorage.removeItem('x_oauth_mode');
|
|
222
|
+
localStorage.removeItem('x_oauth_tssUrl');
|
|
223
|
+
localStorage.removeItem('x_oauth_projectId');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handle OAuth callback (legacy, may not be used if backend handles redirect_uri)
|
|
229
|
+
*/
|
|
230
|
+
async function handleCallback() {
|
|
231
|
+
try {
|
|
232
|
+
showLoading('Completing authentication...');
|
|
233
|
+
|
|
234
|
+
// Verify state matches
|
|
235
|
+
const savedState = localStorage.getItem('x_oauth_state');
|
|
236
|
+
const savedMode = localStorage.getItem('x_oauth_mode');
|
|
237
|
+
|
|
238
|
+
if (savedState && STATE !== savedState) {
|
|
239
|
+
throw new Error('Invalid state parameter. Possible CSRF attack.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (ERROR_PARAM === 'access_denied') {
|
|
243
|
+
console.warn('[X OAuth] User denied authorization during legacy callback');
|
|
244
|
+
redirectWithAccessDenied();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (ERROR_PARAM) {
|
|
249
|
+
throw new Error(`OAuth error: ${ERROR_PARAM}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!CODE) {
|
|
253
|
+
throw new Error('No authorization code received');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log('[X OAuth] Callback successful, mode:', savedMode);
|
|
257
|
+
|
|
258
|
+
// For login mode, the backend callback endpoint handles everything
|
|
259
|
+
// and redirects back with tokens. For link mode, we need to verify.
|
|
260
|
+
if (savedMode === 'login') {
|
|
261
|
+
// The callback endpoint should have set cookies/tokens
|
|
262
|
+
// Get user info to verify
|
|
263
|
+
const response = await fetch(`${TSS_URL}/api/auth/verify`, {
|
|
264
|
+
credentials: 'include'
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new Error('Failed to verify authentication');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const userData = await response.json();
|
|
272
|
+
console.log('[X OAuth] User data:', userData);
|
|
273
|
+
|
|
274
|
+
// Send success to opener
|
|
275
|
+
if (window.opener) {
|
|
276
|
+
window.opener.postMessage({
|
|
277
|
+
type: 'X_AUTH_SUCCESS',
|
|
278
|
+
provider: 'x',
|
|
279
|
+
user: userData,
|
|
280
|
+
mode: 'login'
|
|
281
|
+
}, '*');
|
|
282
|
+
|
|
283
|
+
showSuccess('Authentication successful!');
|
|
284
|
+
|
|
285
|
+
setTimeout(() => {
|
|
286
|
+
window.close();
|
|
287
|
+
}, 1500);
|
|
288
|
+
} else {
|
|
289
|
+
showError('No opener window found');
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// Link mode - backend has already linked the account
|
|
293
|
+
if (window.opener) {
|
|
294
|
+
window.opener.postMessage({
|
|
295
|
+
type: 'X_AUTH_SUCCESS',
|
|
296
|
+
provider: 'x',
|
|
297
|
+
mode: 'link'
|
|
298
|
+
}, '*');
|
|
299
|
+
|
|
300
|
+
showSuccess('Account linked successfully!');
|
|
301
|
+
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
window.close();
|
|
304
|
+
}, 1500);
|
|
305
|
+
} else {
|
|
306
|
+
showError('No opener window found');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('[X OAuth] Callback error:', error);
|
|
312
|
+
showError(error.message || 'Failed to complete authentication');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Determine if this is a callback or initial load
|
|
317
|
+
if (ERROR_PARAM) {
|
|
318
|
+
// Backend redirected with error
|
|
319
|
+
const errorMessage = decodeURIComponent(ERROR_PARAM);
|
|
320
|
+
console.error('[X OAuth] Backend returned error:', errorMessage);
|
|
321
|
+
showError(errorMessage);
|
|
322
|
+
|
|
323
|
+
// Send error to opener
|
|
324
|
+
if (window.opener) {
|
|
325
|
+
window.opener.postMessage({
|
|
326
|
+
type: 'X_AUTH_ERROR',
|
|
327
|
+
provider: 'x',
|
|
328
|
+
error: errorMessage
|
|
329
|
+
}, '*');
|
|
330
|
+
authResultSent = true; // Mark that we've sent result
|
|
331
|
+
}
|
|
332
|
+
} else if (SUCCESS === 'true') {
|
|
333
|
+
// Backend redirected back after successful OAuth
|
|
334
|
+
handleBackendSuccess();
|
|
335
|
+
} else if (CODE) {
|
|
336
|
+
// This is a callback from X (should not happen with backend redirect_uri)
|
|
337
|
+
console.warn('[X OAuth] Received code parameter, but backend should handle this');
|
|
338
|
+
handleCallback();
|
|
339
|
+
} else {
|
|
340
|
+
// This is initial load, start OAuth flow
|
|
341
|
+
if (document.readyState === 'loading') {
|
|
342
|
+
document.addEventListener('DOMContentLoaded', startOAuthFlow);
|
|
343
|
+
} else {
|
|
344
|
+
startOAuthFlow();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Handle popup closing without authentication
|
|
349
|
+
window.addEventListener('beforeunload', () => {
|
|
350
|
+
// Only send cancellation if we haven't sent success/error AND not redirecting to provider
|
|
351
|
+
if (!authResultSent && !isRedirectingToProvider && window.opener) {
|
|
352
|
+
console.log('[X OAuth] Window closing without auth result, sending cancellation');
|
|
353
|
+
window.opener.postMessage({
|
|
354
|
+
type: 'X_AUTH_CANCELLED',
|
|
355
|
+
provider: 'x'
|
|
356
|
+
}, '*');
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
function redirectWithAccessDenied() {
|
|
360
|
+
const storedState = localStorage.getItem('x_oauth_state');
|
|
361
|
+
const storedMode = localStorage.getItem('x_oauth_mode') || MODE || 'login';
|
|
362
|
+
|
|
363
|
+
localStorage.removeItem('x_oauth_state');
|
|
364
|
+
localStorage.removeItem('x_oauth_mode');
|
|
365
|
+
localStorage.removeItem('x_oauth_tssUrl');
|
|
366
|
+
localStorage.removeItem('x_oauth_projectId');
|
|
367
|
+
|
|
368
|
+
const params = new URLSearchParams();
|
|
369
|
+
params.set('success', 'false');
|
|
370
|
+
params.set('error', 'ACCESS_DENIED');
|
|
371
|
+
params.set('mode', storedMode);
|
|
372
|
+
if (storedState) {
|
|
373
|
+
params.set('state', storedState);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const redirectUrl = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
|
377
|
+
window.location.replace(redirectUrl);
|
|
378
|
+
}
|