paskia 0.7.2__py3-none-any.whl → 0.8.0__py3-none-any.whl
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.
- paskia/_version.py +2 -2
- paskia/authsession.py +12 -49
- paskia/bootstrap.py +30 -25
- paskia/db/__init__.py +163 -401
- paskia/db/background.py +128 -0
- paskia/db/jsonl.py +132 -0
- paskia/db/operations.py +1241 -0
- paskia/db/structs.py +148 -0
- paskia/fastapi/admin.py +456 -215
- paskia/fastapi/api.py +16 -15
- paskia/fastapi/authz.py +7 -2
- paskia/fastapi/mainapp.py +2 -1
- paskia/fastapi/remote.py +20 -20
- paskia/fastapi/reset.py +9 -10
- paskia/fastapi/user.py +10 -18
- paskia/fastapi/ws.py +22 -19
- paskia/frontend-build/auth/admin/index.html +3 -3
- paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +8 -0
- paskia/frontend-build/auth/assets/admin-BeNu48FR.css +1 -0
- paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +1 -0
- paskia/frontend-build/auth/assets/{auth-BU_O38k2.css → auth-BKX7shEe.css} +1 -1
- paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +1 -0
- paskia/frontend-build/auth/index.html +3 -3
- paskia/globals.py +7 -10
- paskia/migrate/__init__.py +274 -0
- paskia/migrate/sql.py +381 -0
- paskia/util/permutil.py +16 -5
- paskia/util/sessionutil.py +3 -2
- paskia/util/userinfo.py +12 -26
- {paskia-0.7.2.dist-info → paskia-0.8.0.dist-info}/METADATA +21 -25
- {paskia-0.7.2.dist-info → paskia-0.8.0.dist-info}/RECORD +33 -29
- {paskia-0.7.2.dist-info → paskia-0.8.0.dist-info}/entry_points.txt +1 -0
- paskia/db/sql.py +0 -1424
- paskia/frontend-build/auth/assets/AccessDenied-C-lL9vbN.js +0 -8
- paskia/frontend-build/auth/assets/admin-Cs6Mg773.css +0 -1
- paskia/frontend-build/auth/assets/admin-Df5_Damp.js +0 -1
- paskia/frontend-build/auth/assets/auth-Df3pjeSS.js +0 -1
- paskia/util/tokens.py +0 -44
- {paskia-0.7.2.dist-info → paskia-0.8.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.permissions-section[data-v-ee865306]{margin-bottom:var(--space-xl)}.permissions-section h2[data-v-ee865306]{margin-bottom:var(--space-md)}.actions[data-v-ee865306]{display:flex;flex-wrap:wrap;gap:var(--space-sm);align-items:center}.actions button[data-v-ee865306]{width:auto}.org-table a[data-v-ee865306]{text-decoration:none;color:var(--color-link)}.org-table a[data-v-ee865306]:hover{text-decoration:underline}.org-table .center[data-v-ee865306]{width:6rem;min-width:6rem}.org-table .role-names[data-v-ee865306]{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.perm-name-cell[data-v-ee865306]{display:flex;flex-direction:column;gap:.3rem}.perm-title[data-v-ee865306]{font-weight:600;color:var(--color-heading)}.perm-id-info[data-v-ee865306]{font-size:.8rem;color:var(--color-text-muted);display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.perm-domain[data-v-ee865306]{color:var(--color-text-muted);font-size:.9rem}.icon-btn[data-v-ee865306]{background:none;border:none;color:var(--color-text-muted);padding:.2rem;border-radius:var(--radius-sm);cursor:pointer;transition:background .2s ease,color .2s ease}.icon-btn[data-v-ee865306]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.delete-icon[data-v-ee865306]{color:var(--color-danger)}.delete-icon[data-v-ee865306]:hover{background:var(--color-danger-bg);color:var(--color-danger-text)}.matrix-wrapper[data-v-ee865306]{margin:var(--space-md) 0;padding:var(--space-lg)}.matrix-scroll[data-v-ee865306]{overflow-x:auto}.matrix-hint[data-v-ee865306]{font-size:.8rem;color:var(--color-text-muted)}.perm-matrix-grid[data-v-ee865306]{display:inline-grid;gap:.25rem;align-items:stretch}.perm-matrix-grid[data-v-ee865306]>*{padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .grid-head[data-v-ee865306]{color:var(--color-text-muted);text-transform:uppercase;font-weight:600;letter-spacing:.05em}.perm-matrix-grid .perm-head[data-v-ee865306]{display:flex;align-items:flex-end;justify-content:flex-start;padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .org-head[data-v-ee865306]{display:flex;align-items:flex-end;justify-content:center}.perm-matrix-grid .org-head span[data-v-ee865306]{writing-mode:vertical-rl;transform:rotate(180deg);font-size:.65rem}.perm-name[data-v-ee865306]{font-weight:600;color:var(--color-heading);padding:.35rem .45rem;font-size:.75rem}.display-text[data-v-ee865306]{margin-right:var(--space-xs)}.edit-display-btn[data-v-ee865306]{padding:.1rem .2rem;font-size:.8rem}.edit-org-btn[data-v-ee865306]{padding:.1rem .2rem;font-size:.8rem;margin-left:var(--space-xs)}.perm-actions[data-v-ee865306],.center[data-v-ee865306]{text-align:center}.muted[data-v-ee865306]{color:var(--color-text-muted)}.card.surface[data-v-f76fbbc3]{padding:var(--space-lg)}.org-title[data-v-f76fbbc3]{display:flex;align-items:center;gap:var(--space-sm);margin-bottom:var(--space-lg)}.org-name[data-v-f76fbbc3]{font-size:1.5rem;font-weight:600;color:var(--color-heading)}.icon-btn[data-v-f76fbbc3]{background:none;border:none;color:var(--color-text-muted);padding:.2rem;border-radius:var(--radius-sm);cursor:pointer;transition:background .2s ease,color .2s ease}.icon-btn[data-v-f76fbbc3]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.matrix-wrapper[data-v-f76fbbc3]{margin:var(--space-md) 0;padding:var(--space-lg)}.matrix-scroll[data-v-f76fbbc3]{overflow-x:auto}.matrix-hint[data-v-f76fbbc3]{font-size:.8rem;color:var(--color-text-muted)}.perm-matrix-grid[data-v-f76fbbc3]{display:inline-grid;gap:.25rem;align-items:stretch}.perm-matrix-grid[data-v-f76fbbc3]>*{padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .grid-head[data-v-f76fbbc3]{color:var(--color-text-muted);text-transform:uppercase;font-weight:600;letter-spacing:.05em}.perm-matrix-grid .perm-head[data-v-f76fbbc3]{display:flex;align-items:flex-end;justify-content:flex-start;padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .role-head[data-v-f76fbbc3]{display:flex;align-items:flex-end;justify-content:center}.perm-matrix-grid .role-head span[data-v-f76fbbc3]{writing-mode:vertical-rl;transform:rotate(180deg);font-size:.65rem}.perm-matrix-grid .add-role-head[data-v-f76fbbc3]{cursor:pointer}.perm-name[data-v-f76fbbc3]{font-weight:600;color:var(--color-heading);padding:.35rem .45rem;font-size:.75rem}.roles-grid[data-v-f76fbbc3]{display:flex;gap:var(--space-lg);margin-top:var(--space-lg)}.role-column[data-v-f76fbbc3]{flex:1;min-width:200px;border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-md)}.role-header[data-v-f76fbbc3]{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)}.role-name[data-v-f76fbbc3]{display:flex;align-items:center;gap:var(--space-xs);font-size:1.1rem;color:var(--color-heading)}.role-actions[data-v-f76fbbc3]{display:flex;gap:var(--space-xs)}.plus-btn[data-v-f76fbbc3]{background:var(--color-accent-soft);color:var(--color-accent);border:none;border-radius:var(--radius-sm);padding:.25rem .45rem;font-size:1.1rem;cursor:pointer}.plus-btn[data-v-f76fbbc3]:hover{background:#2563eb2e}.user-list[data-v-f76fbbc3]{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-xs)}.user-chip[data-v-f76fbbc3]{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:.45rem .6rem;display:flex;justify-content:space-between;gap:var(--space-sm);cursor:grab}.user-chip[data-v-f76fbbc3]:focus{outline:2px solid var(--color-accent);outline-offset:1px}.user-chip .meta[data-v-f76fbbc3]{font-size:.7rem;color:var(--color-text-muted)}.empty-role[data-v-f76fbbc3]{border:1px dashed var(--color-border-strong);border-radius:var(--radius-md);padding:var(--space-sm);display:flex;flex-direction:column;gap:var(--space-xs);align-items:flex-start}.empty-text[data-v-f76fbbc3]{margin:0}.delete-icon[data-v-f76fbbc3]{color:var(--color-danger)}.delete-icon[data-v-f76fbbc3]:hover{background:var(--color-danger-bg);color:var(--color-danger-text)}.muted[data-v-f76fbbc3]{color:var(--color-text-muted)}@media(max-width:720px){.roles-grid[data-v-f76fbbc3]{flex-direction:column}}.user-detail[data-v-70289426]{display:flex;flex-direction:column;gap:var(--space-lg)}.actions[data-v-70289426]{display:flex;flex-wrap:wrap;gap:var(--space-sm);align-items:center}.ancillary-actions[data-v-70289426]{margin-top:-.5rem}.reg-token-btn[data-v-70289426]{align-self:flex-start}.registration-actions[data-v-70289426]{display:flex;flex-direction:column;gap:.5rem}.icon-btn[data-v-70289426]{background:none;border:none;color:var(--color-text-muted);padding:.2rem;border-radius:var(--radius-sm);cursor:pointer;transition:background .2s ease,color .2s ease}.icon-btn[data-v-70289426]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.matrix-hint[data-v-70289426]{font-size:.8rem;color:var(--color-text-muted)}.error[data-v-70289426]{color:var(--color-danger-text)}.small[data-v-70289426]{font-size:.9rem}.muted[data-v-70289426]{color:var(--color-text-muted)}.error[data-v-4e9dba95]{color:var(--color-danger-text)}.small[data-v-4e9dba95]{font-size:.9rem}.muted[data-v-4e9dba95]{color:var(--color-text-muted)}.optional[data-v-4e9dba95]{font-weight:400;color:var(--color-text-muted);font-size:.85em}.view-admin[data-v-17f9d23b]{padding-bottom:var(--space-3xl)}.view-header[data-v-17f9d23b]{display:flex;flex-direction:column;gap:var(--space-sm)}.admin-section[data-v-17f9d23b]{margin-top:var(--space-xl)}.admin-section-body[data-v-17f9d23b],.admin-panels[data-v-17f9d23b]{display:flex;flex-direction:column;gap:var(--space-xl)}.access-denied-container[data-v-17f9d23b]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;padding:2rem}.access-denied-content[data-v-17f9d23b]{text-align:center;max-width:480px}.access-denied-content h2[data-v-17f9d23b]{margin:0 0 1rem;color:var(--color-heading);font-size:1.5rem}.access-denied-content .error-detail[data-v-17f9d23b]{margin:0 0 1.5rem;color:var(--color-text-muted)}.access-denied-content .button-row[data-v-17f9d23b]{display:flex;gap:.75rem;justify-content:center}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as de,r as k,c as I,b as v,d as m,e as n,f as x,t as C,F as A,i as Q,j as ke,C as te,B as H,D as ue,l as Se,L as we,y as W,x as re,E as $e,H as E,A as Ye,k as z,h as oe,v as ae,z as le,o as We,a as Ze,q as _e,u as Qe,w as Xe,J as et,K as tt}from"./_plugin-vue_export-helper-rKFEraYH.js";import{u as Oe,U as st,_ as nt,a as ot,R as at,N as fe,M as it,b as rt,L as lt,A as ut,B as dt,c as ct}from"./AccessDenied-aTdCvz9k.js";import{g as De}from"./helpers-DzjFIx78.js";const mt={key:0},ft=["onClick"],gt=["onClick"],yt={class:"role-names"},vt={class:"center"},pt={key:0,class:"center"},ht=["onClick"],bt={key:0,class:"permissions-section"},wt={class:"matrix-scroll"},$t=["title"],Dt=["title"],kt={class:"display-text"},St=["checked","onChange"],Ot={class:"perm-name-cell"},Rt={class:"perm-title"},Ct={class:"display-text"},At=["onClick"],Ut={class:"perm-id-info"},Pt={class:"id-text"},Et={class:"perm-domain"},Mt={class:"perm-members center"},qt={class:"perm-actions center"},Nt=["onClick"],Tt={__name:"AdminOverview",props:{info:Object,orgs:Array,permissions:Array,permissionSummary:Object,navigationDisabled:{type:Boolean,default:!1}},emits:["createOrg","openOrg","updateOrg","deleteOrg","toggleOrgPermission","openDialog","deletePermission","renamePermissionDisplay","navigateOut"],setup(o,{expose:J,emit:G}){const S=o,F=G,U=k(null),c=k(null),p=k(null),q=k(null),N=k(null),B=k(null),T=I(()=>[...S.orgs].sort((r,t)=>{const s=r.display_name.localeCompare(t.display_name);return s!==0?s:r.uuid.localeCompare(t.uuid)})),w=I(()=>[...S.permissions].sort((r,t)=>r.scope.localeCompare(t.scope))),L=I(()=>S.info?.permissions?.includes("auth:admin")??!1),$=I(()=>S.info?.permissions?.includes("auth:org:admin")??!1);function X(r){return r.roles.slice().sort((t,s)=>t.display_name.localeCompare(s.display_name)).map(t=>t.display_name).join(", ")}function Z(r,t){if(S.navigationDisabled)return;const s=H(r);if(!s)return;const D=r.target,l=D.closest("tr");if(!l)return;const d=l.closest("tbody");if(!d)return;const h=Array.from(d.querySelectorAll("tr")),R=h.indexOf(l);if(R===-1)return;if(s==="left"||s==="right"){r.preventDefault();const M=Array.from(l.querySelectorAll("a, button:not([disabled])")),K=M.indexOf(D);if(K===-1)return;s==="left"&&K>0?M[K-1].focus():s==="right"&&K<M.length-1&&M[K+1].focus();return}let V=R;if(s==="up"&&R>0)V=R-1;else if(s==="down"&&R<h.length-1)V=R+1;else if(s==="up"&&R===0){r.preventDefault(),t==="org"?te(c.value,{itemSelector:"button"}):t==="perm"&&te(N.value,{itemSelector:"button"});return}else if(s==="down"&&R===h.length-1){if(r.preventDefault(),t==="org"&&L.value&&q.value){const M=q.value.querySelector('input[type="checkbox"]');M?M.focus():te(N.value,{itemSelector:"button"})}return}if(V!==R){r.preventDefault();const K=h[V].querySelector("a, button:not([disabled])");K&&K.focus()}}function ee(r){if(S.navigationDisabled)return;const t=H(r);if(t){if(r.preventDefault(),t==="left"||t==="right")ue(c.value,r.target,t,{itemSelector:"button"});else if(t==="up")F("navigateOut","up");else if(t==="down"){const s=p.value?.querySelector("tbody tr a, tbody tr button:not([disabled])");s&&s.focus()}}}function g(r){if(S.navigationDisabled)return;const t=H(r);if(!t)return;const s=r.target;if(s.tagName!=="INPUT")return;r.preventDefault();const D=Array.from(q.value.querySelectorAll('input[type="checkbox"]')),l=D.indexOf(s);if(l===-1)return;const d=T.value.length,h=w.value.length;if(d===0||h===0)return;const R=Math.floor(l/d),V=l%d;let M=l;if(t==="left")V>0&&(M=l-1);else if(t==="right")V<d-1&&(M=l+1);else if(t==="up")if(R>0)M=l-d;else{const ie=p.value?.querySelector("tbody tr:last-child")?.querySelector("a, button:not([disabled])");ie&&ie.focus();return}else if(t==="down")if(R<h-1)M=l+d;else{te(N.value,{itemSelector:"button"});return}M!==l&&D[M]&&D[M].focus()}function u(r){if(S.navigationDisabled)return;const t=H(r);if(t){if(r.preventDefault(),t==="left"||t==="right")ue(N.value,r.target,t,{itemSelector:"button"});else if(t==="up"){const s=q.value?.querySelectorAll('input[type="checkbox"]');if(s?.length){const D=T.value.length,d=(w.value.length-1)*D;s[d]?s[d].focus():s[0].focus()}else{const l=p.value?.querySelector("tbody tr:last-child")?.querySelector("a, button:not([disabled])");l&&l.focus()}}else if(t==="down"){const s=B.value?.querySelector("tbody tr button:not([disabled])");s&&s.focus()}}}function f(){if(L.value)te(c.value,{itemSelector:"button"});else{const r=p.value?.querySelector("tbody tr a, tbody tr button:not([disabled])");r&&r.focus()}}return J({focusFirstElement:f}),(r,t)=>(m(),v(A,null,[n("div",{class:"permissions-section",ref_key:"orgSection",ref:U},[n("h2",null,C(L.value?"Organizations":"Your Organizations"),1),n("div",{class:"actions",ref_key:"orgActionsRef",ref:c,onKeydown:ee},[L.value?(m(),v("button",{key:0,onClick:t[0]||(t[0]=s=>r.$emit("createOrg"))},"+ Create Org")):x("",!0)],544),n("table",{class:"org-table",ref_key:"orgTableRef",ref:p,onKeydown:t[1]||(t[1]=s=>Z(s,"org"))},[n("thead",null,[n("tr",null,[t[4]||(t[4]=n("th",null,"Name",-1)),t[5]||(t[5]=n("th",null,"Roles",-1)),t[6]||(t[6]=n("th",null,"Members",-1)),L.value?(m(),v("th",mt,"Actions")):x("",!0)])]),n("tbody",null,[(m(!0),v(A,null,Q(T.value,s=>(m(),v("tr",{key:s.uuid},[n("td",null,[n("a",{href:"#org/{{o.uuid}}",onClick:Se(D=>r.$emit("openOrg",s),["prevent"])},C(s.display_name),9,ft),L.value||$.value?(m(),v("button",{key:0,onClick:D=>r.$emit("updateOrg",s),class:"icon-btn edit-org-btn","aria-label":"Rename organization",title:"Rename organization"},"✏️",8,gt)):x("",!0)]),n("td",yt,C(X(s)),1),n("td",vt,C(s.roles.reduce((D,l)=>D+l.users.length,0)),1),L.value?(m(),v("td",pt,[n("button",{onClick:D=>r.$emit("deleteOrg",s),class:"icon-btn delete-icon","aria-label":"Delete organization",title:"Delete organization"},"❌",8,ht)])):x("",!0)]))),128))])],544)],512),L.value?(m(),v("div",bt,[t[10]||(t[10]=n("h2",null,"Permissions",-1)),n("div",{class:"matrix-wrapper",ref_key:"permMatrixRef",ref:q,onKeydown:g},[n("div",wt,[n("div",{class:"perm-matrix-grid",style:ke({gridTemplateColumns:"minmax(180px, 1fr) "+T.value.map(()=>"2.2rem").join(" ")})},[t[7]||(t[7]=n("div",{class:"grid-head perm-head"},"Permission",-1)),(m(!0),v(A,null,Q(T.value,s=>(m(),v("div",{key:"head-"+s.uuid,class:"grid-head org-head",title:s.display_name},[n("span",null,C(s.display_name),1)],8,$t))),128)),(m(!0),v(A,null,Q(w.value,s=>(m(),v(A,{key:s.uuid},[n("div",{class:"perm-name",title:s.scope},[n("span",kt,C(s.display_name),1)],8,Dt),(m(!0),v(A,null,Q(T.value,D=>(m(),v("div",{key:D.uuid+"-"+s.uuid,class:"matrix-cell"},[n("input",{type:"checkbox",checked:D.permissions.includes(s.uuid),onChange:l=>r.$emit("toggleOrgPermission",D,s.uuid,l.target.checked)},null,40,St)]))),128))],64))),128))],4)]),t[8]||(t[8]=n("p",{class:"matrix-hint muted"},"Toggle which permissions each organization can grant to its members.",-1))],544),n("div",{class:"actions",ref_key:"permActionsRef",ref:N,onKeydown:u},[L.value?(m(),v("button",{key:0,onClick:t[2]||(t[2]=s=>r.$emit("openDialog","perm-create",{display_name:"",scope:"",domain:""}))},"+ Create Permission")):x("",!0)],544),n("table",{class:"org-table",ref_key:"permTableRef",ref:B,onKeydown:t[3]||(t[3]=s=>Z(s,"perm"))},[t[9]||(t[9]=n("thead",null,[n("tr",null,[n("th",{scope:"col"},"Permission"),n("th",{scope:"col"},"Domain"),n("th",{scope:"col",class:"center"},"Members"),n("th",{scope:"col",class:"center"},"Actions")])],-1)),n("tbody",null,[(m(!0),v(A,null,Q(w.value,s=>(m(),v("tr",{key:s.uuid},[n("td",Ot,[n("div",Rt,[n("span",Ct,C(s.display_name),1),n("button",{onClick:D=>r.$emit("renamePermissionDisplay",s),class:"icon-btn edit-display-btn","aria-label":"Edit permission",title:"Edit permission"},"✏️",8,At)]),n("div",Ut,[n("span",Pt,C(s.scope),1)])]),n("td",Et,C(s.domain||"—"),1),n("td",Mt,C(o.permissionSummary[s.uuid]?.userCount||0),1),n("td",qt,[n("button",{onClick:D=>r.$emit("deletePermission",s),class:"icon-btn delete-icon","aria-label":"Delete permission",title:"Delete permission"},"❌",8,Nt)])]))),128))])],544)])):x("",!0)],64))}},xt=de(Tt,[["__scopeId","data-v-ee865306"]]),Lt=["title"],Ft={class:"org-name"},It={class:"matrix-scroll"},Bt=["title"],Kt=["title"],zt=["checked","onChange"],Vt=["onDrop"],jt=["onKeydown"],Ht=["title"],Gt=["onClick"],Jt={class:"role-actions"},Yt=["onClick"],Wt=["onDragstart","onClick","onKeydown","title"],Zt={class:"name"},_t={class:"meta"},Qt=["onKeydown"],Xt=["onClick"],es={__name:"AdminOrgDetail",props:{selectedOrg:Object,permissions:Array,navigationDisabled:{type:Boolean,default:!1}},emits:["updateOrg","createRole","updateRole","deleteRole","createUserInRole","openUser","toggleRolePermission","onRoleDragOver","onRoleDrop","onUserDragStart","navigateOut"],setup(o,{expose:J,emit:G}){const S=o,F=G,U=k(null),c=k(null),p=k(null),q=I(()=>[...S.selectedOrg.roles].sort((g,u)=>{const f=g.display_name.toLowerCase(),r=u.display_name.toLowerCase();return f!==r?f.localeCompare(r):g.uuid.localeCompare(u.uuid)})),N=I(()=>{const g=new Set(S.selectedOrg.permissions||[]);return S.permissions.filter(u=>g.has(u.uuid))});function B(g,u,f){F("toggleRolePermission",g,u,f)}function T(g){if(S.navigationDisabled)return;const u=H(g);if(u){if(g.preventDefault(),u==="left"||u==="right")ue(U.value,g.target,u,{itemSelector:"button"});else if(u==="up")F("navigateOut","up");else if(u==="down"){const f=c.value?.querySelector('input[type="checkbox"]');f?f.focus():Z()}}}function w(g){if(S.navigationDisabled)return;const u=H(g);if(!u)return;const f=g.target;if(f.tagName!=="INPUT")return;g.preventDefault();const r=Array.from(c.value.querySelectorAll('input[type="checkbox"]')),t=r.indexOf(f);if(t===-1)return;const s=q.value.length,D=S.selectedOrg.permissions.length,l=Math.floor(t/s),d=t%s;let h=t;if(u==="left"&&d>0)h=t-1;else if(u==="right"&&d<s-1)h=t+1;else if(u==="up"&&l>0)h=t-s;else if(u==="down"&&l<D-1)h=t+s;else if(u==="up"&&l===0){const R=U.value?.querySelector("button");R&&R.focus();return}else if(u==="down"&&l===D-1){Z();return}h!==t&&r[h]&&r[h].focus()}function L(g){if(S.navigationDisabled)return;const u=H(g);if(!u)return;const f=g.target;if(!f.classList.contains("user-chip"))return;const r=f.closest(".user-list");if(!r)return;const t=Array.from(r.querySelectorAll(".user-chip")),s=t.indexOf(f);if(s!==-1){if(u==="up"&&s>0){g.preventDefault(),t[s-1].focus();return}else if(u==="down"&&s<t.length-1){g.preventDefault(),t[s+1].focus();return}if(u==="up"&&s===0){g.preventDefault();const l=r.closest(".role-column")?.querySelector(".role-header button");l&&l.focus();return}if(!(u==="down"&&s===t.length-1)&&(u==="left"||u==="right")){g.preventDefault();const D=Array.from(p.value?.querySelectorAll(".role-column")||[]),l=r.closest(".role-column"),d=D.indexOf(l);let h=u==="left"?d-1:d+1;if(h>=0&&h<D.length){const R=D[h],V=R.querySelectorAll(".user-chip"),M=Math.min(s,V.length-1);if(V[M])V[M].focus();else{const K=R.querySelector(".plus-btn");K&&K.focus()}}else if(u==="left"&&d===0){const R=c.value?.querySelector('input[type="checkbox"]:last-of-type');R&&R.focus()}}}}function $(g,u){if(S.navigationDisabled)return;const f=H(g);if(!f)return;const r=Array.from(p.value?.querySelectorAll(".role-column")||[]);if(f==="left"||f==="right"){g.preventDefault();const t=g.currentTarget.querySelectorAll("button:not([disabled])"),s=Array.from(t).indexOf(g.target);if(f==="left"&&s>0)t[s-1].focus();else if(f==="right"&&s<t.length-1)t[s+1].focus();else if(f==="left"&&s===0&&u>0){const l=r[u-1]?.querySelectorAll(".role-header button");l?.length&&l[l.length-1].focus()}else if(f==="right"&&s===t.length-1&&u<r.length-1){const l=r[u+1]?.querySelector(".role-header button");l&&l.focus()}}else if(f==="up"){g.preventDefault();const t=c.value?.querySelectorAll('input[type="checkbox"]');if(t?.length){const s=q.value.length,l=(S.selectedOrg.permissions.length-1)*s+u;t[l]?t[l].focus():t[t.length-1].focus()}else{const s=U.value?.querySelector("button");s&&s.focus()}}else if(f==="down"){g.preventDefault();const s=r[u]?.querySelector(".user-chip");s&&s.focus()}}function X(g,u){if(S.navigationDisabled)return;const f=H(g);if(!f)return;const r=Array.from(p.value?.querySelectorAll(".role-column")||[]);if(f==="up"){g.preventDefault();const s=r[u]?.querySelector(".role-header button");s&&s.focus()}else if(f==="left"&&u>0){g.preventDefault();const t=r[u-1],s=t?.querySelector(".empty-role button"),D=t?.querySelector(".user-chip:last-child");s?s.focus():D&&D.focus()}else if(f==="right"&&u<r.length-1){g.preventDefault();const t=r[u+1],s=t?.querySelector(".empty-role button"),D=t?.querySelector(".user-chip");s?s.focus():D&&D.focus()}}function Z(){const u=p.value?.querySelector(".role-column")?.querySelector(".role-header button");u&&u.focus()}function ee(){const g=U.value?.querySelector("button");g&&g.focus()}return J({focusFirstElement:ee}),(g,u)=>(m(),v(A,null,[n("h2",{class:"org-title",ref_key:"orgTitleRef",ref:U,onKeydown:T,title:o.selectedOrg.uuid},[n("span",Ft,C(o.selectedOrg.display_name),1),n("button",{onClick:u[0]||(u[0]=f=>g.$emit("updateOrg",o.selectedOrg)),class:"icon-btn","aria-label":"Rename organization",title:"Rename organization"},"✏️")],40,Lt),n("div",{class:"matrix-wrapper",ref_key:"permMatrixRef",ref:c,onKeydown:w},[n("div",It,[n("div",{class:"perm-matrix-grid",style:ke({gridTemplateColumns:"minmax(180px, 1fr) "+q.value.map(()=>"2.2rem").join(" ")+" 2.2rem"})},[u[5]||(u[5]=n("div",{class:"grid-head perm-head"},"Permission",-1)),(m(!0),v(A,null,Q(q.value,f=>(m(),v("div",{key:"head-"+f.uuid,class:"grid-head role-head",title:f.display_name},[n("span",null,C(f.display_name),1)],8,Bt))),128)),n("div",{class:"grid-head role-head add-role-head",title:"Add role",onClick:u[1]||(u[1]=f=>g.$emit("createRole",o.selectedOrg)),role:"button",tabindex:"0",onKeydown:u[2]||(u[2]=we(f=>g.$emit("createRole",o.selectedOrg),["enter"]))},"➕",32),(m(!0),v(A,null,Q(N.value,f=>(m(),v(A,{key:f.uuid},[n("div",{class:"perm-name",title:f.scope},C(f.display_name),9,Kt),(m(!0),v(A,null,Q(q.value,r=>(m(),v("div",{key:r.uuid+"-"+f.uuid,class:"matrix-cell"},[n("input",{type:"checkbox",checked:r.permissions.includes(f.uuid),onChange:t=>B(r,f.uuid,t.target.checked)},null,40,zt)]))),128)),u[4]||(u[4]=n("div",{class:"matrix-cell add-role-cell"},null,-1))],64))),128))],4)]),u[6]||(u[6]=n("p",{class:"matrix-hint muted"},"Toggle which permissions each role grants.",-1))],544),n("div",{class:"roles-grid",ref_key:"rolesGridRef",ref:p},[(m(!0),v(A,null,Q(q.value,(f,r)=>(m(),v("div",{key:f.uuid,class:"role-column",onDragover:u[3]||(u[3]=t=>g.$emit("onRoleDragOver",t)),onDrop:t=>g.$emit("onRoleDrop",t,o.selectedOrg,f)},[n("div",{class:"role-header",onKeydown:t=>$(t,r)},[n("strong",{class:"role-name",title:f.uuid},[n("span",null,C(f.display_name),1),n("button",{onClick:t=>g.$emit("updateRole",f),class:"icon-btn","aria-label":"Edit role",title:"Edit role"},"✏️",8,Gt)],8,Ht),n("div",Jt,[n("button",{onClick:t=>g.$emit("createUserInRole",o.selectedOrg,f),class:"plus-btn","aria-label":"Add user",title:"Add user"},"➕",8,Yt)])],40,jt),f.users.length>0?(m(),v("ul",{key:0,class:"user-list",onKeydown:L},[(m(!0),v(A,null,Q(f.users.slice().sort((t,s)=>{const D=t.display_name.toLowerCase(),l=s.display_name.toLowerCase();return D!==l?D.localeCompare(l):t.uuid.localeCompare(s.uuid)}),t=>(m(),v("li",{key:t.uuid,class:"user-chip",tabindex:"0",draggable:"true",onDragstart:s=>g.$emit("onUserDragStart",s,t,o.selectedOrg.uuid),onClick:s=>g.$emit("openUser",t),onKeydown:we(s=>g.$emit("openUser",t),["enter"]),title:t.uuid},[n("span",Zt,C(t.display_name),1),n("span",_t,C(t.last_seen?new Date(t.last_seen).toLocaleDateString():"—"),1)],40,Wt))),128))],32)):(m(),v("div",{key:1,class:"empty-role",onKeydown:t=>X(t,r)},[u[7]||(u[7]=n("p",{class:"empty-text muted"},"No members",-1)),n("button",{onClick:t=>g.$emit("deleteRole",f),class:"icon-btn delete-icon","aria-label":"Delete empty role",title:"Delete role"},"❌",8,Xt)],40,Qt))],40,Vt))),128))],512)],64))}},ts=de(es,[["__scopeId","data-v-f76fbbc3"]]),ss={class:"user-detail"},ns={key:0,class:"error small"},os=["disabled"],as={class:"section-block","data-section":"registered-passkeys"},is={class:"section-body"},rs={__name:"AdminUserDetail",props:{selectedUser:Object,userDetail:Object,selectedOrg:Object,loading:Boolean,showRegModal:Boolean,navigationDisabled:{type:Boolean,default:!1}},emits:["generateUserRegistrationLink","goOverview","openOrg","onUserNameSaved","closeRegModal","editUserName","refreshUserDetail","navigateOut"],setup(o,{expose:J,emit:G}){const S=o,F=G,U=Oe(),c=k({}),p=k(null),q=k(null),N=k(null),B=k(null),T=k(null),w=k(null),L=k(null),$=I(()=>S.showRegModal);function X(){U.showMessage(`📋 Link copied! Send it to ${S.selectedUser.display_name}.`),F("closeRegModal")}function Z(){F("editUserName",S.selectedUser)}async function ee(l){try{const d=await E(`/auth/api/admin/orgs/${S.selectedUser.org_uuid}/users/${S.selectedUser.uuid}/credentials/${l.credential_uuid}`,{method:"DELETE"});d.status==="ok"?F("onUserNameSaved"):console.error("Failed to delete credential",d)}catch(d){console.error("Delete credential error",d)}}async function g(l){const d=l?.id;if(d){c.value={...c.value,[d]:!0};try{const h=await E(`/auth/api/admin/orgs/${S.selectedUser.org_uuid}/users/${S.selectedUser.uuid}/sessions/${d}`,{method:"DELETE"});if(h.status==="ok"){if(h.current_session_terminated){sessionStorage.clear(),location.reload();return}F("refreshUserDetail"),U.showMessage("Session terminated","success",2500)}else U.showMessage(h.detail||"Failed to terminate session","error")}catch(h){console.error("Terminate session error",h),U.showMessage(h.message||"Failed to terminate session","error")}finally{const h={...c.value};delete h[d],c.value=h}}}function u(l){if($.value||S.navigationDisabled)return;const d=H(l);d&&(l.preventDefault(),d==="left"||d==="right"?ue(N.value,l.target,d,{itemSelector:".mini-btn"}):d==="up"?F("navigateOut","up"):d==="down"&&te(B.value,{itemSelector:"button"}))}function f(l){if($.value||S.navigationDisabled)return;const d=H(l);d&&(l.preventDefault(),d==="left"||d==="right"?ue(B.value,l.target,d,{itemSelector:"button"}):d==="up"?te(N.value,{itemSelector:".mini-btn"}):d==="down"&&T.value?.$el?.focus())}function r(l){$.value||S.navigationDisabled||(l==="up"?te(B.value,{itemSelector:"button"}):l==="down"&&$e(w.value?.$el,0,{itemSelector:".session-group"}))}function t(l){if(!($.value||S.navigationDisabled)){if(l==="up")T.value?.$el?.focus();else if(l==="down"){const d=L.value?.querySelector("button");d&&d.focus()}}}function s(l){if($.value||S.navigationDisabled)return;const d=H(l);d&&(l.preventDefault(),d==="up"&&$e(w.value?.$el,-1,{itemSelector:".session-group"}))}function D(){te(N.value,{itemSelector:".mini-btn"})}return J({focusFirstElement:D}),(l,d)=>(m(),v("div",ss,[n("div",{ref_key:"userInfoRef",ref:N,onKeydown:u},[o.userDetail&&!o.userDetail.error?(m(),W(st,{key:0,name:o.userDetail.display_name||o.selectedUser.display_name,visits:o.userDetail.visits,"created-at":o.userDetail.created_at,"last-seen":o.userDetail.last_seen,loading:o.loading,"org-display-name":o.userDetail.org.display_name,"role-name":o.userDetail.role,"update-endpoint":`/auth/api/admin/orgs/${o.selectedUser.org_uuid}/users/${o.selectedUser.uuid}/display-name`,onSaved:d[0]||(d[0]=h=>l.$emit("onUserNameSaved")),onEditName:Z},null,8,["name","visits","created-at","last-seen","loading","org-display-name","role-name","update-endpoint"])):x("",!0)],544),o.userDetail?.error?(m(),v("div",ns,C(o.userDetail.error),1)):x("",!0),o.userDetail&&!o.userDetail.error?(m(),v(A,{key:1},[n("div",{class:"registration-actions",ref_key:"regActionsRef",ref:B,onKeydown:f},[n("button",{class:"btn-secondary reg-token-btn",onClick:d[1]||(d[1]=h=>l.$emit("generateUserRegistrationLink",o.selectedUser)),disabled:o.loading},"Generate Registration Token",8,os),d[6]||(d[6]=n("p",{class:"matrix-hint muted"}," Generate a one-time registration link so this user can register or add another passkey. Copy the link from the dialog and send it to the user, or have the user scan the QR code on their device. ",-1))],544),n("section",as,[d[7]||(d[7]=n("div",{class:"section-header"},[n("h2",null,"Registered Passkeys")],-1)),n("div",is,[re(nt,{ref_key:"credentialListRef",ref:T,credentials:o.userDetail.credentials,"aaguid-info":o.userDetail.aaguid_info,"allow-delete":!0,"hovered-credential-uuid":p.value,"hovered-session-credential-uuid":q.value?.credential_uuid,"navigation-disabled":$.value,onDelete:ee,onCredentialHover:d[2]||(d[2]=h=>p.value=h),onNavigateOut:r},null,8,["credentials","aaguid-info","hovered-credential-uuid","hovered-session-credential-uuid","navigation-disabled"])])]),re(ot,{ref_key:"sessionListRef",ref:w,sessions:o.userDetail.sessions||[],"terminating-sessions":c.value,"hovered-credential-uuid":p.value,"navigation-disabled":$.value,"empty-message":"This user has no active sessions.","section-description":"View and manage the active sessions for this user.",onTerminate:g,onSessionHover:d[3]||(d[3]=h=>q.value=h),onNavigateOut:t},null,8,["sessions","terminating-sessions","hovered-credential-uuid","navigation-disabled"])],64)):x("",!0),n("div",{class:"actions ancillary-actions",ref_key:"backButtonRef",ref:L,onKeydown:s},[o.selectedOrg?(m(),v("button",{key:0,onClick:d[4]||(d[4]=h=>l.$emit("openOrg",o.selectedOrg)),class:"icon-btn",title:"Back to Org"},"↩️")):x("",!0)],544),o.showRegModal?(m(),W(at,{key:2,endpoint:`/auth/api/admin/orgs/${o.selectedUser.org_uuid}/users/${o.selectedUser.uuid}/create-link`,"user-name":o.userDetail?.display_name||o.selectedUser.display_name,onClose:d[5]||(d[5]=h=>l.$emit("closeRegModal")),onCopied:X},null,8,["endpoint","user-name"])):x("",!0)]))}},ls=de(rs,[["__scopeId","data-v-70289426"]]),us={class:"modal-title"},ds={key:0},cs={key:2},ms={class:"small muted"},fs=["placeholder","pattern"],gs={class:"small muted"},ys={key:7},vs={key:8,class:"error small"},ps={key:9,class:"modal-actions"},hs=["disabled"],bs=["disabled"],ws={__name:"AdminDialogs",props:{dialog:Object,PERMISSION_ID_PATTERN:String,settings:Object},emits:["submitDialog","closeDialog"],setup(o,{emit:J}){const G=o,S=new Set(["org-update","role-update","user-update-name"]),F=I(()=>G.settings?.rp_id||"the configured domain");return(U,c)=>o.dialog.type?(m(),W(it,{key:0,onClose:c[14]||(c[14]=p=>U.$emit("closeDialog"))},{default:Ye(()=>[n("h3",us,[o.dialog.type==="org-create"?(m(),v(A,{key:0},[z("Create Organization")],64)):o.dialog.type==="org-update"?(m(),v(A,{key:1},[z("Rename Organization")],64)):o.dialog.type==="role-create"?(m(),v(A,{key:2},[z("Create Role")],64)):o.dialog.type==="role-update"?(m(),v(A,{key:3},[z("Edit Role")],64)):o.dialog.type==="user-create"?(m(),v(A,{key:4},[z("Add User To Role")],64)):o.dialog.type==="user-update-name"?(m(),v(A,{key:5},[z("Edit User Name")],64)):o.dialog.type==="perm-create"||o.dialog.type==="perm-display"?(m(),v(A,{key:6},[z(C(o.dialog.type==="perm-create"?"Create Permission":"Edit Permission Display"),1)],64)):o.dialog.type==="confirm"?(m(),v(A,{key:7},[z("Confirm")],64)):x("",!0)]),n("form",{onSubmit:c[13]||(c[13]=Se(p=>U.$emit("submitDialog"),["prevent"])),class:"modal-form"},[o.dialog.type==="org-create"?(m(),v("label",ds,[c[15]||(c[15]=z("Name ",-1)),oe(n("input",{ref:"nameInput","onUpdate:modelValue":c[0]||(c[0]=p=>o.dialog.data.name=p),required:""},null,512),[[ae,o.dialog.data.name]])])):o.dialog.type==="org-update"?(m(),W(fe,{key:1,label:"Organization Name",modelValue:o.dialog.data.name,"onUpdate:modelValue":c[1]||(c[1]=p=>o.dialog.data.name=p),busy:o.dialog.busy,error:o.dialog.error,onCancel:c[2]||(c[2]=p=>U.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):o.dialog.type==="role-create"?(m(),v("label",cs,[c[16]||(c[16]=z("Role Name ",-1)),oe(n("input",{"onUpdate:modelValue":c[3]||(c[3]=p=>o.dialog.data.name=p),placeholder:"Role name",required:""},null,512),[[ae,o.dialog.data.name]])])):o.dialog.type==="role-update"?(m(),W(fe,{key:3,label:"Role Name",modelValue:o.dialog.data.name,"onUpdate:modelValue":c[4]||(c[4]=p=>o.dialog.data.name=p),busy:o.dialog.busy,error:o.dialog.error,onCancel:c[5]||(c[5]=p=>U.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):o.dialog.type==="user-create"?(m(),v(A,{key:4},[n("p",ms,"Role: "+C(o.dialog.data.role.display_name),1),n("label",null,[c[17]||(c[17]=z("Display Name ",-1)),oe(n("input",{"onUpdate:modelValue":c[6]||(c[6]=p=>o.dialog.data.name=p),placeholder:"User display name",required:""},null,512),[[ae,o.dialog.data.name]])])],64)):o.dialog.type==="user-update-name"?(m(),W(fe,{key:5,label:"Display Name",modelValue:o.dialog.data.name,"onUpdate:modelValue":c[7]||(c[7]=p=>o.dialog.data.name=p),busy:o.dialog.busy,error:o.dialog.error,onCancel:c[8]||(c[8]=p=>U.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):o.dialog.type==="perm-create"||o.dialog.type==="perm-display"?(m(),v(A,{key:6},[n("label",null,[c[18]||(c[18]=z("Display Name ",-1)),oe(n("input",{ref:"displayNameInput","onUpdate:modelValue":c[9]||(c[9]=p=>o.dialog.data.display_name=p),required:""},null,512),[[ae,o.dialog.data.display_name]])]),n("label",null,[c[19]||(c[19]=z("Permission Scope ",-1)),oe(n("input",{"onUpdate:modelValue":c[10]||(c[10]=p=>o.dialog.data.scope=p),placeholder:o.dialog.type==="perm-create"?"yourapp:permission":o.dialog.data.permission.scope,required:"",pattern:o.PERMISSION_ID_PATTERN,title:"Allowed: A-Za-z0-9:._~-","data-form-type":"other"},null,8,fs),[[ae,o.dialog.data.scope]])]),c[21]||(c[21]=n("p",{class:"small muted"},"E.g. yourapp:reports. Changing the scope name may break deployed applications.",-1)),n("label",null,[c[20]||(c[20]=z("Domain Scope ",-1)),oe(n("input",{"onUpdate:modelValue":c[11]||(c[11]=p=>o.dialog.data.domain=p),placeholder:"e.g. app.example.com","data-form-type":"other"},null,512),[[ae,o.dialog.data.domain]])]),n("p",gs,"If set, this permission is effective only on the specified domain, which can be "+C(F.value)+" or its subdomain.",1)],64)):o.dialog.type==="confirm"?(m(),v("p",ys,C(o.dialog.data.message),1)):x("",!0),o.dialog.error&&!le(S).has(o.dialog.type)?(m(),v("div",vs,C(o.dialog.error),1)):x("",!0),le(S).has(o.dialog.type)?x("",!0):(m(),v("div",ps,[n("button",{type:"button",class:"btn-secondary",onClick:c[12]||(c[12]=p=>U.$emit("closeDialog")),disabled:o.dialog.busy}," Cancel ",8,hs),n("button",{type:"submit",class:"btn-primary",disabled:o.dialog.busy},C(o.dialog.type==="confirm"?"OK":"Save"),9,bs)]))],32)]),_:1})):x("",!0)}},$s=de(ws,[["__scopeId","data-v-4e9dba95"]]),Ds={class:"app-shell admin-shell"},ks={class:"app-main"},Ss={key:2,class:"access-denied-container"},Os={class:"access-denied-content"},Rs={key:0,class:"error-detail"},Cs={key:1,class:"error-detail"},As={class:"button-row"},Us={key:3,class:"view-root view-root--wide view-admin"},Ps={class:"view-header"},Es={class:"section-block admin-section"},Ms={class:"section-body admin-section-body"},qs={class:"admin-panels"},Ns="^[A-Za-z0-9:._~-]+$",Ts={__name:"AdminApp",setup(o){const J=k(null),G=k(!0),S=k("Loading..."),F=k(!1),U=k(!1),c=k(null),p=k([]),q=k([]),N=k(null),B=k(null),T=k(null),w=Oe(),L=k(null);k(null),k(""),k(null),k("");const $=k({type:null,data:null,busy:!1,error:""}),X=k(null),Z=k(null),ee=k(null),g=k(null),u=k(null),f=I(()=>$.value.type!==null||ce.value),r=I(()=>J.value?.permissions?.includes("auth:admin")??!1),t=I(()=>J.value?.permissions?.includes("auth:org:admin")??!1);function s(e){if(!L.value)return;const a=e.target.closest(".org-add-menu"),i=e.target.closest(".add-org-btn");!a&&!i&&(L.value=null)}We(async()=>{document.addEventListener("click",s),window.addEventListener("hashchange",d),await w.loadSettings(),w.settings?.rp_name&&(document.title=w.settings.rp_name+" Admin"),await M()}),Ze(()=>{document.removeEventListener("click",s),window.removeEventListener("hashchange",d)});const D=I(()=>{const e={};for(const i of p.value){const y={uuid:i.uuid,display_name:i.display_name},b=new Set(i.permissions||[]);for(const O of i.permissions||[])e[O]||(e[O]={orgs:[],orgSet:new Set,userCount:0}),e[O].orgSet.has(i.uuid)||(e[O].orgs.push(y),e[O].orgSet.add(i.uuid));for(const O of i.roles)for(const P of O.permissions)b.has(P)&&(e[P]||(e[P]={orgs:[],orgSet:new Set,userCount:0}),e[P].orgSet.has(i.uuid)||(e[P].orgs.push(y),e[P].orgSet.add(i.uuid)),e[P].userCount+=O.users.length)}const a={};for(const[i,y]of Object.entries(e))a[i]={orgs:y.orgs.sort((b,O)=>b.display_name.localeCompare(O.display_name)),userCount:y.userCount};return a});function l(e){_("perm-display",{permission:e,scope:e.scope,display_name:e.display_name,domain:e.domain||""})}function d(){const e=window.location.hash||"";N.value=null,B.value=null,e.startsWith("#org/")?N.value=e.slice(5):e.startsWith("#user/")&&(B.value=e.slice(6))}async function h(){const e=await E("/auth/api/admin/orgs");p.value=e.map(a=>{const i=a.roles.map(b=>({...b,org_uuid:a.uuid,users:[]})),y=Object.fromEntries(i.map(b=>[b.display_name,b]));for(const b of a.users||[])y[b.role]&&y[b.role].users.push(b);return{...a,roles:i}})}async function R(){q.value=await E("/auth/api/admin/permissions")}async function V(){J.value=await E("/auth/api/user-info",{method:"POST"}),F.value=!0}async function M(){G.value=!0,S.value="Loading...",c.value=null;try{await Promise.all([h(),R()]),await V(),!r.value&&t.value&&p.value.length===1&&(!window.location.hash||window.location.hash==="#overview")?(N.value=p.value[0].uuid,window.location.hash=`#org/${N.value}`,w.showMessage(`Navigating to ${p.value[0].display_name} Administration`,"info",3e3)):d()}catch(e){e.name==="AuthCancelledError"?U.value=!0:c.value=e.message}finally{G.value=!1}}function K(){_("org-create",{})}function ie(e){_("org-update",{org:e,name:e.display_name})}function Ce(e){_("user-update-name",{user:e,name:e.display_name})}async function ge(e){await E(`/auth/api/admin/orgs/${e}`,{method:"DELETE"}),await Promise.all([h(),R()])}function Ae(e){if(!r.value){w.showMessage("Global admin only");return}if(e.roles.reduce((b,O)=>b+O.users.length,0)===0){ge(e.uuid).then(()=>{w.showMessage(`Organization "${e.display_name}" deleted.`,"success",2500)}).catch(b=>{w.showMessage(b.message||"Failed to delete organization","error")});return}const y=e.roles.filter(b=>b.users.length>0).map(b=>`${b.users.length} ${b.display_name}`).join(", ");_("confirm",{message:`Delete organization "${e.display_name}", including accounts of ${y})?`,action:async()=>{await ge(e.uuid)}})}function Ue(e,a){_("user-create",{org:e,role:a})}async function Pe(e,a,i){if(a.role!==i)try{await E(`/auth/api/admin/orgs/${e.uuid}/users/${a.uuid}/role`,{method:"PATCH",body:{role:i}}),await h()}catch(y){w.showMessage(y.message||"Failed to update user role")}}function Ee(e,a,i){e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",JSON.stringify({user_uuid:a.uuid,org_uuid:i}))}function Me(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}function qe(e,a,i){e.preventDefault();try{const y=JSON.parse(e.dataTransfer.getData("text/plain"));if(y.org_uuid!==a.uuid)return;const b=a.roles.flatMap(O=>O.users).find(O=>O.uuid===y.user_uuid);b&&Pe(a,b,i.display_name)}catch{}}function Ne(e){_("role-create",{org:e})}function Te(e){_("role-update",{role:e,name:e.display_name})}function xe(e){E(`/auth/api/admin/orgs/${e.org_uuid}/roles/${e.uuid}`,{method:"DELETE"}).then(()=>{w.showMessage(`Role "${e.display_name}" deleted.`,"success",2500),h()}).catch(a=>{w.showMessage(a.message||"Failed to delete role","error")})}async function Le(e,a,i){const y=[...e.permissions],b=i?[...e.permissions,a]:e.permissions.filter(O=>O!==a);e.permissions=b;try{const O=i?"POST":"DELETE";await E(`/auth/api/admin/orgs/${e.org_uuid}/roles/${e.uuid}/permissions/${a}`,{method:O}),await h()}catch(O){w.showMessage(O.message||"Failed to update role permission"),e.permissions=y}}async function ye(e){const a=new URLSearchParams({permission_id:e});await E(`/auth/api/admin/permission?${a.toString()}`,{method:"DELETE"}),await R()}function Fe(e){const a=D.value[e.uuid]?.userCount||0;let i=0;for(const O of p.value)for(const P of O.roles)P.permissions.includes(e.uuid)&&i++;if(i===0){ye(e.scope).then(()=>{w.showMessage(`Permission "${e.display_name}" deleted.`,"success",2500)}).catch(O=>{w.showMessage(O.message||"Failed to delete permission","error")});return}const y=[];i>0&&y.push(`${i} role${i!==1?"s":""}`),a>0&&y.push(`${a} user${a!==1?"s":""}`);const b=y.join(", ");_("confirm",{message:`Delete permission "${e.display_name}" (${b})?`,action:async()=>{await ye(e.scope)}})}function ve(){window.location.reload()}const ne=I(()=>p.value.find(e=>e.uuid===N.value)||null);function pe(e){window.location.hash=`#org/${e.uuid}`}function Ie(){window.location.hash="#overview"}function Be(e){window.location.hash=`#user/${e.uuid}`}const j=I(()=>{if(!B.value)return null;for(const e of p.value)for(const a of e.roles){const i=a.users.find(y=>y.uuid===B.value);if(i)return{...i,org_uuid:e.uuid,role_display_name:a.display_name}}return null}),Ke=I(()=>j.value?"Admin: User":ne.value?"Admin: Org":(w.settings?.rp_name||"Master")+" Admin"),ze=I(()=>{const e=[{label:"Auth",href:_e()},{label:"Admin",href:Qe()}];let a=null;j.value&&(a=p.value.find(y=>y.uuid===j.value.org_uuid)||null);const i=ne.value||a;return i&&e.push({label:i.display_name,href:`#org/${i.uuid}`}),j.value&&e.push({label:j.value.display_name||"User",href:`#user/${j.value.uuid}`}),e});Xe(j,async e=>{if(!e){T.value=null;return}try{T.value=await E(`/auth/api/admin/orgs/${e.org_uuid}/users/${e.uuid}`)}catch(a){T.value={error:a.message}}});const ce=k(!1);function Ve(e){ce.value=!0}async function je(e,a,i){const y=e.permissions.includes(a);if(i&&y||!i&&!y)return;const b=i?[...e.permissions,a]:e.permissions.filter(P=>P!==a),O=[...e.permissions];e.permissions=b;try{const P=new URLSearchParams({permission_id:a});await E(`/auth/api/admin/orgs/${e.uuid}/permission?${P.toString()}`,{method:i?"POST":"DELETE"}),await h()}catch(P){w.showMessage(P.message||"Failed to update organization permission","error"),e.permissions=O}}function _(e,a){const i=document.activeElement;if(X.value=i,e==="confirm"&&i){const y=i.closest("tr");if(y){const b=y.closest("tbody");if(b){const O=Array.from(b.querySelectorAll("tr")),P=O.indexOf(y);$.value.focusContext={tbody:b,index:P,total:O.length,selector:"button:not([disabled]), a"}}}}$.value={...$.value,type:e,data:a,busy:!1,error:""}}function Y(){const e=X.value,a=$.value.focusContext;$.value={type:null,data:null,busy:!1,error:""},He(e,a),X.value=null}function He(e,a){if(!e)return;if(document.body.contains(e)&&!e.disabled){e.focus();return}if(a?.tbody&&a.selector){const b=Array.from(a.tbody.querySelectorAll("tr"));if(b.length>0){const O=Math.min(a.index,b.length-1),se=b[O]?.querySelector(a.selector);if(se){se.focus();return}}}const i=document.querySelector(".admin-panels");if(!i)return;const y=i.querySelector('button:not([disabled]), a, input:not([disabled]), [tabindex="0"]');y&&y.focus()}function Ge(e){if(f.value)return;const a=H(e);a&&a==="down"&&(e.preventDefault(),ee.value?ee.value.focusFirstElement?.():g.value?g.value.focusFirstElement?.():u.value&&u.value.focusFirstElement?.())}function me(e){f.value||e==="up"&&Z.value?.focusCurrent?.()}async function he(){if(await h(),j.value)try{T.value=await E(`/auth/api/admin/orgs/${j.value.org_uuid}/users/${j.value.uuid}`)}catch(e){w.showMessage(e.message||"Failed to reload user","error")}}async function be(){await he(),w.showMessage("User renamed","success",1500)}async function Je(){if(!(!$.value.type||$.value.busy)){$.value.busy=!0,$.value.error="";try{const e=$.value.type;if(e==="org-create"){const a=$.value.data.name?.trim();if(!a)throw new Error("Name required");Y(),E("/auth/api/admin/orgs",{method:"POST",body:{display_name:a,permissions:[]}}).then(()=>{w.showMessage(`Organization "${a}" created.`,"success",2500),Promise.all([h(),R()])}).catch(i=>{w.showMessage(i.message||"Failed to create organization","error")});return}else if(e==="org-update"){const{org:a}=$.value.data,i=$.value.data.name?.trim();if(!i)throw new Error("Name required");Y(),E(`/auth/api/admin/orgs/${a.uuid}`,{method:"PATCH",body:{display_name:i}}).then(()=>{w.showMessage(`Organization renamed to "${i}".`,"success",2500),h()}).catch(y=>{w.showMessage(y.message||"Failed to update organization","error")});return}else if(e==="role-create"){const{org:a}=$.value.data,i=$.value.data.name?.trim();if(!i)throw new Error("Name required");Y(),E(`/auth/api/admin/orgs/${a.uuid}/roles`,{method:"POST",body:{display_name:i,permissions:[]}}).then(()=>{w.showMessage(`Role "${i}" created.`,"success",2500),h()}).catch(y=>{w.showMessage(y.message||"Failed to create role","error")});return}else if(e==="role-update"){const{role:a}=$.value.data,i=$.value.data.name?.trim();if(!i)throw new Error("Name required");Y(),E(`/auth/api/admin/orgs/${a.org_uuid}/roles/${a.uuid}`,{method:"PATCH",body:{display_name:i}}).then(()=>{w.showMessage(`Role renamed to "${i}".`,"success",2500),h()}).catch(y=>{w.showMessage(y.message||"Failed to update role","error")});return}else if(e==="user-create"){const{org:a,role:i}=$.value.data,y=$.value.data.name?.trim();if(!y)throw new Error("Name required");Y(),E(`/auth/api/admin/orgs/${a.uuid}/users`,{method:"POST",body:{display_name:y,role:i.display_name}}).then(()=>{w.showMessage(`User "${y}" added to ${i.display_name} role.`,"success",2500),h()}).catch(b=>{w.showMessage(b.message||"Failed to add user","error")});return}else if(e==="user-update-name"){const{user:a}=$.value.data,i=$.value.data.name?.trim();if(!i)throw new Error("Name required");Y(),E(`/auth/api/admin/orgs/${a.org_uuid}/users/${a.uuid}/display-name`,{method:"PATCH",body:{display_name:i}}).then(()=>{w.showMessage(`User renamed to "${i}".`,"success",2500),be()}).catch(y=>{w.showMessage(y.message||"Failed to update user name","error")});return}else if(e==="perm-display"){const{permission:a}=$.value.data,i=$.value.data.scope?.trim(),y=$.value.data.display_name?.trim(),b=$.value.data.domain?.trim()||"";if(!y)throw new Error("Display name required");if(!i)throw new Error("Scope required");Y();const O=a.domain||"";let P;if(i!==a.scope)P=E("/auth/api/admin/permission/rename",{method:"POST",body:{old_scope:a.scope,new_scope:i,display_name:y,domain:b}});else if(y!==a.display_name||b!==O){const se=new URLSearchParams({permission_id:a.scope,display_name:y});b&&se.set("domain",b),P=E(`/auth/api/admin/permission?${se.toString()}`,{method:"PATCH"})}else return;P.then(()=>{w.showMessage(`Permission "${y}" updated.`,"success",2500),R()}).catch(se=>{w.showMessage(se.message||"Failed to update permission","error")});return}else if(e==="perm-create"){const a=$.value.data.scope?.trim();if(!a)throw new Error("Scope required");const i=$.value.data.display_name?.trim();if(!i)throw new Error("Display name required");const y=$.value.data.domain?.trim()||"";Y(),E("/auth/api/admin/permissions",{method:"POST",body:{scope:a,display_name:i,domain:y||void 0}}).then(()=>{w.showMessage(`Permission "${i}" created.`,"success",2500),R()}).catch(b=>{w.showMessage(b.message||"Failed to create permission","error")});return}else if(e==="confirm"){const a=$.value.data.action;if(Y(),a)try{await a()}catch(i){w.showMessage(i.message||"Action failed","error")}return}Y()}catch(e){$.value.error=e.message||"Error"}finally{$.value.busy=!1}}}return(e,a)=>(m(),v("div",Ds,[re(rt),n("main",ks,[G.value?(m(),W(lt,{key:0,message:S.value},null,8,["message"])):U.value?(m(),W(ut,{key:1,onReload:ve})):c.value||F.value&&!r.value&&!t.value?(m(),v("div",Ss,[n("div",Os,[a[2]||(a[2]=n("h2",null,"⛔ Access Denied",-1)),c.value?(m(),v("p",Rs,C(c.value),1)):(m(),v("p",Cs,"You do not have admin permissions for this application.")),n("div",As,[n("button",{class:"btn-secondary",onClick:a[0]||(a[0]=(...i)=>le(De)&&le(De)(...i))},"Back"),n("button",{class:"btn-primary",onClick:ve},"Reload Page")])])])):F.value&&(r.value||t.value)?(m(),v("section",Us,[n("header",Ps,[n("h1",null,C(Ke.value),1),re(dt,{ref_key:"breadcrumbsRef",ref:Z,entries:ze.value,onKeydown:Ge},null,8,["entries"])]),n("section",Es,[n("div",Ms,[n("div",qs,[!j.value&&!ne.value&&(r.value||t.value)?(m(),W(xt,{key:0,ref_key:"adminOverviewRef",ref:ee,info:J.value,orgs:p.value,permissions:q.value,"navigation-disabled":f.value,"permission-summary":D.value,onCreateOrg:K,onOpenOrg:pe,onUpdateOrg:ie,onDeleteOrg:Ae,onToggleOrgPermission:je,onOpenDialog:_,onDeletePermission:Fe,onRenamePermissionDisplay:l,onNavigateOut:me},null,8,["info","orgs","permissions","navigation-disabled","permission-summary"])):j.value?(m(),W(ls,{key:1,ref_key:"adminUserDetailRef",ref:u,"selected-user":j.value,"user-detail":T.value,"selected-org":ne.value,loading:G.value,"show-reg-modal":ce.value,"navigation-disabled":f.value,onGenerateUserRegistrationLink:Ve,onGoOverview:Ie,onOpenOrg:pe,onOnUserNameSaved:be,onRefreshUserDetail:he,onEditUserName:Ce,onCloseRegModal:a[1]||(a[1]=i=>ce.value=!1),onNavigateOut:me},null,8,["selected-user","user-detail","selected-org","loading","show-reg-modal","navigation-disabled"])):ne.value?(m(),W(ts,{key:2,ref_key:"adminOrgDetailRef",ref:g,"selected-org":ne.value,permissions:q.value,"navigation-disabled":f.value,onUpdateOrg:ie,onCreateRole:Ne,onUpdateRole:Te,onDeleteRole:xe,onCreateUserInRole:Ue,onOpenUser:Be,onToggleRolePermission:Le,onOnRoleDragOver:Me,onNavigateOut:me,onOnRoleDrop:qe,onOnUserDragStart:Ee},null,8,["selected-org","permissions","navigation-disabled"])):x("",!0)])])])])):x("",!0)]),re($s,{dialog:$.value,"permission-id-pattern":Ns,settings:le(w).settings,onSubmitDialog:Je,onCloseDialog:Y},null,8,["dialog","settings"])]))}},xs=de(Ts,[["__scopeId","data-v-17f9d23b"]]),Re=et(xs);Re.use(ct());Re.mount("#admin-app");tt();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.pairing-entry[data-v-77a073d9]{display:flex;flex-direction:column;gap:1rem}.pairing-form[data-v-77a073d9]{display:flex;flex-direction:column;gap:.5rem}.input-row[data-v-77a073d9]{display:flex;align-items:center;gap:.5rem}.input-wrapper[data-v-77a073d9]{position:relative;display:flex;width:280px;max-width:100%}.slot-machine[data-v-77a073d9]{position:absolute;left:0;top:0;width:100%;height:100%;gap:0;box-sizing:border-box;z-index:1;pointer-events:none}.input-wrapper.focused.has-error .slot-machine[data-v-77a073d9]{background:var(--color-error-bg, rgba(239, 68, 68, .05))}.slot-reel[data-v-77a073d9]{flex:1 1 33.333%;overflow:visible}.slot-reel[data-v-77a073d9]:not(:last-child){margin-right:.5rem}.slot-word[data-v-77a073d9]{font-weight:600;letter-spacing:.05em;text-align:center;width:100%;color:var(--color-text);display:flex;align-items:center;justify-content:center;position:relative}.slot-word .typed-prefix[data-v-77a073d9]{color:var(--color-text)}.slot-word .hint-suffix[data-v-77a073d9]{color:var(--color-text-muted);opacity:.6}.cursor-overlay[data-v-77a073d9]{position:absolute;width:2px;height:1.2em;background:var(--color-text);animation:none;pointer-events:none;left:calc(50% + (var(--cursor-pos) - var(--word-len, 0) / 2) * .65em);transform:translate(-1px);opacity:0}.input-wrapper.focused .cursor-overlay[data-v-77a073d9]{opacity:1;animation:cursorBlink-77a073d9 .25s alternate infinite}.input-wrapper.focused.has-selection .cursor-overlay[data-v-77a073d9]{animation:none}.selection-overlay[data-v-77a073d9]{position:absolute;height:1.2em;background:var(--color-primary, #3b82f6);opacity:.3;pointer-events:none;left:calc(50% + (var(--sel-start) - var(--word-len, 0) / 2) * .65em);width:calc((var(--sel-end) - var(--sel-start)) * .65em)}@keyframes cursorBlink-77a073d9{0%,50%{opacity:1}80%,to{opacity:0}}.slot-reel.invalid-word .slot-word[data-v-77a073d9],.slot-reel.invalid-word .slot-word .typed-prefix[data-v-77a073d9]{color:var(--color-error, #ef4444)}.slot-reel.invalid-word .cursor-overlay[data-v-77a073d9]{background:var(--color-error, #ef4444)}.slot-reel.empty .slot-word[data-v-77a073d9]{color:var(--color-text-muted)}.pairing-input[data-v-77a073d9]{flex:1;width:100%;height:100%;border-radius:var(--radius-sm, 6px);position:relative;z-index:0}.pairing-input.hidden-input[data-v-77a073d9]{opacity:0}.pairing-input[data-v-77a073d9]:disabled{cursor:not-allowed}.pairing-input[data-v-77a073d9]::placeholder{color:transparent}.processing-status[data-v-77a073d9]{display:flex;align-items:center;gap:.25rem;font-size:.875rem;color:var(--color-text-muted)}.processing-icon[data-v-77a073d9]{font-size:.875rem}.processing-spinner-small[data-v-77a073d9]{width:12px;height:12px;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin-77a073d9 .8s linear infinite}@keyframes spin-77a073d9{to{transform:rotate(360deg)}}.device-info[data-v-77a073d9]{display:flex;flex-direction:column;gap:.5rem}.device-permit-text[data-v-77a073d9]{margin:0;font-size:.95rem;color:var(--color-text)}.device-meta[data-v-77a073d9]{margin:0;font-size:.8rem;color:var(--color-text-muted);font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace}.error-message[data-v-77a073d9]{margin:0;font-size:.875rem;color:var(--color-error, #ef4444);margin-bottom:1rem}.view-lede[data-v-
|
|
1
|
+
.pairing-entry[data-v-77a073d9]{display:flex;flex-direction:column;gap:1rem}.pairing-form[data-v-77a073d9]{display:flex;flex-direction:column;gap:.5rem}.input-row[data-v-77a073d9]{display:flex;align-items:center;gap:.5rem}.input-wrapper[data-v-77a073d9]{position:relative;display:flex;width:280px;max-width:100%}.slot-machine[data-v-77a073d9]{position:absolute;left:0;top:0;width:100%;height:100%;gap:0;box-sizing:border-box;z-index:1;pointer-events:none}.input-wrapper.focused.has-error .slot-machine[data-v-77a073d9]{background:var(--color-error-bg, rgba(239, 68, 68, .05))}.slot-reel[data-v-77a073d9]{flex:1 1 33.333%;overflow:visible}.slot-reel[data-v-77a073d9]:not(:last-child){margin-right:.5rem}.slot-word[data-v-77a073d9]{font-weight:600;letter-spacing:.05em;text-align:center;width:100%;color:var(--color-text);display:flex;align-items:center;justify-content:center;position:relative}.slot-word .typed-prefix[data-v-77a073d9]{color:var(--color-text)}.slot-word .hint-suffix[data-v-77a073d9]{color:var(--color-text-muted);opacity:.6}.cursor-overlay[data-v-77a073d9]{position:absolute;width:2px;height:1.2em;background:var(--color-text);animation:none;pointer-events:none;left:calc(50% + (var(--cursor-pos) - var(--word-len, 0) / 2) * .65em);transform:translate(-1px);opacity:0}.input-wrapper.focused .cursor-overlay[data-v-77a073d9]{opacity:1;animation:cursorBlink-77a073d9 .25s alternate infinite}.input-wrapper.focused.has-selection .cursor-overlay[data-v-77a073d9]{animation:none}.selection-overlay[data-v-77a073d9]{position:absolute;height:1.2em;background:var(--color-primary, #3b82f6);opacity:.3;pointer-events:none;left:calc(50% + (var(--sel-start) - var(--word-len, 0) / 2) * .65em);width:calc((var(--sel-end) - var(--sel-start)) * .65em)}@keyframes cursorBlink-77a073d9{0%,50%{opacity:1}80%,to{opacity:0}}.slot-reel.invalid-word .slot-word[data-v-77a073d9],.slot-reel.invalid-word .slot-word .typed-prefix[data-v-77a073d9]{color:var(--color-error, #ef4444)}.slot-reel.invalid-word .cursor-overlay[data-v-77a073d9]{background:var(--color-error, #ef4444)}.slot-reel.empty .slot-word[data-v-77a073d9]{color:var(--color-text-muted)}.pairing-input[data-v-77a073d9]{flex:1;width:100%;height:100%;border-radius:var(--radius-sm, 6px);position:relative;z-index:0}.pairing-input.hidden-input[data-v-77a073d9]{opacity:0}.pairing-input[data-v-77a073d9]:disabled{cursor:not-allowed}.pairing-input[data-v-77a073d9]::placeholder{color:transparent}.processing-status[data-v-77a073d9]{display:flex;align-items:center;gap:.25rem;font-size:.875rem;color:var(--color-text-muted)}.processing-icon[data-v-77a073d9]{font-size:.875rem}.processing-spinner-small[data-v-77a073d9]{width:12px;height:12px;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin-77a073d9 .8s linear infinite}@keyframes spin-77a073d9{to{transform:rotate(360deg)}}.device-info[data-v-77a073d9]{display:flex;flex-direction:column;gap:.5rem}.device-permit-text[data-v-77a073d9]{margin:0;font-size:.95rem;color:var(--color-text)}.device-meta[data-v-77a073d9]{margin:0;font-size:.8rem;color:var(--color-text-muted);font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace}.error-message[data-v-77a073d9]{margin:0;font-size:.875rem;color:var(--color-error, #ef4444);margin-bottom:1rem}.view-lede[data-v-4eb8b156]{margin:0;color:var(--color-text-muted);font-size:1rem}.section-header[data-v-4eb8b156]{display:flex;flex-direction:column;gap:.4rem}.empty-state[data-v-4eb8b156]{margin:0;color:var(--color-text-muted);text-align:center;padding:1rem 0}.logout-note[data-v-4eb8b156]{margin:.75rem 0 0;color:var(--color-text-muted);font-size:.875rem}.remote-auth-inline[data-v-4eb8b156]{display:flex;flex-direction:column;gap:.5rem}.remote-auth-label[data-v-4eb8b156]{display:block;margin:0;font-size:.875rem;color:var(--color-text-muted);font-weight:500}.remote-auth-description[data-v-4eb8b156]{font-size:.75rem;color:var(--color-text-muted)}.host-view[data-v-054244d9]{padding:3rem 1.5rem 4rem}.host-actions[data-v-054244d9]{display:flex;flex-direction:column;gap:.75rem}.host-actions .button-row[data-v-054244d9]{gap:.75rem;flex-wrap:wrap}.host-actions .button-row button[data-v-054244d9]{flex:1 1 0}.note[data-v-054244d9],.empty-state[data-v-054244d9]{margin:0;color:var(--color-text-muted)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as Me,r as d,w as Ze,c as L,o as Ee,n as ye,a as Be,b as w,d as v,e as a,f as V,g as $e,h as lt,F as de,i as rt,j as Le,t as A,k as F,v as it,l as Je,m as ut,s as ct,p as dt,q as ft,u as vt,x as ae,y as G,z as C,A as Ke,B as fe,C as Ae,D as we,E as We,G as ht,H as Pe,I as pt,J as gt,K as mt}from"./_plugin-vue_export-helper-rKFEraYH.js";import{u as _e,B as yt,U as Ge,_ as wt,a as bt,N as _t,M as kt,R as It,L as St,b as xt,A as Ct,c as $t}from"./AccessDenied-aTdCvz9k.js";import{i as Fe,a as ee,g as je,b as qe,c as Oe,s as Ye}from"./pow-2N9bxgAo.js";import{g as be}from"./helpers-DzjFIx78.js";const Lt={class:"pairing-entry"},At={key:0,class:"input-row"},Wt={class:"slot-word"},Pt={class:"typed-prefix"},Mt={class:"hint-suffix"},Et={key:0,class:"cursor-overlay",style:{"--cursor-pos":0,"--word-len":0}},Bt=["placeholder"],Dt={key:0,class:"processing-status"},Nt={class:"processing-icon"},Tt={key:1,class:"device-info"},Vt={class:"device-permit-text"},Ut={class:"device-meta"},Rt={key:0,class:"error-message",style:{"margin-top":"0.5rem"}},zt={class:"button-row",style:{"margin-top":"0.75rem",display:"flex",gap:"0.5rem"}},Ht=["disabled"],Kt=["disabled"],Ft={__name:"RemoteAuthPermit",props:{title:{type:String,default:"Help Another Device Sign In"},description:{type:String,default:"Enter the code shown on the device that needs to sign in."},placeholder:{type:String,default:"Enter three words"},action:{type:String,default:"login"}},emits:["completed","error","cancelled","back","register","deviceInfoVisible"],setup(le,{expose:o,emit:E}){const W=E,_=d(!1),b=d(null),N=d(null);let f=null,T=null;try{T=_e()}catch{}const k=d(null),z=d(null),u=d(""),P=d(!1),B=d(""),g=d(null),h=d("");Ze(g,e=>{W("deviceInfoVisible",!!e)});const p=d(!1),I=d(!1),c=d(0),$=d(0),U=d(0),Q=d(!1),ve=d(!1);let te=0,se=!1,Z=null,ne=null,j=null,q=null,R=null,J=null;function oe(e,s="info",t=3e3){T&&T.showMessage(e,s,t)}async function re(){try{const e=await ut();N.value=e}catch(e){console.warn("Unable to load settings",e)}}function ie(e,s){if(!e||s<0)return{word:"",start:0,end:0};let t=s,i=s;for(;t>0&&/[a-zA-Z]/.test(e[t-1]);)t--;for(;i<e.length&&/[a-zA-Z]/.test(e[i]);)i++;return{word:e.slice(t,i),start:t,end:i}}function X(e){return e.trim().split(/[.\s]+/).filter(s=>s.length>0)}function D(e){const s=X(e),t=[];for(const i of s){let r=i.toLowerCase();for(;r.length>0&&t.length<3;){let x=null;for(let y=Math.min(r.length,6);y>=3;y--){const S=r.slice(0,y);if(ee(S)){x=S;break}}if(x)t.push(x),r=r.slice(x.length);else{t.push(r);break}}if(t.length>=3)break}return t}function he(e){const s=/[.\s]$/.test(e),t=D(e);return s?t.length:Math.max(0,t.length-1)}function pe(e){if(!e)return{valid:!0,segments:[]};const s=[],t=/[.\s]$/.test(e);let i,r=/([a-zA-Z]+)|([.\s]+)/g;for(;(i=r.exec(e))!==null;)i[1]?s.push({text:i[1],isWord:!0,start:i.index}):i[2]&&s.push({text:i[2],isWord:!1,start:i.index});const x=s.filter(S=>S.isWord);let y=!0;return x.forEach((S,H)=>{const M=H===x.length-1,Y=S.text.toLowerCase();M&&!t?S.invalid=!Fe(Y):S.invalid=!ee(Y),S.invalid&&(y=!1)}),{valid:y,segments:s}}L(()=>{const{segments:e}=pe(u.value);return e.map(s=>({text:s.text,invalid:s.invalid||!1}))});function ke(e){return pe(e).valid}function Ie(e){return D(e).length>0&&D(e).every(s=>ee(s))}function ge(e){if(/[.\s]$/.test(e))return"";const t=e.match(/[a-zA-Z]+$/);return t?t[0].toLowerCase():""}function Se(e,s){if(!e||s===0)return{wordIndex:0,charIndex:0};const t=e.slice(0,s),i=/[.\s]$/.test(t),r=D(t);if(r.length===0)return{wordIndex:0,charIndex:0};if(i)return{wordIndex:Math.min(r.length,2),charIndex:0};const x=r[r.length-1],y=r.length-1;D(e);const S=x.length;return y<2&&!ve.value&&ee(x)?{wordIndex:y+1,charIndex:0}:{wordIndex:Math.min(y,2),charIndex:S}}function ue(e,s){if(!e||s===0)return{wordIndex:0,charIndex:0};const t=e.slice(0,s),i=/[.\s]$/.test(t),r=D(t);if(r.length===0)return{wordIndex:0,charIndex:0};if(i)return{wordIndex:Math.min(r.length,2),charIndex:0};const x=r[r.length-1],y=r.length-1;return{wordIndex:Math.min(y,2),charIndex:x.length}}const l=L(()=>{const e=D(u.value),s=[],t=ge(u.value),i=h.value,r=/[.\s]$/.test(u.value),x=$.value!==U.value,y=ue(u.value,Math.min($.value,U.value)),S=ue(u.value,Math.max($.value,U.value)),H=x?ue(u.value,c.value):Se(u.value,c.value);for(let M=0;M<3;M++){const Y=H.wordIndex===M;let O=-1,K=-1;if(x&&(M>y.wordIndex&&M<S.wordIndex?(O=0,K=e[M]?.length??0):M===y.wordIndex&&M===S.wordIndex?(O=y.charIndex,K=S.charIndex):M===y.wordIndex?(O=y.charIndex,K=e[M]?.length??0):M===S.wordIndex&&(O=0,K=S.charIndex)),M<e.length){const me=e[M].toLowerCase(),ze=M===e.length-1,He=ze&&!r?!Fe(me):!ee(me);if(ze&&!r&&i&&t){const at=i.length;s.push({text:"",typedPrefix:t,hintSuffix:i.slice(t.length),invalid:He,hasCursor:Y,cursorCharIndex:Y?H.charIndex:-1,wordLen:at,selectionStartChar:O,selectionEndChar:K})}else s.push({text:me,invalid:He,hasCursor:Y,cursorCharIndex:Y?H.charIndex:-1,wordLen:me.length,selectionStartChar:O,selectionEndChar:K})}else s.push({text:"",invalid:!1,hasCursor:Y,cursorCharIndex:0,wordLen:0,selectionStartChar:O,selectionEndChar:K})}return s}),n=L(()=>$.value!==U.value),m=L(()=>{const e=D(u.value);return e.length===3&&e.every(s=>ee(s))});function De(e){return D(e).join(".")}function Xe(){if(!Z||j)return;const e=Oe(Z);j=Ye(e,ne).then(s=>{q=s,j=null})}async function Ne(){if(q){const s=q;return q=null,s}if(j){await j;const s=q;return q=null,s}if(!Z)throw new Error("No PoW challenge available");const e=Oe(Z);return await Ye(e,ne)}function Te(e){e?.challenge&&(Z=e.challenge,ne=e.work,q=null,j=null,Xe())}async function xe(){if(!(f||se)){se=!0;try{const e=N.value?.auth_host,s="/auth/ws/remote-auth/permit",t=e&&location.host!==e?`//${e}${s}`:s;f=await dt(t);const i=await f.receive_json();if(i.status&&i.detail)throw new Error(i.detail);if(!i.pow?.challenge)throw new Error("Server did not send PoW challenge");Te(i.pow)}catch(e){throw console.error("WebSocket connection error:",e),f=null,e}finally{se=!1}}}function et(e){if(e.key==="Tab"||e.key===" "||e.key==="Escape"){nt(e);return}setTimeout(Ve,0)}function Ve(){const e=k.value,s=e?.selectionStart??u.value.length,t=e?.selectionEnd??s,r=(e?.selectionDirection??"none")==="backward"?s:t;ve.value=r<te,te=r,c.value=r,U.value=t,$.value=s}function tt(){c.value=k.value?.selectionStart??u.value.length;const{word:e,end:s}=ie(u.value,c.value);if(he(u.value)>=3||!e||e.length<1||c.value!==s){h.value="";return}const i=je(e.toLowerCase());i&&i!==e.toLowerCase()?h.value=i:h.value=""}function Ue(){if(!h.value)return!1;const{word:e,start:s,end:t}=ie(u.value,c.value);if(!e)return!1;const i=u.value.slice(0,s),y=D(i).length===2?"":" ",S=u.value.slice(t);u.value=i+h.value+y+S.trimStart();const H=s+h.value.length+y.length;return ye(()=>{k.value?.setSelectionRange(H,H),c.value=H}),h.value="",!0}function ce(){c.value=k.value?.selectionStart??u.value.length;const e=c.value,t=u.value.slice(0,e).match(/([a-zA-Z]+) $/);if(t){const r=t[1].toLowerCase(),x=je(r);if(x&&x!==r&&!ee(r)){const y=e-t[0].length,S=u.value.slice(0,y),H=u.value.slice(e),O=D(S).length===2?"":" ";u.value=S+x+O+H;const K=y+x.length+O.length;ye(()=>{k.value?.setSelectionRange(K,K),c.value=K})}}tt(),R&&(clearTimeout(R),R=null),g.value=null,b.value=null,I.value=!1,p.value=!ke(u.value);const i=D(u.value);if(i.length>=1&&!f&&!se&&xe(),i.length===3){if(!Ie(u.value))return;R=setTimeout(()=>{st()},150)}}async function st(){if(!(P.value||_.value||!m.value||De(u.value)===J&&g.value)){P.value=!0,B.value="pow",b.value=null,I.value=!1;try{if(await xe(),!f)throw new Error("Failed to connect");const s=await Ne(),t=qe(s),i=De(u.value);if(!m.value)return;B.value="server",f.send_json({code:i,pow:t});const r=await f.receive_json();if(Te(r.pow),typeof r.status=="number"&&r.status>=400){oe(r.detail||"Request failed","error"),I.value=!0,g.value=null,J=null;return}r.status==="found"&&r.host?(g.value={host:r.host,user_agent_pretty:r.user_agent_pretty,client_ip:r.client_ip,action:r.action||"login"},J=i,ye(()=>{z.value?.focus()})):(oe("Unexpected response from server","error"),I.value=!0,g.value=null,J=null)}catch(s){console.error("Lookup error:",s),oe(s.message||"Lookup failed","error"),I.value=!0,g.value=null,J=null,f&&(f.close(),f=null)}finally{P.value=!1,B.value=""}}}function nt(e){if(e.key==="Escape"){u.value="",ce(),e.preventDefault();return}if(e.key==="Tab"){if(h.value&&Ue()){e.preventDefault(),ce();return}u.value.trim()&&e.preventDefault();return}e.key===" "&&h.value&&Ue()&&(e.preventDefault(),ce())}async function ot(){if(!(!g.value||_.value)){_.value=!0,b.value=null;try{if(f||await xe(),!f)throw new Error("Failed to connect");const e=await Ne(),s=qe(e);f.send_json({authenticate:!0,pow:s});const t=await f.receive_json();if(typeof t.status=="number"&&t.status>=400)throw new Error(t.detail||"Authentication failed");if(!t.optionsJSON)throw new Error(t.detail||"Failed to get authentication options");const i=await ct(t);f.send_json(i);const r=await f.receive_json();if(typeof r.status=="number"&&r.status>=400)throw new Error(r.detail||"Authentication failed");if(r.status==="success")oe("Device authenticated successfully!","success",3e3),W("completed"),Ce();else throw new Error(r.detail||"Authentication failed")}catch(e){console.error("Pairing error:",e);const s=e.name==="NotAllowedError"?"Passkey authentication was cancelled":e.message||"Authentication failed";b.value=s,W("error",s)}finally{_.value=!1,f&&(f.close(),f=null)}}}async function Re(){if(f){try{f.send_json({deny:!0}),await new Promise(e=>setTimeout(e,100))}catch(e){console.error("Error sending deny message:",e)}f.close(),f=null}Ce()}function Ce(){u.value="",b.value=null,I.value=!1,g.value=null,P.value=!1,B.value="",h.value="",p.value=!1,J=null,f&&(f.close(),f=null),Z=null,ne=null,j=null,q=null}return Ee(async()=>{await re(),ye(()=>{c.value=k.value?.selectionStart??0})}),Be(()=>{R&&(clearTimeout(R),R=null),f&&(f.close(),f=null)}),o({reset:Ce,deny:Re,code:u,handleInput:ce,loading:_,error:b}),(e,s)=>(v(),w("div",Lt,[a("form",{onSubmit:Je(ot,["prevent"]),class:"pairing-form"},[g.value?g.value?(v(),w("div",Tt,[a("p",Vt,[F("Permit "+A(g.value.action==="register"?"registration":"login")+" to ",1),a("strong",null,A(g.value.host),1)]),a("p",Ut,A(g.value.user_agent_pretty),1),b.value?(v(),w("p",Rt,A(b.value),1)):V("",!0),a("div",zt,[a("button",{type:"button",class:"btn-secondary",disabled:_.value,onClick:Re,style:{flex:"1"}}," Deny ",8,Ht),a("button",{ref_key:"submitBtnRef",ref:z,type:"submit",disabled:_.value,class:"btn-primary",style:{flex:"1"}},A(_.value?"Authenticating…":"Authorize"),9,Kt)])])):V("",!0):(v(),w("div",At,[a("div",{class:$e(["input-wrapper",{"has-error":I.value,"is-complete":g.value&&!I.value,focused:Q.value,"has-selection":n.value}])},[a("div",{class:$e(["slot-machine",{"has-error":I.value,"is-complete":g.value&&!I.value}]),"aria-hidden":"true"},[(v(!0),w(de,null,rt(l.value,(t,i)=>(v(),w("div",{key:i,class:$e(["slot-reel",{"invalid-word":t.invalid,empty:!t.text&&!t.typedPrefix}])},[a("div",Wt,[t.selectionStartChar>=0&&t.selectionEndChar>t.selectionStartChar?(v(),w("span",{key:0,class:"selection-overlay",style:Le({"--sel-start":t.selectionStartChar,"--sel-end":t.selectionEndChar,"--word-len":t.wordLen})},null,4)):V("",!0),t.typedPrefix?(v(),w(de,{key:1},[a("span",Pt,A(t.typedPrefix),1),a("span",Mt,A(t.hintSuffix),1),t.hasCursor?(v(),w("span",{key:0,class:"cursor-overlay",style:Le({"--cursor-pos":t.cursorCharIndex,"--word-len":t.wordLen})},null,4)):V("",!0)],64)):t.text?(v(),w(de,{key:2},[F(A(t.text)+" ",1),t.hasCursor?(v(),w("span",{key:0,class:"cursor-overlay",style:Le({"--cursor-pos":t.cursorCharIndex,"--word-len":t.wordLen})},null,4)):V("",!0)],64)):(v(),w(de,{key:3},[t.hasCursor?(v(),w("span",Et)):V("",!0)],64))])],2))),128))],2),lt(a("input",{ref_key:"inputRef",ref:k,"onUpdate:modelValue":s[0]||(s[0]=t=>u.value=t),type:"text",placeholder:le.placeholder,autocomplete:"off",autocapitalize:"none",autocorrect:"off",spellcheck:"false",class:"pairing-input hidden-input",onInput:ce,onKeydown:et,onMouseup:Ve,onFocus:s[1]||(s[1]=t=>Q.value=!0),onBlur:s[2]||(s[2]=t=>Q.value=!1)},null,40,Bt),[[it,u.value]])],2),B.value?(v(),w("div",Dt,[a("span",Nt,A(B.value==="pow"?"🔐":"📡"),1),s[3]||(s[3]=a("span",{class:"processing-spinner-small"},null,-1))])):V("",!0)]))],32)]))}},jt=Me(Ft,[["__scopeId","data-v-77a073d9"]]),qt={class:"view-root","data-view":"profile"},Ot={class:"view-header"},Yt={class:"remote-auth-inline"},Zt={key:0,class:"remote-auth-label"},Jt={class:"section-block"},Gt={class:"section-body"},Qt={class:"section-block"},Xt=["disabled"],es=["disabled"],ts=["disabled"],ss={key:0,class:"logout-note"},ns={key:1,class:"logout-note"},os={__name:"ProfileView",setup(le){const o=_e(),E=d(null),W=d(!1),_=d(!1),b=d(""),N=d(!1),f=d(null),T=d(null),k=d(!1),z=d(null),u=d(null),P=d(null),B=d(null),g=d(null),h=d(null),p=d(null),I=d(null),c=L(()=>W.value||_.value);Ze(W,l=>{l&&(b.value=o.userInfo?.user?.user_name||"")}),Ee(()=>{E.value=setInterval(()=>{o.userInfo&&(o.userInfo={...o.userInfo})},6e4)}),Be(()=>{E.value&&clearInterval(E.value)});const $=async()=>{try{await ht.register(null,null,()=>{o.showMessage("Adding new passkey...","info")}),await o.loadUserInfo(),o.showMessage("New passkey added successfully!","success",3e3)}catch(l){console.error("Failed to add new passkey:",l),o.showMessage(l.message,"error")}},U=()=>{o.showMessage("The other device is now signed in!","success",4e3),setTimeout(()=>z.value?.reset(),3e3)},Q=l=>{l.includes("cancelled")||o.showMessage(l,"error",4e3)},ve=()=>{o.showMessage("📋 Link copied! Send it to your other device."),_.value=!1},te=l=>{Ae(l,{primarySelector:".btn-primary",itemSelector:"button"})},se=l=>{if(c.value)return;const n=fe(l);n&&n==="down"&&(l.preventDefault(),Ae(I.value,{primarySelector:".mini-btn",itemSelector:".mini-btn, .pairing-input"}))},Z=l=>{if(c.value)return;const n=fe(l);if(!n)return;l.preventDefault(),n==="left"||n==="right"?we(I.value,l.target,n,{itemSelector:".mini-btn, .pairing-input"}):n==="up"?h.value?.focusCurrent?.():n==="down"&&u.value?.$el?.focus()},ne=l=>{c.value||(l==="down"||l==="right"?te(P.value):(l==="up"||l==="left")&&Ae(I.value,{primarySelector:".mini-btn",itemSelector:".mini-btn, .pairing-input"}))},j=l=>{if(c.value)return;const n=fe(l);n&&(l.preventDefault(),n==="left"||n==="right"?we(P.value,l.target,n,{itemSelector:"button"}):n==="up"?We(u.value?.$el,0,{itemSelector:".credential-item"}):n==="down"&&We(B.value?.$el,0,{itemSelector:".session-group"}))},q=l=>{c.value||(l==="up"?te(P.value):l==="down"&&te(g.value))},R=l=>{if(c.value)return;const n=fe(l);n&&(l.preventDefault(),n==="left"||n==="right"?we(g.value,l.target,n,{itemSelector:"button"}):n==="up"&&We(B.value?.$el,-1,{itemSelector:".session-group"}))},J=async l=>{const n=l?.credential_uuid;if(n)try{await o.deleteCredential(n),o.showMessage("Passkey deleted! You should also remove it from your password manager or device.","success",3e3)}catch(m){o.showMessage(`Failed to delete passkey: ${m.message}`,"error")}},oe=L(()=>o.settings?.rp_name||"this service"),re=L(()=>o.userInfo?.sessions||[]),ie=L(()=>re.value.find(n=>n.is_current)?.host||"this host"),X=d({}),D=async l=>{const n=l?.id;if(n){X.value={...X.value,[n]:!0};try{await o.terminateSession(n)}catch(m){o.showMessage(m.message||"Failed to terminate session","error",5e3)}finally{const m={...X.value};delete m[n],X.value=m}}},he=async()=>{await o.logoutEverywhere()},pe=async()=>{await o.logout()},ke=()=>{b.value=o.userInfo?.user?.user_name||"",W.value=!0},Ie=L(()=>{const l=o.userInfo?.permissions??[];return l.includes("auth:admin")||l.includes("auth:org:admin")}),ge=L(()=>re.value.length>1),Se=L(()=>{const l=[{label:"Auth",href:ft()}];return Ie.value&&l.push({label:"Admin",href:vt()}),l}),ue=async()=>{const l=b.value.trim();if(!l){o.showMessage("Name cannot be empty","error");return}try{N.value=!0,await Pe("/auth/api/user/display-name",{method:"PATCH",body:{display_name:l}}),W.value=!1,await o.loadUserInfo(),o.showMessage("Name updated successfully!","success",3e3)}catch(n){o.showMessage(n.message||"Failed to update name","error")}finally{N.value=!1}};return(l,n)=>(v(),w("section",qt,[a("header",Ot,[n[10]||(n[10]=a("h1",null,"User Profile",-1)),ae(yt,{ref_key:"breadcrumbs",ref:h,entries:Se.value,onKeydown:se},null,8,["entries"]),n[11]||(n[11]=a("p",{class:"view-lede"},"Account dashboard for managing credentials and authenticating with other devices.",-1))]),a("section",{class:"section-block",ref_key:"userInfoSection",ref:I},[C(o).userInfo?.user?(v(),G(Ge,{key:0,ref_key:"userBasicInfo",ref:p,name:C(o).userInfo.user.user_name,visits:C(o).userInfo.user.visits||0,"created-at":C(o).userInfo.user.created_at,"last-seen":C(o).userInfo.user.last_seen,loading:C(o).isLoading,"update-endpoint":"/auth/api/user/display-name",onSaved:n[1]||(n[1]=m=>C(o).loadUserInfo()),onEditName:ke,onKeydown:Z},{default:Ke(()=>[a("div",Yt,[k.value?V("",!0):(v(),w("label",Zt,"Code words:")),ae(jt,{ref_key:"pairingEntry",ref:z,title:"",description:"",onCompleted:U,onError:Q,onDeviceInfoVisible:n[0]||(n[0]=m=>k.value=m)},null,512)]),n[12]||(n[12]=a("p",{class:"remote-auth-description"},"Provided by another device requesting remote auth.",-1))]),_:1},8,["name","visits","created-at","last-seen","loading"])):V("",!0)],512),a("section",Jt,[n[13]||(n[13]=a("div",{class:"section-header"},[a("h2",null,"Your Passkeys"),a("p",{class:"section-description"},[F("Ideally have at least two passkeys in case you lose one. More than one user can be registered on the same device, giving you a choice at login. "),a("a",{href:"https://bitwarden.com/pricing/",target:"_blank",rel:"noopener noreferrer"},"Bitwarden"),F(" can sync one passkey to all your devices. Other secure options include "),a("b",null,"local passkeys"),F(", as well as hardware keys such as "),a("a",{href:"https://www.yubico.com",target:"_blank",rel:"noopener noreferrer"},"YubiKey"),F(". Cloud sync via Google, Microsoft or iCloud is discouraged.")])],-1)),a("div",Gt,[ae(wt,{ref_key:"credentialList",ref:u,credentials:C(o).userInfo?.credentials||[],"aaguid-info":C(o).userInfo?.aaguid_info||{},loading:C(o).isLoading,"hovered-credential-uuid":f.value,"hovered-session-credential-uuid":T.value?.credential_uuid,"navigation-disabled":c.value,"allow-delete":"",onDelete:J,onCredentialHover:n[2]||(n[2]=m=>f.value=m),onNavigateOut:ne},null,8,["credentials","aaguid-info","loading","hovered-credential-uuid","hovered-session-credential-uuid","navigation-disabled"]),a("div",{class:"button-row",ref_key:"credentialButtons",ref:P},[a("button",{onClick:$,class:"btn-primary",onKeydown:j},"Register New",32),a("button",{onClick:n[3]||(n[3]=m=>_.value=!0),class:"btn-secondary",onKeydown:j},"Another Device",32)],512)])]),ae(bt,{ref_key:"sessionList",ref:B,sessions:re.value,"terminating-sessions":X.value,"hovered-credential-uuid":f.value,"navigation-disabled":c.value,onTerminate:D,onSessionHover:n[4]||(n[4]=m=>T.value=m),onNavigateOut:q,"section-description":"You are currently signed in to the following sessions. If you don't recognize something, consider deleting not only the session but the associated passkey you suspect is compromised, as only this terminates all linked sessions and prevents logging in again."},null,8,["sessions","terminating-sessions","hovered-credential-uuid","navigation-disabled"]),W.value?(v(),G(kt,{key:0,onClose:n[7]||(n[7]=m=>W.value=!1)},{default:Ke(()=>[n[14]||(n[14]=a("h3",null,"Edit Display Name",-1)),a("form",{onSubmit:Je(ue,["prevent"]),class:"modal-form"},[ae(_t,{label:"Display Name",modelValue:b.value,"onUpdate:modelValue":n[5]||(n[5]=m=>b.value=m),busy:N.value,onCancel:n[6]||(n[6]=m=>W.value=!1)},null,8,["modelValue","busy"])],32)]),_:1})):V("",!0),a("section",Qt,[a("div",{class:"button-row",ref_key:"logoutButtons",ref:g},[a("button",{type:"button",class:"btn-secondary",onClick:n[8]||(n[8]=(...m)=>C(be)&&C(be)(...m)),onKeydown:R}," Back ",32),ge.value?(v(),w(de,{key:1},[a("button",{onClick:pe,class:"btn-danger",disabled:C(o).isLoading,onKeydown:R},"Logout",40,es),a("button",{onClick:he,class:"btn-danger",disabled:C(o).isLoading,onKeydown:R},"All",40,ts)],64)):(v(),w("button",{key:0,onClick:he,class:"btn-danger",disabled:C(o).isLoading,onKeydown:R},"Logout",40,Xt))],512),ge.value?(v(),w("p",ns,[n[16]||(n[16]=a("strong",null,"Logout",-1)),F(" this session on "+A(ie.value)+", or ",1),n[17]||(n[17]=a("strong",null,"All",-1)),F(" sessions across all sites and devices for "+A(oe.value)+". You'll need to log in again with your passkey afterwards.",1)])):(v(),w("p",ss,[n[15]||(n[15]=a("strong",null,"Logout",-1)),F(" from "+A(ie.value)+".",1)]))]),_.value?(v(),G(It,{key:1,endpoint:"/auth/api/user/create-link",onClose:n[9]||(n[9]=m=>_.value=!1),onCopied:ve})):V("",!0)]))}},as=Me(os,[["__scopeId","data-v-4eb8b156"]]),ls={class:"view-root host-view","data-view":"host-profile"},rs={class:"view-header"},is={class:"view-lede"},us={class:"section-body"},cs={key:1,class:"empty-state"},ds={class:"section-block"},fs={class:"section-body host-actions"},vs=["disabled"],hs=["disabled"],ps={class:"note"},gs={__name:"HostProfileView",props:{initializing:{type:Boolean,default:!1}},setup(le){const o=_e(),E=window.location.host,W=d(null),_=d(null),b=L(()=>o.userInfo?.user||null),N=L(()=>o.userInfo?.org?.display_name||""),f=L(()=>o.userInfo?.role?.display_name||""),T=L(()=>{const h=o.settings?.rp_name;return h?`${h} account`:"Account overview"}),k=L(()=>`You're signed in to ${E}.`),z=L(()=>o.settings?.auth_host||""),u=L(()=>{const h=z.value;if(!h)return"";let p=o.settings?.ui_base_path??"/auth/";return p.startsWith("/")||(p=`/${p}`),p.endsWith("/")||(p=`${p}/`),`${window.location.protocol||"https:"}//${h}${p}`}),P=()=>{u.value&&(window.location.href=u.value)},B=async()=>{await o.logout()},g=h=>{const p=fe(h);p&&(h.preventDefault(),(p==="left"||p==="right")&&we(_.value,h.target,p,{itemSelector:"button"}))};return(h,p)=>(v(),w("section",ls,[a("header",rs,[a("h1",null,A(T.value),1),a("p",is,A(k.value),1)]),a("section",{class:"section-block",ref_key:"userInfoSection",ref:W},[a("div",us,[b.value?(v(),G(Ge,{key:0,name:b.value.user_name,visits:b.value.visits||0,"created-at":b.value.created_at,"last-seen":b.value.last_seen,"org-display-name":N.value,"role-name":f.value,"can-edit":!1},null,8,["name","visits","created-at","last-seen","org-display-name","role-name"])):(v(),w("p",cs,A(le.initializing?"Loading your account…":"No active session found."),1))])],512),a("section",ds,[a("div",fs,[a("div",{class:"button-row",ref_key:"buttonRow",ref:_,onKeydown:g},[a("button",{type:"button",class:"btn-secondary",onClick:p[0]||(p[0]=(...I)=>C(be)&&C(be)(...I))}," Back "),a("button",{type:"button",class:"btn-danger",disabled:C(o).isLoading,onClick:B},A(C(o).isLoading?"Signing out…":"Logout"),9,vs),u.value?(v(),w("button",{key:0,type:"button",class:"btn-primary",disabled:C(o).isLoading,onClick:P}," Full Profile ",8,hs)):V("",!0)],544),a("p",ps,[p[1]||(p[1]=a("strong",null,"Logout",-1)),F(" from "+A(C(E))+", or access your ",1),p[2]||(p[2]=a("strong",null,"Full Profile",-1)),F(" at "+A(z.value)+" (you may need to sign in again).",1)])])])]))}},ms=Me(gs,[["__scopeId","data-v-054244d9"]]),ys={class:"app-shell"},ws={class:"app-main"},bs={__name:"App",setup(le){const o=_e(),E=d(!0),W=d("Loading..."),_=d(!1),b=d(!1);function N(c){if(!c)return null;const $=c.trim().toLowerCase();return $?$.replace(/:80$/,"").replace(/:443$/,""):null}const f=L(()=>{const c=o.settings?.auth_host;if(!c)return!1;const $=N(window.location.host),U=N(c);return $!==U});let T=null,k=null;async function z(){try{return o.userInfo=await Pe("/auth/api/user-info",{method:"POST"}),_.value=!0,E.value=!1,p(),!0}catch{return!1}}async function u(){P();const c=await pt("login");k=document.createElement("iframe"),k.id="auth-iframe",k.title="Authentication",k.allow="publickey-credentials-get; publickey-credentials-create",k.src=c,document.body.appendChild(k),W.value="Authentication required..."}function P(){k&&(k.remove(),k=null)}function B(){window.location.reload()}function g(c){const $=c.data;if($?.type)switch($.type){case"auth-success":P(),E.value=!0,W.value="Loading user profile...",z();break;case"auth-error":$.cancelled?console.log("Authentication cancelled by user"):o.showMessage($.message||"Authentication failed","error",5e3);break;case"auth-cancelled":console.log("Authentication cancelled");break;case"auth-back":P(),E.value=!1,b.value=!0,o.showMessage("Authentication cancelled","info",3e3);break;case"auth-close-request":P();break}}async function h(){try{await Pe("/auth/api/validate",{method:"POST",credentials:"include"})}catch(c){c.status===401?(console.log("Session expired, requiring re-authentication"),_.value=!1,E.value=!0,I(),u()):console.error("Session validation error:",c)}}function p(){I(),T=setInterval(h,120*1e3)}function I(){T&&(clearInterval(T),T=null)}return Ee(async()=>{window.addEventListener("message",g),await o.loadSettings();const c=o.settings?.rp_name;if(c){const U=o.settings?.auth_host,Q=U&&N(window.location.host)!==N(U);document.title=Q?`${c} · Account summary`:c}await z()||u()}),Be(()=>{window.removeEventListener("message",g),I(),P()}),(c,$)=>(v(),w("div",ys,[ae(xt),a("main",ws,[_.value&&f.value?(v(),G(ms,{key:0,initializing:E.value},null,8,["initializing"])):_.value?(v(),G(as,{key:1})):E.value?(v(),G(St,{key:2,message:W.value},null,8,["message"])):b.value?(v(),G(Ct,{key:3,onReload:B})):V("",!0)])]))}},Qe=gt(bs);Qe.use($t());Qe.mount("#app");mt();
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Auth Profile</title>
|
|
7
|
-
<script type="module" crossorigin src="/auth/assets/auth-
|
|
7
|
+
<script type="module" crossorigin src="/auth/assets/auth-Dk3q4pNS.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-rKFEraYH.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-DzjFIx78.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-aTdCvz9k.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/auth/assets/pow-2N9bxgAo.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/auth/assets/AccessDenied-Bc249ASC.css">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/auth/assets/auth-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/auth/assets/auth-BKX7shEe.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="app"></div>
|
paskia/globals.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Generic, TypeVar
|
|
2
2
|
|
|
3
|
-
from paskia.db import DatabaseInterface
|
|
4
3
|
from paskia.sansio import Passkey
|
|
5
4
|
|
|
6
5
|
T = TypeVar("T")
|
|
@@ -38,8 +37,12 @@ async def init(
|
|
|
38
37
|
If bootstrap=True (default) the system bootstrap_if_needed() will be invoked.
|
|
39
38
|
In FastAPI lifespan we call with bootstrap=False to avoid duplicate bootstrapping
|
|
40
39
|
since the CLI performs it once before servers start.
|
|
40
|
+
|
|
41
|
+
Database configuration:
|
|
42
|
+
Set PASKIA_DB environment variable to specify the JSONL database file path.
|
|
43
|
+
Default: paskia.jsonl
|
|
41
44
|
"""
|
|
42
|
-
from . import remoteauth
|
|
45
|
+
from . import db, remoteauth
|
|
43
46
|
|
|
44
47
|
# Initialize passkey instance with provided parameters
|
|
45
48
|
passkey.instance = Passkey(
|
|
@@ -48,13 +51,8 @@ async def init(
|
|
|
48
51
|
origins=origins,
|
|
49
52
|
)
|
|
50
53
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
db.instance
|
|
54
|
-
except RuntimeError:
|
|
55
|
-
from .db import sql
|
|
56
|
-
|
|
57
|
-
await sql.init()
|
|
54
|
+
# Initialize database
|
|
55
|
+
await db.init()
|
|
58
56
|
|
|
59
57
|
# Initialize remote auth manager
|
|
60
58
|
await remoteauth.init()
|
|
@@ -68,4 +66,3 @@ async def init(
|
|
|
68
66
|
|
|
69
67
|
# Global instances
|
|
70
68
|
passkey = Manager[Passkey]("Passkey")
|
|
71
|
-
db = Manager[DatabaseInterface]("Database")
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQL to JSON migration module for Paskia.
|
|
3
|
+
|
|
4
|
+
This module contains the legacy SQL database implementation and migration tools
|
|
5
|
+
for converting from the old SQLite database to the new JSONL format.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m paskia.migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
|
|
9
|
+
|
|
10
|
+
Or via the CLI entry point (if installed):
|
|
11
|
+
paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
import base64url
|
|
19
|
+
|
|
20
|
+
from paskia.authsession import EXPIRES
|
|
21
|
+
|
|
22
|
+
from .sql import (
|
|
23
|
+
DB as SQLDB,
|
|
24
|
+
)
|
|
25
|
+
from .sql import (
|
|
26
|
+
CredentialModel,
|
|
27
|
+
ResetTokenModel,
|
|
28
|
+
SessionModel,
|
|
29
|
+
UserModel,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Re-export for convenience
|
|
33
|
+
__all__ = ["migrate_from_sql", "main", "SQLDB"]
|
|
34
|
+
|
|
35
|
+
# Default paths
|
|
36
|
+
SQL_DB_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
|
|
37
|
+
JSON_DB_DEFAULT = "paskia.jsonl"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def migrate_from_sql(
|
|
41
|
+
sql_db_path: str = SQL_DB_DEFAULT,
|
|
42
|
+
json_db_path: str = JSON_DB_DEFAULT,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Migrate data from SQL database to JSON format.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
sql_db_path: SQLAlchemy connection string for the source SQL database
|
|
48
|
+
json_db_path: Path for the destination JSONL file
|
|
49
|
+
"""
|
|
50
|
+
# Import here to avoid circular imports and to not require JSON db at import time
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
import uuid7
|
|
54
|
+
from sqlalchemy import select
|
|
55
|
+
|
|
56
|
+
from paskia.db.operations import DB as JSONDB
|
|
57
|
+
from paskia.db.structs import (
|
|
58
|
+
_CredentialData,
|
|
59
|
+
_OrgData,
|
|
60
|
+
_PermissionData,
|
|
61
|
+
_ResetTokenData,
|
|
62
|
+
_RoleData,
|
|
63
|
+
_SessionData,
|
|
64
|
+
_UserData,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Initialize source SQL database
|
|
68
|
+
sql_db = SQLDB(sql_db_path)
|
|
69
|
+
await sql_db.init_db()
|
|
70
|
+
|
|
71
|
+
# Initialize destination JSON database (fresh, don't load existing)
|
|
72
|
+
json_db = JSONDB(json_db_path)
|
|
73
|
+
# Don't call json_db.load() - we want a fresh database, not to load existing
|
|
74
|
+
|
|
75
|
+
print(f"Migrating from {sql_db_path} to {json_db_path}...")
|
|
76
|
+
|
|
77
|
+
# Build all data directly without saving (we'll save once at the end)
|
|
78
|
+
# Track old permission ID -> new scope mapping for migration
|
|
79
|
+
# Also track org-specific admin permissions to consolidate
|
|
80
|
+
old_org_admin_pattern = re.compile(r"^auth:org:([0-9a-f-]+)$", re.IGNORECASE)
|
|
81
|
+
org_admin_uuids = set() # org UUIDs that had org-specific admin permissions
|
|
82
|
+
|
|
83
|
+
# First pass: identify org-specific admin permissions
|
|
84
|
+
permissions = await sql_db.list_permissions()
|
|
85
|
+
for perm in permissions:
|
|
86
|
+
match = old_org_admin_pattern.match(perm.id)
|
|
87
|
+
if match:
|
|
88
|
+
org_admin_uuids.add(match.group(1).lower())
|
|
89
|
+
|
|
90
|
+
# Migrate permissions with UUID keys and scope field
|
|
91
|
+
# Always create exactly one common auth:org:admin permission for all org admin needs
|
|
92
|
+
org_admin_perm_uuid: UUID = uuid7.create()
|
|
93
|
+
json_db._data.permissions[org_admin_perm_uuid] = _PermissionData(
|
|
94
|
+
scope="auth:org:admin",
|
|
95
|
+
display_name="Org Admin",
|
|
96
|
+
orgs={},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Mapping from old permission ID to new permission UUID
|
|
100
|
+
perm_id_to_uuid: dict[str, UUID] = {}
|
|
101
|
+
|
|
102
|
+
for perm in permissions:
|
|
103
|
+
# Skip old org-specific admin permissions (auth:org:{uuid}) - they map to auth:org:admin
|
|
104
|
+
match = old_org_admin_pattern.match(perm.id)
|
|
105
|
+
if match:
|
|
106
|
+
perm_id_to_uuid[perm.id] = org_admin_perm_uuid
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Skip if this is already auth:org:admin - we created one above
|
|
110
|
+
if perm.id == "auth:org:admin":
|
|
111
|
+
perm_id_to_uuid[perm.id] = org_admin_perm_uuid
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Regular permission - create with UUID key
|
|
115
|
+
perm_uuid: UUID = uuid7.create()
|
|
116
|
+
json_db._data.permissions[perm_uuid] = _PermissionData(
|
|
117
|
+
scope=perm.id, # Old ID becomes the scope
|
|
118
|
+
display_name=perm.display_name,
|
|
119
|
+
orgs={},
|
|
120
|
+
)
|
|
121
|
+
perm_id_to_uuid[perm.id] = perm_uuid
|
|
122
|
+
print(
|
|
123
|
+
f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Migrate organizations
|
|
127
|
+
orgs = await sql_db.list_organizations()
|
|
128
|
+
for org in orgs:
|
|
129
|
+
org_key: UUID = org.uuid
|
|
130
|
+
json_db._data.orgs[org_key] = _OrgData(
|
|
131
|
+
display_name=org.display_name,
|
|
132
|
+
)
|
|
133
|
+
# Update permissions to allow this org to grant them (by UUID)
|
|
134
|
+
for old_perm_id in org.permissions:
|
|
135
|
+
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
|
136
|
+
if perm_uuid and perm_uuid in json_db._data.permissions:
|
|
137
|
+
json_db._data.permissions[perm_uuid].orgs[org_key] = True
|
|
138
|
+
# Ensure every org can grant auth:org:admin
|
|
139
|
+
json_db._data.permissions[org_admin_perm_uuid].orgs[org_key] = True
|
|
140
|
+
print(f" Migrated {len(orgs)} organizations")
|
|
141
|
+
|
|
142
|
+
# Migrate roles - convert old permission IDs to UUIDs
|
|
143
|
+
role_count = 0
|
|
144
|
+
for org in orgs:
|
|
145
|
+
for role in org.roles:
|
|
146
|
+
role_key: UUID = role.uuid
|
|
147
|
+
# Convert old permission IDs to UUIDs
|
|
148
|
+
new_permissions: dict[UUID, bool] = {}
|
|
149
|
+
for old_perm_id in role.permissions or []:
|
|
150
|
+
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
|
151
|
+
if perm_uuid:
|
|
152
|
+
new_permissions[perm_uuid] = True
|
|
153
|
+
json_db._data.roles[role_key] = _RoleData(
|
|
154
|
+
org=role.org_uuid,
|
|
155
|
+
display_name=role.display_name,
|
|
156
|
+
permissions=new_permissions,
|
|
157
|
+
)
|
|
158
|
+
role_count += 1
|
|
159
|
+
print(f" Migrated {role_count} roles")
|
|
160
|
+
|
|
161
|
+
# Migrate users
|
|
162
|
+
async with sql_db.session() as session:
|
|
163
|
+
result = await session.execute(select(UserModel))
|
|
164
|
+
user_models = result.scalars().all()
|
|
165
|
+
for um in user_models:
|
|
166
|
+
user = um.as_dataclass()
|
|
167
|
+
user_key: UUID = user.uuid
|
|
168
|
+
json_db._data.users[user_key] = _UserData(
|
|
169
|
+
display_name=user.display_name,
|
|
170
|
+
role=user.role_uuid,
|
|
171
|
+
created_at=user.created_at or datetime.now(timezone.utc),
|
|
172
|
+
last_seen=user.last_seen,
|
|
173
|
+
visits=user.visits,
|
|
174
|
+
)
|
|
175
|
+
print(f" Migrated {len(user_models)} users")
|
|
176
|
+
|
|
177
|
+
# Migrate credentials
|
|
178
|
+
async with sql_db.session() as session:
|
|
179
|
+
result = await session.execute(select(CredentialModel))
|
|
180
|
+
cred_models = result.scalars().all()
|
|
181
|
+
for cm in cred_models:
|
|
182
|
+
cred = cm.as_dataclass()
|
|
183
|
+
cred_key: UUID = cred.uuid
|
|
184
|
+
json_db._data.credentials[cred_key] = _CredentialData(
|
|
185
|
+
credential_id=cred.credential_id,
|
|
186
|
+
user=cred.user_uuid,
|
|
187
|
+
aaguid=cred.aaguid,
|
|
188
|
+
public_key=cred.public_key,
|
|
189
|
+
sign_count=cred.sign_count,
|
|
190
|
+
created_at=cred.created_at,
|
|
191
|
+
last_used=cred.last_used,
|
|
192
|
+
last_verified=cred.last_verified,
|
|
193
|
+
)
|
|
194
|
+
print(f" Migrated {len(cred_models)} credentials")
|
|
195
|
+
|
|
196
|
+
# Migrate sessions
|
|
197
|
+
# Old format: b"sess" + 12 bytes -> New format: base64url string (16 chars)
|
|
198
|
+
async with sql_db.session() as session:
|
|
199
|
+
result = await session.execute(select(SessionModel))
|
|
200
|
+
session_models = result.scalars().all()
|
|
201
|
+
for sm in session_models:
|
|
202
|
+
sess = sm.as_dataclass()
|
|
203
|
+
old_key: bytes = sess.key
|
|
204
|
+
# Strip b"sess" prefix and encode remaining 12 bytes as base64url
|
|
205
|
+
if old_key.startswith(b"sess"):
|
|
206
|
+
session_key = base64url.enc(old_key[4:])
|
|
207
|
+
else:
|
|
208
|
+
# Already in new format or unknown - try to use as-is
|
|
209
|
+
session_key = base64url.enc(old_key[:12])
|
|
210
|
+
json_db._data.sessions[session_key] = _SessionData(
|
|
211
|
+
user=sess.user_uuid,
|
|
212
|
+
credential=sess.credential_uuid,
|
|
213
|
+
host=sess.host,
|
|
214
|
+
ip=sess.ip,
|
|
215
|
+
user_agent=sess.user_agent,
|
|
216
|
+
expiry=sess.renewed + EXPIRES, # Convert renewed to expiry
|
|
217
|
+
)
|
|
218
|
+
print(f" Migrated {len(session_models)} sessions")
|
|
219
|
+
|
|
220
|
+
# Migrate reset tokens
|
|
221
|
+
# Old format: b"rset" + 16 bytes hash -> New format: 9 bytes (truncated hash)
|
|
222
|
+
async with sql_db.session() as session:
|
|
223
|
+
result = await session.execute(select(ResetTokenModel))
|
|
224
|
+
token_models = result.scalars().all()
|
|
225
|
+
for tm in token_models:
|
|
226
|
+
token = tm.as_dataclass()
|
|
227
|
+
old_key: bytes = token.key
|
|
228
|
+
# Strip b"rset" prefix and take first 9 bytes of hash
|
|
229
|
+
if old_key.startswith(b"rset"):
|
|
230
|
+
token_key = old_key[4:13] # 9 bytes after prefix
|
|
231
|
+
else:
|
|
232
|
+
# Already in new format or unknown - truncate to 9 bytes
|
|
233
|
+
token_key = old_key[:9]
|
|
234
|
+
json_db._data.reset_tokens[token_key] = _ResetTokenData(
|
|
235
|
+
user=token.user_uuid,
|
|
236
|
+
expiry=token.expiry,
|
|
237
|
+
token_type=token.token_type,
|
|
238
|
+
)
|
|
239
|
+
print(f" Migrated {len(token_models)} reset tokens")
|
|
240
|
+
|
|
241
|
+
# Queue and flush all changes with actor "migrate"
|
|
242
|
+
json_db._current_actor = "migrate"
|
|
243
|
+
json_db._queue_change()
|
|
244
|
+
from paskia.db.jsonl import flush_changes
|
|
245
|
+
|
|
246
|
+
await flush_changes(json_db.db_path, json_db._pending_changes)
|
|
247
|
+
|
|
248
|
+
print("Migration complete!")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main():
|
|
252
|
+
"""CLI entry point for migration."""
|
|
253
|
+
import argparse
|
|
254
|
+
|
|
255
|
+
parser = argparse.ArgumentParser(
|
|
256
|
+
description="Migrate Paskia database from SQL to JSON"
|
|
257
|
+
)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"--sql",
|
|
260
|
+
default=SQL_DB_DEFAULT,
|
|
261
|
+
help=f"Source SQL database connection string (default: {SQL_DB_DEFAULT})",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--json",
|
|
265
|
+
default=JSON_DB_DEFAULT,
|
|
266
|
+
help=f"Destination JSONL file path (default: {JSON_DB_DEFAULT})",
|
|
267
|
+
)
|
|
268
|
+
args = parser.parse_args()
|
|
269
|
+
|
|
270
|
+
asyncio.run(migrate_from_sql(args.sql, args.json))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
main()
|