@startup-api/cloudflare 0.0.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.
Files changed (39) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +114 -0
  3. package/package.json +53 -0
  4. package/public/index.html +405 -0
  5. package/public/users/accounts.html +504 -0
  6. package/public/users/admin/index.html +765 -0
  7. package/public/users/power-strip.js +658 -0
  8. package/public/users/profile.html +443 -0
  9. package/public/users/style.css +493 -0
  10. package/src/CookieManager.ts +56 -0
  11. package/src/PowerStrip.ts +23 -0
  12. package/src/StartupAPIEnv.ts +12 -0
  13. package/src/auth/GoogleProvider.ts +67 -0
  14. package/src/auth/OAuthProvider.ts +52 -0
  15. package/src/auth/TwitchProvider.ts +64 -0
  16. package/src/auth/index.ts +231 -0
  17. package/src/billing/PaymentEngine.ts +20 -0
  18. package/src/billing/Plan.ts +80 -0
  19. package/src/billing/plansConfig.ts +48 -0
  20. package/src/handlers/account.ts +246 -0
  21. package/src/handlers/admin.ts +144 -0
  22. package/src/handlers/auth.ts +54 -0
  23. package/src/handlers/ssr.ts +274 -0
  24. package/src/handlers/user.ts +168 -0
  25. package/src/handlers/utils.ts +120 -0
  26. package/src/index.ts +190 -0
  27. package/src/schemas/account.ts +37 -0
  28. package/src/schemas/admin.ts +10 -0
  29. package/src/schemas/billing.ts +11 -0
  30. package/src/schemas/credential.ts +38 -0
  31. package/src/schemas/membership.ts +9 -0
  32. package/src/schemas/session.ts +10 -0
  33. package/src/schemas/user.ts +22 -0
  34. package/src/storage/AccountDO.ts +370 -0
  35. package/src/storage/CredentialDO.ts +82 -0
  36. package/src/storage/SystemDO.ts +264 -0
  37. package/src/storage/UserDO.ts +385 -0
  38. package/worker-configuration.d.ts +11696 -0
  39. package/wrangler.template.jsonc +55 -0
@@ -0,0 +1,658 @@
1
+ class PowerStrip extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.basePath = this.detectBasePath();
6
+ this.user = null;
7
+ this.accounts = [];
8
+ }
9
+
10
+ detectBasePath() {
11
+ const script =
12
+ document.currentScript ||
13
+ (function () {
14
+ const scripts = document.getElementsByTagName('script');
15
+ return scripts[scripts.length - 1];
16
+ })();
17
+
18
+ if (script && script.src) {
19
+ try {
20
+ const url = new URL(script.src);
21
+ return url.pathname.substring(0, url.pathname.lastIndexOf('/'));
22
+ } catch (e) {
23
+ console.error('Failed to parse script URL', e);
24
+ }
25
+ }
26
+ return '';
27
+ }
28
+
29
+ async connectedCallback() {
30
+ await this.fetchUser();
31
+ this.render();
32
+ this.addEventListeners();
33
+ }
34
+
35
+ async refresh() {
36
+ await this.fetchUser();
37
+ this.render();
38
+ this.addEventListeners();
39
+ }
40
+
41
+ async fetchUser() {
42
+ try {
43
+ const res = await fetch(`${this.basePath}/api/me`);
44
+ if (res.ok) {
45
+ const data = await res.json();
46
+ if (data.valid) {
47
+ this.user = {
48
+ profile: data.profile,
49
+ credential: data.credential,
50
+ is_admin: data.is_admin,
51
+ is_impersonated: data.is_impersonated,
52
+ };
53
+ // Fetch accounts if logged in
54
+ const accountsRes = await fetch(`${this.basePath}/api/me/accounts`);
55
+ if (accountsRes.ok) {
56
+ this.accounts = await accountsRes.json();
57
+ }
58
+ }
59
+ }
60
+ } catch (e) {
61
+ // Not logged in or error
62
+ }
63
+ }
64
+
65
+ async switchAccount(accountId) {
66
+ try {
67
+ const res = await fetch(`${this.basePath}/api/me/accounts/switch`, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ body: JSON.stringify({ account_id: accountId }),
73
+ });
74
+
75
+ if (res.ok) {
76
+ window.location.reload();
77
+ } else {
78
+ console.error('Failed to switch account');
79
+ }
80
+ } catch (e) {
81
+ console.error('Error switching account', e);
82
+ }
83
+ }
84
+
85
+ getProviderIcon(provider) {
86
+ if (provider === 'google') {
87
+ return `<svg viewBox="0 0 24 24">
88
+ <circle cx="12" cy="12" r="11" fill="white" stroke="#dadce0" stroke-width="0.5"/>
89
+ <path d="M17.64 12.2c0-.41-.03-.81-.1-1.21H12v2.3h3.16c-.14.73-.57 1.35-1.19 1.79v1.48h1.92c1.12-1.03 1.75-2.55 1.75-4.36z" fill="#4285F4"/>
90
+ <path d="M12 18c1.62 0 2.98-.54 3.97-1.46l-1.92-1.48c-.54.37-1.23.59-2.05.59-1.57 0-2.91-1.06-3.39-2.48H6.65v1.53C7.64 16.69 9.68 18 12 18z" fill="#34A853"/>
91
+ <path d="M8.61 13.17c-.12-.37-.19-.76-.19-1.17s.07-.8.19-1.17V9.3H6.65c-.41.81-.65 1.73-.65 2.7s.24 1.89.65 2.7l1.96-1.53z" fill="#FBBC05"/>
92
+ <path d="M12 8.35c.88 0 1.67.3 2.3.91l1.73-1.73C14.98 6.51 13.62 6 12 6c-2.32 0-4.36 1.31-5.35 3.3L8.61 10.83c.48-1.42 1.82-2.48 3.39-2.48z" fill="#EA4335"/>
93
+ </svg>`;
94
+ } else if (provider === 'twitch') {
95
+ return `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill="#9146FF" stroke="white" stroke-width="1"/><path d="M7 6H6v10h2v3l3-3h3l4-4V6H7zm9 6l-2 2h-3l-2 2v-2H8V7h8v5z" fill="white"/><path d="M14 8.5h1.5v2H14V8.5zm-3 0h1.5v2H11v-2z" fill="white"/></svg>`;
96
+ }
97
+ return '';
98
+ }
99
+
100
+ render() {
101
+ const returnUrl = encodeURIComponent(window.location.href);
102
+ const googleLink = `${this.basePath}/auth/google?return_url=${returnUrl}`;
103
+ const twitchLink = `${this.basePath}/auth/twitch?return_url=${returnUrl}`;
104
+ const logoutLink = `${this.basePath}/logout?return_url=${returnUrl}`;
105
+
106
+ const providersStr = this.getAttribute('providers') || '';
107
+ const providers = providersStr.split(',');
108
+
109
+ let authButtons = '';
110
+ if (providers.includes('google')) {
111
+ authButtons += `
112
+ <a href="${googleLink}" class="auth-btn google">
113
+ <svg viewBox="0 0 24 24">
114
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
115
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
116
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
117
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.66l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
118
+ </svg>
119
+ Continue with Google
120
+ </a>`;
121
+ }
122
+ if (providers.includes('twitch')) {
123
+ authButtons += `
124
+ <a href="${twitchLink}" class="auth-btn twitch">
125
+ <svg viewBox="0 0 24 24">
126
+ <path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" fill="currentColor"/>
127
+ </svg>
128
+ Continue with Twitch
129
+ </a>`;
130
+ }
131
+
132
+ let content = '';
133
+ let accountSwitcher = '';
134
+
135
+ if (providers.length > 0 && providers[0] !== '') {
136
+ if (this.user) {
137
+ const providerIcon = this.getProviderIcon(this.user.credential.provider);
138
+ const currentAccount = this.accounts.find((a) => a.is_current) || (this.accounts.length > 0 ? this.accounts[0] : null);
139
+ const accountName = currentAccount ? currentAccount.name : 'No Account';
140
+
141
+ let switchButton = '';
142
+ let accountContainer = '';
143
+
144
+ if (this.accounts.length > 1) {
145
+ switchButton = `
146
+ <button class="trigger switch-btn" id="switch-account-trigger" title="Switch Account">
147
+ <svg viewBox="0 0 24 24" style="width: 0.8rem; height: 0.8rem; fill: currentColor; display: block;">
148
+ <path d="M7 10l5 5 5-5z"/>
149
+ </svg>
150
+ </button>`;
151
+
152
+ const accountSettingsLink =
153
+ currentAccount && (currentAccount.role === 1 || this.user.is_admin)
154
+ ? `<a href="${this.basePath}/accounts.html" class="trigger settings-btn" title="Account Settings">
155
+ <svg viewBox="0 0 24 24" style="width: 0.8rem; height: 0.8rem; fill: currentColor; display: block;">
156
+ <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.81,11.69,4.81,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.5c-1.93,0-3.5-1.57-3.5-3.5 s1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5S13.93,15.5,12,15.5z"/>
157
+ </svg>
158
+ </a>`
159
+ : '';
160
+
161
+ accountContainer = `
162
+ <div class="account-container">
163
+ <span class="account-label">${accountName}</span>
164
+ ${switchButton}
165
+ ${accountSettingsLink}
166
+ </div>
167
+ `;
168
+
169
+ const accountList = this.accounts
170
+ .map(
171
+ (acc) => `
172
+ <button class="account-item ${acc.is_current ? 'active' : ''}" data-id="${acc.account_id}">
173
+ <span class="account-name">${acc.name}</span>
174
+ ${acc.is_current ? '<span class="current-badge">Current</span>' : ''}
175
+ </button>
176
+ `,
177
+ )
178
+ .join('');
179
+
180
+ accountSwitcher = `
181
+ <dialog id="account-dialog">
182
+ <div class="dialog-content">
183
+ <div class="dialog-header">
184
+ <h2 class="dialog-title">Switch Account</h2>
185
+ <button class="close-btn" id="close-account-dialog" aria-label="Close">&times;</button>
186
+ </div>
187
+ <div class="account-list">
188
+ ${accountList}
189
+ </div>
190
+ </div>
191
+ </dialog>
192
+ `;
193
+ }
194
+
195
+ const adminLink = this.user.is_admin
196
+ ? `<a href="${this.basePath}/admin/" class="trigger admin-btn" title="Admin Panel" target="startup-api-admin">Admin</a>`
197
+ : '';
198
+
199
+ const impersonationLink = this.user.is_impersonated
200
+ ? `<button class="trigger stop-impersonation-btn" id="stop-impersonation-trigger" title="Stop Impersonation">Stop Impersonation</button>`
201
+ : '';
202
+
203
+ const avatarContent = this.user.profile.picture
204
+ ? `<img src="${this.user.profile.picture}" alt="${this.user.profile.name}" title="${this.user.profile.name}" class="avatar" width="16" height="16" />`
205
+ : `<div class="avatar placeholder" style="background: #eee; display: flex; align-items: center; justify-content: center;">
206
+ <svg viewBox="0 0 24 24" style="width: 12px; height: 12px; fill: #999;">
207
+ <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
208
+ </svg>
209
+ </div>`;
210
+
211
+ content = `
212
+ <div class="user-profile">
213
+ ${adminLink}
214
+ ${impersonationLink}
215
+ ${accountContainer}
216
+ <div class="avatar-container">
217
+ ${avatarContent}
218
+ <div class="provider-badge ${this.user.credential.provider}">
219
+ ${providerIcon}
220
+ </div>
221
+ </div>
222
+ <div class="user-info">
223
+ <a href="${this.basePath}/profile.html" class="user-name" title="Edit Profile">${this.user.profile.name}</a>
224
+ </div>
225
+ <a href="${logoutLink}" class="trigger logout-btn" title="Logout">Logout</a>
226
+ </div>
227
+ `;
228
+ } else {
229
+ content = `
230
+ <a class="trigger" id="login-trigger" title="Login" role="button" href="javascript:void(0)">
231
+ Login
232
+ </a>
233
+ `;
234
+ }
235
+ }
236
+
237
+ this.shadowRoot.innerHTML = `
238
+ <style>
239
+ :host {
240
+ display: block;
241
+ font-family: system-ui, -apple-system, sans-serif;
242
+ }
243
+
244
+ @keyframes fadeIn {
245
+ from { opacity: 0; }
246
+ to { opacity: 1; }
247
+ }
248
+
249
+ .container {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 0.25rem;
253
+ height: 1.3rem;
254
+ padding: 0.0625rem;
255
+ animation: fadeIn 0.4s ease-out;
256
+ background-color: rgba(255, 255, 255, 0.7);
257
+ border-radius: 0 0 0 0.3rem;
258
+ box-shadow: 0 0.0625rem 0.1875rem rgba(0,0,0,0.1);
259
+ font-size: 1rem;
260
+ }
261
+
262
+ .trigger {
263
+ cursor: pointer;
264
+ padding: 0.125rem 0.375rem;
265
+ transition: background-color 0.2s;
266
+ border-radius: 0.25rem;
267
+ font-size: 0.8rem;
268
+ font-weight: 500;
269
+ color: #444;
270
+ text-decoration: none;
271
+ border: none;
272
+ background: transparent;
273
+ line-height: inherit;
274
+ }
275
+
276
+ .trigger:hover {
277
+ background-color: rgba(0, 0, 0, 0.05);
278
+ text-decoration: underline;
279
+ color: #1a73e8;
280
+ }
281
+
282
+ .switch-btn {
283
+ color: #1a73e8;
284
+ }
285
+
286
+ svg.bolt, ::slotted(svg) {
287
+ width: 1rem !important;
288
+ height: 1rem !important;
289
+ fill: #ffcc00 !important;
290
+ filter: drop-shadow(0.0625rem 0.0625rem 0.0625rem rgba(0, 0, 0, 0.5));
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ .user-profile {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 0.5rem;
298
+ padding: 0.125rem;
299
+ }
300
+
301
+ .avatar-container {
302
+ position: relative;
303
+ width: 1.1rem;
304
+ height: 1.1rem;
305
+ }
306
+
307
+ .avatar {
308
+ width: 1.1rem;
309
+ height: 1.1rem;
310
+ border-radius: 50%;
311
+ object-fit: cover;
312
+ }
313
+
314
+ .provider-badge {
315
+ position: absolute;
316
+ bottom: -0.0625rem;
317
+ right: -0.0625rem;
318
+ width: 0.5rem;
319
+ height: 0.5rem;
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ }
324
+
325
+ .provider-badge svg {
326
+ width: 0.5rem;
327
+ height: 0.5rem;
328
+ }
329
+
330
+ .provider-badge.google { color: #3c4043; }
331
+ .provider-badge.twitch { color: #9146FF; }
332
+
333
+ .user-info {
334
+ display: flex;
335
+ flex-direction: column;
336
+ line-height: 1;
337
+ justify-content: center;
338
+ }
339
+
340
+ .account-container {
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 0;
344
+ }
345
+
346
+ .user-name {
347
+ font-size: 0.8rem;
348
+ color: #333;
349
+ max-width: 10rem;
350
+ white-space: nowrap;
351
+ overflow: hidden;
352
+ text-overflow: ellipsis;
353
+ font-weight: 600;
354
+ text-decoration: none;
355
+ display: block;
356
+ }
357
+
358
+ .user-name:hover {
359
+ text-decoration: underline;
360
+ color: #1a73e8;
361
+ }
362
+
363
+ .account-label {
364
+ font-size: 0.8rem;
365
+ color: #1a73e8;
366
+ max-width: 10rem;
367
+ white-space: nowrap;
368
+ overflow: hidden;
369
+ text-overflow: ellipsis;
370
+ font-weight: 500;
371
+ }
372
+
373
+ .switch-btn {
374
+ padding: 0;
375
+ display: flex;
376
+ align-items: center;
377
+ justify-content: center;
378
+ width: 1rem;
379
+ height: 1rem;
380
+ }
381
+
382
+ .admin-btn {
383
+ color: #d93025 !important;
384
+ }
385
+
386
+ .stop-impersonation-btn {
387
+ color: #fbbc05 !important;
388
+ font-weight: bold;
389
+ }
390
+
391
+ @media (max-width: 25rem) {
392
+ .user-info {
393
+ display: none;
394
+ }
395
+ }
396
+
397
+ /* Dialog Styling */
398
+ dialog {
399
+ border: none;
400
+ border-radius: 0.75rem;
401
+ padding: 0;
402
+ box-shadow: 0 0.625rem 1.5625rem rgba(0,0,0,0.2);
403
+ background: white;
404
+ color: #333;
405
+ max-width: 20rem;
406
+ width: 90%;
407
+ overflow: hidden;
408
+ }
409
+
410
+ dialog::backdrop {
411
+ background: rgba(0, 0, 0, 0.5);
412
+ backdrop-filter: blur(0.125rem);
413
+ }
414
+
415
+ .dialog-content {
416
+ padding: 1.5rem;
417
+ }
418
+
419
+ .dialog-header {
420
+ display: flex;
421
+ justify-content: space-between;
422
+ align-items: center;
423
+ margin-bottom: 1.25rem;
424
+ }
425
+
426
+ .dialog-title {
427
+ font-weight: 700;
428
+ font-size: 1.25rem;
429
+ margin: 0;
430
+ }
431
+
432
+ .close-btn {
433
+ background: none;
434
+ border: none;
435
+ cursor: pointer;
436
+ font-size: 1.5rem;
437
+ color: #999;
438
+ padding: 0;
439
+ line-height: 1;
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ width: 1.5rem;
444
+ height: 1.5rem;
445
+ border-radius: 50%;
446
+ transition: background-color 0.2s, color 0.2s;
447
+ }
448
+
449
+ .close-btn:hover {
450
+ background-color: #f0f0f0;
451
+ color: #333;
452
+ }
453
+
454
+ .auth-buttons {
455
+ display: flex;
456
+ flex-direction: column;
457
+ gap: 0.75rem;
458
+ }
459
+
460
+ .auth-btn {
461
+ padding: 0.75rem 1rem;
462
+ border: 1px solid #ddd;
463
+ border-radius: 0.375rem;
464
+ cursor: pointer;
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ gap: 0.75rem;
469
+ font-weight: 500;
470
+ font-size: 1rem;
471
+ transition: all 0.2s ease;
472
+ text-decoration: none;
473
+ color: inherit;
474
+ background-color: white;
475
+ }
476
+
477
+ .auth-btn:hover {
478
+ transform: translateY(-0.0625rem);
479
+ box-shadow: 0 0.125rem 0.3125rem rgba(0,0,0,0.05);
480
+ }
481
+
482
+ .auth-btn:active {
483
+ transform: translateY(0);
484
+ }
485
+
486
+ .auth-btn svg {
487
+ width: 1.5rem;
488
+ height: 1.5rem;
489
+ }
490
+
491
+ .auth-btn.google {
492
+ color: #3c4043;
493
+ border-color: #dadce0;
494
+ }
495
+ .auth-btn.google:hover {
496
+ background-color: #f8f9fa;
497
+ border-color: #d2e3fc;
498
+ }
499
+
500
+ .auth-btn.twitch {
501
+ background-color: #9146FF;
502
+ color: white;
503
+ border-color: #9146FF;
504
+ }
505
+ .auth-btn.twitch:hover {
506
+ background-color: #7d2ee6;
507
+ border-color: #7d2ee6;
508
+ }
509
+
510
+ /* Account Switcher Styling */
511
+ .account-list {
512
+ display: flex;
513
+ flex-direction: column;
514
+ gap: 0.5rem;
515
+ }
516
+
517
+ .account-item {
518
+ padding: 0.75rem;
519
+ border: 1px solid #eee;
520
+ border-radius: 0.375rem;
521
+ background: white;
522
+ text-align: left;
523
+ cursor: pointer;
524
+ display: flex;
525
+ justify-content: space-between;
526
+ align-items: center;
527
+ transition: background-color 0.2s;
528
+ font-size: 1rem;
529
+ gap: 1rem;
530
+ }
531
+
532
+ .account-item:hover {
533
+ background-color: #f5f5f5;
534
+ }
535
+
536
+ .account-item.active {
537
+ border-color: #1a73e8;
538
+ background-color: #e8f0fe;
539
+ }
540
+
541
+ .current-badge {
542
+ font-size: 0.75rem;
543
+ background: #1a73e8;
544
+ color: white;
545
+ padding: 0.125rem 0.375rem;
546
+ border-radius: 0.75rem;
547
+ }
548
+ </style>
549
+
550
+ <div class="container">
551
+ ${content}
552
+ <slot></slot>
553
+ </div>
554
+
555
+ <dialog id="login-dialog">
556
+ <div class="dialog-content">
557
+ <div class="dialog-header">
558
+ <h2 class="dialog-title">Log in</h2>
559
+ <button class="close-btn" id="close-dialog" aria-label="Close">&times;</button>
560
+ </div>
561
+ <div class="auth-buttons">
562
+ ${authButtons}
563
+ </div>
564
+ </div>
565
+ </dialog>
566
+
567
+ ${accountSwitcher}
568
+ `;
569
+ }
570
+
571
+ async stopImpersonation() {
572
+ try {
573
+ const res = await fetch(`${this.basePath}/api/stop-impersonation`, {
574
+ method: 'POST',
575
+ });
576
+ if (res.ok) {
577
+ window.location.reload();
578
+ } else {
579
+ console.error('Failed to stop impersonation');
580
+ }
581
+ } catch (e) {
582
+ console.error('Error stopping impersonation', e);
583
+ }
584
+ }
585
+
586
+ addEventListeners() {
587
+ const loginTrigger = this.shadowRoot.getElementById('login-trigger');
588
+ const loginDialog = this.shadowRoot.getElementById('login-dialog');
589
+ const closeLoginBtn = this.shadowRoot.getElementById('close-dialog');
590
+
591
+ if (loginTrigger) {
592
+ loginTrigger.addEventListener('click', () => {
593
+ loginDialog.showModal();
594
+ });
595
+ }
596
+
597
+ if (closeLoginBtn) {
598
+ closeLoginBtn.addEventListener('click', () => {
599
+ loginDialog.close();
600
+ });
601
+ }
602
+
603
+ if (loginDialog) {
604
+ loginDialog.addEventListener('click', (e) => {
605
+ const rect = loginDialog.getBoundingClientRect();
606
+ const isInDialog =
607
+ rect.top <= e.clientY && e.clientY <= rect.top + rect.height && rect.left <= e.clientX && e.clientX <= rect.left + rect.width;
608
+ if (!isInDialog) {
609
+ loginDialog.close();
610
+ }
611
+ });
612
+ }
613
+
614
+ // Impersonation logic
615
+ const stopImpersonationTrigger = this.shadowRoot.getElementById('stop-impersonation-trigger');
616
+ if (stopImpersonationTrigger) {
617
+ stopImpersonationTrigger.addEventListener('click', () => {
618
+ this.stopImpersonation();
619
+ });
620
+ }
621
+
622
+ // Account Switcher Logic
623
+ const switchTrigger = this.shadowRoot.getElementById('switch-account-trigger');
624
+ const accountDialog = this.shadowRoot.getElementById('account-dialog');
625
+ const closeAccountBtn = this.shadowRoot.getElementById('close-account-dialog');
626
+
627
+ if (switchTrigger && accountDialog) {
628
+ switchTrigger.addEventListener('click', () => {
629
+ accountDialog.showModal();
630
+ });
631
+
632
+ if (closeAccountBtn) {
633
+ closeAccountBtn.addEventListener('click', () => {
634
+ accountDialog.close();
635
+ });
636
+ }
637
+
638
+ accountDialog.addEventListener('click', (e) => {
639
+ const rect = accountDialog.getBoundingClientRect();
640
+ const isInDialog =
641
+ rect.top <= e.clientY && e.clientY <= rect.top + rect.height && rect.left <= e.clientX && e.clientX <= rect.left + rect.width;
642
+ if (!isInDialog) {
643
+ accountDialog.close();
644
+ }
645
+ });
646
+
647
+ const accountItems = this.shadowRoot.querySelectorAll('.account-item');
648
+ accountItems.forEach((item) => {
649
+ item.addEventListener('click', () => {
650
+ const accountId = item.getAttribute('data-id');
651
+ this.switchAccount(accountId);
652
+ });
653
+ });
654
+ }
655
+ }
656
+ }
657
+
658
+ customElements.define('power-strip', PowerStrip);