web-mojo 2.2.68 → 2.2.69
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/CHANGELOG.md +25 -9
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -1
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/ChatView-CZ3Key2k.js +2 -0
- package/dist/chunks/ChatView-CZ3Key2k.js.map +1 -0
- package/dist/chunks/ChatView-Dw-iVmht.js +2 -0
- package/dist/chunks/ChatView-Dw-iVmht.js.map +1 -0
- package/dist/chunks/{Dialog-DW7PHzUc.js → Dialog-Dhqtd9Yz.js} +2 -2
- package/dist/chunks/{Dialog-DW7PHzUc.js.map → Dialog-Dhqtd9Yz.js.map} +1 -1
- package/dist/chunks/{Dialog-jfBsXy5X.js → Dialog-t_9l2Mou.js} +2 -2
- package/dist/chunks/{Dialog-jfBsXy5X.js.map → Dialog-t_9l2Mou.js.map} +1 -1
- package/dist/chunks/Files-6eRT5k3r.js +2 -0
- package/dist/chunks/{Files-C-ChBvr5.js.map → Files-6eRT5k3r.js.map} +1 -1
- package/dist/chunks/Files-Dh_5PFBn.js +2 -0
- package/dist/chunks/{Files-DNbHDy43.js.map → Files-Dh_5PFBn.js.map} +1 -1
- package/dist/chunks/{FormView-EoB_ZdIB.js → FormView-B1CXO2t8.js} +2 -2
- package/dist/chunks/{FormView-EoB_ZdIB.js.map → FormView-B1CXO2t8.js.map} +1 -1
- package/dist/chunks/{FormView-Q_lFA0nr.js → FormView-BRHAIawp.js} +2 -2
- package/dist/chunks/{FormView-Q_lFA0nr.js.map → FormView-BRHAIawp.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-lzq4lSTF.js → MetricsMiniChartWidget-D1w608Jy.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-lzq4lSTF.js.map → MetricsMiniChartWidget-D1w608Jy.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-ukn-NRMR.js → MetricsMiniChartWidget-Dg1e6EQJ.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-ukn-NRMR.js.map → MetricsMiniChartWidget-Dg1e6EQJ.js.map} +1 -1
- package/dist/chunks/{PDFViewer-iOqYpg-6.js → PDFViewer-CDeV9OBs.js} +2 -2
- package/dist/chunks/{PDFViewer-iOqYpg-6.js.map → PDFViewer-CDeV9OBs.js.map} +1 -1
- package/dist/chunks/{PDFViewer-sFoyopz3.js → PDFViewer-D_3V8QJe.js} +2 -2
- package/dist/chunks/{PDFViewer-sFoyopz3.js.map → PDFViewer-D_3V8QJe.js.map} +1 -1
- package/dist/chunks/TableView-CI_7a-kD.js +2 -0
- package/dist/chunks/TableView-CI_7a-kD.js.map +1 -0
- package/dist/chunks/TableView-CWk5k4LQ.js +2 -0
- package/dist/chunks/TableView-CWk5k4LQ.js.map +1 -0
- package/dist/chunks/ToastService-C2tTooFn.js +3 -0
- package/dist/chunks/ToastService-C2tTooFn.js.map +1 -0
- package/dist/chunks/ToastService-nUaGVpSl.js +3 -0
- package/dist/chunks/ToastService-nUaGVpSl.js.map +1 -0
- package/dist/chunks/{TokenManager-ChNOca0K.js → TokenManager-ien2XzwO.js} +2 -2
- package/dist/chunks/{TokenManager-ChNOca0K.js.map → TokenManager-ien2XzwO.js.map} +1 -1
- package/dist/chunks/{TokenManager-DKzxBt6g.js → TokenManager-sZgt--C9.js} +2 -2
- package/dist/chunks/{TokenManager-DKzxBt6g.js.map → TokenManager-sZgt--C9.js.map} +1 -1
- package/dist/chunks/User-BL9M_PWB.js +2 -0
- package/dist/chunks/User-BL9M_PWB.js.map +1 -0
- package/dist/chunks/{User-BnlvMG5J.js → User-DqHG5Gr1.js} +2 -3
- package/dist/chunks/User-DqHG5Gr1.js.map +1 -0
- package/dist/chunks/UserProfileView-DnVMHcLH.js +2 -0
- package/dist/chunks/UserProfileView-DnVMHcLH.js.map +1 -0
- package/dist/chunks/UserProfileView-kupeq2rN.js +2 -0
- package/dist/chunks/UserProfileView-kupeq2rN.js.map +1 -0
- package/dist/chunks/{WebApp-Bsic6FPo.js → WebApp-Bti0Gqqo.js} +2 -2
- package/dist/chunks/{WebApp-Bsic6FPo.js.map → WebApp-Bti0Gqqo.js.map} +1 -1
- package/dist/chunks/{WebApp-B0m6JCjO.js → WebApp-CcVF73yg.js} +2 -2
- package/dist/chunks/{WebApp-B0m6JCjO.js.map → WebApp-CcVF73yg.js.map} +1 -1
- package/dist/chunks/index-Aq9ke4vg.js +2 -0
- package/dist/chunks/index-Aq9ke4vg.js.map +1 -0
- package/dist/chunks/index-Da9sT-tE.js +2 -0
- package/dist/chunks/index-Da9sT-tE.js.map +1 -0
- package/dist/chunks/{version-BmVUtM_7.js → version-D8JjsPW0.js} +2 -2
- package/dist/chunks/{version-BmVUtM_7.js.map → version-D8JjsPW0.js.map} +1 -1
- package/dist/chunks/{version-i7K_82Qy.js → version-XmirKYWA.js} +2 -2
- package/dist/chunks/{version-i7K_82Qy.js.map → version-XmirKYWA.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.cjs.js.map +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/user-profile.cjs.js +2 -0
- package/dist/user-profile.cjs.js.map +1 -0
- package/dist/user-profile.es.js +2 -0
- package/dist/user-profile.es.js.map +1 -0
- package/dist/web-mojo.lite.iife.js +9 -6
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +11 -11
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunks/ChatView-Cfe0ZGvr.js +0 -2
- package/dist/chunks/ChatView-Cfe0ZGvr.js.map +0 -1
- package/dist/chunks/ChatView-DuQVFrCY.js +0 -2
- package/dist/chunks/ChatView-DuQVFrCY.js.map +0 -1
- package/dist/chunks/Files-C-ChBvr5.js +0 -2
- package/dist/chunks/Files-DNbHDy43.js +0 -2
- package/dist/chunks/User-BnlvMG5J.js.map +0 -1
- package/dist/chunks/User-DSqcOwPL.js +0 -3
- package/dist/chunks/User-DSqcOwPL.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserProfileView-kupeq2rN.js","sources":["../../src/extensions/user-profile/views/ProfileOverviewSection.js","../../src/extensions/user-profile/views/ProfilePersonalSection.js","../../src/core/models/Passkeys.js","../../src/extensions/user-profile/views/PasskeySetupView.js","../../src/extensions/user-profile/views/ProfileSecuritySection.js","../../src/extensions/user-profile/views/ProfileConnectedSection.js","../../src/extensions/user-profile/views/ProfileSessionsSection.js","../../src/extensions/user-profile/views/ProfileDevicesSection.js","../../src/extensions/user-profile/views/ProfileSecurityEventsSection.js","../../src/extensions/user-profile/views/ProfileNotificationsSection.js","../../src/extensions/user-profile/views/ProfileApiKeysSection.js","../../src/extensions/user-profile/views/ProfileGroupsSection.js","../../src/extensions/user-profile/views/ProfilePermissionsSection.js","../../src/extensions/user-profile/views/UserProfileView.js"],"sourcesContent":["/**\n * ProfileOverviewSection - Profile overview tab\n *\n * Shows avatar, contact, personal info, account status, and permissions peek.\n * Editable: display name, timezone, avatar.\n * Verification actions for email/phone.\n *\n * Full user graph fields used:\n * id, username, email, phone_number, display_name, first_name, last_name,\n * avatar, org (int), permissions, is_active, is_superuser, is_staff,\n * is_email_verified, is_phone_verified, requires_mfa, timezone,\n * last_login, last_activity, date_joined\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\nimport { User } from '@core/models/User.js';\n\nexport default class ProfileOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-overview-section',\n template: `\n <style>\n .po-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .po-section-label:first-child { margin-top: 0; }\n .po-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .po-field-row:last-child { border-bottom: none; }\n .po-field-label { width: 130px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .po-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .po-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .po-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .po-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .po-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .po-badge-muted { font-size: 0.65rem; padding: 0.15em 0.45em; background: #f0f0f0; color: #6c757d; border-radius: 3px; }\n .po-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n .po-perm-pill { display: inline-block; font-size: 0.72rem; padding: 0.2em 0.55em; background: #e7f1ff; color: #0d6efd; border-radius: 3px; margin: 0.1rem; }\n .po-perm-more { font-size: 0.72rem; color: #6c757d; cursor: pointer; }\n .po-perm-more:hover { color: #0d6efd; text-decoration: underline; }\n </style>\n\n <!-- Contact -->\n <div class=\"po-section-label\">Contact</div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Email</div>\n <div class=\"po-field-value\">\n {{model.email}}\n {{#model.is_email_verified|bool}}\n <span class=\"po-badge-ok\">Verified</span>\n {{/model.is_email_verified|bool}}\n {{^model.is_email_verified|bool}}\n <span class=\"po-badge-warn\">Unverified</span>\n {{/model.is_email_verified|bool}}\n </div>\n {{^model.is_email_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"verify-email\" title=\"Send verification email\"><i class=\"bi bi-envelope-check\"></i></button>\n {{/model.is_email_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"update-email\" title=\"Change email\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Phone</div>\n <div class=\"po-field-value\">\n {{#hasPhone|bool}}\n {{model.phone_number}}\n {{#model.is_phone_verified|bool}}\n <span class=\"po-badge-ok\">Verified</span>\n {{/model.is_phone_verified|bool}}\n {{^model.is_phone_verified|bool}}\n <span class=\"po-badge-warn\">Unverified</span>\n {{/model.is_phone_verified|bool}}\n {{/hasPhone|bool}}\n {{^hasPhone|bool}}\n <span class=\"po-not-set\">Not set</span>\n {{/hasPhone|bool}}\n </div>\n {{^hasPhone|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"add-phone\" title=\"Add phone number\"><i class=\"bi bi-plus\"></i></button>\n {{/hasPhone|bool}}\n {{#hasPhone|bool}}\n {{^model.is_phone_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"verify-phone\" title=\"Send verification\"><i class=\"bi bi-phone-vibrate\"></i></button>\n {{/model.is_phone_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"update-phone\" title=\"Change phone number\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"po-field-action\" data-action=\"remove-phone\" title=\"Remove phone number\"><i class=\"bi bi-x-lg\"></i></button>\n {{/hasPhone|bool}}\n </div>\n\n <!-- Account -->\n <div class=\"po-section-label\">Account</div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Username</div>\n <div class=\"po-field-value\">{{model.username}}</div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Status</div>\n <div class=\"po-field-value\">\n {{#model.is_active|bool}}<span class=\"po-badge-ok\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"po-badge-warn\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Role</div>\n <div class=\"po-field-value\">\n {{roleLabel}}\n {{#model.is_staff|bool}}<span class=\"po-badge-muted\">Staff</span>{{/model.is_staff|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">MFA</div>\n <div class=\"po-field-value\">\n {{#model.requires_mfa|bool}}<span class=\"po-badge-ok\">Required</span>{{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}<span class=\"po-badge-muted\">Not required</span>{{/model.requires_mfa|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Member Since</div>\n <div class=\"po-field-value\">{{model.date_joined|date}}</div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Last Login</div>\n <div class=\"po-field-value\">{{model.last_login|relative}}</div>\n </div>\n\n\n <!-- Danger zone -->\n <div style=\"margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #f0f0f0;\">\n <button type=\"button\" class=\"btn btn-link text-danger p-0\" style=\"font-size: 0.8rem; text-decoration: none;\" data-action=\"deactivate-account\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Deactivate Account\n </button>\n </div>\n `,\n ...options\n });\n }\n\n get hasPhone() {\n return !!(this.model && this.model.get('phone_number'));\n }\n\n get roleLabel() {\n if (!this.model) return 'User';\n if (this.model.get('is_superuser')) return 'Superuser';\n return 'User';\n }\n\n get permissionPeek() {\n if (!this.model) return null;\n const perms = this.model.get('permissions');\n if (!perms) return null;\n\n const permMap = {};\n User.PERMISSIONS.forEach(p => { permMap[p.name] = p.label; });\n\n const active = Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => permMap[k] || k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n\n if (active.length === 0) return null;\n\n const PEEK_COUNT = 5;\n return {\n items: active.slice(0, PEEK_COUNT),\n remaining: Math.max(0, active.length - PEEK_COUNT)\n };\n }\n\n get hasActiveGroup() {\n const app = this.getApp();\n return !!(app?.activeGroup && this.model?.member);\n }\n\n get activeGroupName() {\n const app = this.getApp();\n return app?.activeGroup?.get('name') || app?.activeGroup?.get('display_name') || 'Current Group';\n }\n\n get groupPermissionPeek() {\n if (!this.model?.member) return null;\n const perms = this.model.member.get('permissions');\n if (!perms) return null;\n\n const active = Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n\n if (active.length === 0) return null;\n\n const PEEK_COUNT = 5;\n return {\n items: active.slice(0, PEEK_COUNT),\n remaining: Math.max(0, active.length - PEEK_COUNT)\n };\n }\n\n async onActionDeactivateAccount() {\n const app = this.getApp();\n const confirmed = await Dialog.confirm(\n 'Are you sure you want to deactivate your account? A confirmation email will be sent to complete the process. This action cannot be undone.',\n 'Deactivate Account'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/account/deactivate');\n if (resp.success) {\n app?.toast?.success('A confirmation email has been sent. Follow the link to complete deactivation.');\n } else {\n app?.toast?.error(resp.message || 'Failed to request deactivation');\n }\n return true;\n }\n\n async onActionVerifyEmail() {\n const app = this.getApp();\n const email = this.model.get('email');\n\n // Step 1: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/email/send', { method: 'code' });\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n return true;\n }\n\n // Step 2: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${email}</strong>`,\n 'Verify Email',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 3: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/email/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Email verified');\n this.model.set('is_email_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionVerifyPhone() {\n const app = this.getApp();\n const phone = this.model.get('phone_number');\n\n // Step 1: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/phone/send');\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n return true;\n }\n\n // Step 2: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${phone}</strong>`,\n 'Verify Phone',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 3: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/phone/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Phone verified');\n this.model.set('is_phone_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionAddPhone() {\n const app = this.getApp();\n\n // Step 1: Collect phone number\n const phone = await Dialog.prompt(\n 'Enter your phone number:',\n 'Add Phone Number',\n { placeholder: '(415) 555-0123' }\n );\n if (!phone || !phone.trim()) return true;\n\n // Step 2: Save to profile\n const saveResp = await this.model.save({ phone_number: phone.trim() });\n if (saveResp.status !== 200) {\n app?.toast?.error(saveResp.message || 'Failed to save phone number');\n return true;\n }\n\n // Step 3: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/phone/send');\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n await this.render();\n return true;\n }\n\n // Step 4: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${phone.trim()}</strong>`,\n 'Verify Phone',\n { placeholder: '000000' }\n );\n if (!code) {\n await this.render();\n return true;\n }\n\n // Step 5: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/phone/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Phone number added and verified');\n this.model.set('is_phone_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n await this.render();\n }\n return true;\n }\n\n async onActionRemovePhone() {\n const app = this.getApp();\n const confirmed = await Dialog.confirm(\n 'Remove your phone number? You will need to add it again to use phone-based verification.',\n 'Remove Phone'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ phone_number: null });\n if (resp.status === 200) {\n app?.toast?.success('Phone number removed');\n this.model.set('is_phone_verified', false);\n await this.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to remove phone number');\n }\n return true;\n }\n\n async onActionNavigate(event, el) {\n // Bubble up to parent UserProfileView\n if (this.parent && this.parent.onActionNavigate) {\n return this.parent.onActionNavigate(event, el);\n }\n return true;\n }\n}\n","/**\n * ProfilePersonalSection - Personal information tab\n *\n * Editable fields: first name, last name, DOB, timezone, address.\n * DOB is a top-level user field with is_dob_verified badge.\n * Address fields are stored in user.metadata.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class ProfilePersonalSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-personal-section',\n template: `\n <style>\n .pp-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .pp-section-label:first-child { margin-top: 0; }\n .pp-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .pp-field-row:last-child { border-bottom: none; }\n .pp-field-label { width: 130px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .pp-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .pp-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .pp-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .pp-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .pp-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .pp-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n </style>\n\n <!-- Name -->\n <div class=\"pp-section-label\">Name</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Display Name</div>\n <div class=\"pp-field-value\">\n {{#model.display_name}}{{model.display_name}}{{/model.display_name}}\n {{^model.display_name}}<span class=\"pp-not-set\">Not set</span>{{/model.display_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-display-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">First Name</div>\n <div class=\"pp-field-value\">\n {{#model.first_name}}{{model.first_name}}{{/model.first_name}}\n {{^model.first_name}}<span class=\"pp-not-set\">Not set</span>{{/model.first_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-first-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Last Name</div>\n <div class=\"pp-field-value\">\n {{#model.last_name}}{{model.last_name}}{{/model.last_name}}\n {{^model.last_name}}<span class=\"pp-not-set\">Not set</span>{{/model.last_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-last-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Details -->\n <div class=\"pp-section-label\">Details</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Date of Birth</div>\n <div class=\"pp-field-value\">\n {{#hasDob|bool}}\n {{dobFormatted}}\n {{#model.is_dob_verified|bool}}<span class=\"pp-badge-ok\">Verified</span>{{/model.is_dob_verified|bool}}\n {{^model.is_dob_verified|bool}}<span class=\"pp-badge-warn\">Unverified</span>{{/model.is_dob_verified|bool}}\n {{/hasDob|bool}}\n {{^hasDob|bool}}<span class=\"pp-not-set\">Not set</span>{{/hasDob|bool}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-dob\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Timezone</div>\n <div class=\"pp-field-value\">{{timezoneDisplay}}</div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-timezone\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Address -->\n <div class=\"pp-section-label\">Address</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Address</div>\n <div class=\"pp-field-value\">\n {{#hasAddress|bool}}\n {{addressSummary}}\n {{/hasAddress|bool}}\n {{^hasAddress|bool}}\n <span class=\"pp-not-set\">Not set</span>\n {{/hasAddress|bool}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-address\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n `,\n ...options\n });\n }\n\n get hasDob() {\n return !!this.model?.get('dob');\n }\n\n get dobFormatted() {\n const dob = this.model?.get('dob');\n if (!dob) return '';\n try {\n const [year, month, day] = dob.split('-');\n return `${month}/${day}/${year}`;\n } catch {\n return dob;\n }\n }\n\n get timezoneDisplay() {\n const meta = this.model?.get('metadata') || {};\n return meta.timezone || 'Not set';\n }\n\n get hasAddress() {\n const meta = this.model?.get('metadata') || {};\n return !!(meta.street || meta.city || meta.state || meta.zip || meta.country);\n }\n\n get addressSummary() {\n const meta = this.model?.get('metadata') || {};\n const parts = [meta.street, meta.city, meta.state, meta.zip, meta.country].filter(Boolean);\n return parts.join(', ');\n }\n\n async onActionEditDisplayName() {\n const name = await Dialog.prompt(\n 'Enter your display name:',\n 'Display Name',\n { defaultValue: this.model.get('display_name') || '' }\n );\n if (name !== null && name.trim()) {\n const resp = await this.model.save({ display_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Display name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update display name');\n }\n }\n return true;\n }\n\n async onActionEditFirstName() {\n const name = await Dialog.prompt(\n 'Enter your first name:',\n 'First Name',\n { defaultValue: this.model.get('first_name') || '' }\n );\n if (name !== null) {\n const resp = await this.model.save({ first_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('First name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update first name');\n }\n }\n return true;\n }\n\n async onActionEditLastName() {\n const name = await Dialog.prompt(\n 'Enter your last name:',\n 'Last Name',\n { defaultValue: this.model.get('last_name') || '' }\n );\n if (name !== null) {\n const resp = await this.model.save({ last_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Last name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update last name');\n }\n }\n return true;\n }\n\n async onActionEditDob() {\n const data = await Dialog.showForm({\n title: 'Date of Birth',\n size: 'sm',\n fields: [{\n name: 'dob',\n type: 'date',\n label: 'Date of Birth',\n cols: 12\n }],\n data: { dob: this.model.get('dob') || '' }\n });\n if (!data) return true;\n\n const resp = await this.model.save({ dob: data.dob || null });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Date of birth updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update date of birth');\n }\n return true;\n }\n\n async onActionEditTimezone() {\n const meta = this.model.get('metadata') || {};\n const data = await Dialog.showForm({\n title: 'Change Timezone',\n fields: [{\n name: 'timezone',\n type: 'select',\n label: 'Timezone',\n cols: 12,\n options: [\n { value: 'America/New_York', text: 'Eastern Time (ET)' },\n { value: 'America/Chicago', text: 'Central Time (CT)' },\n { value: 'America/Denver', text: 'Mountain Time (MT)' },\n { value: 'America/Los_Angeles', text: 'Pacific Time (PT)' },\n { value: 'America/Anchorage', text: 'Alaska Time (AKT)' },\n { value: 'Pacific/Honolulu', text: 'Hawaii Time (HT)' },\n { value: 'UTC', text: 'UTC' },\n { value: 'Europe/London', text: 'London (GMT/BST)' },\n { value: 'Europe/Paris', text: 'Paris (CET/CEST)' },\n { value: 'Europe/Berlin', text: 'Berlin (CET/CEST)' },\n { value: 'Asia/Tokyo', text: 'Tokyo (JST)' },\n { value: 'Asia/Shanghai', text: 'Shanghai (CST)' },\n { value: 'Australia/Sydney', text: 'Sydney (AEST)' }\n ]\n }],\n data: { timezone: meta.timezone || '' },\n size: 'sm'\n });\n if (!data) return true;\n\n const updatedMeta = { ...meta, timezone: data.timezone };\n const resp = await this.model.save({ metadata: updatedMeta });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Timezone updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update timezone');\n }\n return true;\n }\n\n async onActionEditAddress() {\n const meta = this.model.get('metadata') || {};\n const data = await Dialog.showForm({\n title: 'Edit Address',\n size: 'md',\n fields: [\n { name: 'street', type: 'text', label: 'Street', placeholder: '123 Main St', cols: 12 },\n { name: 'city', type: 'text', label: 'City', cols: 6 },\n { name: 'state', type: 'text', label: 'State / Province', cols: 6 },\n { name: 'zip', type: 'text', label: 'Zip / Postal Code', cols: 6 },\n { name: 'country', type: 'text', label: 'Country', cols: 6 }\n ],\n data: {\n street: meta.street || '',\n city: meta.city || '',\n state: meta.state || '',\n zip: meta.zip || '',\n country: meta.country || ''\n }\n });\n\n if (!data) return true;\n\n const updatedMeta = {\n ...meta,\n street: data.street || '',\n city: data.city || '',\n state: data.state || '',\n zip: data.zip || '',\n country: data.country || ''\n };\n const resp = await this.model.save({ metadata: updatedMeta });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Address updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update address');\n }\n return true;\n }\n}\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport rest from '@core/Rest.js';\n\n// ─── WebAuthn base64url helpers ──────────────────────────────────────────────\nfunction base64urlToBytes(base64url) {\n const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);\n return Uint8Array.from(atob(padded), c => c.charCodeAt(0));\n}\n\nfunction bytesToBase64url(buffer) {\n return btoa(String.fromCharCode(...new Uint8Array(buffer)))\n .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * Passkey - WebAuthn/FIDO2 passkey model\n * Maps to REST endpoints under /api/account/passkeys\n *\n * Key operations:\n * - List/View/Update/Delete passkeys (standard CRUD)\n * - Register new passkeys via Passkey.register(friendlyName)\n *\n * Notes:\n * - Login flow is NOT handled here (separate auth flow)\n * - Passkeys are portal-specific (rp_id = domain)\n * - Most fields are read-only; only friendly_name and is_enabled are editable\n */\nclass Passkey extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/account/passkeys',\n ...options\n });\n }\n\n /**\n * Suggest a friendly name based on the user's device and browser.\n * @returns {string} e.g. \"Mac — Chrome\", \"iPhone — Safari\"\n */\n static suggestName() {\n const ua = navigator.userAgent;\n let device = 'Device';\n if (/iPad/.test(ua)) device = 'iPad';\n else if (/iPhone/.test(ua)) device = 'iPhone';\n else if (/Macintosh|MacIntel/.test(ua)) device = 'Mac';\n else if (/Android/.test(ua)) device = 'Android';\n else if (/Windows/.test(ua)) device = 'Windows PC';\n else if (/Linux/.test(ua)) device = 'Linux';\n\n let browser = '';\n if (/Edg\\//.test(ua)) browser = 'Edge';\n else if (/Chrome\\//.test(ua) && !/Chromium/.test(ua)) browser = 'Chrome';\n else if (/Safari\\//.test(ua) && !/Chrome/.test(ua)) browser = 'Safari';\n else if (/Firefox\\//.test(ua)) browser = 'Firefox';\n\n return browser ? `${device} — ${browser}` : device;\n }\n\n /**\n * Full passkey registration flow.\n * Handles: registerBegin → navigator.credentials.create → registerComplete\n *\n * Call this AFTER collecting the friendly name from the user.\n *\n * @param {string} friendlyName - Human-readable label for the passkey\n * @returns {Promise<{success: boolean, passkey?: object, error?: string}>}\n */\n static async register(friendlyName) {\n // 1. Begin — get challenge from server\n const beginResp = await Passkey.registerBegin();\n if (!beginResp?.data?.challenge_id || !beginResp?.data?.publicKey) {\n return { success: false, error: beginResp?.error || 'Could not start registration.' };\n }\n\n const { challenge_id, publicKey } = beginResp.data;\n\n // 2. Decode base64url fields the browser expects as ArrayBuffers\n if (typeof publicKey.challenge === 'string') {\n publicKey.challenge = base64urlToBytes(publicKey.challenge);\n }\n if (typeof publicKey.user?.id === 'string') {\n publicKey.user.id = base64urlToBytes(publicKey.user.id);\n }\n if (publicKey.excludeCredentials) {\n publicKey.excludeCredentials = publicKey.excludeCredentials.map(cred => ({\n ...cred,\n id: typeof cred.id === 'string' ? base64urlToBytes(cred.id) : cred.id\n }));\n }\n\n // 3. OS biometric prompt\n const credential = await navigator.credentials.create({ publicKey });\n if (!credential) {\n return { success: false, error: 'Passkey creation was cancelled.' };\n }\n\n // 4. Encode credential for the server\n const credentialData = {\n id: credential.id,\n rawId: bytesToBase64url(credential.rawId),\n type: credential.type,\n response: {\n clientDataJSON: bytesToBase64url(credential.response.clientDataJSON),\n attestationObject: bytesToBase64url(credential.response.attestationObject)\n }\n };\n if (credential.response.getTransports) {\n credentialData.transports = credential.response.getTransports();\n }\n\n // 5. Complete registration\n const completeResp = await Passkey.registerComplete({\n challenge_id,\n credential: credentialData,\n friendly_name: friendlyName || 'My Passkey'\n });\n\n if (completeResp?.data?.id) {\n return { success: true, passkey: completeResp.data };\n }\n return { success: false, error: completeResp?.error || 'Registration could not be completed.' };\n }\n\n /** @private */\n static async registerBegin(options = {}) {\n try {\n return await rest.POST('/api/account/passkeys/register/begin', {}, options.params, { dataOnly: true });\n } catch (err) {\n return { success: false, error: err?.message || 'Failed to begin passkey registration' };\n }\n }\n\n /** @private */\n static async registerComplete(data = {}, options = {}) {\n if (!data.challenge_id || !data.credential) {\n return { success: false, error: 'Missing challenge_id or credential data' };\n }\n try {\n return await rest.POST('/api/account/passkeys/register/complete', data, options.params, { dataOnly: true });\n } catch (err) {\n return { success: false, error: err?.message || 'Failed to complete passkey registration' };\n }\n }\n}\n\n/**\n * PasskeyList - Collection of Passkey\n * Supports standard MOJO list/search/sort/pagination patterns\n */\nclass PasskeyList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: Passkey,\n endpoint: '/api/account/passkeys',\n size: 10,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for Passkey\n *\n * Notes:\n * - No create form (registration uses WebAuthn flow)\n * - Edit form allows changing friendly_name and is_enabled only\n * - View form shows all fields as read-only for informational purposes\n */\nconst PasskeyForms = {\n edit: {\n title: 'Edit Passkey',\n fields: [\n {\n name: 'friendly_name',\n type: 'text',\n label: 'Name',\n placeholder: 'My iPhone',\n required: true,\n columns: 12,\n help: 'A friendly name to identify this passkey'\n },\n {\n name: 'is_enabled',\n type: 'switch',\n label: 'Enabled',\n columns: 12,\n help: 'Disable to prevent this passkey from being used for authentication'\n }\n ]\n },\n\n view: {\n title: 'Passkey Details',\n fields: [\n {\n name: 'friendly_name',\n type: 'text',\n label: 'Name',\n readonly: true,\n columns: 12\n },\n {\n name: 'is_enabled',\n type: 'switch',\n label: 'Enabled',\n readonly: true,\n columns: 6\n },\n {\n name: 'rp_id',\n type: 'text',\n label: 'Portal (RP ID)',\n readonly: true,\n columns: 6,\n help: 'The portal/domain this passkey is registered for'\n },\n {\n name: 'aaguid',\n type: 'text',\n label: 'Authenticator GUID',\n readonly: true,\n columns: 12,\n help: 'Unique identifier for the authenticator device'\n },\n {\n name: 'transports',\n type: 'text',\n label: 'Transports',\n readonly: true,\n columns: 6,\n help: 'Available transport methods (e.g., internal, usb, nfc, ble)'\n },\n {\n name: 'sign_count',\n type: 'number',\n label: 'Signature Count',\n readonly: true,\n columns: 6,\n help: 'Number of times this passkey has been used (for clone detection)'\n },\n {\n name: 'last_used',\n type: 'datetime',\n label: 'Last Used',\n readonly: true,\n columns: 6\n },\n {\n name: 'created',\n type: 'datetime',\n label: 'Created',\n readonly: true,\n columns: 6\n }\n ]\n }\n};\n\nexport {\n Passkey,\n PasskeyList,\n PasskeyForms\n};\n","/**\n * PasskeySetupView - Post-login passkey setup prompt\n *\n * Compact centered dialog body shown after login to encourage\n * users to register a passkey. Create / Skip / Don't ask again.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport { Passkey } from '@core/models/Passkeys.js';\n\nexport default class PasskeySetupView extends View {\n constructor(options = {}) {\n super({\n className: 'passkey-setup-view',\n template: `\n <style>\n .pks-body { padding: 2rem 1.75rem 1rem; text-align: center; }\n .pks-icon { width: 56px; height: 56px; background: #e7f1ff; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 1.5rem; color: #0d6efd; margin-bottom: 1rem; }\n .pks-body h5 { font-weight: 700; font-size: 1.05rem; margin-bottom: 0.35rem; }\n .pks-body p { font-size: 0.83rem; color: #6c757d; margin-bottom: 1.25rem; line-height: 1.45; }\n .pks-footer { padding: 0 1.75rem 1.5rem; display: flex; flex-direction: column; gap: 0.4rem; }\n .pks-footer .btn-create { padding: 0.6rem; font-weight: 600; font-size: 0.9rem; border-radius: 8px; }\n .pks-footer .btn-skip { background: none; border: none; color: #6c757d; font-size: 0.82rem; padding: 0.4rem; cursor: pointer; }\n .pks-footer .btn-skip:hover { color: #495057; }\n .pks-dont-show { text-align: center; padding: 0 1.75rem 1.25rem; }\n .pks-dont-show label { font-size: 0.73rem; color: #adb5bd; cursor: pointer; }\n </style>\n\n <div class=\"pks-body\">\n <div class=\"pks-icon\"><i class=\"bi bi-fingerprint\"></i></div>\n <h5>Add a Passkey</h5>\n <p>Sign in faster with Face ID, Touch ID, or your device PIN. No passwords needed.</p>\n </div>\n <div class=\"pks-footer\">\n <button class=\"btn btn-primary btn-create\" data-action=\"create-passkey\"><i class=\"bi bi-fingerprint me-1\"></i>Create Passkey</button>\n <button class=\"btn-skip\" data-action=\"skip\">Not now</button>\n </div>\n <div class=\"pks-dont-show\">\n <label><input type=\"checkbox\" class=\"form-check-input form-check-input-sm me-1\" data-action=\"dont-ask\"> Don't ask again</label>\n </div>\n `,\n ...options\n });\n }\n\n async _askPasskeyName() {\n const suggested = Passkey.suggestName();\n return Dialog.showDialog({\n title: '<i class=\"bi bi-fingerprint me-2\"></i>Register a Passkey',\n size: 'sm',\n centered: true,\n body: `\n <div style=\"text-align:center; padding: 0.5rem 0 1rem;\">\n <div style=\"width:72px; height:72px; background:linear-gradient(135deg, #e7f1ff 0%, #d0e2ff 100%); border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:2rem; color:#0d6efd; margin-bottom:1rem;\">\n <i class=\"bi bi-fingerprint\"></i>\n </div>\n <p style=\"font-size:0.85rem; color:#6c757d; margin-bottom:1.25rem; line-height:1.5;\">\n Passkeys use your device's biometrics — fingerprint, face, or PIN — instead of a password.\n They're <strong>phishing-resistant</strong> and the private key never leaves your device.\n </p>\n <div class=\"text-start\" style=\"margin-bottom:0.25rem;\">\n <label class=\"form-label fw-semibold\" style=\"font-size:0.82rem;\">Name this passkey</label>\n <input type=\"text\" class=\"form-control\" id=\"pks-name-input\" value=\"${suggested}\" placeholder=\"e.g., My MacBook\" style=\"border-radius:8px;\">\n <div class=\"form-text\">A label so you can identify this passkey later.</div>\n </div>\n </div>`,\n buttons: [\n { text: 'Cancel', class: 'btn-secondary', dismiss: true },\n {\n text: '<i class=\"bi bi-fingerprint me-1\"></i>Continue',\n class: 'btn-primary',\n handler: ({ dialog }) => {\n const input = dialog.element?.querySelector('#pks-name-input');\n return input?.value?.trim() || suggested;\n }\n }\n ]\n });\n }\n\n static showSuccess(name) {\n return Dialog.showDialog({\n title: '<span class=\"text-success\"><i class=\"bi bi-check-circle-fill me-2\"></i>Passkey Registered</span>',\n size: 'sm',\n centered: true,\n body: `\n <div style=\"text-align:center; padding: 0.5rem 0 0.75rem;\">\n <div style=\"width:72px; height:72px; background:linear-gradient(135deg, #d1e7dd 0%, #badbcc 100%); border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:2rem; color:#198754; margin-bottom:1rem;\">\n <i class=\"bi bi-shield-lock-fill\"></i>\n </div>\n <h6 class=\"fw-bold mb-2\">${name || 'Your passkey'} is ready</h6>\n <p style=\"font-size:0.85rem; color:#6c757d; line-height:1.5; margin-bottom:0;\">\n Next time you sign in, choose <strong>\"Login with Passkey\"</strong> — no username or password needed.\n Just your fingerprint, face, or device PIN.\n </p>\n </div>`,\n buttons: [\n { text: 'Done', class: 'btn-success', value: true }\n ]\n });\n }\n\n static showError(message) {\n return Dialog.alert({\n title: 'Passkey Error',\n message: message || 'Something went wrong during passkey registration.',\n type: 'error'\n });\n }\n\n async onActionCreatePasskey() {\n try {\n // 1. Ask for friendly name\n const friendlyName = await this._askPasskeyName();\n if (!friendlyName) return true;\n\n // 2. Full WebAuthn flow (begin → biometric → complete)\n const result = await Passkey.register(friendlyName);\n\n if (result.success) {\n localStorage.setItem('passkey_setup_dismissed', '1');\n await PasskeySetupView.showSuccess(friendlyName);\n this.emit('dismiss');\n } else {\n PasskeySetupView.showError(result.error);\n }\n } catch (err) {\n if (err.name === 'NotAllowedError') return true;\n if (err.name === 'SecurityError') {\n PasskeySetupView.showError('Passkeys are not supported on this domain. Ensure you are using HTTPS.');\n } else {\n console.error('Passkey registration error:', err);\n PasskeySetupView.showError(err.message || 'An unexpected error occurred.');\n }\n }\n return true;\n }\n\n async onActionSkip() {\n this.emit('dismiss');\n return true;\n }\n\n async onActionDontAsk() {\n const checkbox = this.element?.querySelector('.pks-dont-show input[type=\"checkbox\"]');\n if (checkbox && checkbox.checked) {\n localStorage.setItem('passkey_setup_dismissed', '1');\n this.emit('dismiss');\n } else {\n localStorage.removeItem('passkey_setup_dismissed');\n }\n return true;\n }\n}\n","/**\n * ProfileSecuritySection - Security dashboard tab\n *\n * Compact card rows. Password and Passkeys are actions (dialog/flow).\n * Sessions, Devices, Activity navigate to their own nav sections.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\nimport PasskeySetupView from './PasskeySetupView.js';\nimport { Passkey, PasskeyList, PasskeyForms } from '@core/models/Passkeys.js';\n\nexport default class ProfileSecuritySection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-security-section',\n template: `\n <style>\n .ps-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .ps-section-label:first-child { margin-top: 0; }\n .ps-item { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }\n .ps-item:hover { border-color: #dee2e6; background: #fafbfd; }\n .ps-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }\n .ps-info { flex: 1; min-width: 0; }\n .ps-title { font-weight: 600; font-size: 0.88rem; }\n .ps-desc { font-size: 0.78rem; color: #6c757d; }\n .ps-badge { font-size: 0.72rem; padding: 0.15em 0.5em; border-radius: 3px; flex-shrink: 0; }\n .ps-chevron { color: #ced4da; font-size: 0.8rem; flex-shrink: 0; }\n </style>\n\n <div class=\"ps-section-label\">Authentication</div>\n\n <div class=\"ps-item\" data-action=\"change-password\">\n <div class=\"ps-icon bg-primary bg-opacity-10 text-primary\"><i class=\"bi bi-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Password</div>\n <div class=\"ps-desc\">Change your account password</div>\n </div>\n <span class=\"ps-badge bg-light text-muted border\">Change</span>\n </div>\n\n <div class=\"ps-item\" data-action=\"manage-passkeys\">\n <div class=\"ps-icon bg-success bg-opacity-10 text-success\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Passkeys</div>\n <div class=\"ps-desc\">Passwordless sign-in with biometrics</div>\n </div>\n <i class=\"bi bi-chevron-right ps-chevron\"></i>\n </div>\n\n <div class=\"ps-item\" data-action=\"manage-totp\">\n <div class=\"ps-icon bg-purple bg-opacity-10\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-shield-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Authenticator App</div>\n <div class=\"ps-desc\">Two-factor authentication with TOTP codes</div>\n </div>\n {{#model.requires_mfa|bool}}\n <span class=\"ps-badge bg-success bg-opacity-10 text-success border\">Enabled</span>\n {{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}\n <span class=\"ps-badge bg-light text-muted border\">Setup</span>\n {{/model.requires_mfa|bool}}\n </div>\n\n {{#model.requires_mfa|bool}}\n <div class=\"ps-item\" data-action=\"manage-recovery-codes\">\n <div class=\"ps-icon\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-file-earmark-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Recovery Codes</div>\n <div class=\"ps-desc\">Backup codes for when you lose your authenticator</div>\n </div>\n <i class=\"bi bi-chevron-right ps-chevron\"></i>\n </div>\n {{/model.requires_mfa|bool}}\n\n <div class=\"ps-section-label\">Sessions</div>\n\n <div class=\"ps-item\" data-action=\"revoke-all-sessions\">\n <div class=\"ps-icon\" style=\"background: rgba(220,53,69,0.1); color: #dc3545;\"><i class=\"bi bi-box-arrow-right\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Revoke All Sessions</div>\n <div class=\"ps-desc\">Sign out of all devices except this one</div>\n </div>\n </div>\n `,\n ...options\n });\n }\n\n // --- Actions ---\n\n async onActionChangePassword() {\n const app = this.getApp();\n if (app && app.changePassword) {\n await app.changePassword();\n }\n return true;\n }\n\n async onActionManagePasskeys() {\n const collection = new PasskeyList({ params: { user: this.model.id } });\n try {\n await collection.fetch();\n } catch (e) {\n // ignore fetch errors\n }\n\n const items = collection.models || [];\n const view = new View({\n template: `\n <style>\n .pk-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0.75rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.4rem; }\n .pk-icon { width: 32px; height: 32px; background: #e7f1ff; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #0d6efd; font-size: 0.9rem; flex-shrink: 0; }\n .pk-info { flex: 1; min-width: 0; }\n .pk-name { font-weight: 600; font-size: 0.85rem; }\n .pk-meta { font-size: 0.73rem; color: #6c757d; }\n .pk-actions { display: flex; gap: 0.25rem; }\n .pk-actions .btn { padding: 0.2rem 0.4rem; font-size: 0.75rem; }\n .pk-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pk-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n {{#passkeys}}\n <div class=\"pk-row\">\n <div class=\"pk-icon\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"pk-info\">\n <div class=\"pk-name\">{{.friendly_name|default:'Unnamed Passkey'}}</div>\n <div class=\"pk-meta\">Created {{.created|date}} · Last used {{.last_used|relative|default:'never'}} · {{.sign_count}} uses</div>\n </div>\n <div class=\"pk-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-passkey\" data-id=\"{{.id}}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"delete-passkey\" data-id=\"{{.id}}\" title=\"Delete\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>\n {{/passkeys}}\n {{^passkeys|bool}}\n <div class=\"pk-empty\">\n <i class=\"bi bi-fingerprint\"></i>\n No passkeys registered yet\n </div>\n {{/passkeys|bool}}\n `\n });\n view.passkeys = items.map(p => p.toJSON ? p.toJSON() : p);\n\n view.onActionEditPasskey = async (event, el) => {\n const id = el.dataset.id;\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await Dialog.showModelForm({\n title: 'Edit Passkey',\n model: passkey,\n fields: PasskeyForms.edit.fields,\n size: 'sm'\n });\n }\n return true;\n };\n\n view.onActionDeletePasskey = async (event, el) => {\n const id = el.dataset.id;\n const confirmed = await Dialog.confirm('Delete this passkey? You won\\'t be able to use it to sign in anymore.', 'Delete Passkey');\n if (confirmed) {\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await passkey.destroy();\n this.getApp()?.toast?.success('Passkey deleted');\n }\n }\n return true;\n };\n\n const result = await Dialog.showDialog({\n title: 'Passkeys',\n body: view,\n size: 'md',\n buttons: [\n { text: 'Add Passkey', icon: 'bi-plus-lg', class: 'btn-primary', value: 'add' },\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n\n if (result === 'add') {\n await this._addPasskey();\n }\n return true;\n }\n\n async _addPasskey() {\n // Ask for name via rich dialog\n const suggested = Passkey.suggestName();\n const friendlyName = await Dialog.showDialog({\n title: '<i class=\"bi bi-fingerprint me-2\"></i>Register a Passkey',\n size: 'sm',\n centered: true,\n body: `\n <div style=\"text-align:center; padding: 0.5rem 0 1rem;\">\n <div style=\"width:72px; height:72px; background:linear-gradient(135deg, #e7f1ff 0%, #d0e2ff 100%); border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:2rem; color:#0d6efd; margin-bottom:1rem;\">\n <i class=\"bi bi-fingerprint\"></i>\n </div>\n <p style=\"font-size:0.85rem; color:#6c757d; margin-bottom:1.25rem; line-height:1.5;\">\n Passkeys replace passwords with biometrics — fingerprint, face, or device PIN.\n The private key never leaves your device.\n </p>\n <div class=\"text-start\" style=\"margin-bottom:0.25rem;\">\n <label class=\"form-label fw-semibold\" style=\"font-size:0.82rem;\">Name this passkey</label>\n <input type=\"text\" class=\"form-control\" id=\"pss-name-input\" value=\"${suggested}\" placeholder=\"e.g., My MacBook\" style=\"border-radius:8px;\">\n <div class=\"form-text\">A label so you can identify this passkey later.</div>\n </div>\n </div>`,\n buttons: [\n { text: 'Cancel', class: 'btn-secondary', dismiss: true },\n {\n text: '<i class=\"bi bi-fingerprint me-1\"></i>Continue',\n class: 'btn-primary',\n handler: ({ dialog }) => {\n const input = dialog.element?.querySelector('#pss-name-input');\n return input?.value?.trim() || suggested;\n }\n }\n ]\n });\n if (!friendlyName) return;\n\n try {\n const result = await Passkey.register(friendlyName);\n if (result.success) {\n await PasskeySetupView.showSuccess(friendlyName);\n } else {\n PasskeySetupView.showError(result.error);\n }\n } catch (err) {\n if (err.name === 'NotAllowedError') return;\n if (err.name === 'SecurityError') {\n PasskeySetupView.showError('Passkeys are not supported on this domain. Ensure you are using HTTPS.');\n } else {\n console.error('Passkey registration error:', err);\n PasskeySetupView.showError(err.message || 'An unexpected error occurred.');\n }\n }\n }\n\n async onActionManageTotp() {\n const app = this.getApp();\n const isMfaEnabled = this.model.get('requires_mfa');\n\n if (isMfaEnabled) {\n // Already enabled — offer to disable\n const confirmed = await Dialog.confirm(\n 'Disable authenticator app? You will no longer need a TOTP code to sign in.',\n 'Disable Authenticator'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE('/api/account/totp');\n if (resp.success) {\n app?.toast?.success('Authenticator app disabled');\n this.model.set('requires_mfa', false);\n await this.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to disable authenticator');\n }\n return true;\n }\n\n // Step 1: Call setup to get QR code + secret\n const setupResp = await rest.POST('/api/account/totp/setup', {}, {}, { dataOnly: true });\n if (!setupResp.success || !setupResp.data) {\n app?.toast?.error(setupResp.message || 'Failed to start authenticator setup');\n return true;\n }\n\n const { secret, qr_code } = setupResp.data;\n\n // Step 2: Show QR code dialog\n const setupView = new View({\n template: `\n <div style=\"text-align: center; padding: 0.5rem 0;\">\n <p style=\"font-size: 0.85rem; color: #6c757d; margin-bottom: 1rem;\">\n Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.)\n </p>\n <img src=\"{{{qrCode}}}\" alt=\"TOTP QR Code\" style=\"width: 200px; height: 200px; margin: 0 auto; display: block; border: 1px solid #e9ecef; border-radius: 8px; padding: 8px;\" />\n <div style=\"margin-top: 1rem;\">\n <p style=\"font-size: 0.75rem; color: #adb5bd; margin-bottom: 0.25rem;\">Or enter this key manually:</p>\n <code style=\"font-size: 0.85rem; letter-spacing: 0.1em; user-select: all; padding: 0.35rem 0.75rem; background: #f8f9fa; border-radius: 4px;\">{{secret}}</code>\n </div>\n </div>\n `\n });\n setupView.qrCode = qr_code;\n setupView.secret = secret;\n\n const result = await Dialog.showDialog({\n title: 'Set Up Authenticator',\n body: setupView,\n size: 'sm',\n buttons: [\n { text: 'Cancel', class: 'btn-secondary', dismiss: true },\n { text: 'Next', class: 'btn-primary', value: 'next' }\n ]\n });\n\n if (result !== 'next') return true;\n\n // Step 3: Prompt for the first TOTP code to confirm\n const code = await Dialog.prompt(\n 'Enter the 6-digit code from your authenticator app to verify setup:',\n 'Verify Authenticator',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm\n const confirmResp = await rest.POST('/api/account/totp/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Authenticator app enabled');\n this.model.set('requires_mfa', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid code. Please try setup again.');\n }\n return true;\n }\n\n // Navigate to sections in the parent UserProfileView\n async onActionManageRecoveryCodes() {\n const app = this.getApp();\n\n // Fetch masked recovery codes\n const resp = await rest.GET('/api/account/totp/recovery-codes', {}, { dataOnly: true });\n if (!resp.success || !resp.data) {\n app?.toast?.error(resp.message || 'Failed to load recovery codes');\n return true;\n }\n\n const { remaining, codes } = resp.data;\n\n const view = new View({\n template: `\n <style>\n .rc-info { font-size: 0.82rem; color: #6c757d; margin-bottom: 1rem; }\n .rc-remaining { font-weight: 600; color: #495057; }\n .rc-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; }\n .rc-code { font-family: monospace; font-size: 0.85rem; padding: 0.35rem 0.6rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; text-align: center; }\n </style>\n <div class=\"rc-info\">\n <span class=\"rc-remaining\">{{remaining}}</span> recovery codes remaining\n </div>\n <div class=\"rc-list\">\n {{#codes}}\n <div class=\"rc-code\">{{.}}</div>\n {{/codes}}\n </div>\n `\n });\n view.remaining = remaining;\n view.codes = codes || [];\n\n const result = await Dialog.showDialog({\n title: 'Recovery Codes',\n body: view,\n size: 'sm',\n buttons: [\n { text: 'Regenerate', icon: 'bi-arrow-repeat', class: 'btn-warning', value: 'regenerate' },\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n\n if (result === 'regenerate') {\n // Require current TOTP code to regenerate\n const code = await Dialog.prompt(\n 'Enter your current authenticator code to regenerate recovery codes:',\n 'Regenerate Recovery Codes',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n const regenResp = await rest.POST('/api/account/totp/recovery-codes/regenerate', {\n code: code.trim()\n }, {}, { dataOnly: true });\n\n if (regenResp.success && regenResp.data?.recovery_codes) {\n const newCodes = regenResp.data.recovery_codes;\n const codesText = newCodes.join('\\n');\n\n const newView = new View({\n template: `\n <style>\n .rc-warning { padding: 0.65rem 0.85rem; background: #fff3cd; border: 1px solid #ffecb5; border-radius: 6px; margin-bottom: 1rem; font-size: 0.8rem; color: #664d03; }\n .rc-new-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 1rem; }\n .rc-new-code { font-family: monospace; font-size: 0.85rem; padding: 0.35rem 0.6rem; background: #d1e7dd; border: 1px solid #badbcc; border-radius: 4px; text-align: center; }\n </style>\n <div class=\"rc-warning\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>\n <strong>Save these codes now.</strong> They will not be shown again. Old codes are invalidated.\n </div>\n <div class=\"rc-new-list\">\n {{#newCodes}}\n <div class=\"rc-new-code\">{{.}}</div>\n {{/newCodes}}\n </div>\n `\n });\n newView.newCodes = newCodes;\n\n await Dialog.showDialog({\n title: 'New Recovery Codes',\n body: newView,\n size: 'sm',\n buttons: [\n { text: 'Copy All', icon: 'bi-clipboard', class: 'btn-primary', handler: async () => {\n try {\n await navigator.clipboard.writeText(codesText);\n app?.toast?.success('Recovery codes copied');\n } catch {\n app?.toast?.error('Failed to copy codes');\n }\n return false; // keep dialog open\n }},\n { text: 'Done', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n } else {\n app?.toast?.error(regenResp.message || 'Failed to regenerate recovery codes');\n }\n }\n return true;\n }\n\n async onActionRevokeAllSessions() {\n const app = this.getApp();\n\n const confirmed = await Dialog.confirm(\n 'This will sign you out of all other devices and sessions. Your current session will remain active. This cannot be undone.',\n 'Revoke All Sessions'\n );\n if (!confirmed) return true;\n\n const password = await Dialog.prompt(\n 'Enter your current password to confirm:',\n 'Confirm Password',\n { placeholder: 'Current password' }\n );\n if (!password) return true;\n\n const resp = await rest.POST('/api/auth/sessions/revoke', {\n current_password: password.trim()\n }, {}, { dataOnly: true });\n\n if (resp.success && resp.data) {\n // Store new tokens — old ones are now invalid\n if (resp.data.access_token) {\n app?.auth?.setTokens?.(resp.data);\n }\n app?.toast?.success('All other sessions have been revoked');\n } else {\n app?.toast?.error(resp.message || 'Failed to revoke sessions');\n }\n return true;\n }\n\n // Navigate to sections in the parent UserProfileView\n async onActionNavigate(event, el) {\n if (this.parent && this.parent.onActionNavigate) {\n return this.parent.onActionNavigate(event, el);\n }\n return true;\n }\n}\n","/**\n * ProfileConnectedSection - OAuth connected accounts tab\n *\n * Lists OAuth provider connections (Google, GitHub, etc.).\n * Users can unlink connections with lockout guard protection.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\n\nconst PROVIDER_ICONS = {\n google: 'bi-google',\n github: 'bi-github',\n microsoft: 'bi-microsoft',\n apple: 'bi-apple',\n facebook: 'bi-facebook',\n twitter: 'bi-twitter-x',\n linkedin: 'bi-linkedin'\n};\n\nexport default class ProfileConnectedSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-connected-section',\n template: `\n <style>\n .pc-row { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; }\n .pc-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; background: #f0f0f0; color: #495057; }\n .pc-info { flex: 1; min-width: 0; }\n .pc-provider { font-weight: 600; font-size: 0.88rem; text-transform: capitalize; }\n .pc-meta { font-size: 0.78rem; color: #6c757d; }\n .pc-actions { flex-shrink: 0; }\n .pc-actions .btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; }\n .pc-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pc-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#connections}}\n <div class=\"pc-row\">\n <div class=\"pc-icon\"><i class=\"bi {{.icon}}\"></i></div>\n <div class=\"pc-info\">\n <div class=\"pc-provider\">{{.provider}}</div>\n <div class=\"pc-meta\">{{.email}} · Connected {{.created|relative}}</div>\n </div>\n <div class=\"pc-actions\">\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"unlink\" data-id=\"{{.id}}\" title=\"Unlink\"><i class=\"bi bi-x-lg me-1\"></i>Unlink</button>\n </div>\n </div>\n {{/connections}}\n {{^connections|bool}}\n <div class=\"pc-empty\">\n <i class=\"bi bi-plug\"></i>\n No connected accounts\n <div style=\"font-size: 0.78rem; margin-top: 0.5rem;\">\n Connect a social account by signing in with a provider while logged in.\n </div>\n </div>\n {{/connections|bool}}\n `,\n ...options\n });\n this.connections = [];\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/oauth_connection');\n const results = resp?.data?.results || resp?.data || [];\n this.connections = results.map(c => ({\n ...c,\n icon: PROVIDER_ICONS[c.provider] || 'bi-link-45deg'\n }));\n } catch (e) {\n this.connections = [];\n }\n }\n\n async onActionUnlink(event, el) {\n const id = el.dataset.id;\n const connection = this.connections.find(c => String(c.id) === String(id));\n const provider = connection?.provider || 'this account';\n\n const confirmed = await Dialog.confirm(\n `Unlink ${provider}? You won't be able to sign in with this provider anymore.`,\n 'Unlink Account'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/account/oauth_connection/${id}`);\n if (resp.success) {\n this.getApp()?.toast?.success(`${provider} account unlinked`);\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to unlink account');\n }\n return true;\n }\n}\n","/**\n * ProfileSessionsSection - Sessions tab\n *\n * TableView with pagination showing active sessions.\n * Rich two-line rows: browser+device on top, location+IP below.\n * Click a row to see full session detail in a dialog.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\nimport { UserDeviceLocationList } from '@core/models/User.js';\n\nclass SessionRow extends TableRow {\n get deviceIcon() {\n const dev = this.model?.get('user_device')?.device_info?.device || {};\n const os = this.model?.get('user_device')?.device_info?.os || {};\n const isMobile = ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n return isMobile ? 'bi-phone' : 'bi-laptop';\n }\n\n get browserName() {\n const ua = this.model?.get('user_device')?.device_info?.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : 'Unknown';\n }\n\n get deviceName() {\n const dev = this.model?.get('user_device')?.device_info?.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown';\n }\n\n get osName() {\n const os = this.model?.get('user_device')?.device_info?.os || {};\n return os.family || '';\n }\n\n get locationText() {\n const geo = this.model?.get('geolocation') || {};\n const parts = [geo.city, geo.region].filter(Boolean);\n return parts.length ? parts.join(', ') : (geo.country_name || '—');\n }\n\n get threatFlags() {\n const geo = this.model?.get('geolocation') || {};\n const flags = [];\n if (geo.is_vpn) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">VPN</span>');\n if (geo.is_tor) flags.push('<span class=\"badge bg-danger\" style=\"font-size:0.6rem;\">Tor</span>');\n if (geo.is_proxy) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">Proxy</span>');\n return flags.join(' ');\n }\n\n get hasThreatFlags() {\n const geo = this.model?.get('geolocation') || {};\n return !!(geo.is_vpn || geo.is_tor || geo.is_proxy);\n }\n}\n\nexport default class ProfileSessionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-sessions-section',\n template: `\n <style>\n .pss-primary { font-size: 0.85rem; font-weight: 500; }\n .pss-secondary { font-size: 0.73rem; color: #6c757d; margin-top: 0.15rem; }\n .pss-icon { color: #6c757d; font-size: 1.1rem; vertical-align: middle; margin-right: 0.35rem; }\n </style>\n <div id=\"sessions-table\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.tableView = new TableView({\n containerId: 'sessions-table',\n collection: new UserDeviceLocationList({ size: 10 }),\n defaultQuery: { user: this.model.id },\n searchable: false,\n filterable: false,\n selectable: false,\n actions: null,\n clickAction: 'view',\n itemClass: SessionRow,\n columns: [\n {\n key: 'user_device',\n label: 'Session',\n template: `\n <div class=\"pss-primary\">\n <i class=\"bi {{deviceIcon}} pss-icon\"></i>{{browserName}} <span class=\"text-muted fw-normal\">on</span> {{deviceName}}\n </div>\n <div class=\"pss-secondary\">\n <i class=\"bi bi-geo-alt me-1\"></i>{{locationText}}\n <span class=\"text-muted mx-1\">·</span>\n {{model.ip_address}}\n {{#hasThreatFlags|bool}} <span class=\"ms-1\">{{{threatFlags}}}</span>{{/hasThreatFlags|bool}}\n </div>`\n },\n {\n key: 'last_seen',\n label: 'Last Seen',\n formatter: 'relative'\n }\n ],\n onItemView: (model) => this._showSessionDetail(model)\n });\n this.addChild(this.tableView);\n }\n\n _showSessionDetail(model) {\n const data = model.toJSON ? model.toJSON() : model;\n const ud = data.user_device || {};\n const dev = ud.device_info?.device || {};\n const ua = ud.device_info?.user_agent || {};\n const os = ud.device_info?.os || {};\n const geo = data.geolocation || {};\n\n const browser = ua.family ? `${ua.family} ${[ua.major, ua.minor, ua.patch].filter(Boolean).join('.')}` : 'Unknown';\n const device = `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown';\n const osName = os.family ? `${os.family} ${[os.major, os.minor, os.patch].filter(Boolean).join('.')}` : '—';\n const location = [geo.city, geo.region, geo.country_name].filter(Boolean).join(', ') || '—';\n\n const flags = [];\n if (geo.is_vpn) flags.push('<span class=\"badge bg-warning text-dark\">VPN</span>');\n if (geo.is_tor) flags.push('<span class=\"badge bg-danger\">Tor</span>');\n if (geo.is_proxy) flags.push('<span class=\"badge bg-warning text-dark\">Proxy</span>');\n if (geo.is_datacenter) flags.push('<span class=\"badge bg-secondary\">Datacenter</span>');\n if (geo.is_known_attacker) flags.push('<span class=\"badge bg-danger\">Known Attacker</span>');\n\n const row = (label, value) => `\n <div style=\"display:flex; padding:0.4rem 0; border-bottom:1px solid #f0f0f0;\">\n <div style=\"width:120px; font-size:0.8rem; color:#6c757d; flex-shrink:0;\">${label}</div>\n <div style=\"flex:1; font-size:0.85rem;\">${value || '—'}</div>\n </div>`;\n\n Dialog.showDialog({\n title: `<i class=\"bi bi-clock-history me-2\"></i>${browser} on ${device}`,\n size: 'sm',\n centered: true,\n body: `\n <div style=\"font-size:0.85rem;\">\n ${row('Browser', browser)}\n ${row('Device', device)}\n ${row('OS', osName)}\n ${row('IP Address', data.ip_address)}\n ${row('Location', location)}\n ${row('ISP', geo.isp || geo.asn_org || '—')}\n ${row('ASN', geo.asn || '—')}\n ${row('Threat Level', geo.threat_level || '—')}\n ${flags.length ? row('Flags', flags.join(' ')) : ''}\n ${row('First Seen', data.first_seen ? new Date(data.first_seen * 1000).toLocaleString() : '—')}\n ${row('Last Seen', data.last_seen ? new Date(data.last_seen * 1000).toLocaleString() : '—')}\n </div>`,\n buttons: [\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n }\n}\n","/**\n * ProfileDevicesSection - Devices tab\n *\n * TableView with pagination showing registered devices.\n * Rich two-line rows: device+brand on top, browser+OS below.\n * Click a row to see full device detail in a dialog.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\nimport { UserDeviceList } from '@core/models/User.js';\n\nclass DeviceRow extends TableRow {\n get deviceIcon() {\n const dev = this.model?.get('device_info')?.device || {};\n const os = this.model?.get('device_info')?.os || {};\n const isMobile = ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n return isMobile ? 'bi-phone' : 'bi-laptop';\n }\n\n get deviceName() {\n const dev = this.model?.get('device_info')?.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown Device';\n }\n\n get deviceModel() {\n return this.model?.get('device_info')?.device?.model || '';\n }\n\n get browserName() {\n const ua = this.model?.get('device_info')?.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : '';\n }\n\n get osName() {\n const os = this.model?.get('device_info')?.os || {};\n return os.family ? `${os.family} ${os.major || ''}`.trim() : '';\n }\n\n get deviceMeta() {\n const parts = [this.browserName, this.osName].filter(Boolean);\n return parts.join(' · ') || '—';\n }\n}\n\nexport default class ProfileDevicesSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-devices-section',\n template: `\n <style>\n .pds-primary { font-size: 0.85rem; font-weight: 500; }\n .pds-model { font-weight: 400; color: #6c757d; }\n .pds-secondary { font-size: 0.73rem; color: #6c757d; margin-top: 0.15rem; }\n .pds-icon { color: #6c757d; font-size: 1.1rem; vertical-align: middle; margin-right: 0.35rem; }\n </style>\n <div id=\"devices-table\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.tableView = new TableView({\n containerId: 'devices-table',\n collection: new UserDeviceList({ size: 10 }),\n defaultQuery: { user: this.model.id },\n searchable: false,\n filterable: false,\n selectable: false,\n actions: null,\n clickAction: 'view',\n itemClass: DeviceRow,\n columns: [\n {\n key: 'device_info',\n label: 'Device',\n template: `\n <div class=\"pds-primary\">\n <i class=\"bi {{deviceIcon}} pds-icon\"></i>{{deviceName}}\n {{#deviceModel}} <span class=\"pds-model\">({{deviceModel}})</span>{{/deviceModel}}\n </div>\n <div class=\"pds-secondary\">\n {{deviceMeta}}\n {{#model.last_ip}} <span class=\"text-muted mx-1\">·</span> {{model.last_ip}}{{/model.last_ip}}\n </div>`\n },\n {\n key: 'last_seen',\n label: 'Last Seen',\n formatter: 'relative'\n }\n ],\n onItemView: (model) => this._showDeviceDetail(model)\n });\n this.addChild(this.tableView);\n }\n\n _showDeviceDetail(model) {\n const data = model.toJSON ? model.toJSON() : model;\n const info = data.device_info || {};\n const dev = info.device || {};\n const ua = info.user_agent || {};\n const os = info.os || {};\n\n const device = `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown';\n const browser = ua.family ? `${ua.family} ${[ua.major, ua.minor, ua.patch].filter(Boolean).join('.')}` : 'Unknown';\n const osName = os.family ? `${os.family} ${[os.major, os.minor, os.patch].filter(Boolean).join('.')}` : '—';\n const isMobile = ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n\n const row = (label, value) => `\n <div style=\"display:flex; padding:0.4rem 0; border-bottom:1px solid #f0f0f0;\">\n <div style=\"width:120px; font-size:0.8rem; color:#6c757d; flex-shrink:0;\">${label}</div>\n <div style=\"flex:1; font-size:0.85rem;\">${value || '—'}</div>\n </div>`;\n\n Dialog.showDialog({\n title: `<i class=\"bi ${isMobile ? 'bi-phone' : 'bi-laptop'} me-2\"></i>${device}`,\n size: 'sm',\n centered: true,\n body: `\n <div style=\"font-size:0.85rem;\">\n ${row('Device', `${device} (${dev.model || '—'})`)}\n ${row('Browser', browser)}\n ${row('OS', osName)}\n ${row('Last IP', data.last_ip)}\n ${row('Type', isMobile ? 'Mobile' : 'Desktop')}\n ${row('First Seen', data.first_seen ? new Date(data.first_seen * 1000).toLocaleString() : '—')}\n ${row('Last Seen', data.last_seen ? new Date(data.last_seen * 1000).toLocaleString() : '—')}\n </div>`,\n buttons: [\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n }\n}\n","/**\n * ProfileSecurityEventsSection - Security events feed\n *\n * TableView showing auth-relevant events: logins, failed attempts,\n * password changes, etc. Color-coded by severity.\n */\nimport View from '@core/View.js';\nimport Model from '@core/Model.js';\nimport Collection from '@core/Collection.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\n\nclass SecurityEvent extends Model {\n constructor(data = {}, options = {}) {\n super(data, { endpoint: '/api/account/security-events', ...options });\n }\n}\n\nclass SecurityEventList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: SecurityEvent,\n endpoint: '/api/account/security-events',\n size: 15,\n ...options\n });\n }\n}\n\nconst DANGER_KINDS = [\n 'invalid_password', 'totp:login_failed', 'totp:confirm_failed',\n 'passkey:login_failed', 'sessions:revoke_failed',\n 'email_change:invalid', 'email_change:expired'\n];\nconst SUCCESS_KINDS = [\n 'login', 'oauth', 'email_verify:confirmed', 'email_verify:confirmed_code',\n 'phone_verify:confirmed', 'phone_change:confirmed', 'username:changed'\n];\n\nclass SecurityEventRow extends TableRow {\n get kindBadgeClass() {\n const kind = this.model?.get('kind') || '';\n if (DANGER_KINDS.includes(kind)) return 'bg-danger';\n if (SUCCESS_KINDS.includes(kind)) return 'bg-success';\n return 'bg-secondary';\n }\n\n get kindLabel() {\n const kind = this.model?.get('kind') || '';\n return kind.replace(/[_:]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());\n }\n\n get kindIcon() {\n const kind = this.model?.get('kind') || '';\n if (DANGER_KINDS.includes(kind)) return 'bi-exclamation-triangle';\n if (SUCCESS_KINDS.includes(kind)) return 'bi-check-circle';\n return 'bi-info-circle';\n }\n}\n\nexport default class ProfileSecurityEventsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-security-events-section',\n template: `<div id=\"security-events-table\"></div>`,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.tableView = new TableView({\n containerId: 'security-events-table',\n collection: new SecurityEventList(),\n defaultQuery: { sort: '-created' },\n hideActivePillNames: ['sort'],\n itemClass: SecurityEventRow,\n columns: [\n {\n key: 'kind',\n label: 'Event',\n template: '<span class=\"badge {{kindBadgeClass}}\"><i class=\"bi {{kindIcon}} me-1\"></i>{{kindLabel}}</span>'\n },\n {\n key: 'summary',\n label: 'Details'\n },\n { key: 'ip', label: 'IP', visibility: 'lg' },\n { key: 'created|relative', label: 'Time', sortable: true }\n ],\n searchable: true,\n sortable: true,\n filterable: false,\n paginated: true,\n showAdd: false,\n showExport: false,\n tableOptions: {\n striped: false,\n hover: true,\n size: 'sm'\n },\n emptyMessage: 'No security events'\n });\n this.addChild(this.tableView);\n }\n}\n","/**\n * ProfileNotificationsSection - Notification preferences tab\n *\n * Per-kind, per-channel toggle grid. Kinds are dynamically loaded\n * from the API. Channels: in_app, email, push.\n */\nimport View from '@core/View.js';\nimport rest from '@core/Rest.js';\n\nconst CHANNEL_LABELS = {\n in_app: 'In-App',\n email: 'Email',\n push: 'Push'\n};\n\nconst CHANNELS = ['in_app', 'email', 'push'];\n\nexport default class ProfileNotificationsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-notifications-section',\n template: `\n <style>\n .pn-table { width: 100%; border-collapse: collapse; }\n .pn-table th { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; padding: 0.5rem 0.75rem; border-bottom: 2px solid #e9ecef; }\n .pn-table th:first-child { text-align: left; }\n .pn-table th:not(:first-child) { text-align: center; width: 80px; }\n .pn-table td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f0f0f0; }\n .pn-table td:first-child { font-size: 0.88rem; font-weight: 500; text-transform: capitalize; }\n .pn-table td:not(:first-child) { text-align: center; }\n .pn-table tr:last-child td { border-bottom: none; }\n .pn-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pn-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#hasPreferences|bool}}\n <table class=\"pn-table\">\n <thead>\n <tr>\n <th>Type</th>\n {{#channels}}\n <th>{{.label}}</th>\n {{/channels}}\n </tr>\n </thead>\n <tbody>\n {{#preferenceRows}}\n <tr>\n <td>{{.kindLabel}}</td>\n {{#.toggles}}\n <td>\n <input type=\"checkbox\" class=\"form-check-input\"\n data-action=\"toggle-pref\"\n data-kind=\"{{.kind}}\"\n data-channel=\"{{.channel}}\"\n {{#.checked}}checked{{/.checked}}>\n </td>\n {{/.toggles}}\n </tr>\n {{/preferenceRows}}\n </tbody>\n </table>\n {{/hasPreferences|bool}}\n {{^hasPreferences|bool}}\n <div class=\"pn-empty\">\n <i class=\"bi bi-bell\"></i>\n No notification preferences configured\n <div style=\"font-size: 0.78rem; margin-top: 0.5rem;\">\n Preferences will appear here once notification types are defined.\n </div>\n </div>\n {{/hasPreferences|bool}}\n `,\n ...options\n });\n this.preferences = {};\n }\n\n get channels() {\n return CHANNELS.map(ch => ({ key: ch, label: CHANNEL_LABELS[ch] || ch }));\n }\n\n get hasPreferences() {\n return Object.keys(this.preferences).length > 0;\n }\n\n get preferenceRows() {\n return Object.keys(this.preferences).sort().map(kind => ({\n kind,\n kindLabel: kind.replace(/[_-]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),\n toggles: CHANNELS.map(channel => ({\n kind,\n channel,\n checked: this.preferences[kind]?.[channel] !== false\n }))\n }));\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/notification/preferences', {}, { dataOnly: true });\n this.preferences = resp?.data?.preferences || resp?.data || {};\n } catch (e) {\n this.preferences = {};\n }\n }\n\n async onActionTogglePref(event, el) {\n const kind = el.dataset.kind;\n const channel = el.dataset.channel;\n const checked = el.checked;\n\n // Update local state\n if (!this.preferences[kind]) {\n this.preferences[kind] = {};\n }\n this.preferences[kind][channel] = checked;\n\n // Save to API\n try {\n const resp = await rest.POST('/api/account/notification/preferences', {\n preferences: { [kind]: { [channel]: checked } }\n });\n if (!resp.success) {\n this.getApp()?.toast?.error(resp.message || 'Failed to update preference');\n el.checked = !checked; // revert\n }\n } catch (e) {\n this.getApp()?.toast?.error('Failed to update preference');\n el.checked = !checked; // revert\n }\n return true;\n }\n}\n","/**\n * ProfileApiKeysSection - Personal API key management\n *\n * Lists existing API keys with delete capability.\n * Generate new keys via dialog with optional IP restriction and expiration.\n * Token is shown only once after generation.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\n\nexport default class ProfileApiKeysSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-api-keys-section',\n template: `\n <style>\n .pak-warning { padding: 0.75rem 1rem; background: #fff3cd; border: 1px solid #ffecb5; border-radius: 8px; margin-bottom: 1.25rem; font-size: 0.82rem; color: #664d03; display: flex; align-items: flex-start; gap: 0.6rem; }\n .pak-warning i { font-size: 1rem; flex-shrink: 0; margin-top: 0.1rem; }\n .pak-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .pak-header h6 { margin: 0; font-weight: 600; }\n .pak-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .pak-item { display: flex; align-items: center; padding: 0.75rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 1rem; }\n .pak-item:last-child { border-bottom: none; }\n .pak-item-icon { color: #6c757d; font-size: 1.1rem; flex-shrink: 0; }\n .pak-item-info { flex: 1; min-width: 0; }\n .pak-item-name { font-weight: 600; font-size: 0.85rem; }\n .pak-item-meta { font-size: 0.75rem; color: #6c757d; display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.15rem; }\n .pak-item-actions { flex-shrink: 0; }\n .pak-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .pak-result { padding: 1rem; background: #d1e7dd; border: 1px solid #badbcc; border-radius: 8px; margin-bottom: 1rem; }\n .pak-result-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #0f5132; margin-bottom: 0.5rem; }\n .pak-token-wrap { display: flex; gap: 0.5rem; align-items: center; }\n .pak-token { flex: 1; font-family: monospace; font-size: 0.78rem; padding: 0.5rem 0.75rem; background: #fff; border: 1px solid #dee2e6; border-radius: 4px; word-break: break-all; max-height: 80px; overflow-y: auto; }\n .pak-token-warning { font-size: 0.75rem; color: #dc3545; margin-top: 0.5rem; font-weight: 600; }\n </style>\n\n <div class=\"pak-warning\">\n <i class=\"bi bi-exclamation-triangle-fill\"></i>\n <div>\n <strong>Treat API keys like passwords.</strong> They carry your full account permissions.\n Store them securely and never expose them in client-side code.\n </div>\n </div>\n\n <div id=\"pak-new-token\" style=\"display: none;\">\n <div class=\"pak-result\">\n <div class=\"pak-result-label\">Your New API Key</div>\n <div class=\"pak-token-wrap\">\n <div class=\"pak-token\" id=\"pak-token-display\"></div>\n <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" data-action=\"copy-token\" title=\"Copy\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n <div class=\"pak-token-warning\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>This token will not be shown again. Copy it now.\n </div>\n </div>\n </div>\n\n <div class=\"pak-header\">\n <h6>Your API Keys</h6>\n <button type=\"button\" class=\"btn btn-primary btn-sm\" data-action=\"generate-key\">\n <i class=\"bi bi-plus-lg me-1\"></i>Generate Key\n </button>\n </div>\n\n <div id=\"pak-keys-list\"></div>\n `,\n ...options\n });\n this.apiKeys = [];\n this.generatedToken = null;\n }\n\n async onBeforeRender() {\n await this._loadKeys();\n }\n\n async _loadKeys() {\n const resp = await rest.GET('/api/account/api_keys', {}, {}, { dataOnly: true });\n this.apiKeys = resp.success && Array.isArray(resp.data) ? resp.data : [];\n }\n\n onAfterRender() {\n this._renderKeysList();\n }\n\n _renderKeysList() {\n const container = this.element?.querySelector('#pak-keys-list');\n if (!container) return;\n\n if (!this.apiKeys.length) {\n container.innerHTML = `\n <div class=\"pak-list\">\n <div class=\"pak-empty\">\n <i class=\"bi bi-key\" style=\"font-size: 1.5rem; display: block; margin-bottom: 0.5rem;\"></i>\n No API keys yet. Generate one to get started.\n </div>\n </div>`;\n return;\n }\n\n const rows = this.apiKeys.map(key => {\n const name = key.name || 'API Key';\n const created = key.created ? new Date(key.created * 1000).toLocaleDateString() : '';\n const expires = key.expires ? new Date(key.expires * 1000).toLocaleDateString() : 'Never';\n const lastUsed = key.last_used ? new Date(key.last_used * 1000).toLocaleDateString() : 'Never';\n const ips = key.allowed_ips?.length ? key.allowed_ips.join(', ') : 'Any';\n const isActive = key.is_active !== false;\n const statusBadge = isActive\n ? '<span class=\"badge bg-success\">Active</span>'\n : '<span class=\"badge bg-secondary\">Inactive</span>';\n const tokenPreview = key.token_prefix ? `${key.token_prefix}...` : '••••••••';\n\n return `\n <div class=\"pak-item\">\n <div class=\"pak-item-icon\"><i class=\"bi bi-key\"></i></div>\n <div class=\"pak-item-info\">\n <div class=\"pak-item-name\">${name} ${statusBadge}</div>\n <div class=\"pak-item-meta\">\n <span><i class=\"bi bi-code-square me-1\"></i>${tokenPreview}</span>\n <span><i class=\"bi bi-calendar me-1\"></i>Created ${created}</span>\n <span><i class=\"bi bi-clock me-1\"></i>Expires ${expires}</span>\n <span><i class=\"bi bi-activity me-1\"></i>Last used ${lastUsed}</span>\n <span><i class=\"bi bi-globe me-1\"></i>IPs: ${ips}</span>\n </div>\n </div>\n <div class=\"pak-item-actions\">\n <button type=\"button\" class=\"btn btn-outline-danger btn-sm\" data-action=\"delete-key\" data-id=\"${key.id}\" title=\"Delete\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </div>`;\n }).join('');\n\n container.innerHTML = `<div class=\"pak-list\">${rows}</div>`;\n }\n\n async onActionGenerateKey() {\n const data = await Dialog.showForm({\n title: 'Generate API Key',\n icon: 'bi-key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Key Name',\n placeholder: 'e.g., CI/CD Pipeline, Mobile App',\n required: true,\n help: 'A descriptive name to identify this key.'\n },\n {\n name: 'allowed_ips',\n type: 'text',\n label: 'Allowed IPs',\n placeholder: 'e.g., 203.0.113.0/24, 10.0.0.1',\n help: 'Optional. Comma-separated IP addresses or CIDR ranges.'\n },\n {\n name: 'expire_days',\n type: 'select',\n label: 'Expiration',\n value: '90',\n options: [\n { value: '30', label: '30 days' },\n { value: '60', label: '60 days' },\n { value: '90', label: '90 days' },\n { value: '180', label: '180 days' },\n { value: '360', label: '360 days' }\n ]\n }\n ]\n });\n if (!data) return true;\n\n const body = {\n name: data.name,\n expire_days: parseInt(data.expire_days || '90', 10)\n };\n\n const ipsStr = (data.allowed_ips || '').trim();\n if (ipsStr) {\n body.allowed_ips = ipsStr.split(',').map(ip => ip.trim()).filter(Boolean);\n }\n\n const resp = await rest.POST('/api/auth/generate_api_key', body, {}, { dataOnly: true });\n\n if (resp.success && resp.data?.token) {\n this.generatedToken = resp.data.token;\n const resultEl = this.element.querySelector('#pak-new-token');\n const tokenEl = this.element.querySelector('#pak-token-display');\n if (resultEl && tokenEl) {\n tokenEl.textContent = this.generatedToken;\n resultEl.style.display = 'block';\n }\n this.getApp()?.toast?.success('API key generated');\n await this._loadKeys();\n this._renderKeysList();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to generate API key');\n }\n return true;\n }\n\n async onActionDeleteKey(el) {\n const id = el.dataset.id;\n if (!id) return true;\n\n const confirmed = await Dialog.confirm(\n 'Are you sure you want to delete this API key? Any applications using it will lose access immediately.',\n 'Delete API Key'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/account/api_keys/${id}`, {}, {}, { dataOnly: true });\n if (resp.success) {\n this.getApp()?.toast?.success('API key deleted');\n // Hide the new-token banner if visible\n const resultEl = this.element.querySelector('#pak-new-token');\n if (resultEl) resultEl.style.display = 'none';\n await this._loadKeys();\n this._renderKeysList();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to delete API key');\n }\n return true;\n }\n\n async onActionCopyToken() {\n if (this.generatedToken) {\n try {\n await navigator.clipboard.writeText(this.generatedToken);\n this.getApp()?.toast?.success('Token copied to clipboard');\n } catch {\n this.getApp()?.toast?.error('Failed to copy token');\n }\n }\n return true;\n }\n}\n","/**\n * ProfileGroupsSection - Groups/memberships tab\n *\n * Uses ListView + MemberList to show user's group\n * memberships with role badges.\n */\nimport View from '@core/View.js';\nimport ListView from '@core/views/list/ListView.js';\nimport ListViewItem from '@core/views/list/ListViewItem.js';\nimport { MemberList } from '@core/models/Member.js';\n\nconst AVATAR_COLORS = ['#667eea', '#f5576c', '#38b2ac', '#ed8936', '#9f7aea', '#48bb78', '#4299e1', '#ed64a6'];\n\nclass GroupMemberItem extends ListViewItem {\n get groupName() {\n return this.model?.get('group')?.name || 'Unknown Group';\n }\n\n get groupKind() {\n const kind = this.model?.get('group')?.kind || '';\n return kind.replace(/^\\w/, c => c.toUpperCase());\n }\n\n get initials() {\n return this.groupName.split(/\\s+/).map(w => w[0]).join('').substring(0, 2).toUpperCase();\n }\n\n get avatarColor() {\n const name = this.groupName;\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];\n }\n\n get roleName() {\n let role = this.model?.get('metadata')?.role || '';\n if (!role && this.model?.get('permissions')?.manage_group) {\n role = 'Admin';\n }\n return role;\n }\n\n get hasRole() {\n return !!this.roleName;\n }\n\n get roleBadgeClass() {\n const r = (this.roleName || '').toLowerCase();\n if (r === 'owner') return 'bg-primary';\n if (r === 'admin') return 'bg-info';\n return 'bg-secondary';\n }\n\n get permissionsList() {\n const perms = this.model?.get('permissions');\n if (!perms) return [];\n return Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n }\n\n get hasPermissions() {\n return this.permissionsList.length > 0;\n }\n}\n\nexport default class ProfileGroupsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-groups-section',\n template: `\n <style>\n .pg-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0; border-bottom: 1px solid #f0f0f0; }\n .pg-row:last-child { border-bottom: none; }\n .pg-avatar { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; color: #fff; flex-shrink: 0; }\n .pg-info { flex: 1; }\n .pg-name { font-weight: 600; font-size: 0.88rem; }\n .pg-meta { font-size: 0.73rem; color: #6c757d; }\n .pg-role { font-size: 0.7rem; }\n .pg-perms { display: flex; flex-wrap: wrap; gap: 0.25rem; justify-content: flex-end; }\n .pg-perm-tag { display: inline-flex; align-items: center; font-size: 0.68rem; padding: 0.15em 0.45em; background: #f0f4ff; border: 1px solid #d4deff; border-radius: 3px; color: #4a6cf7; }\n </style>\n <div id=\"groups-list\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.listView = new ListView({\n containerId: 'groups-list',\n collection: new MemberList({ size: 50 }),\n defaultQuery: { user: this.model.id },\n itemClass: GroupMemberItem,\n itemTemplate: `\n <div class=\"pg-row\">\n <div class=\"pg-avatar\" style=\"background: {{avatarColor}};\">{{initials}}</div>\n <div class=\"pg-info\">\n <div class=\"pg-name\">{{groupName}}</div>\n <div class=\"pg-meta\">{{groupKind}}</div>\n </div>\n {{#hasPermissions|bool}}\n <div class=\"pg-perms\">\n {{#permissionsList}}\n <span class=\"pg-perm-tag\">{{.}}</span>\n {{/permissionsList}}\n </div>\n {{/hasPermissions|bool}}\n {{#hasRole|bool}}\n <span class=\"pg-role badge {{roleBadgeClass}}\">{{roleName}}</span>\n {{/hasRole|bool}}\n </div>\n `,\n emptyMessage: 'You are not a member of any groups'\n });\n this.addChild(this.listView);\n }\n}\n","/**\n * ProfilePermissionsSection - Read-only permissions tab\n *\n * Shows system permissions and active group permissions side by side.\n * Pure template view — no fetching needed.\n */\nimport View from '@core/View.js';\nimport { User } from '@core/models/User.js';\n\nexport default class ProfilePermissionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-permissions-section',\n template: `\n <style>\n .pp-role-bar { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 1rem; background: #f0f0ff; border-radius: 8px; margin-bottom: 1.25rem; font-size: 0.85rem; }\n .pp-role-bar i { color: #6f42c1; font-size: 1rem; }\n .pp-role-bar strong { color: #6f42c1; }\n .pp-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .pp-section-label:first-child { margin-top: 0; }\n .pp-grid { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 1.25rem; }\n .pp-tag { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; padding: 0.25em 0.6em; background: #f8f9fc; border: 1px solid #e9ecef; border-radius: 4px; color: #495057; }\n .pp-tag i { font-size: 0.65rem; color: #198754; }\n .pp-tag-group { background: #e7f1ff; border-color: #b6d4fe; }\n .pp-tag-group i { color: #0d6efd; }\n .pp-note { font-size: 0.78rem; color: #adb5bd; margin-top: 1rem; }\n .pp-empty { color: #6c757d; font-style: italic; font-size: 0.85rem; padding: 0.5rem 0; }\n .pp-group-header { display: flex; align-items: center; gap: 0.5rem; }\n .pp-group-name { font-size: 0.78rem; font-weight: 400; color: #6c757d; }\n </style>\n\n {{#model.is_superuser|bool}}\n <div class=\"pp-role-bar\">\n <i class=\"bi bi-star-fill\"></i>\n <span><strong>Superuser</strong> — full system access</span>\n </div>\n {{/model.is_superuser|bool}}\n\n <div class=\"pp-section-label\">System Permissions</div>\n\n {{#systemPermissions|bool}}\n <div class=\"pp-grid\">\n {{#systemPermissions}}\n <span class=\"pp-tag\"><i class=\"bi bi-check-circle-fill\"></i> {{.}}</span>\n {{/systemPermissions}}\n </div>\n {{/systemPermissions|bool}}\n\n {{^systemPermissions|bool}}\n <div class=\"pp-empty\">No system permissions assigned</div>\n {{/systemPermissions|bool}}\n\n {{#hasActiveGroup|bool}}\n <div class=\"pp-section-label\">\n <div class=\"pp-group-header\">\n <span>Group Permissions</span>\n <span class=\"pp-group-name\">— {{activeGroupName}}</span>\n </div>\n </div>\n\n {{#groupPermissions|bool}}\n <div class=\"pp-grid\">\n {{#groupPermissions}}\n <span class=\"pp-tag pp-tag-group\"><i class=\"bi bi-check-circle-fill\"></i> {{.}}</span>\n {{/groupPermissions}}\n </div>\n {{/groupPermissions|bool}}\n\n {{^groupPermissions|bool}}\n <div class=\"pp-empty\">No group permissions assigned</div>\n {{/groupPermissions|bool}}\n {{/hasActiveGroup|bool}}\n\n <div class=\"pp-note\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Permissions are managed by your administrator.\n </div>\n `,\n ...options\n });\n }\n\n get systemPermissions() {\n if (!this.model) return [];\n const perms = this.model.get('permissions');\n if (!perms) return [];\n\n const permMap = {};\n User.PERMISSIONS.forEach(p => { permMap[p.name] = p.label; });\n\n return Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => permMap[k] || k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n }\n\n get hasActiveGroup() {\n const app = this.getApp();\n return !!(app?.activeGroup && this.model?.member);\n }\n\n get activeGroupName() {\n const app = this.getApp();\n return app?.activeGroup?.get('name') || app?.activeGroup?.get('display_name') || 'Current Group';\n }\n\n get groupPermissions() {\n if (!this.model?.member) return [];\n const perms = this.model.member.get('permissions');\n if (!perms) return [];\n\n return Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n }\n}\n","/**\n * UserProfileView - Rich user profile shown in a Dialog\n *\n * Main container with left nav and section switching.\n * 11 sections: Profile, Personal, Security, Connected, Sessions, Devices,\n * Security Events, Notifications, API Keys, Groups, Permissions.\n * Fetches full user data on render.\n * Thin accent bar + compact header with avatar, name, and status badges.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport { File as FileModel } from '@core/models/Files.js';\nimport ProfileOverviewSection from './ProfileOverviewSection.js';\nimport ProfilePersonalSection from './ProfilePersonalSection.js';\nimport ProfileSecuritySection from './ProfileSecuritySection.js';\nimport ProfileConnectedSection from './ProfileConnectedSection.js';\nimport ProfileSessionsSection from './ProfileSessionsSection.js';\nimport ProfileDevicesSection from './ProfileDevicesSection.js';\nimport ProfileSecurityEventsSection from './ProfileSecurityEventsSection.js';\nimport ProfileNotificationsSection from './ProfileNotificationsSection.js';\nimport ProfileApiKeysSection from './ProfileApiKeysSection.js';\nimport ProfileGroupsSection from './ProfileGroupsSection.js';\nimport ProfilePermissionsSection from './ProfilePermissionsSection.js';\n\nconst SECTIONS = {\n profile: ProfileOverviewSection,\n personal: ProfilePersonalSection,\n security: ProfileSecuritySection,\n connected: ProfileConnectedSection,\n sessions: ProfileSessionsSection,\n devices: ProfileDevicesSection,\n security_events: ProfileSecurityEventsSection,\n notifications: ProfileNotificationsSection,\n api_keys: ProfileApiKeysSection,\n groups: ProfileGroupsSection,\n permissions: ProfilePermissionsSection\n};\n\nexport default class UserProfileView extends View {\n constructor(options = {}) {\n super({\n className: 'user-profile-view',\n template: `\n <style>\n .up-layout { display: flex; height: 100%; }\n .up-nav { width: 200px; background: #f8f9fc; border-right: 1px solid #e9ecef; padding: 0.75rem 0; flex-shrink: 0; overflow-y: auto; }\n .up-nav-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; padding: 0.75rem 1.25rem 0.25rem; }\n .up-nav a { color: #495057; padding: 0.45rem 1.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; text-decoration: none; }\n .up-nav a:hover { background: #e9ecef; }\n .up-nav a.active { background: #e7f1ff; color: #0d6efd; font-weight: 600; border-right: 2px solid #0d6efd; }\n .up-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .up-content { flex: 1; overflow-y: auto; padding: 1.5rem 2.5rem; }\n .up-accent { height: 4px; background: linear-gradient(90deg, #1a73e8, #4fc3f7); border-radius: var(--bs-modal-border-radius, 0.5rem) var(--bs-modal-border-radius, 0.5rem) 0 0; }\n .up-header { display: flex; align-items: center; gap: 1rem; padding: 1rem 1.5rem; border-bottom: 1px solid #e9ecef; }\n .up-avatar-wrap { position: relative; flex-shrink: 0; cursor: pointer; }\n .up-avatar-wrap img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; }\n .up-avatar-initials { width: 56px; height: 56px; border-radius: 50%; background: #e7f1ff; color: #0d6efd; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 700; }\n .up-header-info { flex: 1; min-width: 0; }\n .up-header-name { display: flex; align-items: center; gap: 0.5rem; }\n .up-header-name h5 { margin: 0; font-weight: 700; font-size: 1.05rem; }\n .up-header-badge { font-size: 0.65rem; padding: 0.15em 0.5em; border-radius: 3px; font-weight: 600; }\n .up-header-badge-staff { background: #e7f1ff; color: #0d6efd; }\n .up-header-badge-su { background: #fff3cd; color: #856404; }\n .up-header-sub { font-size: 0.78rem; color: #6c757d; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }\n .up-header-sub .up-dot { color: #ced4da; }\n .up-header-verified { display: inline-flex; align-items: center; gap: 0.2rem; font-size: 0.72rem; color: #198754; }\n .up-header-verified i { font-size: 0.7rem; }\n .up-close { flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border: none; background: none; color: #6c757d; font-size: 1.1rem; border-radius: 6px; cursor: pointer; padding: 0; align-self: flex-start; margin-top: -0.15rem; }\n .up-close:hover { background: #f0f0f0; color: #212529; }\n @media (max-width: 576px) {\n .up-nav { display: none; }\n .up-content { padding: 1.25rem; }\n }\n </style>\n <div class=\"up-layout\" style=\"flex-direction: column; min-height: 480px;\">\n <div class=\"up-accent\"></div>\n <div class=\"up-header\">\n <div class=\"up-avatar-wrap\" data-action=\"change-avatar\" title=\"Change avatar\">\n {{{model.avatar|avatar}}}\n </div>\n <div class=\"up-header-info\">\n <div class=\"up-header-name\">\n <h5>{{model.display_name}}</h5>\n {{#model.is_superuser|bool}}<span class=\"up-header-badge up-header-badge-su\">Superuser</span>{{/model.is_superuser|bool}}\n {{^model.is_superuser|bool}}\n {{#model.is_staff|bool}}<span class=\"up-header-badge up-header-badge-staff\">Staff</span>{{/model.is_staff|bool}}\n {{/model.is_superuser|bool}}\n </div>\n <div class=\"up-header-sub\">\n <span>{{model.email}}</span>\n {{#model.is_email_verified|bool}}\n <span class=\"up-dot\">·</span>\n <span class=\"up-header-verified\"><i class=\"bi bi-patch-check-fill\"></i> Email</span>\n {{/model.is_email_verified|bool}}\n {{#model.requires_mfa|bool}}\n <span class=\"up-dot\">·</span>\n <span class=\"up-header-verified\"><i class=\"bi bi-shield-fill-check\"></i> MFA</span>\n {{/model.requires_mfa|bool}}\n </div>\n </div>\n <button type=\"button\" class=\"up-close\" data-action=\"close-dialog\" title=\"Close\"><i class=\"bi bi-x-lg\"></i></button>\n </div>\n <div class=\"up-layout\" style=\"flex: 1; min-height: 0;\">\n <nav class=\"up-nav\">\n <a href=\"#\" class=\"nav-link active\" data-action=\"navigate\" data-section=\"profile\"><i class=\"bi bi-person\"></i> Profile</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"personal\"><i class=\"bi bi-person-vcard\"></i> Personal</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"security\"><i class=\"bi bi-shield-lock\"></i> Security</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"connected\"><i class=\"bi bi-plug\"></i> Connected</a>\n <div class=\"up-nav-label\">Activity</div>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"sessions\"><i class=\"bi bi-clock-history\"></i> Sessions</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"devices\"><i class=\"bi bi-laptop\"></i> Devices</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"security_events\"><i class=\"bi bi-shield-exclamation\"></i> Security Events</a>\n <div class=\"up-nav-label\">Settings</div>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"notifications\"><i class=\"bi bi-bell\"></i> Notifications</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"api_keys\"><i class=\"bi bi-key\"></i> API Keys</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"groups\"><i class=\"bi bi-people\"></i> Groups</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"permissions\"><i class=\"bi bi-shield-check\"></i> Permissions</a>\n </nav>\n <div class=\"up-content\" id=\"profile-section\"></div>\n </div>\n </div>\n `,\n ...options\n });\n this.activeSection = 'profile';\n this.sectionView = null;\n }\n\n get hasAvatar() {\n return !!(this.model && this.model.get('avatar') && this.model.get('avatar').url);\n }\n\n async onBeforeRender() {\n if (this.model) {\n await this.model.fetch({ params: { graph: 'full' } });\n }\n }\n\n async onAfterRender() {\n // Sync nav active state after any re-render (e.g., model change auto-rerender)\n this.element?.querySelectorAll('.up-nav a').forEach(link => {\n link.classList.toggle('active', link.dataset.section === this.activeSection);\n });\n }\n\n async onInit() {\n await super.onInit();\n\n // Default section\n this.sectionView = new ProfileOverviewSection({\n model: this.model,\n containerId: 'profile-section'\n });\n this.addChild(this.sectionView);\n }\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (!section || section === this.activeSection) return true;\n\n const SectionClass = SECTIONS[section];\n if (!SectionClass) return true;\n\n // Remove current section\n if (this.sectionView) {\n this.removeChild(this.sectionView);\n }\n\n // Create and add new section\n this.sectionView = new SectionClass({\n model: this.model,\n containerId: 'profile-section'\n });\n this.addChild(this.sectionView);\n await this.sectionView.render();\n\n this.activeSection = section;\n\n // Update nav active state\n this.element.querySelectorAll('.up-nav a').forEach(link => {\n link.classList.toggle('active', link.dataset.section === section);\n });\n\n return true;\n }\n\n async onActionChangeAvatar() {\n const resp = await Dialog.updateModelImage({\n model: this.model,\n field: 'avatar',\n title: 'Change Avatar',\n upload: true,\n }, {\n name: 'avatar',\n size: 'lg',\n imageSize: { width: 200, height: 200 },\n placeholder: 'Upload your avatar',\n });\n if (resp && resp.status === 200) {\n await this.render();\n }\n }\n\n async onActionCloseDialog() {\n // Find and dismiss the parent dialog\n const modal = this.element?.closest('.modal');\n if (modal) {\n const bsModal = bootstrap?.Modal?.getInstance(modal);\n if (bsModal) bsModal.hide();\n }\n return true;\n }\n\n async onActionUpdateEmail() {\n const app = this.getApp();\n const rest = app.rest;\n\n // Step 1: Collect new email\n const data = await Dialog.showForm({\n title: 'Change Email Address',\n size: 'sm',\n submitText: 'Send Code',\n fields: [\n {\n name: 'new_email_address',\n type: 'text',\n label: 'New Email Address',\n required: true,\n placeholder: 'Enter new email address',\n attributes: { autocomplete: 'off', inputmode: 'email' },\n cols: 12\n }\n ]\n });\n if (!data) return true;\n\n // Step 2: Request the change (sends code to new email)\n const sendResp = await rest.POST('/api/auth/email/change/request', {\n email: data.new_email_address,\n method: 'code'\n });\n if (!sendResp.success) {\n app.toast.error(sendResp.message || 'Failed to request email change');\n return true;\n }\n\n // Step 3: Prompt for the 6-digit code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${data.new_email_address}</strong>`,\n 'Confirm Email Change',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm the code — returns new JWT\n const confirmResp = await rest.POST('/api/auth/email/change/confirm', { code: code.trim() }, {}, { dataOnly: true });\n if (confirmResp.success && confirmResp.data) {\n // Store new tokens (old sessions are invalidated)\n if (confirmResp.data.access_token) {\n app.auth?.setTokens?.(confirmResp.data);\n }\n app.toast.success('Email address updated');\n await this.model.fetch({ params: { graph: 'full' } });\n await this.render();\n } else {\n app.toast.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionUpdatePhone() {\n const app = this.getApp();\n const rest = app.rest;\n const currentPhone = this.model.get('phone_number');\n\n if (!currentPhone) {\n // No phone — redirect to add phone flow in overview section\n app.toast.info('Use the profile section to add a phone number');\n return true;\n }\n\n // Step 1: Collect new phone + password\n const data = await Dialog.showForm({\n title: 'Change Phone Number',\n size: 'sm',\n submitText: 'Send Code',\n fields: [\n {\n name: 'new_phone',\n type: 'tel',\n label: 'New Phone Number',\n required: true,\n placeholder: '(415) 555-0123',\n attributes: { autocomplete: 'off' },\n cols: 12\n }\n ]\n });\n if (!data) return true;\n\n // Step 2: Request the change (sends code to new phone)\n const sendResp = await rest.POST('/api/auth/phone/change/request', {\n phone_number: data.new_phone\n }, {}, { dataOnly: true });\n if (!sendResp.success) {\n app.toast.error(sendResp.message || 'Failed to request phone change');\n return true;\n }\n\n // Hold the session_token for confirm step\n const sessionToken = sendResp.data?.session_token;\n\n // Step 3: Prompt for the 6-digit code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${data.new_phone}</strong>`,\n 'Confirm Phone Change',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm the code\n const confirmResp = await rest.POST('/api/auth/phone/change/confirm', {\n session_token: sessionToken,\n code: code.trim()\n });\n if (confirmResp.success) {\n app.toast.success('Phone number updated');\n await this.model.fetch({ params: { graph: 'full' } });\n await this.render();\n } else {\n app.toast.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n}\n"],"names":["ProfileOverviewSection","View","constructor","options","super","className","template","hasPhone","this","model","get","roleLabel","permissionPeek","perms","permMap","User","PERMISSIONS","forEach","p","name","label","active","Object","keys","filter","k","map","replace","c","toUpperCase","length","items","slice","remaining","Math","max","hasActiveGroup","app","getApp","activeGroup","member","activeGroupName","groupPermissionPeek","onActionDeactivateAccount","Dialog","confirm","resp","rest","POST","success","toast","error","message","onActionVerifyEmail","email","sendResp","method","code","prompt","placeholder","confirmResp","trim","set","render","onActionVerifyPhone","phone","onActionAddPhone","saveResp","save","phone_number","status","onActionRemovePhone","onActionNavigate","event","el","parent","ProfilePersonalSection","hasDob","dobFormatted","dob","year","month","day","split","timezoneDisplay","timezone","hasAddress","meta","street","city","state","zip","country","addressSummary","Boolean","join","onActionEditDisplayName","defaultValue","display_name","onActionEditFirstName","first_name","onActionEditLastName","last_name","onActionEditDob","data","showForm","title","size","fields","type","cols","onActionEditTimezone","value","text","updatedMeta","metadata","onActionEditAddress","base64urlToBytes","base64url","base64","padded","repeat","Uint8Array","from","atob","charCodeAt","bytesToBase64url","buffer","btoa","String","fromCharCode","Passkey","Model","endpoint","suggestName","ua","navigator","userAgent","device","test","browser","register","friendlyName","beginResp","registerBegin","challenge_id","publicKey","challenge","user","id","excludeCredentials","cred","credential","credentials","create","credentialData","rawId","response","clientDataJSON","attestationObject","getTransports","transports","completeResp","registerComplete","friendly_name","passkey","params","dataOnly","err","PasskeyList","Collection","ModelClass","PasskeyForms","edit","required","columns","help","PasskeySetupView","_askPasskeyName","suggested","showDialog","centered","body","buttons","class","dismiss","handler","dialog","input","element","querySelector","showSuccess","showError","alert","onActionCreatePasskey","result","localStorage","setItem","emit","console","onActionSkip","onActionDontAsk","checkbox","checked","removeItem","ProfileSecuritySection","onActionChangePassword","changePassword","onActionManagePasskeys","collection","fetch","e","models","view","passkeys","toJSON","onActionEditPasskey","async","dataset","find","showModelForm","onActionDeletePasskey","destroy","icon","_addPasskey","onActionManageTotp","DELETE","setupResp","secret","qr_code","setupView","qrCode","onActionManageRecoveryCodes","GET","codes","regenResp","recovery_codes","newCodes","codesText","newView","clipboard","writeText","onActionRevokeAllSessions","password","current_password","access_token","auth","setTokens","PROVIDER_ICONS","google","github","microsoft","apple","facebook","twitter","linkedin","ProfileConnectedSection","connections","onBeforeRender","results","provider","onActionUnlink","connection","SessionRow","TableRow","deviceIcon","dev","device_info","os","some","m","family","includes","browserName","user_agent","major","deviceName","brand","osName","locationText","geo","parts","region","country_name","threatFlags","flags","is_vpn","push","is_tor","is_proxy","hasThreatFlags","ProfileSessionsSection","onInit","tableView","TableView","containerId","UserDeviceLocationList","defaultQuery","searchable","filterable","selectable","actions","clickAction","itemClass","key","formatter","onItemView","_showSessionDetail","addChild","ud","user_device","geolocation","minor","patch","location","is_datacenter","is_known_attacker","row","ip_address","isp","asn_org","asn","threat_level","first_seen","Date","toLocaleString","last_seen","DeviceRow","deviceModel","deviceMeta","ProfileDevicesSection","UserDeviceList","_showDeviceDetail","info","isMobile","last_ip","SecurityEvent","SecurityEventList","DANGER_KINDS","SUCCESS_KINDS","SecurityEventRow","kindBadgeClass","kind","kindLabel","kindIcon","ProfileSecurityEventsSection","sort","hideActivePillNames","visibility","sortable","paginated","showAdd","showExport","tableOptions","striped","hover","emptyMessage","CHANNEL_LABELS","in_app","CHANNELS","ProfileNotificationsSection","preferences","channels","ch","hasPreferences","preferenceRows","toggles","channel","onActionTogglePref","ProfileApiKeysSection","apiKeys","generatedToken","_loadKeys","Array","isArray","onAfterRender","_renderKeysList","container","innerHTML","rows","created","toLocaleDateString","expires","lastUsed","last_used","ips","allowed_ips","is_active","token_prefix","onActionGenerateKey","expire_days","parseInt","ipsStr","ip","token","resultEl","tokenEl","textContent","style","display","onActionDeleteKey","onActionCopyToken","AVATAR_COLORS","GroupMemberItem","ListViewItem","groupName","groupKind","initials","w","substring","avatarColor","hash","i","abs","roleName","role","manage_group","hasRole","roleBadgeClass","r","toLowerCase","permissionsList","hasPermissions","ProfileGroupsSection","listView","ListView","MemberList","itemTemplate","ProfilePermissionsSection","systemPermissions","groupPermissions","SECTIONS","profile","personal","security","connected","sessions","devices","security_events","notifications","api_keys","groups","permissions","UserProfileView","activeSection","sectionView","hasAvatar","url","graph","querySelectorAll","link","classList","toggle","section","preventDefault","SectionClass","removeChild","onActionChangeAvatar","updateModelImage","field","upload","imageSize","width","height","onActionCloseDialog","modal","closest","bsModal","bootstrap","Modal","getInstance","hide","onActionUpdateEmail","submitText","attributes","autocomplete","inputmode","new_email_address","onActionUpdatePhone","new_phone","sessionToken","session_token"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./Rest-BJ3Mvx1L.js";class Router{constructor(e={}){this.defaultRoute=e.defaultRoute||"home",this.routes=[],this.currentRoute=null,this.eventEmitter=e.eventEmitter||null,this.boundHandlePopState=this.handlePopState.bind(this)}start(){window.addEventListener("popstate",this.boundHandlePopState),this.handleCurrentLocation()}stop(){window.removeEventListener("popstate",this.boundHandlePopState)}addRoute(e,t){this.routes.push({pattern:this.normalizePattern(e),regex:this.patternToRegex(e),pageName:t,paramNames:this.extractParamNames(e)})}async navigate(e,t={}){const{replace:s=!1,state:i=null,trigger:r=!0}=t,{pageName:n,queryParams:o}=this.parseInput(e);r&&await this.handleRouteChange(n,o)}back(){window.history.back()}forward(){window.history.forward()}getCurrentRoute(){return this.currentRoute}getCurrentPath(){const{pageName:e,queryParams:t}=this.parseCurrentUrl();return this.buildPublicUrl(e,t)}handlePopState(e){this.allowPopState?this.handleCurrentLocation():console.warn("PopStateEvent is not allowed")}async handleCurrentLocation(){const{pageName:e,queryParams:t}=this.parseCurrentUrl();await this.handleRouteChange(e,t)}async handleRouteChange(e,t){const s="/"+e,i=this.matchRoute(s),r=this.buildPublicUrl(e,t);return i?(this.currentRoute=i,this.eventEmitter&&this.eventEmitter.emit("route:changed",{path:r,pageName:i.pageName,params:i.params,query:t,route:i}),i):(this.eventEmitter&&this.eventEmitter.emit("route:notfound",{path:r}),null)}matchRoute(e){for(const t of this.routes){const s=e.match(t.regex);if(s){const i={};return t.paramNames.forEach((e,t)=>{i[e]=s[t+1]}),{...t,params:i,path:e}}}return null}parseInput(e){let t=this.defaultRoute,s={};if(!e)return{pageName:t,queryParams:s};try{if(e.includes("?")){const[i,r]=e.split("?",2),n=new URLSearchParams(r);if(n.has("page")){t=n.get("page")||this.defaultRoute;for(const[e,t]of n)"page"!==e&&(s[e]=t)}else{t=i.startsWith("/")?i.substring(1)||this.defaultRoute:i||this.defaultRoute;for(const[e,t]of n)s[e]=t}}else t=e.startsWith("/")?e.substring(1)||this.defaultRoute:e}catch(i){console.warn("Failed to parse input:",e,i),t=this.defaultRoute,s={}}return{pageName:t,queryParams:s}}parseCurrentUrl(){const e=new URLSearchParams(window.location.search),t=e.get("page")||this.defaultRoute,s={};for(const[i,r]of e)"page"!==i&&(s[i]=r);return{pageName:t,queryParams:s}}buildPublicUrl(e,t={}){const s=new URLSearchParams;return s.set("page",e),Object.entries(t).forEach(([e,t])=>{null!=t&&""!==t&&s.set(e,String(t))}),"?"+s.toString()}updateBrowserUrl(e,t,s,i){const r=new URL(window.location.origin+window.location.pathname);r.searchParams.set("page",e),Object.entries(t).forEach(([e,t])=>{null!=t&&""!==t&&r.searchParams.set(e,String(t))});const n=r.toString();s?window.history.replaceState(i,"",n):window.history.pushState(i,"",n)}patternToRegex(e){let t=e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&").replace(/\/:([^/?]+)\?/g,"(?:/([^/]+))?").replace(/:([^/]+)/g,"([^/]+)");return new RegExp(`^${t}$`)}extractParamNames(e){return(e.match(/:([^/?]+)\??/g)||[]).map(e=>e.replace(/[:?]/g,""))}normalizePattern(e){return e.startsWith("/")?e:`/${e}`}updateUrl(e={},t={}){const{replace:s=!1}=t,{pageName:i}=this.parseCurrentUrl();this.updateBrowserUrl(i,e,s)}buildUrl(e,t={}){return this.buildPublicUrl(e,t)}doRoutesMatch(e,t){if(!e||!t)return!1;const{pageName:s}=this.parseInput(e),{pageName:i}=this.parseInput(t);return s===i}}class EventBus{constructor(){this.listeners={},this.onceListeners={},this.maxListeners=100,this.debugMode=!1,this.eventStats={}}on(e,t){if("function"!=typeof t)throw new Error("Callback must be a function");return Array.isArray(e)?(e.forEach(e=>this.on(e,t)),this):(this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].length>=this.maxListeners&&console.warn(`Max listeners (${this.maxListeners}) exceeded for event: ${e}`),this.listeners[e].push(t),this)}once(e,t){if("function"!=typeof t)throw new Error("Callback must be a function");return Array.isArray(e)?(e.forEach(e=>this.once(e,t)),this):(this.onceListeners[e]||(this.onceListeners[e]=[]),this.onceListeners[e].push(t),this)}off(e,t){if(Array.isArray(e))return e.forEach(e=>this.off(e,t)),this;if(!t)return delete this.listeners[e],delete this.onceListeners[e],this;if(this.listeners[e]){const s=this.listeners[e].indexOf(t);-1!==s&&(this.listeners[e].splice(s,1),0===this.listeners[e].length&&delete this.listeners[e])}if(this.onceListeners[e]){const s=this.onceListeners[e].indexOf(t);-1!==s&&(this.onceListeners[e].splice(s,1),0===this.onceListeners[e].length&&delete this.onceListeners[e])}return this}emit(e,t){this.updateEventStats(e),this.debugMode;const s=[];return this.listeners[e]&&s.push(...this.listeners[e]),this.listeners["*"]&&s.push(...this.listeners["*"]),this.onceListeners[e]&&(s.push(...this.onceListeners[e]),delete this.onceListeners[e]),this.onceListeners["*"]&&(s.push(...this.onceListeners["*"]),delete this.onceListeners["*"]),this.debugMode&&s.length>0&&s.length,s.forEach(s=>{try{s(t,e)&&(e.stopPropagation&&e.stopPropagation(),e.preventDefault&&e.preventDefault())}catch(i){console.error(`Error in event listener for '${e}':`,i),this.emitError(i,e,s)}}),this}async emitAsync(e,t){const s=[];this.listeners[e]&&s.push(...this.listeners[e]),this.listeners["*"]&&s.push(...this.listeners["*"]),this.onceListeners[e]&&(s.push(...this.onceListeners[e]),delete this.onceListeners[e]),this.onceListeners["*"]&&(s.push(...this.onceListeners["*"]),delete this.onceListeners["*"]);const i=s.map(s=>new Promise(i=>{try{i(s(t,e))}catch(r){console.error(`Error in async event listener for '${e}':`,r),this.emitError(r,e,s),i()}}));return await Promise.all(i),this}removeAllListeners(){return this.listeners={},this.onceListeners={},this}listenerCount(e){return(this.listeners[e]?this.listeners[e].length:0)+(this.onceListeners[e]?this.onceListeners[e].length:0)}eventNames(){const e=Object.keys(this.listeners),t=Object.keys(this.onceListeners);return[.../* @__PURE__ */new Set([...e,...t])]}setMaxListeners(e){if("number"!=typeof e||e<0)throw new Error("Max listeners must be a non-negative number");return this.maxListeners=e,this}namespace(e){const t=t=>`${e}:${t}`;return{on:(e,s)=>this.on(t(e),s),once:(e,s)=>this.once(t(e),s),off:(e,s)=>this.off(t(e),s),emit:(e,s)=>this.emit(t(e),s),emitAsync:(e,s)=>this.emitAsync(t(e),s)}}use(e){if("function"!=typeof e)throw new Error("Middleware must be a function");const t=this.emit;return this.emit=(s,i)=>{try{const r=e(s,i);if(!1===r)return this;const n=void 0!==r?r:i;return t.call(this,s,n)}catch(r){return console.error("Error in event middleware:",r),t.call(this,s,i)}},this}emitError(e,t,s){"error"!==t&&setTimeout(()=>{this.emit("error",{error:e,originalEvent:t,callback:s.toString()})},0)}waitFor(e,t=null){return new Promise((s,i)=>{let r=null;const n=e=>{r&&clearTimeout(r),s(e)};this.once(e,n),t&&(r=setTimeout(()=>{this.off(e,n),i(new Error(`Timeout waiting for event: ${e}`))},t))})}debug(e=!0){return this.debugMode=e,this}getStats(){const e=this.eventNames(),t={totalEvents:e.length,totalListeners:0,events:{},emissions:{...this.eventStats}};return e.forEach(e=>{const s=this.listenerCount(e);t.events[e]=s,t.totalListeners+=s}),t}updateEventStats(e){this.eventStats[e]||(this.eventStats[e]={count:0,firstEmission:Date.now(),lastEmission:null}),this.eventStats[e].count++,this.eventStats[e].lastEmission=Date.now()}getEventStats(e){const t=this.eventStats[e];return t?{...t,listenerCount:this.listenerCount(e),avgEmissionsPerMinute:this.calculateEmissionRate(t)}:null}calculateEmissionRate(e){if(!e.firstEmission||!e.lastEmission)return 0;const t=e.lastEmission-e.firstEmission;if(0===t)return 0;const s=t/6e4;return Math.round(e.count/s*100)/100}resetStats(){return this.eventStats={},this}getTopEvents(e=10){return Object.entries(this.eventStats).map(([e,t])=>({event:e,count:t.count,rate:this.calculateEmissionRate(t),listeners:this.listenerCount(e)})).sort((e,t)=>t.count-e.count).slice(0,e)}debugInfo(){this.debugMode,this.maxListeners;const e=this.getStats();return e.totalEvents,e.totalListeners,Object.keys(this.eventStats).length>0&&this.getTopEvents(5),this}}class WebApp{constructor(t={}){this.config=t,this.initPluginRegistry(),this.name=t.name||"MOJO App",this.version=t.version||"1.0.0",this.debug=t.debug||!1,this.container=t.container||"#app",this.layoutType=t.layout||"portal",this.layoutConfig=t.layoutConfig||{},t.sidebar&&(this.layoutConfig.sidebarConfig=t.sidebar),t.topbar&&(this.layoutConfig.topbarConfig=t.topbar),this.layout=null,this.layoutConfig.containerId=this.container||this.containerId||"#app",this.pageContainer=t.pageContainer||"#page-container",this.basePath=t.basePath||"",this.routerMode=t.routerMode||t.router?.mode||"param",this.basePath=t.basePath||t.router?.base||"",this.defaultRoute=t.defaultRoute||"home",this.session=t.session||{},this.router=null,this.navigation=t.navigation||{},this.state={currentPage:null,previousPage:null,loading:!1},this.events=new EventBus,this.rest=e,t.api&&this.rest.configure(t.api),this.router=new Router({mode:"param"===this.routerMode?"params":this.routerMode,basePath:this.basePath,defaultRoute:this.defaultRoute,eventEmitter:this.events}),this.events.on("route:changed",async e=>{const{pageName:t,params:s,query:i}=e;await this.showPage(t,i,s,{fromRouter:!0})}),"undefined"!=typeof window&&(window.MOJO=window.MOJO||{},window.MOJO.router=this.router),this.setupFocusTracking(),this.pageCache=/* @__PURE__ */new Map,this.pageClasses=/* @__PURE__ */new Map,this.componentClasses=/* @__PURE__ */new Map,this.modelClasses=/* @__PURE__ */new Map,this.currentPage=null,this.isStarted=!1,window.matchUUID?window[window.matchUUID]=this:window.MOJO?window.MOJO.app=this:window.__app__=this}async start(){if(this.isStarted)console.warn("WebApp already started");else try{this.setupPageContainer(),this.validateDefaultRoute(),await this.setupRouter(),this.isStarted=!0,this.router.allowPopState=!1,this.events.emit("app:ready",{app:this})}catch(e){throw console.error(`Failed to start ${this.name}:`,e),this.showError("Failed to start application"),e}}async setupRouter(){this.router?(this.events.on("route:notfound",async e=>{console.warn(`Route not found: ${e.path}`),this._show404(e.path)}),this.router.start(),this.routerMode):console.error("Router not initialized")}setupPageContainer(){const e="string"==typeof this.container?document.querySelector(this.container):this.container;e&&!e.querySelector("#page-container")&&(e.innerHTML='<div id="page-container"></div>'),this.pageContainer="#page-container"}registerPage(e,t,s={}){if("string"!=typeof e||!e)return console.error("registerPage: pageName must be a non-empty string"),this;if("function"!=typeof t)return console.error("registerPage: PageClass must be a constructor function"),this;if(s.containerId||(s.containerId=this.pageContainer),this.pageClasses.set(e,{PageClass:t,constructorOptions:s}),this.router){let t=s.route||`/${e}`;t.startsWith("/")||(t=`/${t}`),s.route=t,this.router.addRoute(t,e)}return this}getPage(e){return this.pageCache.get(e)}getPagePermissions(e){if(this.pageCache.has(e))return this.pageCache.get(e).permissions;const t=this.pageClasses.get(e);if(!t)return null;const{PageClass:s,constructorOptions:i}=t;return i?i.permissions:null}getOrCreatePage(e){if(this.pageCache.has(e))return this.pageCache.get(e);const t=this.pageClasses.get(e);if(!t)return console.error(`Page not registered: ${e}`),null;const{PageClass:s,constructorOptions:i}=t;try{const t=new s({pageName:e,...i,app:this});return i.route&&(t.route=i.route),this.pageCache.set(e,t),t.route,t}catch(r){return console.error(`Failed to create page ${e}:`,r),null}}async showPage(e,t={},s={},i={}){const{fromRouter:r=!1,replace:n=!1,force:o=!1}=i;try{let i,n;"string"==typeof e?(n=e,i=this.getOrCreatePage(e)):e&&"object"==typeof e&&(i=e,n=e.pageName);const o=this.currentPage;if(!i)return void this._show404(n,s,t,r);if(this.events.emit("page:showing",{page:i,pageName:i.pageName,params:s,query:t,fromRouter:r}),!i.canEnter())return void this._showDeniedPage(i,s,t,r);o&&o!==i&&await this._exitOldPage(o),await i.onParams(s,t),o!==i&&await i.onEnter(),i.syncUrl(),this.events.emit("page:show",{page:i,pageName:i.pageName,params:s,query:t,fromRouter:r}),await i.render(),this.currentPage=i,i.pageName}catch(a){console.error("Error in showPage:",a),this.showError(`Failed to load page: ${a.message}`),"error"!==e&&await this.showPage("error",{},{error:a,originalPage:e},{fromRouter:r})}}async _show404(e,t,s,i){const r=this.getOrCreatePage("404");r&&(r.setInfo&&r.setInfo(e),await this._exitOldPage(this.currentPage),await r.render(),this.currentPage=r,this.events.emit("page:404",{page:null,pageName:e,params:t,query:s,fromRouter:i}))}async _showDeniedPage(e,t,s,i){const r=this.getOrCreatePage("denied");r.setDeniedPage&&r.setDeniedPage(e),await this._exitOldPage(this.currentPage),await r.render(),this.currentPage=r,this.events.emit("page:denied",{page:e,pageName:e.pageName,params:t,query:s,fromRouter:i})}async _exitOldPage(e){if(e)try{await e.onExit(),await e.unmount(),this.events.emit("page:hide",{page:e})}catch(t){console.error(`Error exiting page ${e.pageName}:`,t)}}async navigate(e,t={},s={}){if(!this.router)return void console.error("Router not initialized");let i=e;if(Object.keys(t).length>0){const s=new URLSearchParams(t).toString();i+=(e.includes("?")?"&":"?")+s}return await this.router.navigate(i,s)}async navigateToDefault(e={}){return await this.showPage(this.defaultRoute,{},{},e)}back(){this.router?this.router.back():console.warn("Router not initialized")}forward(){this.router?this.router.forward():console.warn("Router not initialized")}getCurrentPage(){return this.currentPage}getPageContainer(){return this.layout&&this.layout.getPageContainer?this.layout.getPageContainer():"string"==typeof this.pageContainer?document.querySelector(this.pageContainer):this.pageContainer}async showError(e){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;await t.alert(e,"Error",{size:"md",class:"text-danger"})}catch(t){this.events.emit("notification",{message:e,type:"error"}),"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showError fallback:",t),"undefined"!=typeof window&&alert(`Error: ${e}`)}}async showSuccess(e){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;await t.alert(e,"Success",{size:"md",class:"text-success"})}catch(t){this.events.emit("notification",{message:e,type:"success"}),"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showSuccess fallback:",t),"undefined"!=typeof window&&alert(`Success: ${e}`)}}async showInfo(e){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;await t.alert(e,"Information",{size:"md",class:"text-info"})}catch(t){this.events.emit("notification",{message:e,type:"info"}),"undefined"!=typeof window&&window,"undefined"!=typeof window&&alert(`Info: ${e}`)}}async showWarning(e){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;await t.alert(e,"Warning",{size:"md",class:"text-warning"})}catch(t){this.events.emit("notification",{message:e,type:"warning"}),"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showWarning fallback:",t),"undefined"!=typeof window&&alert(`Warning: ${e}`)}}showNotification(e,t="info"){this.events.emit("notification",{message:e,type:t})}async showLoading(e={}){"string"==typeof e&&(e={message:e});try{(await import("./Dialog-jfBsXy5X.js")).default.showBusy(e)}catch(t){"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showLoading fallback:",t,e),this.events.emit("notification",{message:e.message||"Loading...",type:"info"})}}async hideLoading(){try{(await import("./Dialog-jfBsXy5X.js")).default.hideBusy()}catch(e){"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] hideLoading fallback:",e)}}async showModelView(e,t={}){try{const s=(await import("./Dialog-jfBsXy5X.js")).default;return await s.showModelView(e,t)}catch(s){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showModelForm failed:",s),s}}async showModelForm(e={}){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;return await t.showModelForm(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showModelForm failed:",t),t}}async showForm(e={}){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;return await t.showForm(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showForm failed:",t),t}}async showDialog(e={}){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;return await t.showDialog(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showDialog failed:",t),t}}async showAlert(e={}){try{const t=(await import("./Dialog-jfBsXy5X.js")).default;return await t.showDialog(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showDialog failed:",t),t}}async confirm(e,t="Confirm",s={}){const i=(await import("./Dialog-jfBsXy5X.js")).default;return await i.confirm(e,t,s)}setupFocusTracking(){if("undefined"==typeof window)return;this.isFocused=!document.hidden;const e=()=>{const e=this.isFocused;this.isFocused=!document.hidden,e!==this.isFocused&&(this.isFocused?this.events.emit("browser:focus"):this.events.emit("browser:blur"))},t=()=>{this.isFocused||(this.isFocused=!0,this.events.emit("browser:focus"))},s=()=>{this.isFocused&&(this.isFocused=!1,this.events.emit("browser:blur"))};document.addEventListener("visibilitychange",e),window.addEventListener("focus",t),window.addEventListener("blur",s),this._focusHandlers={visibilitychange:e,focus:t,blur:s}}setupErrorHandling(){window.addEventListener("error",e=>{console.error("Global error:",e.error),this.debug&&this.showError(`Error: ${e.error?.message||"Unknown error"}`)}),window.addEventListener("unhandledrejection",e=>{console.error("Unhandled promise rejection:",e.reason),this.debug&&this.showError(`Promise rejected: ${e.reason?.message||"Unknown error"}`)})}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}getState(e){return e?this.state[e]:this.state}setState(e){const t={...this.state};Object.assign(this.state,e),this.events.emit("state:changed",{oldState:t,newState:this.state,updates:e})}registerComponent(e,t){this.componentClasses.set(e,t)}getComponent(e){return this.componentClasses.get(e)}registerModel(e,t){this.modelClasses.set(e,t)}getModel(e){return this.modelClasses.get(e)}setupRest(){this.rest=e,e.configure(this.api)}async destroy(){this.router&&this.router.stop(),this._focusHandlers&&"undefined"!=typeof window&&(document.removeEventListener("visibilitychange",this._focusHandlers.visibilitychange),window.removeEventListener("focus",this._focusHandlers.focus),window.removeEventListener("blur",this._focusHandlers.blur));const e=Array.from(this.pageCache.values());if(await Promise.allSettled(e.map(async e=>{try{e.destroy&&await e.destroy()}catch(t){console.error("Error destroying page:",t)}})),this.layout&&this.layout.destroy)try{await this.layout.destroy()}catch(t){console.error("Error destroying layout:",t)}this.pageCache.clear(),this.pageClasses.clear(),this.componentClasses.clear(),this.modelClasses.clear(),"undefined"!=typeof window&&window.MOJO&&delete window.MOJO.router,this.isStarted=!1,this.name}buildPagePath(e,t,s){let i=e.route||`/${e.pageName.toLowerCase()}`;if(Object.keys(t).forEach(e=>{"string"!=typeof t[e]&&"number"!=typeof t[e]||(i=i.replace(`:${e}`,t[e]))}),s&&Object.keys(s).length>0){const e=new URLSearchParams(s).toString();i+=(i.includes("?")?"&":"?")+e}return i}validateDefaultRoute(){this.pageClasses.has(this.defaultRoute)?this.defaultRoute:(console.warn(`⚠️ Default route '${this.defaultRoute}' is not registered!`),console.warn(` Please register a page: app.registerPage('${this.defaultRoute}', YourPageClass);`),console.warn(" Or change default route: new WebApp({ defaultRoute: 'your-page' });"))}findFallbackPage(){const e=["404","error","denied"];for(const[t]of this.pageClasses.entries())if(!e.includes(t))return t;return null}static create(e={}){return new WebApp(e)}initPluginRegistry(){"undefined"!=typeof window&&(window.MOJO||(window.MOJO={}),window.MOJO.plugins||(window.MOJO.plugins={}),window.MOJO.app=this)}static registerPlugin(e,t){"undefined"!=typeof window&&(window.MOJO||(window.MOJO={}),window.MOJO.plugins||(window.MOJO.plugins={}),window.MOJO.plugins[e]=t)}}export{EventBus as E,Router as R,WebApp as W};
|
|
2
|
-
//# sourceMappingURL=WebApp-
|
|
1
|
+
import{r as e}from"./Rest-BJ3Mvx1L.js";class Router{constructor(e={}){this.defaultRoute=e.defaultRoute||"home",this.routes=[],this.currentRoute=null,this.eventEmitter=e.eventEmitter||null,this.boundHandlePopState=this.handlePopState.bind(this)}start(){window.addEventListener("popstate",this.boundHandlePopState),this.handleCurrentLocation()}stop(){window.removeEventListener("popstate",this.boundHandlePopState)}addRoute(e,t){this.routes.push({pattern:this.normalizePattern(e),regex:this.patternToRegex(e),pageName:t,paramNames:this.extractParamNames(e)})}async navigate(e,t={}){const{replace:s=!1,state:i=null,trigger:r=!0}=t,{pageName:n,queryParams:o}=this.parseInput(e);r&&await this.handleRouteChange(n,o)}back(){window.history.back()}forward(){window.history.forward()}getCurrentRoute(){return this.currentRoute}getCurrentPath(){const{pageName:e,queryParams:t}=this.parseCurrentUrl();return this.buildPublicUrl(e,t)}handlePopState(e){this.allowPopState?this.handleCurrentLocation():console.warn("PopStateEvent is not allowed")}async handleCurrentLocation(){const{pageName:e,queryParams:t}=this.parseCurrentUrl();await this.handleRouteChange(e,t)}async handleRouteChange(e,t){const s="/"+e,i=this.matchRoute(s),r=this.buildPublicUrl(e,t);return i?(this.currentRoute=i,this.eventEmitter&&this.eventEmitter.emit("route:changed",{path:r,pageName:i.pageName,params:i.params,query:t,route:i}),i):(this.eventEmitter&&this.eventEmitter.emit("route:notfound",{path:r}),null)}matchRoute(e){for(const t of this.routes){const s=e.match(t.regex);if(s){const i={};return t.paramNames.forEach((e,t)=>{i[e]=s[t+1]}),{...t,params:i,path:e}}}return null}parseInput(e){let t=this.defaultRoute,s={};if(!e)return{pageName:t,queryParams:s};try{if(e.includes("?")){const[i,r]=e.split("?",2),n=new URLSearchParams(r);if(n.has("page")){t=n.get("page")||this.defaultRoute;for(const[e,t]of n)"page"!==e&&(s[e]=t)}else{t=i.startsWith("/")?i.substring(1)||this.defaultRoute:i||this.defaultRoute;for(const[e,t]of n)s[e]=t}}else t=e.startsWith("/")?e.substring(1)||this.defaultRoute:e}catch(i){console.warn("Failed to parse input:",e,i),t=this.defaultRoute,s={}}return{pageName:t,queryParams:s}}parseCurrentUrl(){const e=new URLSearchParams(window.location.search),t=e.get("page")||this.defaultRoute,s={};for(const[i,r]of e)"page"!==i&&(s[i]=r);return{pageName:t,queryParams:s}}buildPublicUrl(e,t={}){const s=new URLSearchParams;return s.set("page",e),Object.entries(t).forEach(([e,t])=>{null!=t&&""!==t&&s.set(e,String(t))}),"?"+s.toString()}updateBrowserUrl(e,t,s,i){const r=new URL(window.location.origin+window.location.pathname);r.searchParams.set("page",e),Object.entries(t).forEach(([e,t])=>{null!=t&&""!==t&&r.searchParams.set(e,String(t))});const n=r.toString();s?window.history.replaceState(i,"",n):window.history.pushState(i,"",n)}patternToRegex(e){let t=e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&").replace(/\/:([^/?]+)\?/g,"(?:/([^/]+))?").replace(/:([^/]+)/g,"([^/]+)");return new RegExp(`^${t}$`)}extractParamNames(e){return(e.match(/:([^/?]+)\??/g)||[]).map(e=>e.replace(/[:?]/g,""))}normalizePattern(e){return e.startsWith("/")?e:`/${e}`}updateUrl(e={},t={}){const{replace:s=!1}=t,{pageName:i}=this.parseCurrentUrl();this.updateBrowserUrl(i,e,s)}buildUrl(e,t={}){return this.buildPublicUrl(e,t)}doRoutesMatch(e,t){if(!e||!t)return!1;const{pageName:s}=this.parseInput(e),{pageName:i}=this.parseInput(t);return s===i}}class EventBus{constructor(){this.listeners={},this.onceListeners={},this.maxListeners=100,this.debugMode=!1,this.eventStats={}}on(e,t){if("function"!=typeof t)throw new Error("Callback must be a function");return Array.isArray(e)?(e.forEach(e=>this.on(e,t)),this):(this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].length>=this.maxListeners&&console.warn(`Max listeners (${this.maxListeners}) exceeded for event: ${e}`),this.listeners[e].push(t),this)}once(e,t){if("function"!=typeof t)throw new Error("Callback must be a function");return Array.isArray(e)?(e.forEach(e=>this.once(e,t)),this):(this.onceListeners[e]||(this.onceListeners[e]=[]),this.onceListeners[e].push(t),this)}off(e,t){if(Array.isArray(e))return e.forEach(e=>this.off(e,t)),this;if(!t)return delete this.listeners[e],delete this.onceListeners[e],this;if(this.listeners[e]){const s=this.listeners[e].indexOf(t);-1!==s&&(this.listeners[e].splice(s,1),0===this.listeners[e].length&&delete this.listeners[e])}if(this.onceListeners[e]){const s=this.onceListeners[e].indexOf(t);-1!==s&&(this.onceListeners[e].splice(s,1),0===this.onceListeners[e].length&&delete this.onceListeners[e])}return this}emit(e,t){this.updateEventStats(e),this.debugMode;const s=[];return this.listeners[e]&&s.push(...this.listeners[e]),this.listeners["*"]&&s.push(...this.listeners["*"]),this.onceListeners[e]&&(s.push(...this.onceListeners[e]),delete this.onceListeners[e]),this.onceListeners["*"]&&(s.push(...this.onceListeners["*"]),delete this.onceListeners["*"]),this.debugMode&&s.length>0&&s.length,s.forEach(s=>{try{s(t,e)&&(e.stopPropagation&&e.stopPropagation(),e.preventDefault&&e.preventDefault())}catch(i){console.error(`Error in event listener for '${e}':`,i),this.emitError(i,e,s)}}),this}async emitAsync(e,t){const s=[];this.listeners[e]&&s.push(...this.listeners[e]),this.listeners["*"]&&s.push(...this.listeners["*"]),this.onceListeners[e]&&(s.push(...this.onceListeners[e]),delete this.onceListeners[e]),this.onceListeners["*"]&&(s.push(...this.onceListeners["*"]),delete this.onceListeners["*"]);const i=s.map(s=>new Promise(i=>{try{i(s(t,e))}catch(r){console.error(`Error in async event listener for '${e}':`,r),this.emitError(r,e,s),i()}}));return await Promise.all(i),this}removeAllListeners(){return this.listeners={},this.onceListeners={},this}listenerCount(e){return(this.listeners[e]?this.listeners[e].length:0)+(this.onceListeners[e]?this.onceListeners[e].length:0)}eventNames(){const e=Object.keys(this.listeners),t=Object.keys(this.onceListeners);return[.../* @__PURE__ */new Set([...e,...t])]}setMaxListeners(e){if("number"!=typeof e||e<0)throw new Error("Max listeners must be a non-negative number");return this.maxListeners=e,this}namespace(e){const t=t=>`${e}:${t}`;return{on:(e,s)=>this.on(t(e),s),once:(e,s)=>this.once(t(e),s),off:(e,s)=>this.off(t(e),s),emit:(e,s)=>this.emit(t(e),s),emitAsync:(e,s)=>this.emitAsync(t(e),s)}}use(e){if("function"!=typeof e)throw new Error("Middleware must be a function");const t=this.emit;return this.emit=(s,i)=>{try{const r=e(s,i);if(!1===r)return this;const n=void 0!==r?r:i;return t.call(this,s,n)}catch(r){return console.error("Error in event middleware:",r),t.call(this,s,i)}},this}emitError(e,t,s){"error"!==t&&setTimeout(()=>{this.emit("error",{error:e,originalEvent:t,callback:s.toString()})},0)}waitFor(e,t=null){return new Promise((s,i)=>{let r=null;const n=e=>{r&&clearTimeout(r),s(e)};this.once(e,n),t&&(r=setTimeout(()=>{this.off(e,n),i(new Error(`Timeout waiting for event: ${e}`))},t))})}debug(e=!0){return this.debugMode=e,this}getStats(){const e=this.eventNames(),t={totalEvents:e.length,totalListeners:0,events:{},emissions:{...this.eventStats}};return e.forEach(e=>{const s=this.listenerCount(e);t.events[e]=s,t.totalListeners+=s}),t}updateEventStats(e){this.eventStats[e]||(this.eventStats[e]={count:0,firstEmission:Date.now(),lastEmission:null}),this.eventStats[e].count++,this.eventStats[e].lastEmission=Date.now()}getEventStats(e){const t=this.eventStats[e];return t?{...t,listenerCount:this.listenerCount(e),avgEmissionsPerMinute:this.calculateEmissionRate(t)}:null}calculateEmissionRate(e){if(!e.firstEmission||!e.lastEmission)return 0;const t=e.lastEmission-e.firstEmission;if(0===t)return 0;const s=t/6e4;return Math.round(e.count/s*100)/100}resetStats(){return this.eventStats={},this}getTopEvents(e=10){return Object.entries(this.eventStats).map(([e,t])=>({event:e,count:t.count,rate:this.calculateEmissionRate(t),listeners:this.listenerCount(e)})).sort((e,t)=>t.count-e.count).slice(0,e)}debugInfo(){this.debugMode,this.maxListeners;const e=this.getStats();return e.totalEvents,e.totalListeners,Object.keys(this.eventStats).length>0&&this.getTopEvents(5),this}}class WebApp{constructor(t={}){this.config=t,this.initPluginRegistry(),this.name=t.name||"MOJO App",this.version=t.version||"1.0.0",this.debug=t.debug||!1,this.container=t.container||"#app",this.layoutType=t.layout||"portal",this.layoutConfig=t.layoutConfig||{},t.sidebar&&(this.layoutConfig.sidebarConfig=t.sidebar),t.topbar&&(this.layoutConfig.topbarConfig=t.topbar),this.layout=null,this.layoutConfig.containerId=this.container||this.containerId||"#app",this.pageContainer=t.pageContainer||"#page-container",this.basePath=t.basePath||"",this.routerMode=t.routerMode||t.router?.mode||"param",this.basePath=t.basePath||t.router?.base||"",this.defaultRoute=t.defaultRoute||"home",this.session=t.session||{},this.router=null,this.navigation=t.navigation||{},this.state={currentPage:null,previousPage:null,loading:!1},this.events=new EventBus,this.rest=e,t.api&&this.rest.configure(t.api),this.router=new Router({mode:"param"===this.routerMode?"params":this.routerMode,basePath:this.basePath,defaultRoute:this.defaultRoute,eventEmitter:this.events}),this.events.on("route:changed",async e=>{const{pageName:t,params:s,query:i}=e;await this.showPage(t,i,s,{fromRouter:!0})}),"undefined"!=typeof window&&(window.MOJO=window.MOJO||{},window.MOJO.router=this.router),this.setupFocusTracking(),this.pageCache=/* @__PURE__ */new Map,this.pageClasses=/* @__PURE__ */new Map,this.componentClasses=/* @__PURE__ */new Map,this.modelClasses=/* @__PURE__ */new Map,this.currentPage=null,this.isStarted=!1,window.matchUUID?window[window.matchUUID]=this:window.MOJO?window.MOJO.app=this:window.__app__=this}async start(){if(this.isStarted)console.warn("WebApp already started");else try{this.setupPageContainer(),this.validateDefaultRoute(),await this.setupRouter(),this.isStarted=!0,this.router.allowPopState=!1,this.events.emit("app:ready",{app:this})}catch(e){throw console.error(`Failed to start ${this.name}:`,e),this.showError("Failed to start application"),e}}async setupRouter(){this.router?(this.events.on("route:notfound",async e=>{console.warn(`Route not found: ${e.path}`),this._show404(e.path)}),this.router.start(),this.routerMode):console.error("Router not initialized")}setupPageContainer(){const e="string"==typeof this.container?document.querySelector(this.container):this.container;e&&!e.querySelector("#page-container")&&(e.innerHTML='<div id="page-container"></div>'),this.pageContainer="#page-container"}registerPage(e,t,s={}){if("string"!=typeof e||!e)return console.error("registerPage: pageName must be a non-empty string"),this;if("function"!=typeof t)return console.error("registerPage: PageClass must be a constructor function"),this;if(s.containerId||(s.containerId=this.pageContainer),this.pageClasses.set(e,{PageClass:t,constructorOptions:s}),this.router){let t=s.route||`/${e}`;t.startsWith("/")||(t=`/${t}`),s.route=t,this.router.addRoute(t,e)}return this}getPage(e){return this.pageCache.get(e)}getPagePermissions(e){if(this.pageCache.has(e))return this.pageCache.get(e).permissions;const t=this.pageClasses.get(e);if(!t)return null;const{PageClass:s,constructorOptions:i}=t;return i?i.permissions:null}getOrCreatePage(e){if(this.pageCache.has(e))return this.pageCache.get(e);const t=this.pageClasses.get(e);if(!t)return console.error(`Page not registered: ${e}`),null;const{PageClass:s,constructorOptions:i}=t;try{const t=new s({pageName:e,...i,app:this});return i.route&&(t.route=i.route),this.pageCache.set(e,t),t.route,t}catch(r){return console.error(`Failed to create page ${e}:`,r),null}}async showPage(e,t={},s={},i={}){const{fromRouter:r=!1,replace:n=!1,force:o=!1}=i;try{let i,n;"string"==typeof e?(n=e,i=this.getOrCreatePage(e)):e&&"object"==typeof e&&(i=e,n=e.pageName);const o=this.currentPage;if(!i)return void this._show404(n,s,t,r);if(this.events.emit("page:showing",{page:i,pageName:i.pageName,params:s,query:t,fromRouter:r}),!i.canEnter())return void this._showDeniedPage(i,s,t,r);o&&o!==i&&await this._exitOldPage(o),await i.onParams(s,t),o!==i&&await i.onEnter(),i.syncUrl(),this.events.emit("page:show",{page:i,pageName:i.pageName,params:s,query:t,fromRouter:r}),await i.render(),this.currentPage=i,i.pageName}catch(a){console.error("Error in showPage:",a),this.showError(`Failed to load page: ${a.message}`),"error"!==e&&await this.showPage("error",{},{error:a,originalPage:e},{fromRouter:r})}}async _show404(e,t,s,i){const r=this.getOrCreatePage("404");r&&(r.setInfo&&r.setInfo(e),await this._exitOldPage(this.currentPage),await r.render(),this.currentPage=r,this.events.emit("page:404",{page:null,pageName:e,params:t,query:s,fromRouter:i}))}async _showDeniedPage(e,t,s,i){const r=this.getOrCreatePage("denied");r.setDeniedPage&&r.setDeniedPage(e),await this._exitOldPage(this.currentPage),await r.render(),this.currentPage=r,this.events.emit("page:denied",{page:e,pageName:e.pageName,params:t,query:s,fromRouter:i})}async _exitOldPage(e){if(e)try{await e.onExit(),await e.unmount(),this.events.emit("page:hide",{page:e})}catch(t){console.error(`Error exiting page ${e.pageName}:`,t)}}async navigate(e,t={},s={}){if(!this.router)return void console.error("Router not initialized");let i=e;if(Object.keys(t).length>0){const s=new URLSearchParams(t).toString();i+=(e.includes("?")?"&":"?")+s}return await this.router.navigate(i,s)}async navigateToDefault(e={}){return await this.showPage(this.defaultRoute,{},{},e)}back(){this.router?this.router.back():console.warn("Router not initialized")}forward(){this.router?this.router.forward():console.warn("Router not initialized")}getCurrentPage(){return this.currentPage}getPageContainer(){return this.layout&&this.layout.getPageContainer?this.layout.getPageContainer():"string"==typeof this.pageContainer?document.querySelector(this.pageContainer):this.pageContainer}async showError(e){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;await t.alert(e,"Error",{size:"md",class:"text-danger"})}catch(t){this.events.emit("notification",{message:e,type:"error"}),"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showError fallback:",t),"undefined"!=typeof window&&alert(`Error: ${e}`)}}async showSuccess(e){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;await t.alert(e,"Success",{size:"md",class:"text-success"})}catch(t){this.events.emit("notification",{message:e,type:"success"}),"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showSuccess fallback:",t),"undefined"!=typeof window&&alert(`Success: ${e}`)}}async showInfo(e){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;await t.alert(e,"Information",{size:"md",class:"text-info"})}catch(t){this.events.emit("notification",{message:e,type:"info"}),"undefined"!=typeof window&&window,"undefined"!=typeof window&&alert(`Info: ${e}`)}}async showWarning(e){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;await t.alert(e,"Warning",{size:"md",class:"text-warning"})}catch(t){this.events.emit("notification",{message:e,type:"warning"}),"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showWarning fallback:",t),"undefined"!=typeof window&&alert(`Warning: ${e}`)}}showNotification(e,t="info"){this.events.emit("notification",{message:e,type:t})}async showLoading(e={}){"string"==typeof e&&(e={message:e});try{(await import("./Dialog-t_9l2Mou.js")).default.showBusy(e)}catch(t){"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] showLoading fallback:",t,e),this.events.emit("notification",{message:e.message||"Loading...",type:"info"})}}async hideLoading(){try{(await import("./Dialog-t_9l2Mou.js")).default.hideBusy()}catch(e){"undefined"!=typeof window&&window?.console&&console.warn("[WebApp] hideLoading fallback:",e)}}async showModelView(e,t={}){try{const s=(await import("./Dialog-t_9l2Mou.js")).default;return await s.showModelView(e,t)}catch(s){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showModelForm failed:",s),s}}async showModelForm(e={}){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;return await t.showModelForm(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showModelForm failed:",t),t}}async showForm(e={}){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;return await t.showForm(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showForm failed:",t),t}}async showDialog(e={}){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;return await t.showDialog(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showDialog failed:",t),t}}async showAlert(e={}){try{const t=(await import("./Dialog-t_9l2Mou.js")).default;return await t.showDialog(e)}catch(t){throw"undefined"!=typeof window&&window?.console&&console.error("[WebApp] showDialog failed:",t),t}}async confirm(e,t="Confirm",s={}){const i=(await import("./Dialog-t_9l2Mou.js")).default;return await i.confirm(e,t,s)}setupFocusTracking(){if("undefined"==typeof window)return;this.isFocused=!document.hidden;const e=()=>{const e=this.isFocused;this.isFocused=!document.hidden,e!==this.isFocused&&(this.isFocused?this.events.emit("browser:focus"):this.events.emit("browser:blur"))},t=()=>{this.isFocused||(this.isFocused=!0,this.events.emit("browser:focus"))},s=()=>{this.isFocused&&(this.isFocused=!1,this.events.emit("browser:blur"))};document.addEventListener("visibilitychange",e),window.addEventListener("focus",t),window.addEventListener("blur",s),this._focusHandlers={visibilitychange:e,focus:t,blur:s}}setupErrorHandling(){window.addEventListener("error",e=>{console.error("Global error:",e.error),this.debug&&this.showError(`Error: ${e.error?.message||"Unknown error"}`)}),window.addEventListener("unhandledrejection",e=>{console.error("Unhandled promise rejection:",e.reason),this.debug&&this.showError(`Promise rejected: ${e.reason?.message||"Unknown error"}`)})}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}getState(e){return e?this.state[e]:this.state}setState(e){const t={...this.state};Object.assign(this.state,e),this.events.emit("state:changed",{oldState:t,newState:this.state,updates:e})}registerComponent(e,t){this.componentClasses.set(e,t)}getComponent(e){return this.componentClasses.get(e)}registerModel(e,t){this.modelClasses.set(e,t)}getModel(e){return this.modelClasses.get(e)}setupRest(){this.rest=e,e.configure(this.api)}async destroy(){this.router&&this.router.stop(),this._focusHandlers&&"undefined"!=typeof window&&(document.removeEventListener("visibilitychange",this._focusHandlers.visibilitychange),window.removeEventListener("focus",this._focusHandlers.focus),window.removeEventListener("blur",this._focusHandlers.blur));const e=Array.from(this.pageCache.values());if(await Promise.allSettled(e.map(async e=>{try{e.destroy&&await e.destroy()}catch(t){console.error("Error destroying page:",t)}})),this.layout&&this.layout.destroy)try{await this.layout.destroy()}catch(t){console.error("Error destroying layout:",t)}this.pageCache.clear(),this.pageClasses.clear(),this.componentClasses.clear(),this.modelClasses.clear(),"undefined"!=typeof window&&window.MOJO&&delete window.MOJO.router,this.isStarted=!1,this.name}buildPagePath(e,t,s){let i=e.route||`/${e.pageName.toLowerCase()}`;if(Object.keys(t).forEach(e=>{"string"!=typeof t[e]&&"number"!=typeof t[e]||(i=i.replace(`:${e}`,t[e]))}),s&&Object.keys(s).length>0){const e=new URLSearchParams(s).toString();i+=(i.includes("?")?"&":"?")+e}return i}validateDefaultRoute(){this.pageClasses.has(this.defaultRoute)?this.defaultRoute:(console.warn(`⚠️ Default route '${this.defaultRoute}' is not registered!`),console.warn(` Please register a page: app.registerPage('${this.defaultRoute}', YourPageClass);`),console.warn(" Or change default route: new WebApp({ defaultRoute: 'your-page' });"))}findFallbackPage(){const e=["404","error","denied"];for(const[t]of this.pageClasses.entries())if(!e.includes(t))return t;return null}static create(e={}){return new WebApp(e)}initPluginRegistry(){"undefined"!=typeof window&&(window.MOJO||(window.MOJO={}),window.MOJO.plugins||(window.MOJO.plugins={}),window.MOJO.app=this)}static registerPlugin(e,t){"undefined"!=typeof window&&(window.MOJO||(window.MOJO={}),window.MOJO.plugins||(window.MOJO.plugins={}),window.MOJO.plugins[e]=t)}}export{EventBus as E,Router as R,WebApp as W};
|
|
2
|
+
//# sourceMappingURL=WebApp-Bti0Gqqo.js.map
|