From 2b7481cd58bb2ca59613f52730aaa60ce499e5de Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 16:42:27 +0000 Subject: [PATCH 01/16] Delete frontend build files accidentally committed to repo when renaming the project. --- paskia/frontend-build/auth/admin/index.html | 18 ------------------ .../auth/assets/AccessDenied-TAST_piX.css | 1 - .../auth/assets/AccessDenied-guOGfNm-.js | 8 -------- .../auth/assets/RestrictedAuth-BIGLs28V.js | 1 - .../auth/assets/RestrictedAuth-CMHKrNJh.css | 1 - .../_plugin-vue_export-helper-Bx2cFCEC.css | 1 - .../_plugin-vue_export-helper-R4vr2A9I.js | 2 -- .../auth/assets/admin-D8zxJOk4.js | 1 - .../auth/assets/admin-DIOoLLHy.css | 1 - .../auth/assets/auth-CBojJKUK.css | 1 - .../auth/assets/auth-a0yJ_sei.js | 1 - .../auth/assets/forward-BHNzlQhM.js | 1 - .../auth/assets/helpers-CU0-cyzg.js | 1 - .../auth/assets/reset-DXzuKgh6.css | 1 - .../auth/assets/reset-YnZxhnI5.js | 1 - .../auth/assets/restricted-DVCvYFGN.js | 1 - paskia/frontend-build/auth/index.html | 18 ------------------ .../frontend-build/auth/restricted/index.html | 9 --------- paskia/frontend-build/int/forward/index.html | 17 ----------------- paskia/frontend-build/int/reset/index.html | 15 --------------- 20 files changed, 100 deletions(-) delete mode 100644 paskia/frontend-build/auth/admin/index.html delete mode 100644 paskia/frontend-build/auth/assets/AccessDenied-TAST_piX.css delete mode 100644 paskia/frontend-build/auth/assets/AccessDenied-guOGfNm-.js delete mode 100644 paskia/frontend-build/auth/assets/RestrictedAuth-BIGLs28V.js delete mode 100644 paskia/frontend-build/auth/assets/RestrictedAuth-CMHKrNJh.css delete mode 100644 paskia/frontend-build/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css delete mode 100644 paskia/frontend-build/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js delete mode 100644 paskia/frontend-build/auth/assets/admin-D8zxJOk4.js delete mode 100644 paskia/frontend-build/auth/assets/admin-DIOoLLHy.css delete mode 100644 paskia/frontend-build/auth/assets/auth-CBojJKUK.css delete mode 100644 paskia/frontend-build/auth/assets/auth-a0yJ_sei.js delete mode 100644 paskia/frontend-build/auth/assets/forward-BHNzlQhM.js delete mode 100644 paskia/frontend-build/auth/assets/helpers-CU0-cyzg.js delete mode 100644 paskia/frontend-build/auth/assets/reset-DXzuKgh6.css delete mode 100644 paskia/frontend-build/auth/assets/reset-YnZxhnI5.js delete mode 100644 paskia/frontend-build/auth/assets/restricted-DVCvYFGN.js delete mode 100644 paskia/frontend-build/auth/index.html delete mode 100644 paskia/frontend-build/auth/restricted/index.html delete mode 100644 paskia/frontend-build/int/forward/index.html delete mode 100644 paskia/frontend-build/int/reset/index.html diff --git a/paskia/frontend-build/auth/admin/index.html b/paskia/frontend-build/auth/admin/index.html deleted file mode 100644 index 5b7fd4d..0000000 --- a/paskia/frontend-build/auth/admin/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Admin - - - - - - - - - -
- - diff --git a/paskia/frontend-build/auth/assets/AccessDenied-TAST_piX.css b/paskia/frontend-build/auth/assets/AccessDenied-TAST_piX.css deleted file mode 100644 index 47605f0..0000000 --- a/paskia/frontend-build/auth/assets/AccessDenied-TAST_piX.css +++ /dev/null @@ -1 +0,0 @@ -.breadcrumbs[data-v-2924b990]{margin:.25rem 0 .5rem;line-height:1.2;color:var(--color-text-muted)}.breadcrumbs ol[data-v-2924b990]{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;align-items:center;gap:.25rem}.breadcrumbs li[data-v-2924b990]{display:inline-flex;align-items:center;gap:.25rem;font-size:.9rem}.breadcrumbs a[data-v-2924b990]{text-decoration:none;color:var(--color-link);padding:0 .25rem;border-radius:4px;transition:color .2s ease,background .2s ease}.breadcrumbs a[data-v-2924b990]:hover,.breadcrumbs a[data-v-2924b990]:focus-visible{text-decoration:underline;color:var(--color-link-hover);outline:none}.breadcrumbs .sep[data-v-2924b990]{color:var(--color-text-muted);margin:0}.user-info[data-v-2994268a]{display:grid;grid-template-columns:auto 1fr;gap:10px}.user-info h3[data-v-2994268a]{grid-column:span 2}.org-role-sub[data-v-2994268a]{grid-column:span 2;display:flex;flex-direction:column;margin:-.15rem 0 .25rem}.org-line[data-v-2994268a]{font-size:.7rem;font-weight:600;line-height:1.1;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.05em}.role-line[data-v-2994268a]{font-size:.65rem;color:var(--color-text-muted);line-height:1.1}.user-info span[data-v-2994268a]{text-align:left}.user-name-heading[data-v-2994268a]{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin:0 0 .25rem}.user-name-row[data-v-2994268a]{display:inline-flex;align-items:center;gap:.35rem;max-width:100%}.user-name-row.editing[data-v-2994268a]{flex:1 1 auto}.icon[data-v-2994268a]{flex:0 0 auto}.display-name[data-v-2994268a]{font-weight:600;font-size:1.05em;line-height:1.2;max-width:14ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.name-input[data-v-2994268a]{width:auto;flex:1 1 140px;min-width:120px;padding:6px 8px;font-size:.9em;border:1px solid var(--color-border-strong);border-radius:6px;background:var(--color-surface);color:var(--color-text)}.user-name-heading .name-input[data-v-2994268a]{width:auto}.name-input[data-v-2994268a]:focus{outline:none;border-color:var(--color-accent);box-shadow:var(--focus-ring)}.mini-btn[data-v-2994268a]{width:auto;padding:4px 6px;margin:0;font-size:.75em;line-height:1;background:var(--color-surface-muted);border:1px solid var(--color-border-strong);border-radius:6px;cursor:pointer;transition:background .2s,transform .15s,color .2s ease;color:var(--color-text)}.mini-btn[data-v-2994268a]:hover:not(:disabled){background:var(--color-accent-soft);color:var(--color-accent)}.mini-btn[data-v-2994268a]:active:not(:disabled){transform:translateY(1px)}.mini-btn[data-v-2994268a]:disabled{opacity:.5;cursor:not-allowed}@media(max-width:480px){.user-name-heading[data-v-2994268a]{flex-direction:column;align-items:flex-start}.user-name-row.editing[data-v-2994268a]{width:100%}.display-name[data-v-2994268a]{max-width:100%}}.modal-overlay[data-v-d5e0cc32]{position:fixed;inset:0;background:transparent;backdrop-filter:blur(.1rem) brightness(.7);-webkit-backdrop-filter:blur(.1rem) brightness(.7);display:flex;align-items:center;justify-content:center;z-index:1000}.modal[data-v-d5e0cc32]{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-xl);padding:calc(var(--space-lg) - var(--space-xs));max-width:500px;width:min(500px,90vw);max-height:90vh;overflow-y:auto}.modal[data-v-d5e0cc32] .modal-title,.modal[data-v-d5e0cc32] h3{margin:0 0 var(--space-md);font-size:1.25rem;font-weight:600;color:var(--color-heading)}.modal[data-v-d5e0cc32] form,.modal[data-v-d5e0cc32] .modal-form{display:flex;flex-direction:column;gap:var(--space-md)}.modal[data-v-d5e0cc32] .modal-form label{display:flex;flex-direction:column;gap:var(--space-xs);font-weight:500}.modal[data-v-d5e0cc32] .modal-form input,.modal[data-v-d5e0cc32] .modal-form textarea{padding:var(--space-md);border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text);font-size:1rem;line-height:1.4;min-height:2.5rem}.modal[data-v-d5e0cc32] .modal-form input:focus,.modal[data-v-d5e0cc32] .modal-form textarea:focus{outline:none;border-color:var(--color-accent);box-shadow:0 0 0 2px #c7d2fe}.modal[data-v-d5e0cc32] .modal-actions{display:flex;justify-content:flex-end;gap:var(--space-sm);margin-top:var(--space-md);margin-bottom:var(--space-xs)}.name-edit-form[data-v-944620d3]{display:flex;flex-direction:column;gap:var(--space-md)}.error[data-v-944620d3]{color:var(--color-danger-text)}.small[data-v-944620d3]{font-size:.9rem}.session-meta-info{grid-column:span 2}[data-component=session-list-section] .session-list{display:flex;flex-direction:column;gap:1.5em}.session-group{display:flex;flex-direction:column;gap:.5em}.session-group-host{font-size:1em;font-weight:600;margin:0}.session-group-host a{color:inherit;text-decoration:none}.session-group-host a:hover{text-decoration:underline}.session-group-host.is-current-site{color:var(--color-accent)}.session-group-sessions{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-width),1fr));gap:.5em;align-items:start}.session-group-sessions .session-item{width:auto;height:auto;padding:.75rem;gap:.5rem}.session-group-sessions .session-item .item-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-group-sessions .session-item .item-details{margin-left:0}.session-group-sessions .session-item .session-dates{grid-template-columns:auto 1fr}.icon-btn[data-v-5d6bcbcb]{background:none;border:none;cursor:pointer;font-size:1rem;opacity:.6}.icon-btn[data-v-5d6bcbcb]:hover{opacity:1}.qr-link[data-v-5d6bcbcb]{text-decoration:none;color:inherit}.reg-header-row[data-v-5d6bcbcb]{display:flex;justify-content:space-between;align-items:center;gap:.75rem;margin-bottom:.75rem}.reg-title[data-v-5d6bcbcb]{margin:0;font-size:1.25rem;font-weight:600}.device-dialog[data-v-5d6bcbcb]{background:var(--color-surface);padding:1.25rem 1.25rem 1rem;border-radius:var(--radius-md);max-width:480px;width:100%;box-shadow:0 6px 28px #00000040}.qr-container[data-v-5d6bcbcb]{display:flex;flex-direction:column;align-items:center;gap:.5rem}.qr-code[data-v-5d6bcbcb]{display:block}.reg-help[data-v-5d6bcbcb]{margin-top:.5rem;margin-bottom:.75rem;font-size:.85rem;line-height:1.25rem;text-align:center}.reg-actions[data-v-5d6bcbcb]{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.registration-inline-block .qr-container[data-v-5d6bcbcb]{align-items:flex-start}.registration-inline-block .reg-help[data-v-5d6bcbcb]{text-align:left}.loading-container[data-v-130f5abf]{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;gap:1rem}.loading-spinner[data-v-130f5abf]{width:40px;height:40px;border:4px solid var(--color-border);border-top:4px solid var(--color-primary);border-radius:50%;animation:spin-130f5abf 1s linear infinite}@keyframes spin-130f5abf{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-container p[data-v-130f5abf]{color:var(--color-text-muted);margin:0}.message-container[data-v-744305d5]{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;padding:2rem}.message-content[data-v-744305d5]{text-align:center;max-width:480px}.message-content h2[data-v-744305d5]{margin:0 0 1.5rem;color:var(--color-heading)}.message-content .button-row[data-v-744305d5]{display:flex;gap:.75rem;justify-content:center} diff --git a/paskia/frontend-build/auth/assets/AccessDenied-guOGfNm-.js b/paskia/frontend-build/auth/assets/AccessDenied-guOGfNm-.js deleted file mode 100644 index 18a6adb..0000000 --- a/paskia/frontend-build/auth/assets/AccessDenied-guOGfNm-.js +++ /dev/null @@ -1,8 +0,0 @@ -import{D as Bt,r as J,E as kt,G as Ht,H as ae,I as It,J as jt,K as zt,L as $t,B as ue,M as Kt,c as z,w as Nt,N as Jt,O as Yt,q as Y,C as Gt,P as Qt,Q as xt,d as N,i as U,u as F,e as R,f as h,t as P,n as re,_ as Q,F as ne,x as le,R as Wt,S as Rt,o as At,l as ce,z as Zt,T as Xt,k as ye,U as en,V as tn}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{f as G,g as Ge}from"./helpers-CU0-cyzg.js";let Tt;const de=e=>Tt=e,Mt=Symbol();function He(e){return e&&typeof e=="object"&&Object.prototype.toString.call(e)==="[object Object]"&&typeof e.toJSON!="function"}var se;(function(e){e.direct="direct",e.patchObject="patch object",e.patchFunction="patch function"})(se||(se={}));function Sr(){const e=Bt(!0),r=e.run(()=>J({}));let o=[],t=[];const n=kt({install(s){de(n),n._a=s,s.provide(Mt,n),s.config.globalProperties.$pinia=n,t.forEach(i=>o.push(i)),t=[]},use(s){return this._a?o.push(s):t.push(s),this},_p:o,_a:null,_e:e,_s:new Map,state:r});return n}const Pt=()=>{};function Qe(e,r,o,t=Pt){e.add(r);const n=()=>{e.delete(r)&&t()};return!o&&zt()&&$t(n),n}function X(e,...r){e.forEach(o=>{o(...r)})}const nn=e=>e(),xe=Symbol(),we=Symbol();function je(e,r){e instanceof Map&&r instanceof Map?r.forEach((o,t)=>e.set(t,o)):e instanceof Set&&r instanceof Set&&r.forEach(e.add,e);for(const o in r){if(!r.hasOwnProperty(o))continue;const t=r[o],n=e[o];He(n)&&He(t)&&e.hasOwnProperty(o)&&!ae(t)&&!It(t)?e[o]=je(n,t):e[o]=t}return e}const sn=Symbol();function rn(e){return!He(e)||!Object.prototype.hasOwnProperty.call(e,sn)}const{assign:K}=Object;function on(e){return!!(ae(e)&&e.effect)}function an(e,r,o,t){const{state:n,actions:s,getters:i}=r,u=o.state.value[e];let a;function c(){u||(o.state.value[e]=n?n():{});const f=Kt(o.state.value[e]);return K(f,s,Object.keys(i||{}).reduce((y,w)=>(y[w]=kt(z(()=>{de(o);const d=o._s.get(e);return i[w].call(d,d)})),y),{}))}return a=Lt(e,c,r,o,t,!0),a}function Lt(e,r,o={},t,n,s){let i;const u=K({actions:{}},o),a={deep:!0};let c,f,y=new Set,w=new Set,d;const l=t.state.value[e];!s&&!l&&(t.state.value[e]={}),J({});let _;function L(m){let g;c=f=!1,typeof m=="function"?(m(t.state.value[e]),g={type:se.patchFunction,storeId:e,events:d}):(je(t.state.value[e],m),g={type:se.patchObject,payload:m,storeId:e,events:d});const v=_=Symbol();ue().then(()=>{_===v&&(c=!0)}),f=!0,X(y,g,t.state.value[e])}const I=s?function(){const{state:g}=o,v=g?g():{};this.$patch(b=>{K(b,v)})}:Pt;function p(){i.stop(),y.clear(),w.clear(),t._s.delete(e)}const C=(m,g="")=>{if(xe in m)return m[we]=g,m;const v=function(){de(t);const b=Array.from(arguments),B=new Set,k=new Set;function T(D){B.add(D)}function V(D){k.add(D)}X(w,{args:b,name:v[we],store:M,after:T,onError:V});let q;try{q=m.apply(this&&this.$id===e?this:M,b)}catch(D){throw X(k,D),D}return q instanceof Promise?q.then(D=>(X(B,D),D)).catch(D=>(X(k,D),Promise.reject(D))):(X(B,q),q)};return v[xe]=!0,v[we]=g,v},A={_p:t,$id:e,$onAction:Qe.bind(null,w),$patch:L,$reset:I,$subscribe(m,g={}){const v=Qe(y,m,g.detached,()=>b()),b=i.run(()=>Nt(()=>t.state.value[e],B=>{(g.flush==="sync"?f:c)&&m({storeId:e,type:se.direct,events:d},B)},K({},a,g)));return v},$dispose:p},M=Ht(A);t._s.set(e,M);const E=(t._a&&t._a.runWithContext||nn)(()=>t._e.run(()=>(i=Bt()).run(()=>r({action:C}))));for(const m in E){const g=E[m];if(ae(g)&&!on(g)||It(g))s||(l&&rn(g)&&(ae(g)?g.value=l[m]:je(g,l[m])),t.state.value[e][m]=g);else if(typeof g=="function"){const v=C(g,m);E[m]=v,u.actions[m]=g}}return K(M,E),K(jt(M),E),Object.defineProperty(M,"$state",{get:()=>t.state.value[e],set:m=>{L(g=>{K(g,m)})}}),t._p.forEach(m=>{K(M,i.run(()=>m({store:M,app:t._a,pinia:t,options:u})))}),l&&s&&o.hydrate&&o.hydrate(M.$state,l),c=!0,f=!0,M}function un(e,r,o){let t;const n=typeof r=="function";t=n?o:r;function s(i,u){const a=Yt();return i=i||(a?Jt(Mt,null):null),i&&de(i),i=Tt,i._s.has(e)||(n?Lt(e,r,t,i):an(e,t,i)),i._s.get(e)}return s.$id=e,s}const ze=un("auth",{state:()=>({userInfo:null,isLoading:!1,settings:null,currentView:"login",status:{message:"",type:"info",show:!1}}),getters:{},actions:{setLoading(e){this.isLoading=!!e},showMessage(e,r="info",o=3e3){this.status={message:e,type:r,show:!0},o>0&&setTimeout(()=>{this.status.show=!1},o)},async setSessionCookie(e){if(!e?.session_token)throw console.error("setSessionCookie called with missing session_token:",e),new Error("Authentication response missing session_token");return await Y("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})},async register(){this.isLoading=!0;try{const e=await xt();return await this.setSessionCookie(e),await this.loadUserInfo(),this.selectView(),e}finally{this.isLoading=!1}},async authenticate(){this.isLoading=!0;try{const e=await Qt();return await this.setSessionCookie(e),await this.loadUserInfo(),this.selectView(),e}finally{this.isLoading=!1}},selectView(){this.userInfo?this.currentView="profile":this.currentView="login"},async loadSettings(){this.settings=await Gt()},async loadUserInfo(){try{this.userInfo=await Y("/auth/api/user-info",{method:"POST"}),console.log("User info loaded:",this.userInfo)}catch(e){throw e.status===401||e.status===403?console.log("Authentication required:",e.message):this.showMessage(e.message||"Failed to load user info","error",5e3),e}},async deleteCredential(e){await Y(`/auth/api/user/credential/${e}`,{method:"DELETE"}),await this.loadUserInfo()},async terminateSession(e){try{if((await Y(`/auth/api/user/session/${e}`,{method:"DELETE"}))?.current_session_terminated){sessionStorage.clear(),location.reload();return}await this.loadUserInfo(),this.showMessage("Session terminated","success",2500)}catch(r){throw console.error("Terminate session error:",r),r}},async logout(){try{await Y("/auth/api/logout",{method:"POST"}),sessionStorage.clear(),location.reload()}catch(e){console.error("Logout error:",e),e.status!==401&&e.status!==403&&this.showMessage(e.message,"error")}},async logoutEverywhere(){try{await Y("/auth/api/user/logout-all",{method:"POST"}),sessionStorage.clear(),location.reload()}catch(e){console.error("Logout-all error:",e),e.status!==401&&e.status!==403&&this.showMessage(e.message,"error")}}}}),ln={key:0,class:"global-status",style:{display:"block"}},Er={__name:"StatusMessage",setup(e){const r=ze();return(o,t)=>F(r).status.show?(R(),N("div",ln,[h("div",{class:re(["status",F(r).status.type])},P(F(r).status.message),3)])):U("",!0)}},cn={key:0,class:"breadcrumbs","aria-label":"Breadcrumb"},dn=["href"],fn={key:0,class:"sep"},hn={__name:"Breadcrumbs",props:{entries:{type:Array,default:()=>[]},showHome:{type:Boolean,default:!0},homeHref:{type:String,default:"/"}},setup(e){const r=e,o=z(()=>[...r.showHome?[{label:"🏠",href:r.homeHref}]:[],...r.entries]);return(t,n)=>o.value.length?(R(),N("nav",cn,[h("ol",null,[(R(!0),N(ne,null,le(o.value,(s,i)=>(R(),N("li",{key:i},[h("a",{href:s.href},P(s.label),9,dn),i[]},aaguidInfo:{type:Object,default:()=>({})},loading:{type:Boolean,default:!1},allowDelete:{type:Boolean,default:!1},hoveredCredentialUuid:{type:String,default:null},hoveredSessionCredentialUuid:{type:String,default:null}},emits:["delete","credentialHover"],setup(e,{emit:r}){const o=e,t=r,n=a=>{t("credentialHover",a)},s=a=>{a.currentTarget.contains(a.relatedTarget)||t("credentialHover",null)},i=a=>{const c=o.aaguidInfo?.[a.aaguid];return c?c.name:"Unknown Authenticator"},u=a=>{const c=o.aaguidInfo?.[a.aaguid];if(!c)return null;const y=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"icon_dark":"icon_light";return c[y]||null};return(a,c)=>(R(),N("div",gn,[e.loading?(R(),N("div",mn,[...c[1]||(c[1]=[h("p",null,"Loading credentials...",-1)])])):e.credentials?.length?(R(!0),N(ne,{key:2},le(e.credentials,f=>(R(),N("div",{key:f.credential_uuid,class:re(["credential-item",{"current-session":f.is_current_session&&!e.hoveredCredentialUuid&&!e.hoveredSessionCredentialUuid,"is-hovered":e.hoveredCredentialUuid===f.credential_uuid,"is-linked-session":e.hoveredSessionCredentialUuid===f.credential_uuid}]),tabindex:"0",onFocusin:y=>n(f.credential_uuid),onFocusout:c[0]||(c[0]=y=>s(y))},[h("div",pn,[h("div",vn,[u(f)?(R(),N("img",{key:0,src:u(f),alt:i(f),class:"auth-icon",width:"32",height:"32"},null,8,Cn)):(R(),N("span",bn,"🔑"))]),h("h4",_n,P(i(f)),1),h("div",Sn,[f.is_current_session&&!e.hoveredCredentialUuid&&!e.hoveredSessionCredentialUuid?(R(),N("span",En,"Current")):e.hoveredCredentialUuid===f.credential_uuid?(R(),N("span",Bn,"Selected")):e.hoveredSessionCredentialUuid===f.credential_uuid?(R(),N("span",kn,"Linked")):U("",!0),e.allowDelete?(R(),N("button",{key:3,onClick:y=>a.$emit("delete",f),class:"btn-card-delete",disabled:f.is_current_session,title:f.is_current_session?"Cannot delete current session credential":"Delete passkey"},"🗑️",8,In)):U("",!0)])]),h("div",Nn,[h("div",Rn,[c[3]||(c[3]=h("span",{class:"date-label"},"Created:",-1)),h("span",An,P(F(G)(f.created_at)),1),c[4]||(c[4]=h("span",{class:"date-label"},"Last used:",-1)),h("span",Tn,P(F(G)(f.last_used)),1),c[5]||(c[5]=h("span",{class:"date-label"},"Last verified:",-1)),h("span",Mn,P(F(G)(f.last_verified)),1)])])],42,wn))),128)):(R(),N("div",yn,[...c[2]||(c[2]=[h("p",null,"No passkeys found.",-1)])]))]))}},Pn={key:0,class:"user-info"},Ln={class:"user-name-heading"},Dn={class:"user-name-row"},Un=["title"],qn={key:0,class:"org-role-sub"},Fn={key:0,class:"org-line"},Vn={key:1,class:"role-line"},On={__name:"UserBasicInfo",props:{name:{type:String,required:!0},visits:{type:[Number,String],default:0},createdAt:{type:[String,Number,Date],default:null},lastSeen:{type:[String,Number,Date],default:null},updateEndpoint:{type:String,default:null},canEdit:{type:Boolean,default:!0},loading:{type:Boolean,default:!1},orgDisplayName:{type:String,default:""},roleName:{type:String,default:""}},emits:["saved","editName"],setup(e,{emit:r}){const o=e,t=r;ze();const n=z(()=>!!o.name);return(s,i)=>n.value?(R(),N("div",Pn,[h("h3",Ln,[i[1]||(i[1]=h("span",{class:"icon"},"👤",-1)),h("span",Dn,[h("span",{class:"display-name",title:e.name},P(e.name),9,Un),e.canEdit&&e.updateEndpoint?(R(),N("button",{key:0,class:"mini-btn",onClick:i[0]||(i[0]=u=>t("editName")),title:"Edit name"},"✏️")):U("",!0)])]),e.orgDisplayName||e.roleName?(R(),N("div",qn,[e.orgDisplayName?(R(),N("div",Fn,P(e.orgDisplayName),1)):U("",!0),e.roleName?(R(),N("div",Vn,P(e.roleName),1)):U("",!0)])):U("",!0),i[2]||(i[2]=h("span",null,[h("strong",null,"Visits:")],-1)),h("span",null,P(e.visits||0),1),i[3]||(i[3]=h("span",null,[h("strong",null,"Registered:")],-1)),h("span",null,P(F(G)(e.createdAt)),1),i[4]||(i[4]=h("span",null,[h("strong",null,"Last seen:")],-1)),h("span",null,P(F(G)(e.lastSeen)),1)])):U("",!0)}},Ir=Q(On,[["__scopeId","data-v-2994268a"]]),Hn={class:"modal",role:"dialog","aria-modal":"true"},jn={__name:"Modal",emits:["close"],setup(e){return(r,o)=>(R(),N("div",{class:"modal-overlay",onKeydown:o[0]||(o[0]=Rt(t=>r.$emit("close"),["esc"])),tabindex:"-1"},[h("div",Hn,[Wt(r.$slots,"default",{},void 0)])],32))}},Nr=Q(jn,[["__scopeId","data-v-d5e0cc32"]]),zn={class:"name-edit-form"},$n=["for"],Kn=["id","type","placeholder","disabled"],Jn={key:0,class:"error small"},Yn={class:"modal-actions"},Gn=["disabled"],Qn=["disabled"],xn={__name:"NameEditForm",props:{modelValue:{type:String,default:""},label:{type:String,default:"Name"},placeholder:{type:String,default:""},submitText:{type:String,default:"Save"},cancelText:{type:String,default:"Cancel"},busy:{type:Boolean,default:!1},error:{type:String,default:""},autoFocus:{type:Boolean,default:!0},autoSelect:{type:Boolean,default:!0},inputId:{type:String,default:null},inputType:{type:String,default:"text"}},emits:["update:modelValue","cancel"],setup(e,{emit:r}){const o=e,t=r,n=J(null),s=`name-edit-${Math.random().toString(36).slice(2,10)}`,i=z({get:()=>o.modelValue,set:c=>t("update:modelValue",c)}),u=z(()=>o.inputId||s);At(()=>{o.autoFocus&&ue(()=>{o.autoSelect?n.value?.select():n.value?.focus()})});function a(){t("cancel")}return(c,f)=>(R(),N("div",zn,[h("label",{for:u.value},[ce(P(e.label)+" ",1),Zt(h("input",{id:u.value,ref_key:"inputRef",ref:n,type:e.inputType,placeholder:e.placeholder,"onUpdate:modelValue":f[0]||(f[0]=y=>i.value=y),disabled:e.busy,required:""},null,8,Kn),[[Xt,i.value]])],8,$n),e.error?(R(),N("div",Jn,P(e.error),1)):U("",!0),h("div",Yn,[h("button",{type:"button",class:"btn-secondary",onClick:a,disabled:e.busy},P(e.cancelText),9,Gn),h("button",{type:"submit",class:"btn-primary",disabled:e.busy},P(e.submitText),9,Qn)])]))}},Rr=Q(xn,[["__scopeId","data-v-944620d3"]]),Wn={class:"section-block","data-component":"session-list-section"},Zn={class:"section-header"},Xn={class:"section-description"},es={class:"section-body"},ts={class:re(["session-list"])},ns=["href"],ss={class:"session-group-sessions"},rs=["onFocusin"],os={class:"item-top"},is={class:"item-title"},as={class:"item-actions"},us={key:0,class:"badge badge-current"},ls={key:1,class:"badge badge-current"},cs={key:2,class:"badge badge-current"},ds={key:3,class:"badge"},fs=["onClick","disabled","title"],hs={class:"item-details"},gs={class:"session-dates"},ms={class:"date-label"},ys={class:"date-value"},ws={key:1,class:"empty-state"},Ar={__name:"SessionList",props:{sessions:{type:Array,default:()=>[]},emptyMessage:{type:String,default:"You currently have no other active sessions."},sectionDescription:{type:String,default:"Review where you're signed in and end any sessions you no longer recognize."},terminatingSessions:{type:Object,default:()=>({})},hoveredCredentialUuid:{type:String,default:null}},emits:["terminate","sessionHover"],setup(e,{emit:r}){const o=e,t=r,n=J(null),s=J(null),i=l=>{s.value=l,n.value=l.ip||null,t("sessionHover",l)},u=l=>{l.currentTarget.contains(l.relatedTarget)||(s.value=null,n.value=null,t("sessionHover",null))},a=l=>!!o.terminatingSessions[l],c=l=>`${l.includes(":")?"http":"https"}://${l}`,f=l=>l?l.includes(":")?new URL(`http://[${l}]/`).hostname.split(":").slice(0,4).join(":"):l:null,y=z(()=>{if(n.value)return f(n.value);const l=o.sessions.find(_=>_.is_current);return l?f(l.ip):null}),w=l=>!y.value||!l?!1:f(l)===y.value,d=z(()=>{const l={};for(const p of o.sessions){const C=p.host||"";l[C]||(l[C]={sessions:[],isCurrentSite:!1}),l[C].sessions.push(p),p.is_current_host&&(l[C].isCurrentSite=!0)}for(const p in l)l[p].sessions.sort((C,A)=>new Date(A.last_renewed)-new Date(C.last_renewed));const _=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"}),L=Object.keys(l).sort(_.compare),I={};for(const p of L)I[p]=l[p];return I});return(l,_)=>(R(),N("section",Wn,[h("div",Zn,[_[1]||(_[1]=h("h2",null,"Active Sessions",-1)),h("p",Xn,P(e.sectionDescription),1)]),h("div",es,[h("div",ts,[Array.isArray(e.sessions)&&e.sessions.length?(R(!0),N(ne,{key:0},le(d.value,(L,I)=>(R(),N("div",{key:I,class:"session-group"},[h("h3",{class:re(["session-group-host",{"is-current-site":L.isCurrentSite}])},[I?(R(),N("a",{key:0,href:c(I)},"🌐 "+P(I),9,ns)):(R(),N(ne,{key:1},[ce("🌐 Unbound host")],64))],2),h("div",ss,[(R(!0),N(ne,null,le(L.sessions,p=>(R(),N("div",{key:p.id,class:re(["session-item",{"is-current":p.is_current&&!n.value&&!e.hoveredCredentialUuid,"is-hovered":s.value?.id===p.id,"is-linked-credential":e.hoveredCredentialUuid===p.credential_uuid}]),tabindex:"0",onFocusin:C=>i(p),onFocusout:_[0]||(_[0]=C=>u(C))},[h("div",os,[h("h4",is,P(p.user_agent),1),h("div",as,[p.is_current&&!n.value&&!e.hoveredCredentialUuid?(R(),N("span",us,"Current")):s.value?.id===p.id?(R(),N("span",ls,"Selected")):e.hoveredCredentialUuid===p.credential_uuid?(R(),N("span",cs,"Linked")):!e.hoveredCredentialUuid&&w(p.ip)?(R(),N("span",ds,"Same IP")):U("",!0),h("button",{onClick:C=>l.$emit("terminate",p),class:"btn-card-delete",disabled:a(p.id),title:a(p.id)?"Terminating...":"Terminate session"},"🗑️",8,fs)])]),h("div",hs,[h("div",gs,[h("span",ms,P(F(G)(p.last_renewed)),1),h("span",ys,P(p.ip),1)])])],42,rs))),128))])]))),128)):(R(),N("div",ws,[h("p",null,P(e.emptyMessage),1)]))])])]))}};function ps(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ee={},pe,We;function vs(){return We||(We=1,pe=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}),pe}var ve={},$={},Ze;function x(){if(Ze)return $;Ze=1;let e;const r=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];return $.getSymbolSize=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return t*4+17},$.getSymbolTotalCodewords=function(t){return r[t]},$.getBCHDigit=function(o){let t=0;for(;o!==0;)t++,o>>>=1;return t},$.setToSJISFunction=function(t){if(typeof t!="function")throw new Error('"toSJISFunc" is not a valid function.');e=t},$.isKanjiModeEnabled=function(){return typeof e<"u"},$.toSJIS=function(t){return e(t)},$}var Ce={},Xe;function $e(){return Xe||(Xe=1,(function(e){e.L={bit:1},e.M={bit:0},e.Q={bit:3},e.H={bit:2};function r(o){if(typeof o!="string")throw new Error("Param is not a string");switch(o.toLowerCase()){case"l":case"low":return e.L;case"m":case"medium":return e.M;case"q":case"quartile":return e.Q;case"h":case"high":return e.H;default:throw new Error("Unknown EC Level: "+o)}}e.isValid=function(t){return t&&typeof t.bit<"u"&&t.bit>=0&&t.bit<4},e.from=function(t,n){if(e.isValid(t))return t;try{return r(t)}catch{return n}}})(Ce)),Ce}var be,et;function Cs(){if(et)return be;et=1;function e(){this.buffer=[],this.length=0}return e.prototype={get:function(r){const o=Math.floor(r/8);return(this.buffer[o]>>>7-r%8&1)===1},put:function(r,o){for(let t=0;t>>o-t-1&1)===1)},getLengthInBits:function(){return this.length},putBit:function(r){const o=Math.floor(this.length/8);this.buffer.length<=o&&this.buffer.push(0),r&&(this.buffer[o]|=128>>>this.length%8),this.length++}},be=e,be}var _e,tt;function bs(){if(tt)return _e;tt=1;function e(r){if(!r||r<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=r,this.data=new Uint8Array(r*r),this.reservedBit=new Uint8Array(r*r)}return e.prototype.set=function(r,o,t,n){const s=r*this.size+o;this.data[s]=t,n&&(this.reservedBit[s]=!0)},e.prototype.get=function(r,o){return this.data[r*this.size+o]},e.prototype.xor=function(r,o,t){this.data[r*this.size+o]^=t},e.prototype.isReserved=function(r,o){return this.reservedBit[r*this.size+o]},_e=e,_e}var Se={},nt;function _s(){return nt||(nt=1,(function(e){const r=x().getSymbolSize;e.getRowColCoords=function(t){if(t===1)return[];const n=Math.floor(t/7)+2,s=r(t),i=s===145?26:Math.ceil((s-13)/(2*n-2))*2,u=[s-7];for(let a=1;a=0&&n<=7},e.from=function(n){return e.isValid(n)?parseInt(n,10):void 0},e.getPenaltyN1=function(n){const s=n.size;let i=0,u=0,a=0,c=null,f=null;for(let y=0;y=5&&(i+=r.N1+(u-5)),c=d,u=1),d=n.get(w,y),d===f?a++:(a>=5&&(i+=r.N1+(a-5)),f=d,a=1)}u>=5&&(i+=r.N1+(u-5)),a>=5&&(i+=r.N1+(a-5))}return i},e.getPenaltyN2=function(n){const s=n.size;let i=0;for(let u=0;u=10&&(u===1488||u===93)&&i++,a=a<<1&2047|n.get(f,c),f>=10&&(a===1488||a===93)&&i++}return i*r.N3},e.getPenaltyN4=function(n){let s=0;const i=n.data.length;for(let a=0;a=0;){const i=s[0];for(let a=0;a0){const u=new Uint8Array(this.degree);return u.set(s,i),u}return s},Ie=r,Ie}var Ne={},Re={},Ae={},lt;function Ut(){return lt||(lt=1,Ae.isValid=function(r){return!isNaN(r)&&r>=1&&r<=40}),Ae}var O={},ct;function qt(){if(ct)return O;ct=1;const e="[0-9]+",r="[A-Z $%*+\\-./:]+";let o="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";o=o.replace(/u/g,"\\u");const t="(?:(?![A-Z0-9 $%*+\\-./:]|"+o+`)(?:.|[\r -]))+`;O.KANJI=new RegExp(o,"g"),O.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),O.BYTE=new RegExp(t,"g"),O.NUMERIC=new RegExp(e,"g"),O.ALPHANUMERIC=new RegExp(r,"g");const n=new RegExp("^"+o+"$"),s=new RegExp("^"+e+"$"),i=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");return O.testKanji=function(a){return n.test(a)},O.testNumeric=function(a){return s.test(a)},O.testAlphanumeric=function(a){return i.test(a)},O}var dt;function W(){return dt||(dt=1,(function(e){const r=Ut(),o=qt();e.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},e.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},e.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},e.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},e.MIXED={bit:-1},e.getCharCountIndicator=function(s,i){if(!s.ccBits)throw new Error("Invalid mode: "+s);if(!r.isValid(i))throw new Error("Invalid version: "+i);return i>=1&&i<10?s.ccBits[0]:i<27?s.ccBits[1]:s.ccBits[2]},e.getBestModeForData=function(s){return o.testNumeric(s)?e.NUMERIC:o.testAlphanumeric(s)?e.ALPHANUMERIC:o.testKanji(s)?e.KANJI:e.BYTE},e.toString=function(s){if(s&&s.id)return s.id;throw new Error("Invalid mode")},e.isValid=function(s){return s&&s.bit&&s.ccBits};function t(n){if(typeof n!="string")throw new Error("Param is not a string");switch(n.toLowerCase()){case"numeric":return e.NUMERIC;case"alphanumeric":return e.ALPHANUMERIC;case"kanji":return e.KANJI;case"byte":return e.BYTE;default:throw new Error("Unknown mode: "+n)}}e.from=function(s,i){if(e.isValid(s))return s;try{return t(s)}catch{return i}}})(Re)),Re}var ft;function Ns(){return ft||(ft=1,(function(e){const r=x(),o=Dt(),t=$e(),n=W(),s=Ut(),i=7973,u=r.getBCHDigit(i);function a(w,d,l){for(let _=1;_<=40;_++)if(d<=e.getCapacity(_,l,w))return _}function c(w,d){return n.getCharCountIndicator(w,d)+4}function f(w,d){let l=0;return w.forEach(function(_){const L=c(_.mode,d);l+=L+_.getBitsLength()}),l}function y(w,d){for(let l=1;l<=40;l++)if(f(w,l)<=e.getCapacity(l,d,n.MIXED))return l}e.from=function(d,l){return s.isValid(d)?parseInt(d,10):l},e.getCapacity=function(d,l,_){if(!s.isValid(d))throw new Error("Invalid QR Code version");typeof _>"u"&&(_=n.BYTE);const L=r.getSymbolTotalCodewords(d),I=o.getTotalCodewordsCount(d,l),p=(L-I)*8;if(_===n.MIXED)return p;const C=p-c(_,d);switch(_){case n.NUMERIC:return Math.floor(C/10*3);case n.ALPHANUMERIC:return Math.floor(C/11*2);case n.KANJI:return Math.floor(C/13);case n.BYTE:default:return Math.floor(C/8)}},e.getBestVersionForData=function(d,l){let _;const L=t.from(l,t.M);if(Array.isArray(d)){if(d.length>1)return y(d,L);if(d.length===0)return 1;_=d[0]}else _=d;return a(_.mode,_.getLength(),L)},e.getEncodedBits=function(d){if(!s.isValid(d)||d<7)throw new Error("Invalid QR Code version");let l=d<<12;for(;r.getBCHDigit(l)-u>=0;)l^=i<=0;)a^=r<0&&(s=this.data.substr(n),i=parseInt(s,10),t.put(i,u*3+1))},Pe=r,Pe}var Le,mt;function Ts(){if(mt)return Le;mt=1;const e=W(),r=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function o(t){this.mode=e.ALPHANUMERIC,this.data=t}return o.getBitsLength=function(n){return 11*Math.floor(n/2)+6*(n%2)},o.prototype.getLength=function(){return this.data.length},o.prototype.getBitsLength=function(){return o.getBitsLength(this.data.length)},o.prototype.write=function(n){let s;for(s=0;s+2<=this.data.length;s+=2){let i=r.indexOf(this.data[s])*45;i+=r.indexOf(this.data[s+1]),n.put(i,11)}this.data.length%2&&n.put(r.indexOf(this.data[s]),6)},Le=o,Le}var De,yt;function Ms(){if(yt)return De;yt=1;const e=W();function r(o){this.mode=e.BYTE,typeof o=="string"?this.data=new TextEncoder().encode(o):this.data=new Uint8Array(o)}return r.getBitsLength=function(t){return t*8},r.prototype.getLength=function(){return this.data.length},r.prototype.getBitsLength=function(){return r.getBitsLength(this.data.length)},r.prototype.write=function(o){for(let t=0,n=this.data.length;t=33088&&s<=40956)s-=33088;else if(s>=57408&&s<=60351)s-=49472;else throw new Error("Invalid SJIS character: "+this.data[n]+` -Make sure your charset is UTF-8`);s=(s>>>8&255)*192+(s&255),t.put(s,13)}},Ue=o,Ue}var qe={exports:{}},pt;function Ls(){return pt||(pt=1,(function(e){var r={single_source_shortest_paths:function(o,t,n){var s={},i={};i[t]=0;var u=r.PriorityQueue.make();u.push(t,0);for(var a,c,f,y,w,d,l,_,L;!u.empty();){a=u.pop(),c=a.value,y=a.cost,w=o[c]||{};for(f in w)w.hasOwnProperty(f)&&(d=w[f],l=y+d,_=i[f],L=typeof i[f]>"u",(L||_>l)&&(i[f]=l,u.push(f,l),s[f]=c))}if(typeof n<"u"&&typeof i[n]>"u"){var I=["Could not find a path from ",t," to ",n,"."].join("");throw new Error(I)}return s},extract_shortest_path_from_predecessor_list:function(o,t){for(var n=[],s=t;s;)n.push(s),o[s],s=o[s];return n.reverse(),n},find_path:function(o,t,n){var s=r.single_source_shortest_paths(o,t,n);return r.extract_shortest_path_from_predecessor_list(s,n)},PriorityQueue:{make:function(o){var t=r.PriorityQueue,n={},s;o=o||{};for(s in t)t.hasOwnProperty(s)&&(n[s]=t[s]);return n.queue=[],n.sorter=o.sorter||t.default_sorter,n},default_sorter:function(o,t){return o.cost-t.cost},push:function(o,t){var n={value:o,cost:t};this.queue.push(n),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};e.exports=r})(qe)),qe.exports}var vt;function Ds(){return vt||(vt=1,(function(e){const r=W(),o=As(),t=Ts(),n=Ms(),s=Ps(),i=qt(),u=x(),a=Ls();function c(I){return unescape(encodeURIComponent(I)).length}function f(I,p,C){const A=[];let M;for(;(M=I.exec(C))!==null;)A.push({data:M[0],index:M.index,mode:p,length:M[0].length});return A}function y(I){const p=f(i.NUMERIC,r.NUMERIC,I),C=f(i.ALPHANUMERIC,r.ALPHANUMERIC,I);let A,M;return u.isKanjiModeEnabled()?(A=f(i.BYTE,r.BYTE,I),M=f(i.KANJI,r.KANJI,I)):(A=f(i.BYTE_KANJI,r.BYTE,I),M=[]),p.concat(C,A,M).sort(function(E,m){return E.index-m.index}).map(function(E){return{data:E.data,mode:E.mode,length:E.length}})}function w(I,p){switch(p){case r.NUMERIC:return o.getBitsLength(I);case r.ALPHANUMERIC:return t.getBitsLength(I);case r.KANJI:return s.getBitsLength(I);case r.BYTE:return n.getBitsLength(I)}}function d(I){return I.reduce(function(p,C){const A=p.length-1>=0?p[p.length-1]:null;return A&&A.mode===C.mode?(p[p.length-1].data+=C.data,p):(p.push(C),p)},[])}function l(I){const p=[];for(let C=0;C=0&&k<=6&&(T===0||T===6)||T>=0&&T<=6&&(k===0||k===6)||k>=2&&k<=4&&T>=2&&T<=4?S.set(b+k,B+T,!0,!0):S.set(b+k,B+T,!1,!0))}}function l(S){const E=S.size;for(let m=8;m>k&1)===1,S.set(v,b,B,!0),S.set(b,v,B,!0)}function I(S,E,m){const g=S.size,v=f.getEncodedBits(E,m);let b,B;for(b=0;b<15;b++)B=(v>>b&1)===1,b<6?S.set(b,8,B,!0):b<8?S.set(b+1,8,B,!0):S.set(g-15+b,8,B,!0),b<8?S.set(8,g-b-1,B,!0):b<9?S.set(8,15-b-1+1,B,!0):S.set(8,15-b-1,B,!0);S.set(g-8,8,1,!0)}function p(S,E){const m=S.size;let g=-1,v=m-1,b=7,B=0;for(let k=m-1;k>0;k-=2)for(k===6&&k--;;){for(let T=0;T<2;T++)if(!S.isReserved(v,k-T)){let V=!1;B>>b&1)===1),S.set(v,k-T,V),b--,b===-1&&(B++,b=7)}if(v+=g,v<0||m<=v){v-=g,g=-g;break}}}function C(S,E,m){const g=new o;m.forEach(function(T){g.put(T.mode.bit,4),g.put(T.getLength(),y.getCharCountIndicator(T.mode,S)),T.write(g)});const v=e.getSymbolTotalCodewords(S),b=u.getTotalCodewordsCount(S,E),B=(v-b)*8;for(g.getLengthInBits()+4<=B&&g.put(0,4);g.getLengthInBits()%8!==0;)g.putBit(0);const k=(B-g.getLengthInBits())/8;for(let T=0;T=7&&L(T,E),p(T,B),isNaN(g)&&(g=i.getBestMask(T,I.bind(null,T,m))),i.applyMask(g,T),I(T,m,g),{modules:T,version:E,errorCorrectionLevel:m,maskPattern:g,segments:v}}return ve.create=function(E,m){if(typeof E>"u"||E==="")throw new Error("No input text");let g=r.M,v,b;return typeof m<"u"&&(g=r.from(m.errorCorrectionLevel,r.M),v=c.from(m.version),b=i.from(m.maskPattern),m.toSJISFunc&&e.setToSJISFunction(m.toSJISFunc)),M(E,v,g,b)},ve}var Fe={},Ve={},bt;function Ft(){return bt||(bt=1,(function(e){function r(o){if(typeof o=="number"&&(o=o.toString()),typeof o!="string")throw new Error("Color should be defined as hex string");let t=o.slice().replace("#","").split("");if(t.length<3||t.length===5||t.length>8)throw new Error("Invalid hex color: "+o);(t.length===3||t.length===4)&&(t=Array.prototype.concat.apply([],t.map(function(s){return[s,s]}))),t.length===6&&t.push("F","F");const n=parseInt(t.join(""),16);return{r:n>>24&255,g:n>>16&255,b:n>>8&255,a:n&255,hex:"#"+t.slice(0,6).join("")}}e.getOptions=function(t){t||(t={}),t.color||(t.color={});const n=typeof t.margin>"u"||t.margin===null||t.margin<0?4:t.margin,s=t.width&&t.width>=21?t.width:void 0,i=t.scale||4;return{width:s,scale:s?4:i,margin:n,color:{dark:r(t.color.dark||"#000000ff"),light:r(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},e.getScale=function(t,n){return n.width&&n.width>=t+n.margin*2?n.width/(t+n.margin*2):n.scale},e.getImageWidth=function(t,n){const s=e.getScale(t,n);return Math.floor((t+n.margin*2)*s)},e.qrToImageData=function(t,n,s){const i=n.modules.size,u=n.modules.data,a=e.getScale(i,s),c=Math.floor((i+s.margin*2)*a),f=s.margin*a,y=[s.color.light,s.color.dark];for(let w=0;w=f&&d>=f&&w"u"&&(!i||!i.getContext)&&(a=i,i=void 0),i||(c=t()),a=r.getOptions(a);const f=r.getImageWidth(s.modules.size,a),y=c.getContext("2d"),w=y.createImageData(f,f);return r.qrToImageData(w.data,s,a),o(y,c,f),y.putImageData(w,0,0),c},e.renderToDataURL=function(s,i,u){let a=u;typeof a>"u"&&(!i||!i.getContext)&&(a=i,i=void 0),a||(a={});const c=e.render(s,i,a),f=a.type||"image/png",y=a.rendererOpts||{};return c.toDataURL(f,y.quality)}})(Fe)),Fe}var Oe={},St;function Fs(){if(St)return Oe;St=1;const e=Ft();function r(n,s){const i=n.a/255,u=s+'="'+n.hex+'"';return i<1?u+" "+s+'-opacity="'+i.toFixed(2).slice(1)+'"':u}function o(n,s,i){let u=n+s;return typeof i<"u"&&(u+=" "+i),u}function t(n,s,i){let u="",a=0,c=!1,f=0;for(let y=0;y0&&w>0&&n[y-1]||(u+=c?o("M",w+i,.5+d+i):o("m",a,0),a=0,c=!1),w+1':"",d="',l='viewBox="0 0 '+y+" "+y+'"',L=''+w+d+` -`;return typeof u=="function"&&u(null,L),L},Oe}var Et;function Vs(){if(Et)return ee;Et=1;const e=vs(),r=Us(),o=qs(),t=Fs();function n(s,i,u,a,c){const f=[].slice.call(arguments,1),y=f.length,w=typeof f[y-1]=="function";if(!w&&!e())throw new Error("Callback required as last argument");if(w){if(y<2)throw new Error("Too few arguments provided");y===2?(c=u,u=i,i=a=void 0):y===3&&(i.getContext&&typeof c>"u"?(c=a,a=void 0):(c=a,a=u,u=i,i=void 0))}else{if(y<1)throw new Error("Too few arguments provided");return y===1?(u=i,i=a=void 0):y===2&&!i.getContext&&(a=u,u=i,i=void 0),new Promise(function(d,l){try{const _=r.create(u,a);d(s(_,i,a))}catch(_){l(_)}})}try{const d=r.create(u,a);c(null,s(d,i,a))}catch(d){c(d)}}return ee.create=r.create,ee.toCanvas=n.bind(null,o.render),ee.toDataURL=n.bind(null,o.renderToDataURL),ee.toString=n.bind(null,function(s,i,u){return t.render(s,u)}),ee}var Os=Vs();const Hs=ps(Os),js={class:"device-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"regTitle"},zs={class:"reg-header-row"},$s={id:"regTitle",class:"reg-title"},Ks={key:0},Js={key:1},Ys={class:"device-link-section"},Gs={class:"qr-container"},Qs=["href"],xs={class:"reg-help"},Ws={key:0},Zs={key:1},Xs={class:"reg-actions"},er={key:1,class:"registration-inline-wrapper"},tr={class:"registration-inline-block section-block"},nr={class:"section-header"},sr={class:"inline-heading"},rr={key:0},or={key:1},ir={class:"section-body"},ar={class:"device-link-section"},ur={class:"qr-container"},lr=["href"],cr={class:"reg-help"},dr={key:0},fr={key:1},hr={class:"button-row",style:{"margin-top":"1rem"}},gr={__name:"RegistrationLinkModal",props:{endpoint:{type:String,required:!0},autoCopy:{type:Boolean,default:!0},userName:{type:String,default:null},inline:{type:Boolean,default:!1},showCloseInInline:{type:Boolean,default:!1},prefixCopyWithUserName:{type:Boolean,default:!1}},emits:["close","generated","copied"],setup(e,{emit:r}){const o=ze(),t=e,n=r,s=J(null),i=J(null),u=J(null),a=z(()=>s.value?s.value.replace(/^[^:]+:\/\//,""):""),c=z(()=>{const d=G(i.value);return`⚠️ Expires ${d.startsWith("In ")?d.substring(3):d} and can only be used once.`});async function f(){try{const d=await Y(t.endpoint,{method:"POST"});s.value=d.url,i.value=d.expires,n("generated",{url:d.url,expires:d.expires}),await ue(),y(),t.autoCopy&&w()}catch(d){console.error("Failed to create link",d),en(d)&&o.showMessage(tn(d),"error",4e3),n("close")}}async function y(){s.value&&(await ue(),u.value&&Hs.toCanvas(u.value,s.value,{scale:8},d=>{d&&console.error(d)}))}async function w(){if(!s.value)return;let d=s.value;t.prefixCopyWithUserName&&t.userName&&(d=`${t.userName} ${d}`);try{await navigator.clipboard.writeText(d),n("copied",d),t.inline||n("close")}catch{}}return At(f),Nt(s,()=>y(),{flush:"post"}),(d,l)=>!e.inline&&s.value?(R(),N("div",{key:0,class:"dialog-overlay",onKeydown:l[2]||(l[2]=Rt(ye(_=>d.$emit("close"),["prevent"]),["esc"]))},[h("div",js,[h("div",zs,[h("h2",$s,[l[4]||(l[4]=ce(" 📱 ",-1)),e.userName?(R(),N("span",Ks,"Registration for "+P(e.userName),1)):(R(),N("span",Js,"Device Registration Link"))]),h("button",{class:"icon-btn",onClick:l[0]||(l[0]=_=>d.$emit("close")),"aria-label":"Close"},"❌")]),h("div",Ys,[h("div",Gs,[h("a",{href:s.value,onClick:ye(w,["prevent"]),class:"qr-link"},[h("canvas",{ref_key:"qrCanvas",ref:u,class:"qr-code"},null,512),h("p",null,P(a.value),1)],8,Qs),h("p",xs,[e.userName?(R(),N("span",Ws,"The user should open this link on the device where they want to register.")):(R(),N("span",Zs,"Open or scan this link on the device you wish to register to your account.")),l[5]||(l[5]=h("br",null,null,-1)),h("small",null,P(c.value),1)])])]),h("div",Xs,[h("button",{class:"btn-secondary",onClick:l[1]||(l[1]=_=>d.$emit("close"))},"Close"),h("button",{class:"btn-primary",onClick:w},"Copy Link")])])],32)):e.inline&&s.value?(R(),N("div",er,[h("div",tr,[h("div",nr,[h("h2",sr,[l[6]||(l[6]=ce("📱 ",-1)),e.userName?(R(),N("span",rr,"Registration for "+P(e.userName),1)):(R(),N("span",or,"Device Registration Link"))])]),h("div",ir,[h("div",ar,[h("div",ur,[h("a",{href:s.value,onClick:ye(w,["prevent"]),class:"qr-link"},[h("canvas",{ref_key:"qrCanvas",ref:u,class:"qr-code"},null,512),h("p",null,P(a.value),1)],8,lr),h("p",cr,[e.userName?(R(),N("span",dr,"The user should open this link on the device where they want to register.")):(R(),N("span",fr,"Open this link on the device you wish to connect with.")),l[7]||(l[7]=h("br",null,null,-1)),h("small",null,P(c.value),1)])])]),h("div",hr,[h("button",{class:"btn-primary",onClick:w},"Copy Link"),e.showCloseInInline?(R(),N("button",{key:0,class:"btn-secondary",onClick:l[3]||(l[3]=_=>d.$emit("close"))},"Close")):U("",!0)])])])])):U("",!0)}},Tr=Q(gr,[["__scopeId","data-v-5d6bcbcb"]]),mr={class:"loading-container"},yr={__name:"LoadingView",props:{message:{type:String,default:"Loading..."}},setup(e){return(r,o)=>(R(),N("div",mr,[o[0]||(o[0]=h("div",{class:"loading-spinner"},null,-1)),h("p",null,P(e.message),1)]))}},Mr=Q(yr,[["__scopeId","data-v-130f5abf"]]),wr={class:"message-container"},pr={class:"message-content"},vr={class:"button-row"},Cr={__name:"AccessDenied",emits:["reload"],setup(e){return(r,o)=>(R(),N("div",wr,[h("div",pr,[o[2]||(o[2]=h("h2",null,"🔒 Access Denied",-1)),h("div",vr,[h("button",{class:"btn-secondary",onClick:o[0]||(o[0]=(...t)=>F(Ge)&&F(Ge)(...t))},"Back"),h("button",{class:"btn-primary",onClick:o[1]||(o[1]=t=>r.$emit("reload"))},"Reload Page")])])]))}},Pr=Q(Cr,[["__scopeId","data-v-744305d5"]]);export{Pr as A,Br as B,Mr as L,Nr as M,Rr as N,Tr as R,Ir as U,kr as _,Ar as a,Er as b,Sr as c,ze as u}; diff --git a/paskia/frontend-build/auth/assets/RestrictedAuth-BIGLs28V.js b/paskia/frontend-build/auth/assets/RestrictedAuth-BIGLs28V.js deleted file mode 100644 index f09fc06..0000000 --- a/paskia/frontend-build/auth/assets/RestrictedAuth-BIGLs28V.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as z,G as E,r as g,c as y,o as L,d,e as h,i as f,f as n,t as _,n as N,R as O,C as Y,Y as w,V as T,p as D}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},J={class:"view-root"},W={key:0,class:"surface surface--tight"},j={class:"view-header center"},H={key:0,class:"user-line"},K={class:"view-lede"},Q={class:"section-block"},X={class:"section-body center"},Z={class:"button-row center"},ee=["disabled"],te=["disabled"],ae=["disabled"],se=["disabled"],ne={__name:"RestrictedAuth",props:{mode:{type:String,default:"login",validator:o=>["login","reauth","forbidden"].includes(o)}},emits:["authenticated","forbidden","logout","back","home","auth-error"],setup(o,{expose:V,emit:$}){const v=o,m=$,i=E({show:!1,message:"",type:"info"}),b=g(!0),t=g(!1),S=g(null),r=g(null),l=g("initial");let p=null;const u=y(()=>!!r.value?.authenticated),k=y(()=>b.value?!1:v.mode==="reauth"?!0:l.value!=="forbidden"),U=y(()=>v.mode==="reauth"?"🔐 Additional Authentication":l.value==="forbidden"?"🚫 Forbidden":`🔐 ${S.value?.rp_name||location.origin}`),B=y(()=>v.mode==="reauth"?"Please verify your identity to continue with this action.":l.value==="forbidden"?"You lack the required permissions.":"Please sign in with your passkey."),F=y(()=>r.value?.user?.user_name||"User");function c(e,a="info",s=3e3){i.show=!0,i.message=e,i.type=a,p&&clearTimeout(p),s>0&&(p=setTimeout(()=>{i.show=!1},s))}async function I(){try{const e=await Y();if(S.value=e,e?.rp_name){const a=v.mode==="reauth"?"Verify Identity":u.value?"Forbidden":"Sign In";document.title=`${e.rp_name} · ${a}`}}catch(e){console.warn("Unable to load settings",e)}}async function M(){try{r.value=await w("/auth/api/user-info",{method:"POST"}),u.value&&v.mode!=="reauth"?(l.value="forbidden",m("forbidden",r.value)):l.value="login"}catch(e){console.error("Failed to load user info",e),e.status!==401&&e.status!==403&&c(T(e),"error",4e3),r.value=null,l.value="login"}}async function A(){if(!k.value||t.value)return;t.value=!0,c("Starting authentication…","info");let e;try{e=await D.authenticate()}catch(a){t.value=!1;const s=a?.message||"Passkey authentication cancelled",P=s==="Passkey authentication cancelled";c(s,P?"info":"error",4e3),m("auth-error",{message:s,cancelled:P});return}try{await x(e)}catch(a){t.value=!1;const s=a?.message||"Failed to establish session";c(s,"error",4e3),m("auth-error",{message:s,cancelled:!1});return}t.value=!1,m("authenticated",e)}async function C(){if(!t.value){t.value=!0;try{await w("/auth/api/logout",{method:"POST"}),r.value=null,l.value="login",c("Logged out. You can sign in with a different account.","info",3e3)}catch(e){c(T(e),"error",4e3)}finally{t.value=!1}m("logout")}}function R(){const e=window.open("/auth/","passkey_auth_profile");e&&e.focus()}async function x(e){if(!e?.session_token)throw console.error("setSessionCookie called with missing session_token:",e),new Error("Authentication response missing session_token");return await w("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}return L(async()=>{await I(),await M(),b.value=!1}),V({showMessage:c,isAuthenticated:u,userInfo:r}),(e,a)=>(h(),d("div",q,[i.show?(h(),d("div",G,[n("div",{class:N(["status",i.type])},_(i.message),3)])):f("",!0),n("main",J,[b.value?f("",!0):(h(),d("div",W,[n("header",j,[n("h1",null,_(U.value),1),u.value?(h(),d("p",H,"👤 "+_(F.value),1)):f("",!0),n("p",K,_(B.value),1)]),n("section",Q,[n("div",X,[n("div",Z,[O(e.$slots,"actions",{loading:t.value,canAuthenticate:k.value,isAuthenticated:u.value,authenticate:A,logout:C,mode:o.mode},()=>[n("button",{class:"btn-secondary",disabled:t.value,onClick:a[0]||(a[0]=s=>e.$emit("back"))},"Back",8,ee),k.value?(h(),d("button",{key:0,class:"btn-primary",disabled:t.value,onClick:A},_(t.value?o.mode==="reauth"?"Verifying…":"Signing in…":o.mode==="reauth"?"Verify":"Login"),9,te)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:1,class:"btn-danger",disabled:t.value,onClick:C},"Logout",8,ae)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:2,class:"btn-primary",disabled:t.value,onClick:R},"Profile",8,se)):f("",!0)])])])])]))])]))}},ie=z(ne,[["__scopeId","data-v-d00079a6"]]);export{ie as R}; diff --git a/paskia/frontend-build/auth/assets/RestrictedAuth-CMHKrNJh.css b/paskia/frontend-build/auth/assets/RestrictedAuth-CMHKrNJh.css deleted file mode 100644 index 0eacc5f..0000000 --- a/paskia/frontend-build/auth/assets/RestrictedAuth-CMHKrNJh.css +++ /dev/null @@ -1 +0,0 @@ -.button-row.center[data-v-d00079a6]{display:flex;justify-content:center;gap:.75rem}.user-line[data-v-d00079a6]{margin:.5rem 0 0;font-weight:500;color:var(--color-text)}main.view-root[data-v-d00079a6]{min-height:100vh;align-items:center;justify-content:center;padding:2rem 1rem}.surface.surface--tight[data-v-d00079a6]{max-width:520px;margin:0 auto;width:100%;display:flex;flex-direction:column;gap:1.75rem} diff --git a/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css b/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css deleted file mode 100644 index f1e5723..0000000 --- a/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css +++ /dev/null @@ -1 +0,0 @@ -:root{--font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;--font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;--color-canvas: #f5f6f8;--color-surface: #ffffff;--color-surface-subtle: #f1f3f7;--color-border: #d0d5dd;--color-border-strong: #9aa2af;--color-heading: #101828;--color-text: #1f2933;--color-text-muted: #52616b;--color-link: #2563eb;--color-link-hover: #1d4ed8;--color-accent: #2563eb;--color-accent-strong: #1e3faa;--color-accent-contrast: #ffffff;--color-success-text: #0f5132;--color-success-bg: #d1fadf;--color-error-text: #b42318;--color-error-bg: #ffe3e3;--color-info-text: #0f609b;--color-info-bg: #d6ecff;--color-danger: #dc2626;--shadow-soft: 0 10px 30px rgba(15, 23, 42, .08);--radius-none: 0;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 10px;--space-xxs: .25rem;--space-xs: .5rem;--space-sm: .75rem;--space-md: 1rem;--space-lg: 1.5rem;--space-xl: 2.25rem;--space-xxl: 3.5rem;--layout-max-width: 1400px;--layout-padding: clamp(1.5rem, 3vw + 1rem, 3.25rem);--transition-base: .16s ease}@media(prefers-color-scheme:dark){:root{--color-canvas: #0f172a;--color-surface: #141b2f;--color-surface-subtle: #1b243b;--color-border: #25304a;--color-border-strong: #3d4d6b;--color-heading: #f8fafc;--color-text: #e2e8f0;--color-text-muted: #94a3b8;--color-link: #60a5fa;--color-link-hover: #93c5fd;--color-accent: #60a5fa;--color-accent-strong: #3b82f6;--color-accent-contrast: #0b1120;--color-success-text: #34d399;--color-success-bg: #1a4d2e;--color-error-text: #fca5a5;--color-error-bg: #4a1f1f;--color-info-text: #bae6fd;--color-info-bg: #1e3a5f;--color-danger: #f87171;--shadow-soft: 0 0 0 #000000}}*,*:before,*:after{box-sizing:border-box}html{height:100%;background:var(--color-canvas)}body{height:100%;margin:0;font-family:var(--font-sans);background:none;color:var(--color-text);line-height:1.55;-webkit-font-smoothing:antialiased}body,#app,#admin-app{display:flex;flex-direction:column;min-height:100vh}#app,#admin-app{flex:1}a,a:visited{color:var(--color-link);text-decoration:none}a:hover,a:focus-visible{color:var(--color-link-hover);text-decoration:underline}a:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-sm)}.app-shell{flex:1;display:flex;flex-direction:column;min-height:100vh}.app-main{flex:1;display:flex;flex-direction:column}.view-root{flex:1;width:100%;display:flex;flex-direction:column;gap:2rem;padding:var(--layout-padding);box-sizing:border-box;margin:0 auto;width:min(100%,var(--layout-max-width))}.view-root--wide{width:min(100%,1200px)}.view-root--narrow{max-width:540px}.view-header{display:flex;flex-direction:column;gap:.75rem}.view-header h1{margin:0;font-weight:600;color:var(--color-heading)}.view-lede{margin:0;color:var(--color-text-muted);font-size:1rem}.section-block{display:flex;flex-direction:column;gap:1rem}.section-block h2{margin:0;font-size:clamp(1.25rem,1.5vw + 1rem,1.65rem);font-weight:600;color:var(--color-heading)}.section-body{display:flex;flex-direction:column;gap:1rem}.button-row{display:flex;flex-wrap:wrap;gap:.75rem;justify-content:flex-start}.surface{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-lg);box-shadow:var(--shadow-soft)}.surface--tight{padding:var(--space-md)}button{font-family:inherit;font-size:1rem;font-weight:500;border-radius:var(--radius-sm);border:1px solid transparent;padding:.65rem 1.1rem;cursor:pointer;transition:all var(--transition-base);display:inline-flex;align-items:center;justify-content:center;gap:.4rem;background:var(--color-surface);color:var(--color-text)}button:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}button:disabled{cursor:not-allowed;filter:opacity(.6)}.btn-primary{background:var(--color-accent);color:var(--color-accent-contrast);border-color:var(--color-accent);box-shadow:var(--shadow-soft)}.btn-primary:hover:not(:disabled){background:var(--color-accent-strong);border-color:var(--color-accent-strong)}.btn-secondary{background:transparent;color:var(--color-text);border-color:var(--color-border)}.btn-secondary:hover:not(:disabled){border-color:var(--color-border-strong);background:var(--color-surface-subtle)}.btn-danger{background:var(--color-danger);color:var(--color-accent-contrast);border-color:transparent}.btn-danger:hover:not(:disabled){filter:brightness(.92)}input[type=text],input[type=search],input[type=email],textarea,select{font:inherit;width:100%;padding:.65rem .75rem;border-radius:var(--radius-sm);border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-text);transition:border-color var(--transition-base),box-shadow var(--transition-base)}input:focus-visible,textarea:focus-visible,select:focus-visible{border-color:var(--color-accent);box-shadow:0 0 0 3px #c7d2fe;outline:none}label{display:flex;flex-direction:column;gap:.5rem;color:var(--color-text)}p{margin:0;color:var(--color-text)}small{color:var(--color-text-muted)}.table-wrapper{overflow-x:auto;background:var(--color-surface);border:1px solid var(--color-border)}table{width:100%;border-collapse:collapse;font-size:.95rem}thead tr{background:var(--color-surface-subtle);color:var(--color-text-muted)}td,th{padding:.65rem .75rem;border-bottom:1px solid var(--color-border);text-align:left}.center{text-align:center}.badge{display:inline-flex;align-items:center;gap:.35rem;padding:.2rem .6rem;border-radius:var(--radius-sm);background:var(--color-surface-subtle);border:1px solid var(--color-border);color:var(--color-text-muted);font-size:.75rem}.global-status{position:fixed;top:1.5rem;left:50%;transform:translate(-50%);z-index:1200;min-width:min(520px,calc(100vw - 2rem));display:none}.global-status .status{display:flex;align-items:center;justify-content:center;padding:.85rem 1.25rem;border-radius:var(--radius-sm);border-width:1px;border-style:solid;background:var(--color-surface);box-shadow:var(--shadow-soft);font-weight:550}.status.info{border-color:#3b82f6;color:var(--color-info-text);background:var(--color-info-bg)}.status.success{border-color:#16a34a;color:var(--color-success-text);background:var(--color-success-bg)}.status.error{border-color:#dc2626;color:var(--color-error-text);background:var(--color-error-bg)}.dialog-overlay{position:fixed;inset:0;background:transparent;backdrop-filter:blur(.1rem) brightness(.7);-webkit-backdrop-filter:blur(.1rem) brightness(.7);z-index:1100;display:flex;align-items:center;justify-content:center;padding:1.5rem}.device-dialog,.modal{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);width:min(520px,100%);max-height:calc(100vh - 3rem);overflow-y:auto;padding:1.75rem;box-shadow:var(--shadow-soft);color:var(--color-text)}.qr-container{display:flex;flex-direction:column;align-items:center;gap:.75rem;text-align:center;color:var(--color-text-muted)}.qr-code{border:1px solid var(--color-border);padding:.75rem;background:var(--color-surface)}.link-container,.token-display,.token-info{background:var(--color-surface-subtle);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:.75rem;color:var(--color-text)}:root{--card-width: 22rem}.record-list,.credential-list,.session-list{width:100%;display:grid;grid-auto-flow:row;grid-template-columns:repeat(auto-fill,minmax(var(--card-width),1fr));justify-content:start;gap:1rem 1.25rem;align-items:stretch;margin:0 auto}@media(max-width:720px){.record-list{display:flex;flex-direction:column;max-width:100%}}.record-item,.credential-item,.session-item{display:flex;flex-direction:column;gap:.75rem;padding:1rem;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);height:100%;transition:border-color .2s ease,box-shadow .2s ease,transform .2s ease;position:relative;cursor:pointer}.record-item:hover,.credential-item:hover,.session-item:hover{border-color:var(--color-border-strong);box-shadow:0 10px 24px #0f172a1f;transform:translateY(-1px)}.record-item.is-current,.credential-item.current-session,.credential-item.is-hovered,.session-item.is-current,.session-item.is-hovered{border-color:var(--color-accent)}.credential-item.is-linked-session,.session-item.is-linked-credential{border-color:var(--color-accent);background-color:var(--color-surface-subtle)}.item-top{display:flex;align-items:center;gap:1rem}.item-icon{width:40px;height:40px;display:grid;place-items:center;background:var(--color-surface-subtle, transparent);border-radius:var(--radius-sm);border:1px solid var(--color-border);flex-shrink:0}.auth-icon{border-radius:var(--radius-sm)}.item-title{flex:1;margin:0;font-size:1rem;font-weight:600;color:var(--color-heading)}.item-actions{flex-shrink:0;display:flex;gap:.5rem;align-items:center}.item-actions .badge+.btn-card-delete{margin-left:.25rem}.item-actions .badge+.badge{margin-left:.25rem}.item-details{margin-left:calc(40px + 1rem);display:flex;flex-direction:column;gap:.5rem}.credential-dates,.session-dates{display:grid;grid-auto-flow:row;grid-template-columns:7rem 1fr;gap:.35rem .5rem;font-size:.75rem;color:var(--color-text-muted);align-items:center}.date-label{font-weight:500;color:inherit}.date-value{color:var(--color-text)}.btn-card-delete{background:transparent;border:none;color:var(--color-danger);padding:.35rem .5rem;font-size:1.05rem;line-height:1;border-radius:var(--radius-sm);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.btn-card-delete:hover:not(:disabled){background:#fee}.btn-card-delete:disabled{filter:opacity(.4);cursor:not-allowed}.session-emoji{font-size:1.2rem}.badge{padding:.2rem .5rem;border-radius:var(--radius-sm);font-size:.8rem;font-weight:500}.badge-current{background:var(--color-accent);color:var(--color-accent-contrast);box-shadow:0 0 0 1px var(--color-accent) inset}.badge:not(.badge-current){background:var(--color-surface-subtle);color:var(--color-text-muted);border:1px solid var(--color-border)}.session-meta-info{font-size:.75rem;color:var(--color-text-muted);font-family:monospace}.empty-state{text-align:center;padding:var(--space-lg);color:var(--color-text-muted)}.empty-state p{margin:0}.user-info{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:1.1rem 1.25rem;display:grid;grid-template-columns:auto 1fr;gap:.75rem 1.25rem}.user-info h3{margin:0;grid-column:span 2;display:flex;align-items:center;gap:.5rem;font-size:1.15rem;font-weight:600}.user-info span{text-align:left;color:var(--color-text)}.toggle-link{color:var(--color-link);cursor:pointer}.toggle-link:hover{color:var(--color-link-hover)}.token-info code{font-family:var(--font-mono)}@media(max-width:720px){.view-root{padding:clamp(1rem,3vw + .75rem,2rem);gap:1.75rem}.credential-dates{grid-auto-flow:row;grid-template-columns:auto auto}.global-status{top:1rem}}@media(max-width:500px)and (orientation:portrait)and (pointer:coarse),(max-width:350px){button{width:100%}.button-row{flex-direction:column}}.dialog-backdrop{position:fixed;top:0;left:0;width:100vw;height:100vh;background:transparent;backdrop-filter:blur(.1rem) brightness(.7);-webkit-backdrop-filter:blur(.1rem) brightness(.7);display:flex;align-items:center;justify-content:center;z-index:1000}.dialog-container{max-width:90vw;max-height:90vh;overflow-y:auto}.dialog-content{flex:none;width:100%;max-width:480px;padding:2rem;background:var(--color-surface);border-radius:var(--radius-lg);box-shadow:0 20px 60px #1e293b;border:1px solid var(--color-border)}.dialog-content--wide{max-width:540px}.dialog-content--narrow{max-width:420px}@media(max-width:720px){.dialog-content{padding:1.5rem}}body:has(#auth-iframe){overflow:hidden}#auth-iframe{border:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;color-scheme:auto;background:transparent;backdrop-filter:blur(.1rem) brightness(.7);-webkit-backdrop-filter:blur(.1rem) brightness(.7)} diff --git a/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js b/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js deleted file mode 100644 index cf6e033..0000000 --- a/paskia/frontend-build/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js +++ /dev/null @@ -1,2 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();function Zn(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const K={},ft=[],Ae=()=>{},ir=()=>!1,un=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),es=e=>e.startsWith("onUpdate:"),re=Object.assign,ts=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Si=Object.prototype.hasOwnProperty,U=(e,t)=>Si.call(e,t),T=Array.isArray,ut=e=>jt(e)==="[object Map]",bt=e=>jt(e)==="[object Set]",As=e=>jt(e)==="[object Date]",P=e=>typeof e=="function",J=e=>typeof e=="string",pe=e=>typeof e=="symbol",V=e=>e!==null&&typeof e=="object",or=e=>(V(e)||P(e))&&P(e.then)&&P(e.catch),lr=Object.prototype.toString,jt=e=>lr.call(e),xi=e=>jt(e).slice(8,-1),cr=e=>jt(e)==="[object Object]",dn=e=>J(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,At=Zn(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),hn=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},vi=/-\w/g,qe=hn(e=>e.replace(vi,t=>t.slice(1).toUpperCase())),Ai=/\B([A-Z])/g,Ye=hn(e=>e.replace(Ai,"-$1").toLowerCase()),ar=hn(e=>e.charAt(0).toUpperCase()+e.slice(1)),Rn=hn(e=>e?`on${ar(e)}`:""),Je=(e,t)=>!Object.is(e,t),zt=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},pn=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Rs;const gn=()=>Rs||(Rs=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ns(e){if(T(e)){const t={};for(let n=0;n{if(n){const s=n.split(Ti);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function ss(e){let t="";if(J(e))t=e;else if(T(e))for(let n=0;nlt(n,t))}const dr=e=>!!(e&&e.__v_isRef===!0),Ni=e=>J(e)?e:e==null?"":T(e)||V(e)&&(e.toString===lr||!P(e.toString))?dr(e)?Ni(e.value):JSON.stringify(e,hr,2):String(e),hr=(e,t)=>dr(t)?hr(e,t.value):ut(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Tn(s,i)+" =>"]=r,n),{})}:bt(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Tn(n))}:pe(t)?Tn(t):V(t)&&!T(t)&&!cr(t)?String(t):t,Tn=(e,t="")=>{var n;return pe(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};let z;class pr{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=z,!t&&z&&(this.index=(z.scopes||(z.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(z=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Tt){let t=Tt;for(Tt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Rt;){let t=Rt;for(Rt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function _r(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function yr(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),ls(s),Fi(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function $n(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(wr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function wr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Mt)||(e.globalVersion=Mt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!$n(e))))return;e.flags|=2;const t=e.dep,n=H,s=he;H=e,he=!0;try{_r(e);const r=e.fn(e._value);(t.version===0||Je(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{H=n,he=s,yr(e),e.flags&=-3}}function ls(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)ls(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Fi(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let he=!0;const Er=[];function Ue(){Er.push(he),he=!1}function je(){const e=Er.pop();he=e===void 0?!0:e}function Ts(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=H;H=void 0;try{t()}finally{H=n}}}let Mt=0;class Ui{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class cs{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!H||!he||H===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==H)n=this.activeLink=new Ui(H,this),H.deps?(n.prevDep=H.depsTail,H.depsTail.nextDep=n,H.depsTail=n):H.deps=H.depsTail=n,Sr(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=H.depsTail,n.nextDep=void 0,H.depsTail.nextDep=n,H.depsTail=n,H.deps===n&&(H.deps=s)}return n}trigger(t){this.version++,Mt++,this.notify(t)}notify(t){is();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{os()}}}function Sr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Sr(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const nn=new WeakMap,rt=Symbol(""),Hn=Symbol(""),Nt=Symbol("");function X(e,t,n){if(he&&H){let s=nn.get(e);s||nn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new cs),r.map=s,r.key=n),r.track()}}function Ne(e,t,n,s,r,i){const o=nn.get(e);if(!o){Mt++;return}const l=c=>{c&&c.trigger()};if(is(),t==="clear")o.forEach(l);else{const c=T(e),d=c&&dn(n);if(c&&n==="length"){const u=Number(s);o.forEach((p,S)=>{(S==="length"||S===Nt||!pe(S)&&S>=u)&&l(p)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),d&&l(o.get(Nt)),t){case"add":c?d&&l(o.get("length")):(l(o.get(rt)),ut(e)&&l(o.get(Hn)));break;case"delete":c||(l(o.get(rt)),ut(e)&&l(o.get(Hn)));break;case"set":ut(e)&&l(o.get(rt));break}}os()}function ji(e,t){const n=nn.get(e);return n&&n.get(t)}function ct(e){const t=F(e);return t===e?t:(X(t,"iterate",Nt),fe(e)?t:t.map(ge))}function mn(e){return X(e=F(e),"iterate",Nt),e}function We(e,t){return Ke(e)?it(e)?pt(ge(t)):pt(t):ge(t)}const Ki={__proto__:null,[Symbol.iterator](){return Cn(this,Symbol.iterator,e=>We(this,e))},concat(...e){return ct(this).concat(...e.map(t=>T(t)?ct(t):t))},entries(){return Cn(this,"entries",e=>(e[1]=We(this,e[1]),e))},every(e,t){return Oe(this,"every",e,t,void 0,arguments)},filter(e,t){return Oe(this,"filter",e,t,n=>n.map(s=>We(this,s)),arguments)},find(e,t){return Oe(this,"find",e,t,n=>We(this,n),arguments)},findIndex(e,t){return Oe(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Oe(this,"findLast",e,t,n=>We(this,n),arguments)},findLastIndex(e,t){return Oe(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Oe(this,"forEach",e,t,void 0,arguments)},includes(...e){return In(this,"includes",e)},indexOf(...e){return In(this,"indexOf",e)},join(e){return ct(this).join(e)},lastIndexOf(...e){return In(this,"lastIndexOf",e)},map(e,t){return Oe(this,"map",e,t,void 0,arguments)},pop(){return St(this,"pop")},push(...e){return St(this,"push",e)},reduce(e,...t){return Os(this,"reduce",e,t)},reduceRight(e,...t){return Os(this,"reduceRight",e,t)},shift(){return St(this,"shift")},some(e,t){return Oe(this,"some",e,t,void 0,arguments)},splice(...e){return St(this,"splice",e)},toReversed(){return ct(this).toReversed()},toSorted(e){return ct(this).toSorted(e)},toSpliced(...e){return ct(this).toSpliced(...e)},unshift(...e){return St(this,"unshift",e)},values(){return Cn(this,"values",e=>We(this,e))}};function Cn(e,t,n){const s=mn(e),r=s[t]();return s!==e&&!fe(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const Li=Array.prototype;function Oe(e,t,n,s,r,i){const o=mn(e),l=o!==e&&!fe(e),c=o[t];if(c!==Li[t]){const p=c.apply(e,i);return l?ge(p):p}let d=n;o!==e&&(l?d=function(p,S){return n.call(this,We(e,p),S,e)}:n.length>2&&(d=function(p,S){return n.call(this,p,S,e)}));const u=c.call(o,d,s);return l&&r?r(u):u}function Os(e,t,n,s){const r=mn(e);let i=n;return r!==e&&(fe(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,We(e,l),c,e)}),r[t](i,...s)}function In(e,t,n){const s=F(e);X(s,"iterate",Nt);const r=s[t](...n);return(r===-1||r===!1)&&bn(n[0])?(n[0]=F(n[0]),s[t](...n)):r}function St(e,t,n=[]){Ue(),is();const s=F(e)[t].apply(e,n);return os(),je(),s}const $i=Zn("__proto__,__v_isRef,__isVue"),xr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(pe));function Hi(e){pe(e)||(e=String(e));const t=F(this);return X(t,"has",e),t.hasOwnProperty(e)}class vr{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Xi:Or:i?Tr:Rr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=T(t);if(!r){let c;if(o&&(c=Ki[n]))return c;if(n==="hasOwnProperty")return Hi}const l=Reflect.get(t,n,G(t)?t:s);if((pe(n)?xr.has(n):$i(n))||(r||X(t,"get",n),i))return l;if(G(l)){const c=o&&dn(n)?l:l.value;return r&&V(c)?Wn(c):c}return V(l)?r?Wn(l):fs(l):l}}class Ar extends vr{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const o=T(t)&&dn(n);if(!this._isShallow){const d=Ke(i);if(!fe(s)&&!Ke(s)&&(i=F(i),s=F(s)),!o&&G(i)&&!G(s))return d||(i.value=s),!0}const l=o?Number(n)e,kt=e=>Reflect.getPrototypeOf(e);function Ji(e,t,n){return function(...s){const r=this.__v_raw,i=F(r),o=ut(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,d=r[e](...s),u=n?Vn:t?pt:ge;return!t&&X(i,"iterate",c?Hn:rt),{next(){const{value:p,done:S}=d.next();return S?{value:p,done:S}:{value:l?[u(p[0]),u(p[1])]:u(p),done:S}},[Symbol.iterator](){return this}}}}function Jt(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function qi(e,t){const n={get(r){const i=this.__v_raw,o=F(i),l=F(r);e||(Je(r,l)&&X(o,"get",r),X(o,"get",l));const{has:c}=kt(o),d=t?Vn:e?pt:ge;if(c.call(o,r))return d(i.get(r));if(c.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&X(F(r),"iterate",rt),r.size},has(r){const i=this.__v_raw,o=F(i),l=F(r);return e||(Je(r,l)&&X(o,"has",r),X(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=F(l),d=t?Vn:e?pt:ge;return!e&&X(c,"iterate",rt),l.forEach((u,p)=>r.call(i,d(u),d(p),o))}};return re(n,e?{add:Jt("add"),set:Jt("set"),delete:Jt("delete"),clear:Jt("clear")}:{add(r){!t&&!fe(r)&&!Ke(r)&&(r=F(r));const i=F(this);return kt(i).has.call(i,r)||(i.add(r),Ne(i,"add",r,r)),this},set(r,i){!t&&!fe(i)&&!Ke(i)&&(i=F(i));const o=F(this),{has:l,get:c}=kt(o);let d=l.call(o,r);d||(r=F(r),d=l.call(o,r));const u=c.call(o,r);return o.set(r,i),d?Je(i,u)&&Ne(o,"set",r,i):Ne(o,"add",r,i),this},delete(r){const i=F(this),{has:o,get:l}=kt(i);let c=o.call(i,r);c||(r=F(r),c=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return c&&Ne(i,"delete",r,void 0),d},clear(){const r=F(this),i=r.size!==0,o=r.clear();return i&&Ne(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Ji(r,e,t)}),n}function as(e,t){const n=qi(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(U(n,r)&&r in s?n:s,r,i)}const Gi={get:as(!1,!1)},Yi={get:as(!1,!0)},zi={get:as(!0,!1)};const Rr=new WeakMap,Tr=new WeakMap,Or=new WeakMap,Xi=new WeakMap;function Qi(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Zi(e){return e.__v_skip||!Object.isExtensible(e)?0:Qi(xi(e))}function fs(e){return Ke(e)?e:us(e,!1,Wi,Gi,Rr)}function eo(e){return us(e,!1,ki,Yi,Tr)}function Wn(e){return us(e,!0,Bi,zi,Or)}function us(e,t,n,s,r){if(!V(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=Zi(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function it(e){return Ke(e)?it(e.__v_raw):!!(e&&e.__v_isReactive)}function Ke(e){return!!(e&&e.__v_isReadonly)}function fe(e){return!!(e&&e.__v_isShallow)}function bn(e){return e?!!e.__v_raw:!1}function F(e){const t=e&&e.__v_raw;return t?F(t):e}function to(e){return!U(e,"__v_skip")&&Object.isExtensible(e)&&fr(e,"__v_skip",!0),e}const ge=e=>V(e)?fs(e):e,pt=e=>V(e)?Wn(e):e;function G(e){return e?e.__v_isRef===!0:!1}function _c(e){return no(e,!1)}function no(e,t){return G(e)?e:new so(e,t)}class so{constructor(t,n){this.dep=new cs,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:F(t),this._value=n?t:ge(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||fe(t)||Ke(t);t=s?t:F(t),Je(t,n)&&(this._rawValue=t,this._value=s?t:ge(t),this.dep.trigger())}}function Cr(e){return G(e)?e.value:e}const ro={get:(e,t,n)=>t==="__v_raw"?e:Cr(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return G(r)&&!G(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Ir(e){return it(e)?e:new Proxy(e,ro)}function yc(e){const t=T(e)?new Array(e.length):{};for(const n in e)t[n]=oo(e,n);return t}class io{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0,this._raw=F(t);let r=!0,i=t;if(!T(t)||!dn(String(n)))do r=!bn(i)||fe(i);while(r&&(i=i.__v_raw));this._shallow=r}get value(){let t=this._object[this._key];return this._shallow&&(t=Cr(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&G(this._raw[this._key])){const n=this._object[this._key];if(G(n)){n.value=t;return}}this._object[this._key]=t}get dep(){return ji(this._raw,this._key)}}function oo(e,t,n){return new io(e,t,n)}class lo{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new cs(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Mt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&H!==this)return br(this,!0),!0}get value(){const t=this.dep.track();return wr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function co(e,t,n=!1){let s,r;return P(e)?s=e:(s=e.get,r=e.set),new lo(s,r,n)}const qt={},sn=new WeakMap;let tt;function ao(e,t=!1,n=tt){if(n){let s=sn.get(n);s||sn.set(n,s=[]),s.push(e)}}function fo(e,t,n=K){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,d=C=>r?C:fe(C)||r===!1||r===0?De(C,1):De(C);let u,p,S,v,O=!1,N=!1;if(G(e)?(p=()=>e.value,O=fe(e)):it(e)?(p=()=>d(e),O=!0):T(e)?(N=!0,O=e.some(C=>it(C)||fe(C)),p=()=>e.map(C=>{if(G(C))return C.value;if(it(C))return d(C);if(P(C))return c?c(C,2):C()})):P(e)?t?p=c?()=>c(e,2):e:p=()=>{if(S){Ue();try{S()}finally{je()}}const C=tt;tt=u;try{return c?c(e,3,[v]):e(v)}finally{tt=C}}:p=Ae,t&&r){const C=p,q=r===!0?1/0:r;p=()=>De(C(),q)}const Z=Di(),D=()=>{u.stop(),Z&&Z.active&&ts(Z.effects,u)};if(i&&t){const C=t;t=(...q)=>{C(...q),D()}}let W=N?new Array(e.length).fill(qt):qt;const k=C=>{if(!(!(u.flags&1)||!u.dirty&&!C))if(t){const q=u.run();if(r||O||(N?q.some((He,me)=>Je(He,W[me])):Je(q,W))){S&&S();const He=tt;tt=u;try{const me=[q,W===qt?void 0:N&&W[0]===qt?[]:W,v];W=q,c?c(t,3,me):t(...me)}finally{tt=He}}}else u.run()};return l&&l(k),u=new gr(p),u.scheduler=o?()=>o(k,!1):k,v=C=>ao(C,!1,u),S=u.onStop=()=>{const C=sn.get(u);if(C){if(c)c(C,4);else for(const q of C)q();sn.delete(u)}},t?s?k(!0):W=u.run():o?o(k.bind(null,!0),!0):u.run(),D.pause=u.pause.bind(u),D.resume=u.resume.bind(u),D.stop=D,D}function De(e,t=1/0,n){if(t<=0||!V(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,G(e))De(e.value,t,n);else if(T(e))for(let s=0;s{De(s,t,n)});else if(cr(e)){for(const s in e)De(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&De(e[s],t,n)}return e}function Kt(e,t,n,s){try{return s?e(...s):e()}catch(r){_n(r,t,n)}}function Te(e,t,n,s){if(P(e)){const r=Kt(e,t,n,s);return r&&or(r)&&r.catch(i=>{_n(i,t,n)}),r}if(T(e)){const r=[];for(let i=0;i>>1,r=ne[s],i=Dt(r);i=Dt(n)?ne.push(e):ne.splice(ho(t),0,e),e.flags|=1,Nr()}}function Nr(){rn||(rn=Pr.then(Fr))}function po(e){T(e)?dt.push(...e):Be&&e.id===-1?Be.splice(at+1,0,e):e.flags&1||(dt.push(e),e.flags|=1),Nr()}function Cs(e,t,n=Se+1){for(;nDt(n)-Dt(s));if(dt.length=0,Be){Be.push(...t);return}for(Be=t,at=0;ate.id==null?e.flags&2?-1:1/0:e.id;function Fr(e){try{for(Se=0;Se{s._d&&Ls(-1);const i=on(t);let o;try{o=e(...r)}finally{on(i),s._d&&Ls(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function wc(e,t){if(Q===null)return e;const n=Sn(Q),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,_o=Symbol("_leaveCb");function hs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,hs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function jr(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const ln=new WeakMap;function Ot(e,t,n,s,r=!1){if(T(e)){e.forEach((O,N)=>Ot(O,t&&(T(t)?t[N]:t),n,s,r));return}if(ht(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Ot(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Sn(s.component):s.el,o=r?null:i,{i:l,r:c}=e,d=t&&t.r,u=l.refs===K?l.refs={}:l.refs,p=l.setupState,S=F(p),v=p===K?ir:O=>U(S,O);if(d!=null&&d!==c){if(Is(t),J(d))u[d]=null,v(d)&&(p[d]=null);else if(G(d)){d.value=null;const O=t;O.k&&(u[O.k]=null)}}if(P(c))Kt(c,l,12,[o,u]);else{const O=J(c),N=G(c);if(O||N){const Z=()=>{if(e.f){const D=O?v(c)?p[c]:u[c]:c.value;if(r)T(D)&&ts(D,i);else if(T(D))D.includes(i)||D.push(i);else if(O)u[c]=[i],v(c)&&(p[c]=u[c]);else{const W=[i];c.value=W,e.k&&(u[e.k]=W)}}else O?(u[c]=o,v(c)&&(p[c]=o)):N&&(c.value=o,e.k&&(u[e.k]=o))};if(o){const D=()=>{Z(),ln.delete(e)};D.id=-1,ln.set(e,D),ce(D,n)}else Is(e),Z()}}}function Is(e){const t=ln.get(e);t&&(t.flags|=8,ln.delete(e))}gn().requestIdleCallback;gn().cancelIdleCallback;const ht=e=>!!e.type.__asyncLoader,Kr=e=>e.type.__isKeepAlive;function yo(e,t){Lr(e,"a",t)}function wo(e,t){Lr(e,"da",t)}function Lr(e,t,n=se){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(yn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Kr(r.parent.vnode)&&Eo(s,t,n,r),r=r.parent}}function Eo(e,t,n,s){const r=yn(t,e,s,!0);$r(()=>{ts(s[t],r)},n)}function yn(e,t,n=se,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{Ue();const l=Lt(n),c=Te(t,n,e,o);return l(),je(),c});return s?r.unshift(i):r.push(i),i}}const $e=e=>(t,n=se)=>{(!Ut||e==="sp")&&yn(e,(...s)=>t(...s),n)},So=$e("bm"),xo=$e("m"),vo=$e("bu"),Ao=$e("u"),Ro=$e("bum"),$r=$e("um"),To=$e("sp"),Oo=$e("rtg"),Co=$e("rtc");function Io(e,t=se){yn("ec",e,t)}const Po=Symbol.for("v-ndc");function Ec(e,t,n,s){let r;const i=n,o=T(e);if(o||J(e)){const l=o&&it(e);let c=!1,d=!1;l&&(c=!fe(e),d=Ke(e),e=mn(e)),r=new Array(e.length);for(let u=0,p=e.length;ut(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,d=l.length;c0;return t!=="default"&&(n.name=t),Gn(),Yn(ue,null,[Re("slot",n,s&&s())],d?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),Gn();const o=i&&Hr(i(n)),l=n.key||o&&o.key,c=Yn(ue,{key:(l&&!pe(l)?l:`_${t}`)+(!o&&s?"_fb":"")},o||(s?s():[]),o&&e._===1?64:-2);return i&&i._c&&(i._d=!0),c}function Hr(e){return e.some(t=>ms(t)?!(t.type===Le||t.type===ue&&!Hr(t.children)):!0)?e:null}const Bn=e=>e?ai(e)?Sn(e):Bn(e.parent):null,Ct=re(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Bn(e.parent),$root:e=>Bn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Wr(e),$forceUpdate:e=>e.f||(e.f=()=>{ds(e.update)}),$nextTick:e=>e.n||(e.n=Mr.bind(e.proxy)),$watch:e=>Wo.bind(e)}),Pn=(e,t)=>e!==K&&!e.__isScriptSetup&&U(e,t),Mo={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;if(t[0]!=="$"){const S=o[t];if(S!==void 0)switch(S){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Pn(s,t))return o[t]=1,s[t];if(r!==K&&U(r,t))return o[t]=2,r[t];if(U(i,t))return o[t]=3,i[t];if(n!==K&&U(n,t))return o[t]=4,n[t];kn&&(o[t]=0)}}const d=Ct[t];let u,p;if(d)return t==="$attrs"&&X(e.attrs,"get",""),d(e);if((u=l.__cssModules)&&(u=u[t]))return u;if(n!==K&&U(n,t))return o[t]=4,n[t];if(p=c.config.globalProperties,U(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Pn(r,t)?(r[t]=n,!0):s!==K&&U(s,t)?(s[t]=n,!0):U(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:o}},l){let c;return!!(n[l]||e!==K&&l[0]!=="$"&&U(e,l)||Pn(t,l)||U(i,l)||U(s,l)||U(Ct,l)||U(r.config.globalProperties,l)||(c=o.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:U(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ps(e){return T(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let kn=!0;function No(e){const t=Wr(e),n=e.proxy,s=e.ctx;kn=!1,t.beforeCreate&&Ms(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:d,created:u,beforeMount:p,mounted:S,beforeUpdate:v,updated:O,activated:N,deactivated:Z,beforeDestroy:D,beforeUnmount:W,destroyed:k,unmounted:C,render:q,renderTracked:He,renderTriggered:me,errorCaptured:Ve,serverPrefetch:$t,expose:ze,inheritAttrs:_t,components:Ht,directives:Vt,filters:vn}=t;if(d&&Do(d,s,null),o)for(const B in o){const L=o[B];P(L)&&(s[B]=L.bind(n))}if(r){const B=r.call(n,n);V(B)&&(e.data=fs(B))}if(kn=!0,i)for(const B in i){const L=i[B],Xe=P(L)?L.bind(n,n):P(L.get)?L.get.bind(n,n):Ae,Wt=!P(L)&&P(L.set)?L.set.bind(n):Ae,Qe=El({get:Xe,set:Wt});Object.defineProperty(s,B,{enumerable:!0,configurable:!0,get:()=>Qe.value,set:be=>Qe.value=be})}if(l)for(const B in l)Vr(l[B],s,n,B);if(c){const B=P(c)?c.call(n):c;Reflect.ownKeys(B).forEach(L=>{$o(L,B[L])})}u&&Ms(u,e,"c");function ee(B,L){T(L)?L.forEach(Xe=>B(Xe.bind(n))):L&&B(L.bind(n))}if(ee(So,p),ee(xo,S),ee(vo,v),ee(Ao,O),ee(yo,N),ee(wo,Z),ee(Io,Ve),ee(Co,He),ee(Oo,me),ee(Ro,W),ee($r,C),ee(To,$t),T(ze))if(ze.length){const B=e.exposed||(e.exposed={});ze.forEach(L=>{Object.defineProperty(B,L,{get:()=>n[L],set:Xe=>n[L]=Xe,enumerable:!0})})}else e.exposed||(e.exposed={});q&&e.render===Ae&&(e.render=q),_t!=null&&(e.inheritAttrs=_t),Ht&&(e.components=Ht),Vt&&(e.directives=Vt),$t&&jr(e)}function Do(e,t,n=Ae){T(e)&&(e=Jn(e));for(const s in e){const r=e[s];let i;V(r)?"default"in r?i=Xt(r.from||s,r.default,!0):i=Xt(r.from||s):i=Xt(r),G(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function Ms(e,t,n){Te(T(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Vr(e,t,n,s){let r=s.includes(".")?Jr(n,s):()=>n[s];if(J(e)){const i=t[e];P(i)&&Mn(r,i)}else if(P(e))Mn(r,e.bind(n));else if(V(e))if(T(e))e.forEach(i=>Vr(i,t,n,s));else{const i=P(e.handler)?e.handler.bind(n):t[e.handler];P(i)&&Mn(r,i,e)}}function Wr(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(d=>cn(c,d,o,!0)),cn(c,t,o)),V(t)&&i.set(t,c),c}function cn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&cn(e,i,n,!0),r&&r.forEach(o=>cn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=Fo[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const Fo={data:Ns,props:Ds,emits:Ds,methods:vt,computed:vt,beforeCreate:te,created:te,beforeMount:te,mounted:te,beforeUpdate:te,updated:te,beforeDestroy:te,beforeUnmount:te,destroyed:te,unmounted:te,activated:te,deactivated:te,errorCaptured:te,serverPrefetch:te,components:vt,directives:vt,watch:jo,provide:Ns,inject:Uo};function Ns(e,t){return t?e?function(){return re(P(e)?e.call(this,this):e,P(t)?t.call(this,this):t)}:t:e}function Uo(e,t){return vt(Jn(e),Jn(t))}function Jn(e){if(T(e)){const t={};for(let n=0;n1)return n&&P(t)?t.call(s&&s.proxy):t}}function xc(){return!!(ci()||ot)}const Ho=Symbol.for("v-scx"),Vo=()=>Xt(Ho);function Mn(e,t,n){return kr(e,t,n)}function kr(e,t,n=K){const{immediate:s,deep:r,flush:i,once:o}=n,l=re({},n),c=t&&s||!t&&i!=="post";let d;if(Ut){if(i==="sync"){const v=Vo();d=v.__watcherHandles||(v.__watcherHandles=[])}else if(!c){const v=()=>{};return v.stop=Ae,v.resume=Ae,v.pause=Ae,v}}const u=se;l.call=(v,O,N)=>Te(v,u,O,N);let p=!1;i==="post"?l.scheduler=v=>{ce(v,u&&u.suspense)}:i!=="sync"&&(p=!0,l.scheduler=(v,O)=>{O?v():ds(v)}),l.augmentJob=v=>{t&&(v.flags|=4),p&&(v.flags|=2,u&&(v.id=u.uid,v.i=u))};const S=fo(e,t,l);return Ut&&(d?d.push(S):c&&S()),S}function Wo(e,t,n){const s=this.proxy,r=J(e)?e.includes(".")?Jr(s,e):()=>s[e]:e.bind(s,s);let i;P(t)?i=t:(i=t.handler,n=t);const o=Lt(this),l=kr(r,i.bind(s),n);return o(),l}function Jr(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${qe(t)}Modifiers`]||e[`${Ye(t)}Modifiers`];function ko(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||K;let r=n;const i=t.startsWith("update:"),o=i&&Bo(s,t.slice(7));o&&(o.trim&&(r=n.map(u=>J(u)?u.trim():u)),o.number&&(r=n.map(pn)));let l,c=s[l=Rn(t)]||s[l=Rn(qe(t))];!c&&i&&(c=s[l=Rn(Ye(t))]),c&&Te(c,e,6,r);const d=s[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Te(d,e,6,r)}}const Jo=new WeakMap;function qr(e,t,n=!1){const s=n?Jo:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!P(e)){const c=d=>{const u=qr(d,t,!0);u&&(l=!0,re(o,u))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(V(e)&&s.set(e,null),null):(T(i)?i.forEach(c=>o[c]=null):re(o,i),V(e)&&s.set(e,o),o)}function wn(e,t){return!e||!un(t)?!1:(t=t.slice(2).replace(/Once$/,""),U(e,t[0].toLowerCase()+t.slice(1))||U(e,Ye(t))||U(e,t))}function Fs(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:d,renderCache:u,props:p,data:S,setupState:v,ctx:O,inheritAttrs:N}=e,Z=on(e);let D,W;try{if(n.shapeFlag&4){const C=r||s,q=C;D=xe(d.call(q,C,u,p,v,S,O)),W=l}else{const C=t;D=xe(C.length>1?C(p,{attrs:l,slots:o,emit:c}):C(p,null)),W=t.props?l:qo(l)}}catch(C){It.length=0,_n(C,e,1),D=Re(Le)}let k=D;if(W&&N!==!1){const C=Object.keys(W),{shapeFlag:q}=k;C.length&&q&7&&(i&&C.some(es)&&(W=Go(W,i)),k=gt(k,W,!1,!0))}return n.dirs&&(k=gt(k,null,!1,!0),k.dirs=k.dirs?k.dirs.concat(n.dirs):n.dirs),n.transition&&hs(k,n.transition),D=k,on(Z),D}const qo=e=>{let t;for(const n in e)(n==="class"||n==="style"||un(n))&&((t||(t={}))[n]=e[n]);return t},Go=(e,t)=>{const n={};for(const s in e)(!es(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Yo(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Us(s,o,d):!!o;if(c&8){const u=t.dynamicProps;for(let p=0;pObject.create(Gr),zr=e=>Object.getPrototypeOf(e)===Gr;function Xo(e,t,n,s=!1){const r={},i=Yr();e.propsDefaults=Object.create(null),Xr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:eo(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Qo(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=F(r),[c]=e.propsOptions;let d=!1;if((s||o>0)&&!(o&16)){if(o&8){const u=e.vnode.dynamicProps;for(let p=0;p{c=!0;const[S,v]=Qr(p,t,!0);re(o,S),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!i&&!c)return V(e)&&s.set(e,ft),ft;if(T(i))for(let u=0;ue==="_"||e==="_ctx"||e==="$stable",gs=e=>T(e)?e.map(xe):[xe(e)],el=(e,t,n)=>{if(t._n)return t;const s=go((...r)=>gs(t(...r)),n);return s._c=!1,s},Zr=(e,t,n)=>{const s=e._ctx;for(const r in e){if(ps(r))continue;const i=e[r];if(P(i))t[r]=el(r,i,s);else if(i!=null){const o=gs(i);t[r]=()=>o}}},ei=(e,t)=>{const n=gs(t);e.slots.default=()=>n},ti=(e,t,n)=>{for(const s in t)(n||!ps(s))&&(e[s]=t[s])},tl=(e,t,n)=>{const s=e.slots=Yr();if(e.vnode.shapeFlag&32){const r=t._;r?(ti(s,t,n),n&&fr(s,"_",r,!0)):Zr(t,s)}else t&&ei(e,t)},nl=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=K;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:ti(r,t,n):(i=!t.$stable,Zr(t,r)),o=t}else t&&(ei(e,t),o={default:1});if(i)for(const l in r)!ps(l)&&o[l]==null&&delete r[l]},ce=ll;function sl(e){return rl(e)}function rl(e,t){const n=gn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:d,setElementText:u,parentNode:p,nextSibling:S,setScopeId:v=Ae,insertStaticContent:O}=e,N=(a,f,h,_=null,g=null,m=null,E=void 0,w=null,y=!!f.dynamicChildren)=>{if(a===f)return;a&&!xt(a,f)&&(_=Bt(a),be(a,g,m,!0),a=null),f.patchFlag===-2&&(y=!1,f.dynamicChildren=null);const{type:b,ref:R,shapeFlag:x}=f;switch(b){case En:Z(a,f,h,_);break;case Le:D(a,f,h,_);break;case Dn:a==null&&W(f,h,_,E);break;case ue:Ht(a,f,h,_,g,m,E,w,y);break;default:x&1?q(a,f,h,_,g,m,E,w,y):x&6?Vt(a,f,h,_,g,m,E,w,y):(x&64||x&128)&&b.process(a,f,h,_,g,m,E,w,y,wt)}R!=null&&g?Ot(R,a&&a.ref,m,f||a,!f):R==null&&a&&a.ref!=null&&Ot(a.ref,null,m,a,!0)},Z=(a,f,h,_)=>{if(a==null)s(f.el=l(f.children),h,_);else{const g=f.el=a.el;f.children!==a.children&&d(g,f.children)}},D=(a,f,h,_)=>{a==null?s(f.el=c(f.children||""),h,_):f.el=a.el},W=(a,f,h,_)=>{[a.el,a.anchor]=O(a.children,f,h,_,a.el,a.anchor)},k=({el:a,anchor:f},h,_)=>{let g;for(;a&&a!==f;)g=S(a),s(a,h,_),a=g;s(f,h,_)},C=({el:a,anchor:f})=>{let h;for(;a&&a!==f;)h=S(a),r(a),a=h;r(f)},q=(a,f,h,_,g,m,E,w,y)=>{if(f.type==="svg"?E="svg":f.type==="math"&&(E="mathml"),a==null)He(f,h,_,g,m,E,w,y);else{const b=a.el&&a.el._isVueCE?a.el:null;try{b&&b._beginPatch(),$t(a,f,g,m,E,w,y)}finally{b&&b._endPatch()}}},He=(a,f,h,_,g,m,E,w)=>{let y,b;const{props:R,shapeFlag:x,transition:A,dirs:I}=a;if(y=a.el=o(a.type,m,R&&R.is,R),x&8?u(y,a.children):x&16&&Ve(a.children,y,null,_,g,Nn(a,m),E,w),I&&Ze(a,null,_,"created"),me(y,a,a.scopeId,E,_),R){for(const $ in R)$!=="value"&&!At($)&&i(y,$,null,R[$],m,_);"value"in R&&i(y,"value",null,R.value,m),(b=R.onVnodeBeforeMount)&&Ee(b,_,a)}I&&Ze(a,null,_,"beforeMount");const M=il(g,A);M&&A.beforeEnter(y),s(y,f,h),((b=R&&R.onVnodeMounted)||M||I)&&ce(()=>{b&&Ee(b,_,a),M&&A.enter(y),I&&Ze(a,null,_,"mounted")},g)},me=(a,f,h,_,g)=>{if(h&&v(a,h),_)for(let m=0;m<_.length;m++)v(a,_[m]);if(g){let m=g.subTree;if(f===m||ri(m.type)&&(m.ssContent===f||m.ssFallback===f)){const E=g.vnode;me(a,E,E.scopeId,E.slotScopeIds,g.parent)}}},Ve=(a,f,h,_,g,m,E,w,y=0)=>{for(let b=y;b{const w=f.el=a.el;let{patchFlag:y,dynamicChildren:b,dirs:R}=f;y|=a.patchFlag&16;const x=a.props||K,A=f.props||K;let I;if(h&&et(h,!1),(I=A.onVnodeBeforeUpdate)&&Ee(I,h,f,a),R&&Ze(f,a,h,"beforeUpdate"),h&&et(h,!0),(x.innerHTML&&A.innerHTML==null||x.textContent&&A.textContent==null)&&u(w,""),b?ze(a.dynamicChildren,b,w,h,_,Nn(f,g),m):E||L(a,f,w,null,h,_,Nn(f,g),m,!1),y>0){if(y&16)_t(w,x,A,h,g);else if(y&2&&x.class!==A.class&&i(w,"class",null,A.class,g),y&4&&i(w,"style",x.style,A.style,g),y&8){const M=f.dynamicProps;for(let $=0;${I&&Ee(I,h,f,a),R&&Ze(f,a,h,"updated")},_)},ze=(a,f,h,_,g,m,E)=>{for(let w=0;w{if(f!==h){if(f!==K)for(const m in f)!At(m)&&!(m in h)&&i(a,m,f[m],null,g,_);for(const m in h){if(At(m))continue;const E=h[m],w=f[m];E!==w&&m!=="value"&&i(a,m,w,E,g,_)}"value"in h&&i(a,"value",f.value,h.value,g)}},Ht=(a,f,h,_,g,m,E,w,y)=>{const b=f.el=a?a.el:l(""),R=f.anchor=a?a.anchor:l("");let{patchFlag:x,dynamicChildren:A,slotScopeIds:I}=f;I&&(w=w?w.concat(I):I),a==null?(s(b,h,_),s(R,h,_),Ve(f.children||[],h,R,g,m,E,w,y)):x>0&&x&64&&A&&a.dynamicChildren?(ze(a.dynamicChildren,A,h,g,m,E,w),(f.key!=null||g&&f===g.subTree)&&ni(a,f,!0)):L(a,f,h,R,g,m,E,w,y)},Vt=(a,f,h,_,g,m,E,w,y)=>{f.slotScopeIds=w,a==null?f.shapeFlag&512?g.ctx.activate(f,h,_,E,y):vn(f,h,_,g,m,E,y):ys(a,f,y)},vn=(a,f,h,_,g,m,E)=>{const w=a.component=gl(a,_,g);if(Kr(a)&&(w.ctx.renderer=wt),ml(w,!1,E),w.asyncDep){if(g&&g.registerDep(w,ee,E),!a.el){const y=w.subTree=Re(Le);D(null,y,f,h),a.placeholder=y.el}}else ee(w,a,f,h,g,m,E)},ys=(a,f,h)=>{const _=f.component=a.component;if(Yo(a,f,h))if(_.asyncDep&&!_.asyncResolved){B(_,f,h);return}else _.next=f,_.update();else f.el=a.el,_.vnode=f},ee=(a,f,h,_,g,m,E)=>{const w=()=>{if(a.isMounted){let{next:x,bu:A,u:I,parent:M,vnode:$}=a;{const ye=si(a);if(ye){x&&(x.el=$.el,B(a,x,E)),ye.asyncDep.then(()=>{a.isUnmounted||w()});return}}let j=x,ie;et(a,!1),x?(x.el=$.el,B(a,x,E)):x=$,A&&zt(A),(ie=x.props&&x.props.onVnodeBeforeUpdate)&&Ee(ie,M,x,$),et(a,!0);const oe=Fs(a),_e=a.subTree;a.subTree=oe,N(_e,oe,p(_e.el),Bt(_e),a,g,m),x.el=oe.el,j===null&&zo(a,oe.el),I&&ce(I,g),(ie=x.props&&x.props.onVnodeUpdated)&&ce(()=>Ee(ie,M,x,$),g)}else{let x;const{el:A,props:I}=f,{bm:M,m:$,parent:j,root:ie,type:oe}=a,_e=ht(f);et(a,!1),M&&zt(M),!_e&&(x=I&&I.onVnodeBeforeMount)&&Ee(x,j,f),et(a,!0);{ie.ce&&ie.ce._def.shadowRoot!==!1&&ie.ce._injectChildStyle(oe);const ye=a.subTree=Fs(a);N(null,ye,h,_,a,g,m),f.el=ye.el}if($&&ce($,g),!_e&&(x=I&&I.onVnodeMounted)){const ye=f;ce(()=>Ee(x,j,ye),g)}(f.shapeFlag&256||j&&ht(j.vnode)&&j.vnode.shapeFlag&256)&&a.a&&ce(a.a,g),a.isMounted=!0,f=h=_=null}};a.scope.on();const y=a.effect=new gr(w);a.scope.off();const b=a.update=y.run.bind(y),R=a.job=y.runIfDirty.bind(y);R.i=a,R.id=a.uid,y.scheduler=()=>ds(R),et(a,!0),b()},B=(a,f,h)=>{f.component=a;const _=a.vnode.props;a.vnode=f,a.next=null,Qo(a,f.props,_,h),nl(a,f.children,h),Ue(),Cs(a),je()},L=(a,f,h,_,g,m,E,w,y=!1)=>{const b=a&&a.children,R=a?a.shapeFlag:0,x=f.children,{patchFlag:A,shapeFlag:I}=f;if(A>0){if(A&128){Wt(b,x,h,_,g,m,E,w,y);return}else if(A&256){Xe(b,x,h,_,g,m,E,w,y);return}}I&8?(R&16&&yt(b,g,m),x!==b&&u(h,x)):R&16?I&16?Wt(b,x,h,_,g,m,E,w,y):yt(b,g,m,!0):(R&8&&u(h,""),I&16&&Ve(x,h,_,g,m,E,w,y))},Xe=(a,f,h,_,g,m,E,w,y)=>{a=a||ft,f=f||ft;const b=a.length,R=f.length,x=Math.min(b,R);let A;for(A=0;AR?yt(a,g,m,!0,!1,x):Ve(f,h,_,g,m,E,w,y,x)},Wt=(a,f,h,_,g,m,E,w,y)=>{let b=0;const R=f.length;let x=a.length-1,A=R-1;for(;b<=x&&b<=A;){const I=a[b],M=f[b]=y?ke(f[b]):xe(f[b]);if(xt(I,M))N(I,M,h,null,g,m,E,w,y);else break;b++}for(;b<=x&&b<=A;){const I=a[x],M=f[A]=y?ke(f[A]):xe(f[A]);if(xt(I,M))N(I,M,h,null,g,m,E,w,y);else break;x--,A--}if(b>x){if(b<=A){const I=A+1,M=IA)for(;b<=x;)be(a[b],g,m,!0),b++;else{const I=b,M=b,$=new Map;for(b=M;b<=A;b++){const le=f[b]=y?ke(f[b]):xe(f[b]);le.key!=null&&$.set(le.key,b)}let j,ie=0;const oe=A-M+1;let _e=!1,ye=0;const Et=new Array(oe);for(b=0;b=oe){be(le,g,m,!0);continue}let we;if(le.key!=null)we=$.get(le.key);else for(j=M;j<=A;j++)if(Et[j-M]===0&&xt(le,f[j])){we=j;break}we===void 0?be(le,g,m,!0):(Et[we-M]=b+1,we>=ye?ye=we:_e=!0,N(le,f[we],h,null,g,m,E,w,y),ie++)}const Ss=_e?ol(Et):ft;for(j=Ss.length-1,b=oe-1;b>=0;b--){const le=M+b,we=f[le],xs=f[le+1],vs=le+1{const{el:m,type:E,transition:w,children:y,shapeFlag:b}=a;if(b&6){Qe(a.component.subTree,f,h,_);return}if(b&128){a.suspense.move(f,h,_);return}if(b&64){E.move(a,f,h,wt);return}if(E===ue){s(m,f,h);for(let x=0;xw.enter(m),g);else{const{leave:x,delayLeave:A,afterLeave:I}=w,M=()=>{a.ctx.isUnmounted?r(m):s(m,f,h)},$=()=>{m._isLeaving&&m[_o](!0),x(m,()=>{M(),I&&I()})};A?A(m,M,$):$()}else s(m,f,h)},be=(a,f,h,_=!1,g=!1)=>{const{type:m,props:E,ref:w,children:y,dynamicChildren:b,shapeFlag:R,patchFlag:x,dirs:A,cacheIndex:I}=a;if(x===-2&&(g=!1),w!=null&&(Ue(),Ot(w,null,h,a,!0),je()),I!=null&&(f.renderCache[I]=void 0),R&256){f.ctx.deactivate(a);return}const M=R&1&&A,$=!ht(a);let j;if($&&(j=E&&E.onVnodeBeforeUnmount)&&Ee(j,f,a),R&6)Ei(a.component,h,_);else{if(R&128){a.suspense.unmount(h,_);return}M&&Ze(a,null,f,"beforeUnmount"),R&64?a.type.remove(a,f,h,wt,_):b&&!b.hasOnce&&(m!==ue||x>0&&x&64)?yt(b,f,h,!1,!0):(m===ue&&x&384||!g&&R&16)&&yt(y,f,h),_&&ws(a)}($&&(j=E&&E.onVnodeUnmounted)||M)&&ce(()=>{j&&Ee(j,f,a),M&&Ze(a,null,f,"unmounted")},h)},ws=a=>{const{type:f,el:h,anchor:_,transition:g}=a;if(f===ue){wi(h,_);return}if(f===Dn){C(a);return}const m=()=>{r(h),g&&!g.persisted&&g.afterLeave&&g.afterLeave()};if(a.shapeFlag&1&&g&&!g.persisted){const{leave:E,delayLeave:w}=g,y=()=>E(h,m);w?w(a.el,m,y):y()}else m()},wi=(a,f)=>{let h;for(;a!==f;)h=S(a),r(a),a=h;r(f)},Ei=(a,f,h)=>{const{bum:_,scope:g,job:m,subTree:E,um:w,m:y,a:b}=a;Ks(y),Ks(b),_&&zt(_),g.stop(),m&&(m.flags|=8,be(E,a,f,h)),w&&ce(w,f),ce(()=>{a.isUnmounted=!0},f)},yt=(a,f,h,_=!1,g=!1,m=0)=>{for(let E=m;E{if(a.shapeFlag&6)return Bt(a.component.subTree);if(a.shapeFlag&128)return a.suspense.next();const f=S(a.anchor||a.el),h=f&&f[mo];return h?S(h):f};let An=!1;const Es=(a,f,h)=>{a==null?f._vnode&&be(f._vnode,null,null,!0):N(f._vnode||null,a,f,null,null,null,h),f._vnode=a,An||(An=!0,Cs(),Dr(),An=!1)},wt={p:N,um:be,m:Qe,r:ws,mt:vn,mc:Ve,pc:L,pbc:ze,n:Bt,o:e};return{render:Es,hydrate:void 0,createApp:Lo(Es)}}function Nn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function et({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function il(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function ni(e,t,n=!1){const s=e.children,r=t.children;if(T(s)&&T(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function si(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:si(t)}function Ks(e){if(e)for(let t=0;te.__isSuspense;function ll(e,t){t&&t.pendingBranch?T(e)?t.effects.push(...e):t.effects.push(e):po(e)}const ue=Symbol.for("v-fgt"),En=Symbol.for("v-txt"),Le=Symbol.for("v-cmt"),Dn=Symbol.for("v-stc"),It=[];let ae=null;function Gn(e=!1){It.push(ae=e?null:[])}function cl(){It.pop(),ae=It[It.length-1]||null}let Ft=1;function Ls(e,t=!1){Ft+=e,e<0&&ae&&t&&(ae.hasOnce=!0)}function ii(e){return e.dynamicChildren=Ft>0?ae||ft:null,cl(),Ft>0&&ae&&ae.push(e),e}function vc(e,t,n,s,r,i){return ii(li(e,t,n,s,r,i,!0))}function Yn(e,t,n,s,r){return ii(Re(e,t,n,s,r,!0))}function ms(e){return e?e.__v_isVNode===!0:!1}function xt(e,t){return e.type===t.type&&e.key===t.key}const oi=({key:e})=>e??null,Qt=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?J(e)||G(e)||P(e)?{i:Q,r:e,k:t,f:!!n}:e:null);function li(e,t=null,n=null,s=0,r=null,i=e===ue?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&oi(t),ref:t&&Qt(t),scopeId:Ur,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Q};return l?(bs(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=J(n)?8:16),Ft>0&&!o&&ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&ae.push(c),c}const Re=al;function al(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Po)&&(e=Le),ms(e)){const l=gt(e,t,!0);return n&&bs(l,n),Ft>0&&!i&&ae&&(l.shapeFlag&6?ae[ae.indexOf(e)]=l:ae.push(l)),l.patchFlag=-2,l}if(wl(e)&&(e=e.__vccOpts),t){t=fl(t);let{class:l,style:c}=t;l&&!J(l)&&(t.class=ss(l)),V(c)&&(bn(c)&&!T(c)&&(c=re({},c)),t.style=ns(c))}const o=J(e)?1:ri(e)?128:bo(e)?64:V(e)?4:P(e)?2:0;return li(e,t,n,s,r,o,i,!0)}function fl(e){return e?bn(e)||zr(e)?re({},e):e:null}function gt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,d=t?dl(r||{},t):r,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&oi(d),ref:t&&t.ref?n&&i?T(i)?i.concat(Qt(t)):[i,Qt(t)]:Qt(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ue?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&>(e.ssContent),ssFallback:e.ssFallback&>(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&hs(u,c.clone(u)),u}function ul(e=" ",t=0){return Re(En,null,e,t)}function Ac(e="",t=!1){return t?(Gn(),Yn(Le,null,e)):Re(Le,null,e)}function xe(e){return e==null||typeof e=="boolean"?Re(Le):T(e)?Re(ue,null,e.slice()):ms(e)?ke(e):Re(En,null,String(e))}function ke(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:gt(e)}function bs(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(T(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),bs(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!zr(t)?t._ctx=Q:r===3&&Q&&(Q.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else P(t)?(t={default:t,_ctx:Q},n=32):(t=String(t),s&64?(n=16,t=[ul(t)]):n=8);e.children=t,e.shapeFlag|=n}function dl(...e){const t={};for(let n=0;nse||Q;let an,zn;{const e=gn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};an=t("__VUE_INSTANCE_SETTERS__",n=>se=n),zn=t("__VUE_SSR_SETTERS__",n=>Ut=n)}const Lt=e=>{const t=se;return an(e),e.scope.on(),()=>{e.scope.off(),an(t)}},$s=()=>{se&&se.scope.off(),an(null)};function ai(e){return e.vnode.shapeFlag&4}let Ut=!1;function ml(e,t=!1,n=!1){t&&zn(t);const{props:s,children:r}=e.vnode,i=ai(e);Xo(e,s,i,t),tl(e,r,n||t);const o=i?bl(e,t):void 0;return t&&zn(!1),o}function bl(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Mo);const{setup:s}=n;if(s){Ue();const r=e.setupContext=s.length>1?yl(e):null,i=Lt(e),o=Kt(s,e,0,[e.props,r]),l=or(o);if(je(),i(),(l||e.sp)&&!ht(e)&&jr(e),l){if(o.then($s,$s),t)return o.then(c=>{Hs(e,c)}).catch(c=>{_n(c,e,0)});e.asyncDep=o}else Hs(e,o)}else fi(e)}function Hs(e,t,n){P(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:V(t)&&(e.setupState=Ir(t)),fi(e)}function fi(e,t,n){const s=e.type;e.render||(e.render=s.render||Ae);{const r=Lt(e);Ue();try{No(e)}finally{je(),r()}}}const _l={get(e,t){return X(e,"get",""),e[t]}};function yl(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,_l),slots:e.slots,emit:e.emit,expose:t}}function Sn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Ir(to(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Ct)return Ct[n](e)},has(t,n){return n in t||n in Ct}})):e.proxy}function wl(e){return P(e)&&"__vccOpts"in e}const El=(e,t)=>co(e,t,Ut),Sl="3.5.25";let Xn;const Vs=typeof window<"u"&&window.trustedTypes;if(Vs)try{Xn=Vs.createPolicy("vue",{createHTML:e=>e})}catch{}const ui=Xn?e=>Xn.createHTML(e):e=>e,xl="http://www.w3.org/2000/svg",vl="http://www.w3.org/1998/Math/MathML",Ce=typeof document<"u"?document:null,Ws=Ce&&Ce.createElement("template"),Al={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ce.createElementNS(xl,e):t==="mathml"?Ce.createElementNS(vl,e):n?Ce.createElement(e,{is:n}):Ce.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ce.createTextNode(e),createComment:e=>Ce.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ce.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Ws.innerHTML=ui(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Ws.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Rl=Symbol("_vtc");function Tl(e,t,n){const s=e[Rl];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Bs=Symbol("_vod"),Ol=Symbol("_vsh"),Cl=Symbol(""),Il=/(?:^|;)\s*display\s*:/;function Pl(e,t,n){const s=e.style,r=J(n);let i=!1;if(n&&!r){if(t)if(J(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&Zt(s,l,"")}else for(const o in t)n[o]==null&&Zt(s,o,"");for(const o in n)o==="display"&&(i=!0),Zt(s,o,n[o])}else if(r){if(t!==n){const o=s[Cl];o&&(n+=";"+o),s.cssText=n,i=Il.test(n)}}else t&&e.removeAttribute("style");Bs in e&&(e[Bs]=i?s.display:"",e[Ol]&&(s.display="none"))}const ks=/\s*!important$/;function Zt(e,t,n){if(T(n))n.forEach(s=>Zt(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Ml(e,t);ks.test(n)?e.setProperty(Ye(s),n.replace(ks,""),"important"):e[s]=n}}const Js=["Webkit","Moz","ms"],Fn={};function Ml(e,t){const n=Fn[t];if(n)return n;let s=qe(t);if(s!=="filter"&&s in e)return Fn[t]=s;s=ar(s);for(let r=0;rUn||(Ul.then(()=>Un=0),Un=Date.now());function Kl(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Te(Ll(s,n.value),t,5,[s])};return n.value=e,n.attached=jl(),n}function Ll(e,t){if(T(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Qs=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,$l=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?Tl(e,s,o):t==="style"?Pl(e,n,s):un(t)?es(t)||Dl(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Hl(e,t,s,o))?(Ys(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Gs(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!J(s))?Ys(e,qe(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Gs(e,t,s,o))};function Hl(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Qs(t)&&P(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Qs(t)&&J(n)?!1:t in e}const Ge=e=>{const t=e.props["onUpdate:modelValue"]||!1;return T(t)?n=>zt(t,n):t};function Vl(e){e.target.composing=!0}function Zs(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const de=Symbol("_assign");function er(e,t,n){return t&&(e=e.trim()),n&&(e=pn(e)),e}const tr={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[de]=Ge(r);const i=s||r.props&&r.props.type==="number";Fe(e,t?"change":"input",o=>{o.target.composing||e[de](er(e.value,n,i))}),(n||i)&&Fe(e,"change",()=>{e.value=er(e.value,n,i)}),t||(Fe(e,"compositionstart",Vl),Fe(e,"compositionend",Zs),Fe(e,"change",Zs))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[de]=Ge(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?pn(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},Wl={deep:!0,created(e,t,n){e[de]=Ge(n),Fe(e,"change",()=>{const s=e._modelValue,r=mt(e),i=e.checked,o=e[de];if(T(s)){const l=rs(s,r),c=l!==-1;if(i&&!c)o(s.concat(r));else if(!i&&c){const d=[...s];d.splice(l,1),o(d)}}else if(bt(s)){const l=new Set(s);i?l.add(r):l.delete(r),o(l)}else o(di(e,i))})},mounted:nr,beforeUpdate(e,t,n){e[de]=Ge(n),nr(e,t,n)}};function nr(e,{value:t,oldValue:n},s){e._modelValue=t;let r;if(T(t))r=rs(t,s.props.value)>-1;else if(bt(t))r=t.has(s.props.value);else{if(t===n)return;r=lt(t,di(e,!0))}e.checked!==r&&(e.checked=r)}const Bl={created(e,{value:t},n){e.checked=lt(t,n.props.value),e[de]=Ge(n),Fe(e,"change",()=>{e[de](mt(e))})},beforeUpdate(e,{value:t,oldValue:n},s){e[de]=Ge(s),t!==n&&(e.checked=lt(t,s.props.value))}},kl={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const r=bt(t);Fe(e,"change",()=>{const i=Array.prototype.filter.call(e.options,o=>o.selected).map(o=>n?pn(mt(o)):mt(o));e[de](e.multiple?r?new Set(i):i:i[0]),e._assigning=!0,Mr(()=>{e._assigning=!1})}),e[de]=Ge(s)},mounted(e,{value:t}){sr(e,t)},beforeUpdate(e,t,n){e[de]=Ge(n)},updated(e,{value:t}){e._assigning||sr(e,t)}};function sr(e,t){const n=e.multiple,s=T(t);if(!(n&&!s&&!bt(t))){for(let r=0,i=e.options.length;rString(d)===String(l)):o.selected=rs(t,l)>-1}else o.selected=t.has(l);else if(lt(mt(o),t)){e.selectedIndex!==r&&(e.selectedIndex=r);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function mt(e){return"_value"in e?e._value:e.value}function di(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const Rc={created(e,t,n){Gt(e,t,n,null,"created")},mounted(e,t,n){Gt(e,t,n,null,"mounted")},beforeUpdate(e,t,n,s){Gt(e,t,n,s,"beforeUpdate")},updated(e,t,n,s){Gt(e,t,n,s,"updated")}};function Jl(e,t){switch(e){case"SELECT":return kl;case"TEXTAREA":return tr;default:switch(t){case"checkbox":return Wl;case"radio":return Bl;default:return tr}}}function Gt(e,t,n,s,r){const o=Jl(e.tagName,n.props&&n.props.type)[r];o&&o(e,t,n,s)}const ql=["ctrl","shift","alt","meta"],Gl={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>ql.some(n=>e[`${n}Key`]&&!t.includes(n))},Tc=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=((r,...i)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=(r=>{if(!("key"in r))return;const i=Ye(r.key);if(t.some(o=>o===i||Yl[o]===i))return e(r)}))},zl=re({patchProp:$l},Al);let rr;function Xl(){return rr||(rr=sl(zl))}const Cc=((...e)=>{const t=Xl().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Zl(s);if(!r)return;const i=t._component;!P(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Ql(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function Ql(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Zl(e){return J(e)?document.querySelector(e):e}function ve(e){const t=new Uint8Array(e);let n="";for(const r of t)n+=String.fromCharCode(r);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function fn(e){const t=e.replace(/-/g,"+").replace(/_/g,"/"),n=(4-t.length%4)%4,s=t.padEnd(t.length+n,"="),r=atob(s),i=new ArrayBuffer(r.length),o=new Uint8Array(i);for(let l=0;le};function hi(e){const{id:t}=e;return{...e,id:fn(t),transports:e.transports}}function pi(e){return e==="localhost"||/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(e)}class Y extends Error{constructor({message:t,code:n,cause:s,name:r}){super(t,{cause:s}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=r??s.name,this.code=n}}function tc({error:e,options:t}){const{publicKey:n}=t;if(!n)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new Y({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(n.authenticatorSelection?.requireResidentKey===!0)return new Y({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&n.authenticatorSelection?.userVerification==="required")return new Y({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(n.authenticatorSelection?.userVerification==="required")return new Y({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new Y({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new Y({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return n.pubKeyCredParams.filter(r=>r.type==="public-key").length===0?new Y({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new Y({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){const s=globalThis.location.hostname;if(pi(s)){if(n.rp.id!==s)return new Y({message:`The RP ID "${n.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new Y({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(n.user.id.byteLength<1||n.user.id.byteLength>64)return new Y({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new Y({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}class nc{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){const n=new Error("Cancelling existing WebAuthn API call for new one");n.name="AbortError",this.controller.abort(n)}const t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){const t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}}const gi=new nc,sc=["cross-platform","platform"];function mi(e){if(e&&!(sc.indexOf(e)<0))return e}async function rc(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});const{optionsJSON:t,useAutoRegister:n=!1}=e;if(!_s())throw new Error("WebAuthn is not supported in this browser");const s={...t,challenge:fn(t.challenge),user:{...t.user,id:fn(t.user.id)},excludeCredentials:t.excludeCredentials?.map(hi)},r={};n&&(r.mediation="conditional"),r.publicKey=s,r.signal=gi.createNewAbortSignal();let i;try{i=await navigator.credentials.create(r)}catch(O){throw tc({error:O,options:r})}if(!i)throw new Error("Registration was not completed");const{id:o,rawId:l,response:c,type:d}=i;let u;typeof c.getTransports=="function"&&(u=c.getTransports());let p;if(typeof c.getPublicKeyAlgorithm=="function")try{p=c.getPublicKeyAlgorithm()}catch(O){jn("getPublicKeyAlgorithm()",O)}let S;if(typeof c.getPublicKey=="function")try{const O=c.getPublicKey();O!==null&&(S=ve(O))}catch(O){jn("getPublicKey()",O)}let v;if(typeof c.getAuthenticatorData=="function")try{v=ve(c.getAuthenticatorData())}catch(O){jn("getAuthenticatorData()",O)}return{id:o,rawId:ve(l),response:{attestationObject:ve(c.attestationObject),clientDataJSON:ve(c.clientDataJSON),transports:u,publicKeyAlgorithm:p,publicKey:S,authenticatorData:v},type:d,clientExtensionResults:i.getClientExtensionResults(),authenticatorAttachment:mi(i.authenticatorAttachment)}}function jn(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them. -`,t)}function ic(){if(!_s())return Kn.stubThis(new Promise(t=>t(!1)));const e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?Kn.stubThis(new Promise(t=>t(!1))):Kn.stubThis(e.isConditionalMediationAvailable())}const Kn={stubThis:e=>e};function oc({error:e,options:t}){const{publicKey:n}=t;if(!n)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new Y({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new Y({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){const s=globalThis.location.hostname;if(pi(s)){if(n.rpId!==s)return new Y({message:`The RP ID "${n.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new Y({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new Y({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function lc(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});const{optionsJSON:t,useBrowserAutofill:n=!1,verifyBrowserAutofillInput:s=!0}=e;if(!_s())throw new Error("WebAuthn is not supported in this browser");let r;t.allowCredentials?.length!==0&&(r=t.allowCredentials?.map(hi));const i={...t,challenge:fn(t.challenge),allowCredentials:r},o={};if(n){if(!await ic())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&s)throw Error('No with "webauthn" as the only or last value in its `autocomplete` attribute was detected');o.mediation="conditional",i.allowCredentials=[]}o.publicKey=i,o.signal=gi.createNewAbortSignal();let l;try{l=await navigator.credentials.get(o)}catch(v){throw oc({error:v,options:o})}if(!l)throw new Error("Authentication was not completed");const{id:c,rawId:d,response:u,type:p}=l;let S;return u.userHandle&&(S=ve(u.userHandle)),{id:c,rawId:ve(d),response:{authenticatorData:ve(u.authenticatorData),clientDataJSON:ve(u.clientDataJSON),signature:ve(u.signature),userHandle:S},type:p,clientExtensionResults:l.getClientExtensionResults(),authenticatorAttachment:mi(l.authenticatorAttachment)}}class cc extends WebSocket{#n=[];#e=[];#t=null;#s=!1;constructor(t,n,s,r,i){super(new URL(s,document.baseURI.replace(/^http/,"ws")),r),this.binaryType=i||"blob",this.onopen=()=>{this.#s=!0,t(this)},this.onmessage=o=>{this.#e.length?this.#e.shift().resolve(o.data):this.#n.push(o.data)},this.onclose=o=>{if(!this.#s){n(new Error(`WebSocket ${this.url} failed to connect, code ${o.code}`));return}this.#t=o.wasClean?new Error(`Websocket ${this.url} closed ${o.code}`):new Error(`WebSocket ${this.url} closed with error ${o.code}`),this.#e.splice(0).forEach(l=>l.reject(this.#t))}}receive(){return this.#n.length?Promise.resolve(this.#n.shift()):this.#t?Promise.reject(this.#t):new Promise((t,n)=>this.#e.push({resolve:t,reject:n}))}async receive_bytes(){const t=await this.receive();if(typeof t=="string")throw console.error("WebSocket received text data, expected a binary message",t),new Error("WebSocket received text data, expected a binary message");return t instanceof Blob?t.bytes():new Uint8Array(t)}async receive_json(){const t=await this.receive();if(typeof t!="string")throw console.error("WebSocket received binary data, expected JSON string",t),new Error("WebSocket received binary data, expected JSON string");try{return JSON.parse(t)}catch(n){throw console.error("Failed to parse JSON from WebSocket message",t,n),new Error("Failed to parse JSON from WebSocket message")}}send_json(t){let n;try{n=JSON.stringify(t)}catch(s){throw new Error(`Failed to stringify data for WebSocket: ${s.message}`)}this.send(n)}}function bi(e,t={}){const{protocols:n,binaryType:s}=t;return new Promise((r,i)=>{new cc(r,i,e,n,s)})}let Yt=null,nt=null;async function ac(){return nt||Yt||(Yt=fetch("/auth/api/settings").then(e=>e.ok?e.json():{}).then(e=>(nt=e||{},nt)).catch(()=>(nt={},nt)),Yt)}function Pt(){const e=nt?.ui_base_path||"/auth/";return e==="/"?"/":e.endsWith("/")?e:e+"/"}function Ic(){return Pt()==="/"?"/admin/":Pt()+"admin/"}function Pc(e=""){const t=e.startsWith("/")?e.slice(1):e;return t?Pt()==="/"?"/"+t:Pt()+t:Pt()}const fc=1e3;class xn extends Error{constructor(t,n,s){super(s?.detail||`Request failed: ${n.status}`),this.name="ApiError",this.url=t,this.status=n.status,this.statusText=n.statusText,this.data=s}}class en extends Error{constructor(t,n=null){super(t),this.name="NetworkError",this.originalError=n}}class Qn extends Error{constructor(){super("Authentication cancelled"),this.name="AuthCancelledError"}}let Pe=null,Me=null,st=null,Ie=null;const Ln={};async function Mc(e="login"){if(Ln[e])return Ln[e];const t=await fetch("/auth/api/forward",{credentials:"include"});if(t.status===401||t.status===403){const n=await t.json();if(n.auth?.iframe){let s=n.auth.iframe;return e!==n.auth.mode&&(s=s.replace(/mode=[^&]*/,`mode=${e}`)),Ln[e]=s,s}}throw new Error("Unable to fetch auth iframe URL")}function _i(e){return Me||(document.getElementById("auth-iframe")?(Me=new Promise((t,n)=>{st=t,Ie=n}),Me):(Me=new Promise((t,n)=>{st=t,Ie=n}),tn(),Pe=document.createElement("iframe"),Pe.id="auth-iframe",Pe.title="Authentication",Pe.allow="publickey-credentials-get; publickey-credentials-create",Pe.src=e,document.body.appendChild(Pe),Me))}function tn(){Pe&&(Pe.remove(),Pe=null)}function uc(e){const t=e.data;if(t?.type)switch(t.type){case"auth-success":tn(),st&&(st(),Me=null,st=null,Ie=null);break;case"auth-back":case"auth-close-request":tn(),Ie&&(Ie(new Qn),Me=null,st=null,Ie=null);break;case"auth-error":t.cancelled&&Ie&&(tn(),Ie(new Qn),Me=null,st=null,Ie=null);break}}typeof window<"u"&&window.addEventListener("message",uc);async function dc(e,t={}){const{timeout:n=fc,...s}=t;for(s.credentials=s.credentials||"include";;){let r;try{r=await fetch(e,{...s,signal:n&&AbortSignal.timeout(n)})}catch(i){throw i.name==="TimeoutError"?new en("Request timed out",i):i.name==="AbortError"?i:i.name==="TypeError"&&i.message==="Failed to fetch"?new en("Unable to connect to server",i):new en(i.message||"Network error",i)}if(r.status===401||r.status===403){let i=null;try{i=(await r.clone().json()).auth}catch{}if(i?.iframe&&window===window.top){await _i(i.iframe);continue}}return r}}async function Nc(e,t={}){const n={...t};n.headers={Accept:"application/json",...n.headers},n.body&&typeof n.body=="object"&&!(n.body instanceof FormData)&&(n.headers={"Content-Type":"application/json",...n.headers},n.body=JSON.stringify(n.body));const s=await dc(e,n),r=await s.json();if(!s.ok)throw new xn(e,s,r);return r}async function Dc(e,t={}){const n={credentials:"include",...t,headers:{Accept:"application/json",...t.headers}},s=await fetch(e,n),r=await s.json();if(!s.ok)throw new xn(e,s,r);return r}function Fc(e){return e instanceof en||e instanceof xn?e.message:e.name==="TimeoutError"?"Request timed out":e.name==="TypeError"&&e.message==="Failed to fetch"?"Unable to connect to server":e.message||"An error occurred"}function Uc(e){return!(e instanceof Qn||e.name==="AbortError"||e instanceof xn&&(e.status===401||e.status===403))}async function yi(e){const n=(await ac())?.auth_host;return n&&location.host!==n?`//${n}${e}`:e}async function hc(e=null,t=null,n=null){let s=[];e&&s.push(`reset=${encodeURIComponent(e)}`),t&&s.push(`name=${encodeURIComponent(t)}`);const r=s.length?`?${s.join("&")}`:"";for(;;){const i=await bi(await yi(`/auth/ws/register${r}`));try{const o=await i.receive_json();if((o.status===401||o.status===403)&&o.auth?.iframe){i.close(),await _i(o.auth.iframe);continue}if(o.status)throw new Error(o.detail||`Registration failed: ${o.status}`);n&&n();const l=await rc(o);i.send_json(l);const c=await i.receive_json();if(c.status)throw new Error(c.detail||`Registration failed: ${c.status}`);return c}catch(o){throw i.close(),console.error("Registration error:",o),Error(o.name==="NotAllowedError"?"Passkey registration cancelled":o.message)}}}async function pc(){const e=await bi(await yi("/auth/ws/authenticate"));try{const t=await e.receive_json();if(console.log("Authentication options:",t),t.status)throw new Error(t.detail||`Authentication failed: ${t.status}`);const n=await lc(t);e.send_json(n);const s=await e.receive_json();if(s.status)throw new Error(s.detail||`Authentication failed: ${s.status}`);return s}catch(t){throw console.error("Authentication error:",t),Error(t.name==="NotAllowedError"?"Passkey authentication cancelled":t.message)}finally{e.close()}}const jc={authenticate:pc,register:hc},Kc=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n};export{tr as A,Mr as B,ac as C,mc as D,to as E,ue as F,fs as G,G as H,it as I,F as J,Di as K,bc as L,yc as M,Xt as N,xc as O,pc as P,hc as Q,Sc as R,Oc as S,Rc as T,Uc as U,Fc as V,Pt as W,xn as X,Dc as Y,Kc as _,$r as a,Ic as b,El as c,vc as d,Gn as e,li as f,Re as g,Yn as h,Ac as i,go as j,Tc as k,ul as l,Pc as m,ss as n,xo as o,jc as p,Nc as q,_c as r,Mc as s,Ni as t,Cr as u,Cc as v,Mn as w,Ec as x,ns as y,wc as z}; diff --git a/paskia/frontend-build/auth/assets/admin-D8zxJOk4.js b/paskia/frontend-build/auth/assets/admin-D8zxJOk4.js deleted file mode 100644 index b517ab5..0000000 --- a/paskia/frontend-build/auth/assets/admin-D8zxJOk4.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as j,c as L,d as r,e as o,f as i,i as O,t as f,F as p,x as I,y as ee,k as ae,r as v,h as M,g as B,q as h,w as se,j as Ee,l as N,z,A as q,u as Q,B as X,o as Ne,C as Te,a as Ae,m as Me,b as Ie,v as Le}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{u as ie,U as _e,_ as Ve,a as ze,R as qe,N as G,M as Be,b as je,L as Fe,A as xe,B as Ge,c as He}from"./AccessDenied-guOGfNm-.js";import"./helpers-CU0-cyzg.js";const Je={class:"permissions-section"},We={class:"actions"},Ye={class:"org-table"},Ze={key:0},Ke=["onClick"],Qe=["onClick"],Xe={class:"role-names"},ea={class:"center"},aa={key:0,class:"center"},sa=["onClick"],ia={key:0,class:"permissions-section"},ta={class:"matrix-wrapper"},na={class:"matrix-scroll"},oa=["title"],la=["title"],ra={class:"display-text"},da=["checked","onChange"],ua={class:"actions"},ma={class:"org-table"},ca={class:"perm-name-cell"},ga={class:"perm-title"},ya={class:"display-text"},va=["onClick"],pa={class:"perm-id-info"},fa={class:"id-text"},ha={class:"perm-members center"},$a={class:"perm-actions center"},ba=["onClick"],wa={__name:"AdminOverview",props:{info:Object,orgs:Array,permissions:Array,permissionSummary:Object},emits:["createOrg","openOrg","updateOrg","deleteOrg","toggleOrgPermission","openDialog","deletePermission","renamePermissionDisplay"],setup(a,{emit:C}){const D=a,U=L(()=>[...D.orgs].sort((m,s)=>{const t=m.display_name.localeCompare(s.display_name);return t!==0?t:m.uuid.localeCompare(s.uuid)})),k=L(()=>[...D.permissions].sort((m,s)=>m.id.localeCompare(s.id)));function R(m){return m.roles.slice().sort((s,t)=>s.display_name.localeCompare(t.display_name)).map(s=>s.display_name).join(", ")}return(m,s)=>(o(),r(p,null,[i("div",Je,[i("h2",null,f(a.info.is_global_admin?"Organizations":"Your Organizations"),1),i("div",We,[a.info.is_global_admin?(o(),r("button",{key:0,onClick:s[0]||(s[0]=t=>m.$emit("createOrg"))},"+ Create Org")):O("",!0)]),i("table",Ye,[i("thead",null,[i("tr",null,[s[2]||(s[2]=i("th",null,"Name",-1)),s[3]||(s[3]=i("th",null,"Roles",-1)),s[4]||(s[4]=i("th",null,"Members",-1)),a.info.is_global_admin?(o(),r("th",Ze,"Actions")):O("",!0)])]),i("tbody",null,[(o(!0),r(p,null,I(U.value,t=>(o(),r("tr",{key:t.uuid},[i("td",null,[i("a",{href:"#org/{{o.uuid}}",onClick:ae(u=>m.$emit("openOrg",t),["prevent"])},f(t.display_name),9,Ke),a.info.is_global_admin||a.info.is_org_admin?(o(),r("button",{key:0,onClick:u=>m.$emit("updateOrg",t),class:"icon-btn edit-org-btn","aria-label":"Rename organization",title:"Rename organization"},"✏️",8,Qe)):O("",!0)]),i("td",Xe,f(R(t)),1),i("td",ea,f(t.roles.reduce((u,c)=>u+c.users.length,0)),1),a.info.is_global_admin?(o(),r("td",aa,[i("button",{onClick:u=>m.$emit("deleteOrg",t),class:"icon-btn delete-icon","aria-label":"Delete organization",title:"Delete organization"},"❌",8,sa)])):O("",!0)]))),128))])])]),a.info.is_global_admin?(o(),r("div",ia,[s[8]||(s[8]=i("h2",null,"Permissions",-1)),i("div",ta,[i("div",na,[i("div",{class:"perm-matrix-grid",style:ee({gridTemplateColumns:"minmax(180px, 1fr) "+U.value.map(()=>"2.2rem").join(" ")})},[s[5]||(s[5]=i("div",{class:"grid-head perm-head"},"Permission",-1)),(o(!0),r(p,null,I(U.value,t=>(o(),r("div",{key:"head-"+t.uuid,class:"grid-head org-head",title:t.display_name},[i("span",null,f(t.display_name),1)],8,oa))),128)),(o(!0),r(p,null,I(k.value,t=>(o(),r(p,{key:t.id},[i("div",{class:"perm-name",title:t.id},[i("span",ra,f(t.display_name),1)],8,la),(o(!0),r(p,null,I(U.value,u=>(o(),r("div",{key:u.uuid+"-"+t.id,class:"matrix-cell"},[i("input",{type:"checkbox",checked:u.permissions.includes(t.id),onChange:c=>m.$emit("toggleOrgPermission",u,t.id,c.target.checked)},null,40,da)]))),128))],64))),128))],4)]),s[6]||(s[6]=i("p",{class:"matrix-hint muted"},"Toggle which permissions each organization can grant to its members.",-1))]),i("div",ua,[a.info.is_global_admin?(o(),r("button",{key:0,onClick:s[1]||(s[1]=t=>m.$emit("openDialog","perm-create",{display_name:"",id:""}))},"+ Create Permission")):O("",!0)]),i("table",ma,[s[7]||(s[7]=i("thead",null,[i("tr",null,[i("th",{scope:"col"},"Permission"),i("th",{scope:"col",class:"center"},"Members"),i("th",{scope:"col",class:"center"},"Actions")])],-1)),i("tbody",null,[(o(!0),r(p,null,I(k.value,t=>(o(),r("tr",{key:t.id},[i("td",ca,[i("div",ga,[i("span",ya,f(t.display_name),1),i("button",{onClick:u=>m.$emit("renamePermissionDisplay",t),class:"icon-btn edit-display-btn","aria-label":"Edit display name",title:"Edit display name"},"✏️",8,va)]),i("div",pa,[i("span",fa,f(t.id),1)])]),i("td",ha,f(a.permissionSummary[t.id]?.userCount||0),1),i("td",$a,[i("button",{onClick:u=>m.$emit("deletePermission",t),class:"icon-btn delete-icon","aria-label":"Delete permission",title:"Delete permission"},"❌",8,ba)])]))),128))])])])):O("",!0)],64))}},ka=j(wa,[["__scopeId","data-v-3b9e6d03"]]),Da=["title"],Oa={class:"org-name"},Ua={class:"matrix-wrapper"},Ca={class:"matrix-scroll"},Ra=["title"],Sa=["title"],Pa=["checked","onChange"],Ea={class:"roles-grid"},Na=["onDrop"],Ta={class:"role-header"},Aa=["title"],Ma=["onClick"],Ia={class:"role-actions"},La=["onClick"],_a={key:0,class:"user-list"},Va=["onDragstart","onClick","title"],za={class:"name"},qa={class:"meta"},Ba={key:1,class:"empty-role"},ja=["onClick"],Fa={__name:"AdminOrgDetail",props:{selectedOrg:Object,permissions:Array},emits:["updateOrg","createRole","updateRole","deleteRole","createUserInRole","openUser","toggleRolePermission","onRoleDragOver","onRoleDrop","onUserDragStart"],setup(a,{emit:C}){const D=a,U=C,k=L(()=>[...D.selectedOrg.roles].sort((s,t)=>{const u=s.display_name.toLowerCase(),c=t.display_name.toLowerCase();return u!==c?u.localeCompare(c):s.uuid.localeCompare(t.uuid)}));function R(s){return D.permissions.find(t=>t.id===s)?.display_name||s}function m(s,t,u){U("toggleRolePermission",s,t,u)}return(s,t)=>(o(),r(p,null,[i("h2",{class:"org-title",title:a.selectedOrg.uuid},[i("span",Oa,f(a.selectedOrg.display_name),1),i("button",{onClick:t[0]||(t[0]=u=>s.$emit("updateOrg",a.selectedOrg)),class:"icon-btn","aria-label":"Rename organization",title:"Rename organization"},"✏️")],8,Da),i("div",Ua,[i("div",Ca,[i("div",{class:"perm-matrix-grid",style:ee({gridTemplateColumns:"minmax(180px, 1fr) "+k.value.map(()=>"2.2rem").join(" ")+" 2.2rem"})},[t[4]||(t[4]=i("div",{class:"grid-head perm-head"},"Permission",-1)),(o(!0),r(p,null,I(k.value,u=>(o(),r("div",{key:"head-"+u.uuid,class:"grid-head role-head",title:u.display_name},[i("span",null,f(u.display_name),1)],8,Ra))),128)),i("div",{class:"grid-head role-head add-role-head",title:"Add role",onClick:t[1]||(t[1]=u=>s.$emit("createRole",a.selectedOrg)),role:"button"},"➕"),(o(!0),r(p,null,I(a.selectedOrg.permissions,u=>(o(),r(p,{key:u},[i("div",{class:"perm-name",title:u},f(R(u)),9,Sa),(o(!0),r(p,null,I(k.value,c=>(o(),r("div",{key:c.uuid+"-"+u,class:"matrix-cell"},[i("input",{type:"checkbox",checked:c.permissions.includes(u),onChange:S=>m(c,u,S.target.checked)},null,40,Pa)]))),128)),t[3]||(t[3]=i("div",{class:"matrix-cell add-role-cell"},null,-1))],64))),128))],4)]),t[5]||(t[5]=i("p",{class:"matrix-hint muted"},"Toggle which permissions each role grants.",-1))]),i("div",Ea,[(o(!0),r(p,null,I(k.value,u=>(o(),r("div",{key:u.uuid,class:"role-column",onDragover:t[2]||(t[2]=c=>s.$emit("onRoleDragOver",c)),onDrop:c=>s.$emit("onRoleDrop",c,a.selectedOrg,u)},[i("div",Ta,[i("strong",{class:"role-name",title:u.uuid},[i("span",null,f(u.display_name),1),i("button",{onClick:c=>s.$emit("updateRole",u),class:"icon-btn","aria-label":"Edit role",title:"Edit role"},"✏️",8,Ma)],8,Aa),i("div",Ia,[i("button",{onClick:c=>s.$emit("createUserInRole",a.selectedOrg,u),class:"plus-btn","aria-label":"Add user",title:"Add user"},"➕",8,La)])]),u.users.length>0?(o(),r("ul",_a,[(o(!0),r(p,null,I(u.users.slice().sort((c,S)=>{const b=c.display_name.toLowerCase(),g=S.display_name.toLowerCase();return b!==g?b.localeCompare(g):c.uuid.localeCompare(S.uuid)}),c=>(o(),r("li",{key:c.uuid,class:"user-chip",draggable:"true",onDragstart:S=>s.$emit("onUserDragStart",S,c,a.selectedOrg.uuid),onClick:S=>s.$emit("openUser",c),title:c.uuid},[i("span",za,f(c.display_name),1),i("span",qa,f(c.last_seen?new Date(c.last_seen).toLocaleDateString():"—"),1)],40,Va))),128))])):(o(),r("div",Ba,[t[6]||(t[6]=i("p",{class:"empty-text muted"},"No members",-1)),i("button",{onClick:c=>s.$emit("deleteRole",u),class:"icon-btn delete-icon","aria-label":"Delete empty role",title:"Delete role"},"❌",8,ja)]))],40,Na))),128))])],64))}},xa=j(Fa,[["__scopeId","data-v-b8b4bfbc"]]),Ga={class:"user-detail"},Ha={key:1,class:"error small"},Ja={class:"registration-actions"},Wa=["disabled"],Ya={class:"section-block","data-section":"registered-passkeys"},Za={class:"section-body"},Ka={class:"actions ancillary-actions"},Qa={__name:"AdminUserDetail",props:{selectedUser:Object,userDetail:Object,selectedOrg:Object,loading:Boolean,showRegModal:Boolean},emits:["generateUserRegistrationLink","goOverview","openOrg","onUserNameSaved","closeRegModal","editUserName","refreshUserDetail"],setup(a,{emit:C}){const D=a,U=C,k=ie(),R=v({}),m=v(null),s=v(null);function t(){k.showMessage("Link copied to clipboard!")}function u(){U("editUserName",D.selectedUser)}async function c(b){try{const g=await h(`/auth/api/admin/orgs/${D.selectedUser.org_uuid}/users/${D.selectedUser.uuid}/credentials/${b.credential_uuid}`,{method:"DELETE"});g.status==="ok"?U("onUserNameSaved"):console.error("Failed to delete credential",g)}catch(g){console.error("Delete credential error",g)}}async function S(b){const g=b?.id;if(g){R.value={...R.value,[g]:!0};try{const d=await h(`/auth/api/admin/orgs/${D.selectedUser.org_uuid}/users/${D.selectedUser.uuid}/sessions/${g}`,{method:"DELETE"});if(d.status==="ok"){if(d.current_session_terminated){sessionStorage.clear(),location.reload();return}U("refreshUserDetail"),k.showMessage("Session terminated","success",2500)}else k.showMessage(d.detail||"Failed to terminate session","error")}catch(d){console.error("Terminate session error",d),k.showMessage(d.message||"Failed to terminate session","error")}finally{const d={...R.value};delete d[g],R.value=d}}}return(b,g)=>(o(),r("div",Ga,[a.userDetail&&!a.userDetail.error?(o(),M(_e,{key:0,name:a.userDetail.display_name||a.selectedUser.display_name,visits:a.userDetail.visits,"created-at":a.userDetail.created_at,"last-seen":a.userDetail.last_seen,loading:a.loading,"org-display-name":a.userDetail.org.display_name,"role-name":a.userDetail.role,"update-endpoint":`/auth/api/admin/orgs/${a.selectedUser.org_uuid}/users/${a.selectedUser.uuid}/display-name`,onSaved:g[0]||(g[0]=d=>b.$emit("onUserNameSaved")),onEditName:u},null,8,["name","visits","created-at","last-seen","loading","org-display-name","role-name","update-endpoint"])):a.userDetail?.error?(o(),r("div",Ha,f(a.userDetail.error),1)):O("",!0),a.userDetail&&!a.userDetail.error?(o(),r(p,{key:2},[i("div",Ja,[i("button",{class:"btn-secondary reg-token-btn",onClick:g[1]||(g[1]=d=>b.$emit("generateUserRegistrationLink",a.selectedUser)),disabled:a.loading},"Generate Registration Token",8,Wa),g[6]||(g[6]=i("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))]),i("section",Ya,[g[7]||(g[7]=i("div",{class:"section-header"},[i("h2",null,"Registered Passkeys")],-1)),i("div",Za,[B(Ve,{credentials:a.userDetail.credentials,"aaguid-info":a.userDetail.aaguid_info,"allow-delete":!0,"hovered-credential-uuid":m.value,"hovered-session-credential-uuid":s.value?.credential_uuid,onDelete:c,onCredentialHover:g[2]||(g[2]=d=>m.value=d)},null,8,["credentials","aaguid-info","hovered-credential-uuid","hovered-session-credential-uuid"])])]),B(ze,{sessions:a.userDetail.sessions||[],"terminating-sessions":R.value,"hovered-credential-uuid":m.value,"empty-message":"This user has no active sessions.","section-description":"View and manage the active sessions for this user.",onTerminate:S,onSessionHover:g[3]||(g[3]=d=>s.value=d)},null,8,["sessions","terminating-sessions","hovered-credential-uuid"])],64)):O("",!0),i("div",Ka,[a.selectedOrg?(o(),r("button",{key:0,onClick:g[4]||(g[4]=d=>b.$emit("openOrg",a.selectedOrg)),class:"icon-btn",title:"Back to Org"},"↩️")):O("",!0)]),a.showRegModal?(o(),M(qe,{key:3,endpoint:`/auth/api/admin/orgs/${a.selectedUser.org_uuid}/users/${a.selectedUser.uuid}/create-link`,"auto-copy":!1,"user-name":a.userDetail?.display_name||a.selectedUser.display_name,onClose:g[5]||(g[5]=d=>b.$emit("closeRegModal")),onCopied:t},null,8,["endpoint","user-name"])):O("",!0)]))}},Xa=j(Qa,[["__scopeId","data-v-9226fea7"]]),es={class:"modal-title"},as={key:0},ss={key:2},is={class:"small muted"},ts=["placeholder","pattern"],ns={key:7},os={key:8,class:"error small"},ls={key:9,class:"modal-actions"},rs=["disabled"],ds=["disabled"],us={__name:"AdminDialogs",props:{dialog:Object,PERMISSION_ID_PATTERN:String},emits:["submitDialog","closeDialog"],setup(a,{emit:C}){const D=a,U=v(null),k=v(null),R=new Set(["org-update","role-update","user-update-name"]);return se(()=>D.dialog.type,m=>{m==="org-create"?X(()=>{U.value?.focus()}):(m==="perm-display"||m==="perm-create")&&X(()=>{k.value?.focus(),m==="perm-display"&&k.value?.select()})}),(m,s)=>a.dialog.type?(o(),M(Be,{key:0,onClose:s[13]||(s[13]=t=>m.$emit("closeDialog"))},{default:Ee(()=>[i("h3",es,[a.dialog.type==="org-create"?(o(),r(p,{key:0},[N("Create Organization")],64)):a.dialog.type==="org-update"?(o(),r(p,{key:1},[N("Rename Organization")],64)):a.dialog.type==="role-create"?(o(),r(p,{key:2},[N("Create Role")],64)):a.dialog.type==="role-update"?(o(),r(p,{key:3},[N("Edit Role")],64)):a.dialog.type==="user-create"?(o(),r(p,{key:4},[N("Add User To Role")],64)):a.dialog.type==="user-update-name"?(o(),r(p,{key:5},[N("Edit User Name")],64)):a.dialog.type==="perm-create"||a.dialog.type==="perm-display"?(o(),r(p,{key:6},[N(f(a.dialog.type==="perm-create"?"Create Permission":"Edit Permission Display"),1)],64)):a.dialog.type==="confirm"?(o(),r(p,{key:7},[N("Confirm")],64)):O("",!0)]),i("form",{onSubmit:s[12]||(s[12]=ae(t=>m.$emit("submitDialog"),["prevent"])),class:"modal-form"},[a.dialog.type==="org-create"?(o(),r("label",as,[s[14]||(s[14]=N("Name ",-1)),z(i("input",{ref_key:"nameInput",ref:U,"onUpdate:modelValue":s[0]||(s[0]=t=>a.dialog.data.name=t),required:""},null,512),[[q,a.dialog.data.name]])])):a.dialog.type==="org-update"?(o(),M(G,{key:1,label:"Organization Name",modelValue:a.dialog.data.name,"onUpdate:modelValue":s[1]||(s[1]=t=>a.dialog.data.name=t),busy:a.dialog.busy,error:a.dialog.error,onCancel:s[2]||(s[2]=t=>m.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):a.dialog.type==="role-create"?(o(),r("label",ss,[s[15]||(s[15]=N("Role Name ",-1)),z(i("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>a.dialog.data.name=t),placeholder:"Role name",required:""},null,512),[[q,a.dialog.data.name]])])):a.dialog.type==="role-update"?(o(),M(G,{key:3,label:"Role Name",modelValue:a.dialog.data.name,"onUpdate:modelValue":s[4]||(s[4]=t=>a.dialog.data.name=t),busy:a.dialog.busy,error:a.dialog.error,onCancel:s[5]||(s[5]=t=>m.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):a.dialog.type==="user-create"?(o(),r(p,{key:4},[i("p",is,"Role: "+f(a.dialog.data.role.display_name),1),i("label",null,[s[16]||(s[16]=N("Display Name ",-1)),z(i("input",{"onUpdate:modelValue":s[6]||(s[6]=t=>a.dialog.data.name=t),placeholder:"User display name",required:""},null,512),[[q,a.dialog.data.name]])])],64)):a.dialog.type==="user-update-name"?(o(),M(G,{key:5,label:"Display Name",modelValue:a.dialog.data.name,"onUpdate:modelValue":s[7]||(s[7]=t=>a.dialog.data.name=t),busy:a.dialog.busy,error:a.dialog.error,onCancel:s[8]||(s[8]=t=>m.$emit("closeDialog"))},null,8,["modelValue","busy","error"])):a.dialog.type==="perm-create"||a.dialog.type==="perm-display"?(o(),r(p,{key:6},[i("label",null,[s[17]||(s[17]=N("Display Name ",-1)),z(i("input",{ref_key:"displayNameInput",ref:k,"onUpdate:modelValue":s[9]||(s[9]=t=>a.dialog.data.display_name=t),required:""},null,512),[[q,a.dialog.data.display_name]])]),i("label",null,[s[18]||(s[18]=N("Permission ID ",-1)),z(i("input",{"onUpdate:modelValue":s[10]||(s[10]=t=>a.dialog.data.id=t),placeholder:a.dialog.type==="perm-create"?"yourapp:login":a.dialog.data.permission.id,required:"",pattern:a.PERMISSION_ID_PATTERN,title:"Allowed: A-Za-z0-9:._~-"},null,8,ts),[[q,a.dialog.data.id]])]),s[19]||(s[19]=i("p",{class:"small muted"},"The permission ID is used for permission checks in the application. Changing it may break deployed applications that reference this permission.",-1))],64)):a.dialog.type==="confirm"?(o(),r("p",ns,f(a.dialog.data.message),1)):O("",!0),a.dialog.error&&!Q(R).has(a.dialog.type)?(o(),r("div",os,f(a.dialog.error),1)):O("",!0),Q(R).has(a.dialog.type)?O("",!0):(o(),r("div",ls,[i("button",{type:"button",class:"btn-secondary",onClick:s[11]||(s[11]=t=>m.$emit("closeDialog")),disabled:a.dialog.busy}," Cancel ",8,rs),i("button",{type:"submit",class:"btn-primary",disabled:a.dialog.busy},f(a.dialog.type==="confirm"?"OK":"Save"),9,ds)]))],32)]),_:1})):O("",!0)}},ms=j(us,[["__scopeId","data-v-42b70397"]]),cs={class:"app-shell admin-shell"},gs={class:"app-main"},ys={key:2,class:"view-root view-root--wide view-admin"},vs={class:"view-header"},ps={class:"section-block admin-section"},fs={class:"section-body admin-section-body"},hs={key:0,class:"surface surface--tight error"},$s={key:1,class:"admin-panels"},bs="^[A-Za-z0-9:._~-]+$",ws={__name:"AdminApp",setup(a){const C=v(null),D=v(!0),U=v("Loading..."),k=v(!1),R=v(!1),m=v(null),s=v([]),t=v([]),u=v(null),c=v(null),S=v(null);v(null),v(null);const b=ie(),g=v(null);v(null),v(""),v(null),v("");const d=v({type:null,data:null,busy:!1,error:""});function H(e){if(!g.value)return;const n=e.target.closest(".org-add-menu"),l=e.target.closest(".add-org-btn");!n&&!l&&(g.value=null)}Ne(async()=>{document.addEventListener("click",H),window.addEventListener("hashchange",F);const e=await Te();e?.rp_name&&(document.title=e.rp_name+" Admin"),await re()}),Ae(()=>{document.removeEventListener("click",H),window.removeEventListener("hashchange",F)});const ne=L(()=>{const e={};for(const l of s.value){const y={uuid:l.uuid,display_name:l.display_name},$=new Set(l.permissions||[]);for(const w of l.permissions||[])e[w]||(e[w]={orgs:[],orgSet:new Set,userCount:0}),e[w].orgSet.has(l.uuid)||(e[w].orgs.push(y),e[w].orgSet.add(l.uuid));for(const w of l.roles)for(const E of w.permissions)$.has(E)&&(e[E]||(e[E]={orgs:[],orgSet:new Set,userCount:0}),e[E].orgSet.has(l.uuid)||(e[E].orgs.push(y),e[E].orgSet.add(l.uuid)),e[E].userCount+=w.users.length)}const n={};for(const[l,y]of Object.entries(e))n[l]={orgs:y.orgs.sort(($,w)=>$.display_name.localeCompare(w.display_name)),userCount:y.userCount};return n});function oe(e){A("perm-display",{permission:e,id:e.id,display_name:e.display_name})}function F(){const e=window.location.hash||"";u.value=null,c.value=null,e.startsWith("#org/")?u.value=e.slice(5):e.startsWith("#user/")&&(c.value=e.slice(6))}async function T(){const e=await h("/auth/api/admin/orgs");s.value=e.map(n=>{const l=n.roles.map($=>({...$,org_uuid:n.uuid,users:[]})),y=Object.fromEntries(l.map($=>[$.display_name,$]));for(const $ of n.users||[])y[$.role]&&y[$.role].users.push($);return{...n,roles:l}})}async function _(){t.value=await h("/auth/api/admin/permissions")}async function le(){C.value=await h("/auth/api/user-info",{method:"POST"}),k.value=!0}async function re(){D.value=!0,U.value="Loading...",m.value=null;try{await Promise.all([T(),_()]),await le(),!C.value.is_global_admin&&C.value.is_org_admin&&s.value.length===1&&(!window.location.hash||window.location.hash==="#overview")?(u.value=s.value[0].uuid,window.location.hash=`#org/${u.value}`,b.showMessage(`Navigating to ${s.value[0].display_name} Administration`,"info",3e3)):F()}catch(e){e.name==="AuthCancelledError"?R.value=!0:m.value=e.message}finally{D.value=!1}}function de(){A("org-create",{})}function J(e){A("org-update",{org:e,name:e.display_name})}function ue(e){A("user-update-name",{user:e,name:e.display_name})}function me(e){if(!C.value?.is_global_admin){b.showMessage("Global admin only");return}A("confirm",{message:`Delete organization ${e.display_name}?`,action:async()=>{await h(`/auth/api/admin/orgs/${e.uuid}`,{method:"DELETE"}),await Promise.all([T(),_()])}})}function ce(e,n){A("user-create",{org:e,role:n})}async function ge(e,n,l){if(n.role!==l)try{await h(`/auth/api/admin/orgs/${e.uuid}/users/${n.uuid}/role`,{method:"PUT",body:{role:l}}),await T()}catch(y){b.showMessage(y.message||"Failed to update user role")}}function ye(e,n,l){e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",JSON.stringify({user_uuid:n.uuid,org_uuid:l}))}function ve(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}function pe(e,n,l){e.preventDefault();try{const y=JSON.parse(e.dataTransfer.getData("text/plain"));if(y.org_uuid!==n.uuid)return;const $=n.roles.flatMap(w=>w.users).find(w=>w.uuid===y.user_uuid);$&&ge(n,$,l.display_name)}catch{}}function fe(e){A("role-create",{org:e})}function he(e){A("role-update",{role:e,name:e.display_name})}function $e(e){A("confirm",{message:`Delete role ${e.display_name}?`,action:async()=>{await h(`/auth/api/admin/orgs/${e.org_uuid}/roles/${e.uuid}`,{method:"DELETE"}),await T()}})}async function be(e,n,l){const y=l?[...e.permissions,n]:e.permissions.filter(w=>w!==n),$=[...e.permissions];e.permissions=y;try{await h(`/auth/api/admin/orgs/${e.org_uuid}/roles/${e.uuid}`,{method:"PUT",body:{display_name:e.display_name,permissions:y}}),await T()}catch(w){b.showMessage(w.message||"Failed to update role permission"),e.permissions=$}}function we(e){A("confirm",{message:`Delete permission ${e.id}?`,action:async()=>{const n=new URLSearchParams({permission_id:e.id});await h(`/auth/api/admin/permission?${n.toString()}`,{method:"DELETE"}),await _()}})}function ke(){window.location.reload()}const V=L(()=>s.value.find(e=>e.uuid===u.value)||null);function W(e){window.location.hash=`#org/${e.uuid}`}function De(){window.location.hash="#overview"}function Oe(e){window.location.hash=`#user/${e.uuid}`}const P=L(()=>{if(!c.value)return null;for(const e of s.value)for(const n of e.roles){const l=n.users.find(y=>y.uuid===c.value);if(l)return{...l,org_uuid:e.uuid,role_display_name:n.display_name}}return null}),Ue=L(()=>P.value?"Admin: User":V.value?"Admin: Org":(b.settings?.rp_name||"Master")+" Admin"),Ce=L(()=>{const e=[{label:"Auth",href:Me()},{label:"Admin",href:Ie()}];let n=null;P.value&&(n=s.value.find(y=>y.uuid===P.value.org_uuid)||null);const l=V.value||n;return l&&e.push({label:l.display_name,href:`#org/${l.uuid}`}),P.value&&e.push({label:P.value.display_name||"User",href:`#user/${P.value.uuid}`}),e});se(P,async e=>{if(!e){S.value=null;return}try{S.value=await h(`/auth/api/admin/orgs/${e.org_uuid}/users/${e.uuid}`)}catch(n){S.value={error:n.message}}});const x=v(!1);function Re(e){x.value=!0}async function Se(e,n,l){const y=e.permissions.includes(n);if(l&&y||!l&&!y)return;const $=l?[...e.permissions,n]:e.permissions.filter(E=>E!==n),w=[...e.permissions];e.permissions=$;try{const E=new URLSearchParams({permission_id:n});await h(`/auth/api/admin/orgs/${e.uuid}/permission?${E.toString()}`,{method:l?"POST":"DELETE"}),await T()}catch(E){b.showMessage(E.message||"Failed to update organization permission"),e.permissions=w}}function A(e,n){d.value={type:e,data:n,busy:!1,error:""}}function Y(){d.value={type:null,data:null,busy:!1,error:""}}async function Z(){if(await T(),P.value)try{S.value=await h(`/auth/api/admin/orgs/${P.value.org_uuid}/users/${P.value.uuid}`)}catch(e){b.showMessage(e.message||"Failed to reload user","error")}}async function K(){await Z(),b.showMessage("User renamed","success",1500)}async function Pe(){if(!(!d.value.type||d.value.busy)){d.value.busy=!0,d.value.error="";try{const e=d.value.type;if(e==="org-create"){const n=d.value.data.name?.trim();if(!n)throw new Error("Name required");await h("/auth/api/admin/orgs",{method:"POST",body:{display_name:n,permissions:[]}}),await Promise.all([T(),_()])}else if(e==="org-update"){const{org:n}=d.value.data,l=d.value.data.name?.trim();if(!l)throw new Error("Name required");await h(`/auth/api/admin/orgs/${n.uuid}`,{method:"PUT",body:{display_name:l,permissions:n.permissions}}),await T()}else if(e==="role-create"){const{org:n}=d.value.data,l=d.value.data.name?.trim();if(!l)throw new Error("Name required");await h(`/auth/api/admin/orgs/${n.uuid}/roles`,{method:"POST",body:{display_name:l,permissions:[]}}),await T()}else if(e==="role-update"){const{role:n}=d.value.data,l=d.value.data.name?.trim();if(!l)throw new Error("Name required");await h(`/auth/api/admin/orgs/${n.org_uuid}/roles/${n.uuid}`,{method:"PUT",body:{display_name:l,permissions:n.permissions}}),await T()}else if(e==="user-create"){const{org:n,role:l}=d.value.data,y=d.value.data.name?.trim();if(!y)throw new Error("Name required");await h(`/auth/api/admin/orgs/${n.uuid}/users`,{method:"POST",body:{display_name:y,role:l.display_name}}),await T()}else if(e==="user-update-name"){const{user:n}=d.value.data,l=d.value.data.name?.trim();if(!l)throw new Error("Name required");await h(`/auth/api/admin/orgs/${n.org_uuid}/users/${n.uuid}/display-name`,{method:"PUT",body:{display_name:l}}),await K()}else if(e==="perm-display"){const{permission:n}=d.value.data,l=d.value.data.id?.trim(),y=d.value.data.display_name?.trim();if(!y)throw new Error("Display name required");if(!l)throw new Error("ID required");if(l!==n.id)await h("/auth/api/admin/permission/rename",{method:"POST",body:{old_id:n.id,new_id:l,display_name:y}});else if(y!==n.display_name){const $=new URLSearchParams({permission_id:n.id,display_name:y});await h(`/auth/api/admin/permission?${$.toString()}`,{method:"PUT"})}await _()}else if(e==="perm-create"){const n=d.value.data.id?.trim();if(!n)throw new Error("ID required");const l=d.value.data.display_name?.trim();if(!l)throw new Error("Display name required");await h("/auth/api/admin/permissions",{method:"POST",body:{id:n,display_name:l}}),await _(),d.value.data.display_name="",d.value.data.id=""}else if(e==="confirm"){const n=d.value.data.action;n&&await n()}Y()}catch(e){d.value.error=e.message||"Error"}finally{d.value.busy=!1}}}return(e,n)=>(o(),r("div",cs,[B(je),i("main",gs,[D.value?(o(),M(Fe,{key:0,message:U.value},null,8,["message"])):R.value?(o(),M(xe,{key:1,onReload:ke})):k.value&&(C.value?.is_global_admin||C.value?.is_org_admin)?(o(),r("section",ys,[i("header",vs,[i("h1",null,f(Ue.value),1),B(Ge,{entries:Ce.value},null,8,["entries"])]),i("section",ps,[i("div",fs,[m.value?(o(),r("div",hs,f(m.value),1)):(o(),r("div",$s,[!P.value&&!V.value&&(C.value.is_global_admin||C.value.is_org_admin)?(o(),M(ka,{key:0,info:C.value,orgs:s.value,permissions:t.value,"permission-summary":ne.value,onCreateOrg:de,onOpenOrg:W,onUpdateOrg:J,onDeleteOrg:me,onToggleOrgPermission:Se,onOpenDialog:A,onDeletePermission:we,onRenamePermissionDisplay:oe},null,8,["info","orgs","permissions","permission-summary"])):P.value?(o(),M(Xa,{key:1,"selected-user":P.value,"user-detail":S.value,"selected-org":V.value,loading:D.value,"show-reg-modal":x.value,onGenerateUserRegistrationLink:Re,onGoOverview:De,onOpenOrg:W,onOnUserNameSaved:K,onRefreshUserDetail:Z,onEditUserName:ue,onCloseRegModal:n[0]||(n[0]=l=>x.value=!1)},null,8,["selected-user","user-detail","selected-org","loading","show-reg-modal"])):V.value?(o(),M(xa,{key:2,"selected-org":V.value,permissions:t.value,onUpdateOrg:J,onCreateRole:fe,onUpdateRole:he,onDeleteRole:$e,onCreateUserInRole:ce,onOpenUser:Oe,onToggleRolePermission:be,onOnRoleDragOver:ve,onOnRoleDrop:pe,onOnUserDragStart:ye},null,8,["selected-org","permissions"])):O("",!0)]))])])])):O("",!0)]),B(ms,{dialog:d.value,"permission-id-pattern":bs,onSubmitDialog:Pe,onCloseDialog:Y},null,8,["dialog"])]))}},ks=j(ws,[["__scopeId","data-v-c67ea2c5"]]),te=Le(ks);te.use(He());te.mount("#admin-app"); diff --git a/paskia/frontend-build/auth/assets/admin-DIOoLLHy.css b/paskia/frontend-build/auth/assets/admin-DIOoLLHy.css deleted file mode 100644 index 988b2bf..0000000 --- a/paskia/frontend-build/auth/assets/admin-DIOoLLHy.css +++ /dev/null @@ -1 +0,0 @@ -.permissions-section[data-v-3b9e6d03]{margin-bottom:var(--space-xl)}.permissions-section h2[data-v-3b9e6d03]{margin-bottom:var(--space-md)}.actions[data-v-3b9e6d03]{display:flex;flex-wrap:wrap;gap:var(--space-sm);align-items:center}.actions button[data-v-3b9e6d03]{width:auto}.org-table a[data-v-3b9e6d03]{text-decoration:none;color:var(--color-link)}.org-table a[data-v-3b9e6d03]:hover{text-decoration:underline}.org-table .center[data-v-3b9e6d03]{width:6rem;min-width:6rem}.org-table .role-names[data-v-3b9e6d03]{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.perm-name-cell[data-v-3b9e6d03]{display:flex;flex-direction:column;gap:.3rem}.perm-title[data-v-3b9e6d03]{font-weight:600;color:var(--color-heading)}.perm-id-info[data-v-3b9e6d03]{font-size:.8rem;color:var(--color-text-muted)}.icon-btn[data-v-3b9e6d03]{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-3b9e6d03]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.delete-icon[data-v-3b9e6d03]{color:var(--color-danger)}.delete-icon[data-v-3b9e6d03]:hover{background:var(--color-danger-bg);color:var(--color-danger-text)}.matrix-wrapper[data-v-3b9e6d03]{margin:var(--space-md) 0;padding:var(--space-lg)}.matrix-scroll[data-v-3b9e6d03]{overflow-x:auto}.matrix-hint[data-v-3b9e6d03]{font-size:.8rem;color:var(--color-text-muted)}.perm-matrix-grid[data-v-3b9e6d03]{display:inline-grid;gap:.25rem;align-items:stretch}.perm-matrix-grid[data-v-3b9e6d03]>*{padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .grid-head[data-v-3b9e6d03]{color:var(--color-text-muted);text-transform:uppercase;font-weight:600;letter-spacing:.05em}.perm-matrix-grid .perm-head[data-v-3b9e6d03]{display:flex;align-items:flex-end;justify-content:flex-start;padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .org-head[data-v-3b9e6d03]{display:flex;align-items:flex-end;justify-content:center}.perm-matrix-grid .org-head span[data-v-3b9e6d03]{writing-mode:vertical-rl;transform:rotate(180deg);font-size:.65rem}.perm-name[data-v-3b9e6d03]{font-weight:600;color:var(--color-heading);padding:.35rem .45rem;font-size:.75rem}.display-text[data-v-3b9e6d03]{margin-right:var(--space-xs)}.edit-display-btn[data-v-3b9e6d03]{padding:.1rem .2rem;font-size:.8rem}.edit-org-btn[data-v-3b9e6d03]{padding:.1rem .2rem;font-size:.8rem;margin-left:var(--space-xs)}.perm-actions[data-v-3b9e6d03],.center[data-v-3b9e6d03]{text-align:center}.muted[data-v-3b9e6d03]{color:var(--color-text-muted)}.card.surface[data-v-b8b4bfbc]{padding:var(--space-lg)}.org-title[data-v-b8b4bfbc]{display:flex;align-items:center;gap:var(--space-sm);margin-bottom:var(--space-lg)}.org-name[data-v-b8b4bfbc]{font-size:1.5rem;font-weight:600;color:var(--color-heading)}.icon-btn[data-v-b8b4bfbc]{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-b8b4bfbc]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.matrix-wrapper[data-v-b8b4bfbc]{margin:var(--space-md) 0;padding:var(--space-lg)}.matrix-scroll[data-v-b8b4bfbc]{overflow-x:auto}.matrix-hint[data-v-b8b4bfbc]{font-size:.8rem;color:var(--color-text-muted)}.perm-matrix-grid[data-v-b8b4bfbc]{display:inline-grid;gap:.25rem;align-items:stretch}.perm-matrix-grid[data-v-b8b4bfbc]>*{padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .grid-head[data-v-b8b4bfbc]{color:var(--color-text-muted);text-transform:uppercase;font-weight:600;letter-spacing:.05em}.perm-matrix-grid .perm-head[data-v-b8b4bfbc]{display:flex;align-items:flex-end;justify-content:flex-start;padding:.35rem .45rem;font-size:.75rem}.perm-matrix-grid .role-head[data-v-b8b4bfbc]{display:flex;align-items:flex-end;justify-content:center}.perm-matrix-grid .role-head span[data-v-b8b4bfbc]{writing-mode:vertical-rl;transform:rotate(180deg);font-size:.65rem}.perm-matrix-grid .add-role-head[data-v-b8b4bfbc]{cursor:pointer}.perm-name[data-v-b8b4bfbc]{font-weight:600;color:var(--color-heading);padding:.35rem .45rem;font-size:.75rem}.roles-grid[data-v-b8b4bfbc]{display:flex;gap:var(--space-lg);margin-top:var(--space-lg)}.role-column[data-v-b8b4bfbc]{flex:1;min-width:200px;border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-md)}.role-header[data-v-b8b4bfbc]{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)}.role-name[data-v-b8b4bfbc]{display:flex;align-items:center;gap:var(--space-xs);font-size:1.1rem;color:var(--color-heading)}.role-actions[data-v-b8b4bfbc]{display:flex;gap:var(--space-xs)}.plus-btn[data-v-b8b4bfbc]{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-b8b4bfbc]:hover{background:#2563eb2e}.user-list[data-v-b8b4bfbc]{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-xs)}.user-chip[data-v-b8b4bfbc]{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 .meta[data-v-b8b4bfbc]{font-size:.7rem;color:var(--color-text-muted)}.empty-role[data-v-b8b4bfbc]{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-b8b4bfbc]{margin:0}.delete-icon[data-v-b8b4bfbc]{color:var(--color-danger)}.delete-icon[data-v-b8b4bfbc]:hover{background:var(--color-danger-bg);color:var(--color-danger-text)}.muted[data-v-b8b4bfbc]{color:var(--color-text-muted)}@media(max-width:720px){.roles-grid[data-v-b8b4bfbc]{flex-direction:column}}.user-detail[data-v-9226fea7]{display:flex;flex-direction:column;gap:var(--space-lg)}.actions[data-v-9226fea7]{display:flex;flex-wrap:wrap;gap:var(--space-sm);align-items:center}.ancillary-actions[data-v-9226fea7]{margin-top:-.5rem}.reg-token-btn[data-v-9226fea7]{align-self:flex-start}.registration-actions[data-v-9226fea7]{display:flex;flex-direction:column;gap:.5rem}.icon-btn[data-v-9226fea7]{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-9226fea7]:hover{color:var(--color-heading);background:var(--color-surface-muted)}.matrix-hint[data-v-9226fea7]{font-size:.8rem;color:var(--color-text-muted)}.error[data-v-9226fea7]{color:var(--color-danger-text)}.small[data-v-9226fea7]{font-size:.9rem}.muted[data-v-9226fea7]{color:var(--color-text-muted)}.error[data-v-42b70397]{color:var(--color-danger-text)}.small[data-v-42b70397]{font-size:.9rem}.muted[data-v-42b70397]{color:var(--color-text-muted)}.view-admin[data-v-c67ea2c5]{padding-bottom:var(--space-3xl)}.view-header[data-v-c67ea2c5]{display:flex;flex-direction:column;gap:var(--space-sm)}.admin-section[data-v-c67ea2c5]{margin-top:var(--space-xl)}.admin-section-body[data-v-c67ea2c5],.admin-panels[data-v-c67ea2c5]{display:flex;flex-direction:column;gap:var(--space-xl)} diff --git a/paskia/frontend-build/auth/assets/auth-CBojJKUK.css b/paskia/frontend-build/auth/assets/auth-CBojJKUK.css deleted file mode 100644 index 65fde81..0000000 --- a/paskia/frontend-build/auth/assets/auth-CBojJKUK.css +++ /dev/null @@ -1 +0,0 @@ -.view-lede[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);font-size:1rem}.section-header[data-v-0cc830bd]{display:flex;flex-direction:column;gap:.4rem}.section-description[data-v-0cc830bd]{margin:0;color:var(--color-text-muted)}.empty-state[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);text-align:center;padding:1rem 0}.logout-button[data-v-0cc830bd]{align-self:flex-start}.logout-row[data-v-0cc830bd]{gap:1rem}.logout-row.single[data-v-0cc830bd]{justify-content:flex-start}.logout-note[data-v-0cc830bd]{margin:.75rem 0 0;color:var(--color-text-muted);font-size:.875rem}@media(max-width:720px){.logout-button[data-v-0cc830bd]{width:100%}}.host-view[data-v-88828278]{padding:3rem 1.5rem 4rem}.host-actions[data-v-88828278]{display:flex;flex-direction:column;gap:.75rem}.host-actions .button-row[data-v-88828278]{gap:.75rem;flex-wrap:wrap}.host-actions .button-row button[data-v-88828278]{flex:0 0 auto}.note[data-v-88828278],.empty-state[data-v-88828278]{margin:0;color:var(--color-text-muted)}@media(max-width:600px){.host-actions .button-row[data-v-88828278]{flex-direction:column}.host-actions .button-row button[data-v-88828278]{width:100%}} diff --git a/paskia/frontend-build/auth/assets/auth-a0yJ_sei.js b/paskia/frontend-build/auth/assets/auth-a0yJ_sei.js deleted file mode 100644 index 2a42247..0000000 --- a/paskia/frontend-build/auth/assets/auth-a0yJ_sei.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as F,r as m,w as Y,o as T,a as z,c as v,m as O,b as j,d as k,e as l,f as t,g as N,h as $,i as C,u as o,j as J,k as K,n as G,F as Q,l as H,t as b,p as X,q as E,s as Z,v as ee}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{u as x,B as se,U as R,_ as te,a as oe,N as ae,M as ne,R as ie,L as le,b as re,A as ue,c as de}from"./AccessDenied-guOGfNm-.js";import{g as U}from"./helpers-CU0-cyzg.js";const ce={class:"view-root","data-view":"profile"},ve={class:"view-header"},fe={class:"section-block"},ge={class:"section-block"},me={class:"section-body"},pe={class:"button-row"},he={class:"section-block"},ye=["disabled"],we=["disabled"],_e=["disabled"],be={key:0,class:"logout-note"},ke={key:1,class:"logout-note"},Ie={__name:"ProfileView",setup(V){const e=x(),f=m(null),r=m(!1),p=m(!1),y=m(""),w=m(!1),A=m(null),_=m(null);Y(r,d=>{d&&(y.value=e.userInfo?.user?.user_name||"")}),T(()=>{f.value=setInterval(()=>{e.userInfo&&(e.userInfo={...e.userInfo})},6e4)}),z(()=>{f.value&&clearInterval(f.value)});const c=async()=>{try{await X.register(null,null,()=>{e.showMessage("Adding new passkey...","info")}),await e.loadUserInfo(),e.showMessage("New passkey added successfully!","success",3e3)}catch(d){console.error("Failed to add new passkey:",d),e.showMessage(d.message,"error")}},S=async d=>{const s=d?.credential_uuid;if(s&&confirm("Are you sure you want to delete this passkey?"))try{await e.deleteCredential(s),e.showMessage("Passkey deleted successfully!","success",3e3)}catch(n){e.showMessage(`Failed to delete passkey: ${n.message}`,"error")}},L=v(()=>e.settings?.rp_name||"this service"),u=v(()=>e.userInfo?.sessions||[]),i=v(()=>u.value.find(s=>s.is_current)?.host||"this host"),h=m({}),B=async d=>{const s=d?.id;if(s){h.value={...h.value,[s]:!0};try{await e.terminateSession(s)}catch(n){e.showMessage(n.message||"Failed to terminate session","error",5e3)}finally{const n={...h.value};delete n[s],h.value=n}}},P=async()=>{await e.logoutEverywhere()},M=async()=>{await e.logout()},a=()=>{y.value=e.userInfo?.user?.user_name||"",r.value=!0},g=v(()=>!!(e.userInfo?.is_global_admin||e.userInfo?.is_org_admin)),I=v(()=>u.value.length>1),D=v(()=>{const d=[{label:"Auth",href:O()}];return g.value&&d.push({label:"Admin",href:j()}),d}),W=async()=>{const d=y.value.trim();if(!d){e.showMessage("Name cannot be empty","error");return}try{w.value=!0,await E("/auth/api/user/display-name",{method:"PUT",body:{display_name:d}}),r.value=!1,await e.loadUserInfo(),e.showMessage("Name updated successfully!","success",3e3)}catch(s){e.showMessage(s.message||"Failed to update name","error")}finally{w.value=!1}};return(d,s)=>(l(),k("section",ce,[t("header",ve,[s[10]||(s[10]=t("h1",null,"👋 Welcome!",-1)),N(se,{entries:D.value},null,8,["entries"]),s[11]||(s[11]=t("p",{class:"view-lede"},"Manage your account details and passkeys.",-1))]),t("section",fe,[o(e).userInfo?.user?(l(),$(R,{key:0,name:o(e).userInfo.user.user_name,visits:o(e).userInfo.user.visits||0,"created-at":o(e).userInfo.user.created_at,"last-seen":o(e).userInfo.user.last_seen,loading:o(e).isLoading,"update-endpoint":"/auth/api/user/display-name",onSaved:s[0]||(s[0]=n=>o(e).loadUserInfo()),onEditName:a},null,8,["name","visits","created-at","last-seen","loading"])):C("",!0)]),t("section",ge,[s[12]||(s[12]=t("div",{class:"section-header"},[t("h2",null,"Your Passkeys"),t("p",{class:"section-description"},"Keep at least one trusted passkey so you can always sign in.")],-1)),t("div",me,[N(te,{credentials:o(e).userInfo?.credentials||[],"aaguid-info":o(e).userInfo?.aaguid_info||{},loading:o(e).isLoading,"hovered-credential-uuid":A.value,"hovered-session-credential-uuid":_.value?.credential_uuid,"allow-delete":"",onDelete:S,onCredentialHover:s[1]||(s[1]=n=>A.value=n)},null,8,["credentials","aaguid-info","loading","hovered-credential-uuid","hovered-session-credential-uuid"]),t("div",pe,[t("button",{onClick:c,class:"btn-primary"},"Add New Passkey"),t("button",{onClick:s[2]||(s[2]=n=>p.value=!0),class:"btn-secondary"},"Add Another Device")])])]),N(oe,{sessions:u.value,"terminating-sessions":h.value,"hovered-credential-uuid":A.value,onTerminate:B,onSessionHover:s[3]||(s[3]=n=>_.value=n),"section-description":"Review where you're signed in and end any sessions you no longer recognize."},null,8,["sessions","terminating-sessions","hovered-credential-uuid"]),r.value?(l(),$(ne,{key:0,onClose:s[6]||(s[6]=n=>r.value=!1)},{default:J(()=>[s[13]||(s[13]=t("h3",null,"Edit Display Name",-1)),t("form",{onSubmit:K(W,["prevent"]),class:"modal-form"},[N(ae,{label:"Display Name",modelValue:y.value,"onUpdate:modelValue":s[4]||(s[4]=n=>y.value=n),busy:w.value,onCancel:s[5]||(s[5]=n=>r.value=!1)},null,8,["modelValue","busy"])],32)]),_:1})):C("",!0),t("section",he,[t("div",{class:G(["button-row logout-row",{single:!I.value}])},[t("button",{type:"button",class:"btn-secondary",onClick:s[7]||(s[7]=(...n)=>o(U)&&o(U)(...n))}," Back "),I.value?(l(),k(Q,{key:1},[t("button",{onClick:M,class:"btn-danger logout-button",disabled:o(e).isLoading},"Logout",8,we),t("button",{onClick:P,class:"btn-danger logout-button",disabled:o(e).isLoading},"All",8,_e)],64)):(l(),k("button",{key:0,onClick:P,class:"btn-danger logout-button",disabled:o(e).isLoading},"Logout",8,ye))],2),I.value?(l(),k("p",ke,[s[15]||(s[15]=t("strong",null,"Logout",-1)),H(" this session on "+b(i.value)+", or ",1),s[16]||(s[16]=t("strong",null,"All",-1)),H(" sessions across all sites and devices for "+b(L.value)+". You'll need to log in again with your passkey afterwards.",1)])):(l(),k("p",be,[s[14]||(s[14]=t("strong",null,"Logout",-1)),H(" from "+b(i.value)+".",1)]))]),p.value?(l(),$(ie,{key:1,endpoint:"/auth/api/user/create-link","auto-copy":!1,"prefix-copy-with-user-name":!1,onClose:s[8]||(s[8]=n=>p.value=!1),onCopied:s[9]||(s[9]=n=>{p.value=!1,o(e).showMessage("Link copied to clipboard!","success",2500)})})):C("",!0)]))}},$e=F(Ie,[["__scopeId","data-v-0cc830bd"]]),Ae={class:"view-root host-view","data-view":"host-profile"},Se={class:"view-header"},Le={class:"view-lede"},Me={class:"section-block"},Ne={class:"section-body"},Ce={key:1,class:"empty-state"},He={class:"section-block"},Pe={class:"section-body host-actions"},Ue={class:"button-row"},Ve=["disabled"],Be=["disabled"],De={class:"note"},Ee={__name:"HostProfileView",props:{initializing:{type:Boolean,default:!1}},setup(V){const e=x(),f=window.location.host,r=v(()=>e.userInfo?.user||null),p=v(()=>e.userInfo?.org?.display_name||""),y=v(()=>e.userInfo?.role?.display_name||""),w=v(()=>{const u=e.settings?.rp_name;return u?`${u} account`:"Account overview"}),A=v(()=>`You're signed in to ${f}.`),_=v(()=>e.settings?.auth_host||""),c=v(()=>{const u=_.value;if(!u)return"";let i=e.settings?.ui_base_path??"/auth/";return i.startsWith("/")||(i=`/${i}`),i.endsWith("/")||(i=`${i}/`),`${window.location.protocol||"https:"}//${u}${i}`}),S=()=>{c.value&&(window.location.href=c.value)},L=async()=>{await e.logout()};return(u,i)=>(l(),k("section",Ae,[t("header",Se,[t("h1",null,b(w.value),1),t("p",Le,b(A.value),1)]),t("section",Me,[t("div",Ne,[r.value?(l(),$(R,{key:0,name:r.value.user_name,visits:r.value.visits||0,"created-at":r.value.created_at,"last-seen":r.value.last_seen,"org-display-name":p.value,"role-name":y.value,"can-edit":!1},null,8,["name","visits","created-at","last-seen","org-display-name","role-name"])):(l(),k("p",Ce,b(V.initializing?"Loading your account…":"No active session found."),1))])]),t("section",He,[t("div",Pe,[t("div",Ue,[t("button",{type:"button",class:"btn-secondary",onClick:i[0]||(i[0]=(...h)=>o(U)&&o(U)(...h))}," Back "),t("button",{type:"button",class:"btn-danger",disabled:o(e).isLoading,onClick:L},b(o(e).isLoading?"Signing out…":"Logout"),9,Ve),c.value?(l(),k("button",{key:0,type:"button",class:"btn-primary",disabled:o(e).isLoading,onClick:S}," Full Profile ",8,Be)):C("",!0)]),t("p",De,[i[1]||(i[1]=t("strong",null,"Logout",-1)),H(" from "+b(o(f))+", or access your ",1),i[2]||(i[2]=t("strong",null,"Full Profile",-1)),H(" at "+b(_.value)+" (you may need to sign in again).",1)])])])]))}},xe=F(Ee,[["__scopeId","data-v-88828278"]]),Fe={class:"app-shell"},Te={class:"app-main"},ze={__name:"App",setup(V){const e=x(),f=m(!0),r=m("Loading..."),p=m(!1),y=m(!1);function w(a){if(!a)return null;const g=a.trim().toLowerCase();return g?g.replace(/:80$/,"").replace(/:443$/,""):null}const A=v(()=>{const a=e.settings?.auth_host;if(!a)return!1;const g=w(window.location.host),I=w(a);return g!==I});let _=null,c=null;async function S(){try{return e.userInfo=await E("/auth/api/user-info",{method:"POST"}),p.value=!0,f.value=!1,P(),!0}catch{return!1}}async function L(){u();const a=await Z("login");c=document.createElement("iframe"),c.id="auth-iframe",c.title="Authentication",c.allow="publickey-credentials-get; publickey-credentials-create",c.src=a,document.body.appendChild(c),r.value="Authentication required..."}function u(){c&&(c.remove(),c=null)}function i(){window.location.reload()}function h(a){const g=a.data;if(g?.type)switch(g.type){case"auth-success":u(),f.value=!0,r.value="Loading user profile...",S();break;case"auth-error":g.cancelled?console.log("Authentication cancelled by user"):e.showMessage(g.message||"Authentication failed","error",5e3);break;case"auth-cancelled":console.log("Authentication cancelled");break;case"auth-back":u(),f.value=!1,y.value=!0,e.showMessage("Authentication cancelled","info",3e3);break;case"auth-close-request":u();break}}async function B(){try{await E("/auth/api/validate",{method:"POST",credentials:"include"})}catch(a){a.status===401?(console.log("Session expired, requiring re-authentication"),p.value=!1,f.value=!0,M(),L()):console.error("Session validation error:",a)}}function P(){M(),_=setInterval(B,120*1e3)}function M(){_&&(clearInterval(_),_=null)}return T(async()=>{window.addEventListener("message",h),await e.loadSettings();const a=e.settings?.rp_name;if(a){const I=e.settings?.auth_host,D=I&&w(window.location.host)!==w(I);document.title=D?`${a} · Account summary`:a}await S()||L()}),z(()=>{window.removeEventListener("message",h),M(),u()}),(a,g)=>(l(),k("div",Fe,[N(re),t("main",Te,[p.value&&A.value?(l(),$(xe,{key:0,initializing:f.value},null,8,["initializing"])):p.value?(l(),$($e,{key:1})):f.value?(l(),$(le,{key:2,message:r.value},null,8,["message"])):y.value?(l(),$(ue,{key:3,onReload:i})):C("",!0)])]))}},q=ee(ze);q.use(de());q.mount("#app"); diff --git a/paskia/frontend-build/auth/assets/forward-BHNzlQhM.js b/paskia/frontend-build/auth/assets/forward-BHNzlQhM.js deleted file mode 100644 index da1a690..0000000 --- a/paskia/frontend-build/auth/assets/forward-BHNzlQhM.js +++ /dev/null @@ -1 +0,0 @@ -import{c as o,W as d,o as i,h as s,e as m,u as l,v as h}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";import{g as n}from"./helpers-CU0-cyzg.js";const f={__name:"RestrictedForward",setup(w){const a=o(()=>d()),r=o(()=>{const t=document.documentElement.getAttribute("data-mode");return t==="reauth"?"reauth":t==="forbidden"?"forbidden":"login"});function c(){location.reload()}function u(){const e=a.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.href=e}return i(()=>{window.addEventListener("keydown",e=>{e.key==="Escape"&&n()})}),(e,t)=>(m(),s(p,{mode:r.value,onAuthenticated:c,onBack:l(n),onHome:u},null,8,["mode","onBack"]))}};h(f).mount("#app"); diff --git a/paskia/frontend-build/auth/assets/helpers-CU0-cyzg.js b/paskia/frontend-build/auth/assets/helpers-CU0-cyzg.js deleted file mode 100644 index b724508..0000000 --- a/paskia/frontend-build/auth/assets/helpers-CU0-cyzg.js +++ /dev/null @@ -1 +0,0 @@ -function f(r){if(!r)return"Never";const s=new Date(r),u=s-new Date,e=u>0,a=Math.abs(u),n=Math.round(a/(1e3*60)),o=Math.round(a/(1e3*60*60)),t=Math.round(a/(1e3*60*60*24));return a<1e3*60?"Now":n<=60?e?`In ${n} minute${n===1?"":"s"}`:n===1?"a minute ago":`${n} minutes ago`:o<=24?e?`In ${o} hour${o===1?"":"s"}`:o===1?"an hour ago":`${o} hours ago`:t<=14?e?`In ${t} day${t===1?"":"s"}`:t===1?"a day ago":`${t} days ago`:s.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})}const c=()=>history.back()||window.close();export{f,c as g}; diff --git a/paskia/frontend-build/auth/assets/reset-DXzuKgh6.css b/paskia/frontend-build/auth/assets/reset-DXzuKgh6.css deleted file mode 100644 index 88ab400..0000000 --- a/paskia/frontend-build/auth/assets/reset-DXzuKgh6.css +++ /dev/null @@ -1 +0,0 @@ -.center[data-v-4f202f9a]{text-align:center}.button-row.center[data-v-4f202f9a]{display:flex;justify-content:center}.section-body[data-v-4f202f9a]{gap:1.25rem}.name-edit span[data-v-4f202f9a]{color:var(--color-text-muted);font-size:.9rem} diff --git a/paskia/frontend-build/auth/assets/reset-YnZxhnI5.js b/paskia/frontend-build/auth/assets/reset-YnZxhnI5.js deleted file mode 100644 index c9ccd44..0000000 --- a/paskia/frontend-build/auth/assets/reset-YnZxhnI5.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as M,G as F,r as i,c as v,W as b,o as U,d as c,e as u,i as V,f as t,t as g,n as $,z,A as E,S as I,C as N,q as R,X as D,V as K,p as O,v as j}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},H={class:"view-root"},J={class:"surface surface--tight",style:{"max-width":"560px",margin:"0 auto",width:"100%"}},L={class:"view-header",style:{"text-align":"center"}},W={class:"view-lede"},X={key:0,class:"section-block"},Y={key:1,class:"section-block"},Q={class:"section-body center"},Z={key:2,class:"section-block"},ee={class:"section-body"},se={class:"name-edit"},te=["disabled"],ae=["disabled"],ne={__name:"ResetApp",setup(ie){const o=F({show:!1,message:"",type:"info"}),d=i(!0),n=i(!1),r=i(""),x=i(null),p=i(null),f=i(""),m=i("");let h=null;const P=v(()=>p.value?.session_type||"your enrollment"),S=v(()=>d.value?"Preparing your secure enrollment…":y.value?`Finish up ${P.value}. You may edit the name below if needed, and it will be saved to your passkey.`:"This reset link is no longer valid.");v(()=>b());const y=v(()=>!!(r.value&&p.value));function l(e,s="info",a=3e3){o.show=!0,o.message=e,o.type=s,h&&clearTimeout(h),a>0&&(h=setTimeout(()=>{o.show=!1},a))}async function T(){try{const e=await N();x.value=e,e?.rp_name&&(document.title=`${e.rp_name} · Passkey Setup`)}catch(e){console.warn("Unable to load settings",e)}}async function C(){if(r.value)try{p.value=await R(`/auth/api/user-info?reset=${encodeURIComponent(r.value)}`,{method:"POST"}),f.value=p.value?.user?.user_name||""}catch(e){console.error("Failed to load user info",e);const s=e instanceof D?e.data?.detail||"Reset link is invalid or expired.":K(e);m.value=s,l(s,"error",0)}}async function _(){if(!y.value||n.value)return;n.value=!0,l("Starting passkey registration…","info");let e;try{const s=f.value.trim()||null;e=await O.register(r.value,s)}catch(s){n.value=!1;const a=s?.message||"Passkey registration cancelled",k=a==="Passkey registration cancelled";l(k?a:`Registration failed: ${a}`,k?"info":"error",4e3);return}try{await A(e)}catch(s){n.value=!1;const a=s?.message||"Failed to establish session";l(a,"error",4e3);return}l("Passkey registered successfully!","success",800),setTimeout(()=>{n.value=!1,w()},800)}async function A(e){if(!e?.session_token)throw new Error("Registration response missing session_token");return await R("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}function w(){const e=b.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.reload()}function B(){const e=window.location.pathname.split("/").filter(Boolean);if(!e.length)return"";const s=e[e.length-1],a=e.slice(0,-1);return a.length>1||a.length===1&&a[0]!=="auth"||!s.includes(".")?"":s}return U(async()=>{if(r.value=B(),await T(),!r.value){const e="Reset link is missing or malformed.";m.value=e,l(e,"error",0),d.value=!1;return}await C(),d.value=!1}),(e,s)=>(u(),c("div",q,[o.show?(u(),c("div",G,[t("div",{class:$(["status",o.type])},g(o.message),3)])):V("",!0),t("main",H,[t("div",J,[t("header",L,[s[1]||(s[1]=t("h1",null,"🔑 Registration",-1)),t("p",W,g(S.value),1)]),d.value?(u(),c("section",X,[...s[2]||(s[2]=[t("div",{class:"section-body center"},[t("p",null,"Loading reset details…")],-1)])])):y.value?(u(),c("section",Z,[t("div",ee,[t("label",se,[s[3]||(s[3]=t("span",null,"👤 Name",-1)),z(t("input",{type:"text","onUpdate:modelValue":s[0]||(s[0]=a=>f.value=a),disabled:n.value,maxlength:"64",onKeyup:I(_,["enter"])},null,40,te),[[E,f.value]])]),t("button",{class:"btn-primary",disabled:n.value,onClick:_},g(n.value?"Registering…":"Register Passkey"),9,ae)])])):(u(),c("section",Y,[t("div",Q,[t("p",null,g(m.value),1),t("div",{class:"button-row center",style:{"justify-content":"center"}},[t("button",{class:"btn-secondary",onClick:w},"Return to sign-in")])])]))])])]))}},oe=M(ne,[["__scopeId","data-v-4f202f9a"]]);j(oe).mount("#app"); diff --git a/paskia/frontend-build/auth/assets/restricted-DVCvYFGN.js b/paskia/frontend-build/auth/assets/restricted-DVCvYFGN.js deleted file mode 100644 index 35fff72..0000000 --- a/paskia/frontend-build/auth/assets/restricted-DVCvYFGN.js +++ /dev/null @@ -1 +0,0 @@ -import{c as r,o as c,h as i,e as d,v as u}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";const h={__name:"RestrictedApi",setup(m){const a=r(()=>{const n=new URLSearchParams(window.location.hash.slice(1)).get("mode");return n==="reauth"?"reauth":n==="forbidden"?"forbidden":"login"});function t(e){window.parent&&window.parent!==window&&window.parent.postMessage(e,"*")}function s(e){t({type:"auth-success",authenticated:!0,sessionToken:e.session_token})}function o(){t({type:"auth-back"})}return c(()=>{t({type:"auth-ready"}),window.addEventListener("keydown",e=>{e.key==="Escape"&&o()})}),(e,n)=>(d(),i(p,{mode:a.value,onAuthenticated:s,onBack:o},null,8,["mode"]))}};u(h).mount("#app"); diff --git a/paskia/frontend-build/auth/index.html b/paskia/frontend-build/auth/index.html deleted file mode 100644 index 3ec617f..0000000 --- a/paskia/frontend-build/auth/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Auth Profile - - - - - - - - - -
- - diff --git a/paskia/frontend-build/auth/restricted/index.html b/paskia/frontend-build/auth/restricted/index.html deleted file mode 100644 index 89a2b4d..0000000 --- a/paskia/frontend-build/auth/restricted/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - -
diff --git a/paskia/frontend-build/int/forward/index.html b/paskia/frontend-build/int/forward/index.html deleted file mode 100644 index 4aa970e..0000000 --- a/paskia/frontend-build/int/forward/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Access Restricted - - - - - - - - -
- - diff --git a/paskia/frontend-build/int/reset/index.html b/paskia/frontend-build/int/reset/index.html deleted file mode 100644 index 73593be..0000000 --- a/paskia/frontend-build/int/reset/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Complete Passkey Setup - - - - - - -
- - -- 2.55.0 From 4da1127976a558b6546c6874f1e0060b866bb33d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 16:43:47 +0000 Subject: [PATCH 02/16] Drafting remote auth linking. --- frontend/auth/restricted/RestrictedApi.vue | 36 +- frontend/src/components/PairingCodeEntry.vue | 216 +++++++++ frontend/src/components/ProfileView.vue | 32 ++ .../src/components/RemoteAuthComplete.vue | 224 ++++++++++ .../src/components/RemoteAuthLinkModal.vue | 417 ++++++++++++++++++ frontend/src/components/RestrictedAuth.vue | 41 +- paskia/fastapi/mainapp.py | 16 +- paskia/fastapi/ws.py | 292 +++++++++++- paskia/remoteauth.py | 284 ++++++++++++ 9 files changed, 1554 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/PairingCodeEntry.vue create mode 100644 frontend/src/components/RemoteAuthComplete.vue create mode 100644 frontend/src/components/RemoteAuthLinkModal.vue create mode 100644 paskia/remoteauth.py diff --git a/frontend/auth/restricted/RestrictedApi.vue b/frontend/auth/restricted/RestrictedApi.vue index 950aa2e..e3eeeb3 100644 --- a/frontend/auth/restricted/RestrictedApi.vue +++ b/frontend/auth/restricted/RestrictedApi.vue @@ -1,5 +1,14 @@ @@ -45,6 +58,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import passkey from '@/utils/passkey' import { getSettings } from '@/utils/settings' import { fetchJson, getUserFriendlyErrorMessage } from '@/utils/api' +import RemoteAuthLinkModal from '@/components/RemoteAuthLinkModal.vue' const props = defineProps({ mode: { @@ -62,6 +76,7 @@ const loading = ref(false) const settings = ref(null) const userInfo = ref(null) const currentView = ref('initial') // 'initial', 'login', 'forbidden' +const showRemoteAuth = ref(false) let statusTimer = null const isAuthenticated = computed(() => !!userInfo.value?.authenticated) @@ -196,6 +211,30 @@ async function setSessionCookie(result) { }) } +// Remote authentication from another device +function startRemoteAuth() { + showRemoteAuth.value = true +} + +async function handleRemoteAuthenticated(result) { + showRemoteAuth.value = false + showMessage('Authenticated from another device!', 'success', 2000) + try { + await setSessionCookie(result) + } catch (error) { + const message = error?.message || 'Failed to establish session' + showMessage(message, 'error', 4000) + emit('auth-error', { message, cancelled: false }) + return + } + emit('authenticated', result) +} + +function handleRemoteAuthError(errorMsg) { + showRemoteAuth.value = false + showMessage(errorMsg || 'Remote authentication failed', 'error', 4000) +} + onMounted(async () => { await fetchSettings() await fetchUserInfo() diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index 95c159f..5928645 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -7,6 +7,7 @@ from fastapi import FastAPI, HTTPException, Request, Response from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles +from paskia import remoteauth from paskia.fastapi import admin, api, auth_host, ws from paskia.fastapi.session import AUTH_COOKIE from paskia.util import frontend, hostutil, passphrase @@ -37,13 +38,17 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path origins=config["origins"], bootstrap=False, ) + # Initialize remote authentication manager + await remoteauth.init() except ValueError as e: logging.error(f"⚠️ {e}") # Re-raise to fail fast raise yield - # (Optional) add shutdown cleanup here later + + # Shutdown cleanup + await remoteauth.shutdown() app = FastAPI(lifespan=lifespan) @@ -113,6 +118,15 @@ async def examples_page(): # Note: this catch-all handler must be the last route defined +@app.get("/remote/{token}") +@app.get("/auth/remote/{token}") +async def remote_auth_link(token: str): + """Serve the restricted app for cross-device login.""" + if not passphrase.is_well_formed(token): + raise HTTPException(status_code=404) + return Response(*await frontend.read("/auth/restricted/index.html")) + + @app.get("/{reset}") @app.get("/auth/{reset}") async def reset_link(reset: str): diff --git a/paskia/fastapi/ws.py b/paskia/fastapi/ws.py index 4efca50..a600abf 100644 --- a/paskia/fastapi/ws.py +++ b/paskia/fastapi/ws.py @@ -1,3 +1,4 @@ +import asyncio import logging from functools import wraps from uuid import UUID @@ -5,11 +6,12 @@ from uuid import UUID from fastapi import FastAPI, WebSocket, WebSocketDisconnect from webauthn.helpers.exceptions import InvalidAuthenticationResponse +from paskia import remoteauth from paskia.authsession import create_session, get_reset, get_session from paskia.fastapi import authz from paskia.fastapi.session import AUTH_COOKIE, infodict from paskia.globals import db, passkey -from paskia.util import passphrase +from paskia.util import hostutil, passphrase from paskia.util.tokens import create_token, session_key @@ -198,3 +200,291 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE): "session_token": token, } ) + + +@app.websocket("/remote-auth/request") +@websocket_error_handler +async def websocket_remote_auth_request(ws: WebSocket): + """Request authentication from another device. + + This endpoint is called by the device that wants to be authenticated. + It creates a remote auth request and waits for another device to authenticate. + + Flow: + 1. Client connects + 2. Server creates a remote auth token and sends it with URL/expiry/pairing_code + 3. Server waits for another device to authenticate via /remote-auth/complete + 4. When auth completes, server sends session_token to this client + 5. Client can then use the session token to set a cookie + """ + origin = _validate_origin(ws) + host = origin.split("://", 1)[1] + + if remoteauth.instance is None: + raise ValueError("Remote authentication is not available") + + metadata = infodict(ws, "remote-auth-request") + + # Create the remote auth request + token, pairing_code, expiry = await remoteauth.instance.create_request( + host=host, + ip=metadata.get("ip") or "", + user_agent=metadata.get("user_agent") or "", + ) + + # Build the URL for the authenticating device + url = hostutil.auth_site_base_url() + f"remote/{token}" + + # Send the token, pairing code, and URL to the client + await ws.send_json( + { + "token": token, + "pairing_code": pairing_code, + "url": url, + "expires": expiry.isoformat().replace("+00:00", "Z"), + } + ) + + # Set up async notification + result_event = asyncio.Event() + result_data: dict = {} + + def on_complete( + session_token: str | None, + user_uuid: UUID | None, + credential_uuid: UUID | None, + ): + result_data["session_token"] = session_token + result_data["user_uuid"] = user_uuid + result_data["credential_uuid"] = credential_uuid + result_event.set() + + await remoteauth.instance.set_notify_callback(token, on_complete) + + try: + # Wait for either: + # 1. Authentication to complete (result_event set) + # 2. Client to disconnect + # 3. Client to send a cancel message + # 4. Timeout (handled by remoteauth cleanup) + + while True: + # Use asyncio.wait to handle both event and websocket + receive_task = asyncio.create_task(ws.receive_json()) + event_task = asyncio.create_task(result_event.wait()) + + done, pending = await asyncio.wait( + [receive_task, event_task], + return_when=asyncio.FIRST_COMPLETED, + ) + + # Cancel pending tasks + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + if event_task in done: + # Authentication completed (or expired/cancelled) + if result_data.get("session_token"): + await ws.send_json( + { + "status": "authenticated", + "user_uuid": str(result_data["user_uuid"]), + "session_token": result_data["session_token"], + } + ) + else: + await ws.send_json( + { + "status": "expired", + "detail": "Remote authentication request expired or was cancelled", + } + ) + break + + if receive_task in done: + # Client sent a message + msg = receive_task.result() + if msg.get("action") == "cancel": + await remoteauth.instance.cancel_request(token) + await ws.send_json({"status": "cancelled"}) + break + # Ignore other messages + + except WebSocketDisconnect: + # Client disconnected, cancel the request + await remoteauth.instance.cancel_request(token) + except Exception: + await remoteauth.instance.cancel_request(token) + raise + + +@app.websocket("/remote-auth/complete/{token}") +@websocket_error_handler +async def websocket_remote_auth_complete(ws: WebSocket, token: str): + """Complete a remote authentication request. + + This endpoint is called by the authenticating device (the one with the passkey). + It performs WebAuthn authentication and notifies the requesting device. + + Flow: + 1. Client opens the remote auth link and connects here + 2. Server verifies the token is valid + 3. Server sends WebAuthn options + 4. Client authenticates with passkey + 5. Server creates session for the REQUESTING device's host + 6. Server notifies the requesting device via the callback + 7. Server sends confirmation to this client + """ + origin = _validate_origin(ws) + + if remoteauth.instance is None: + raise ValueError("Remote authentication is not available") + + # Validate the remote auth token + request = await remoteauth.instance.get_request(token) + if request is None: + raise ValueError("This remote authentication link is invalid or has expired") + + if request.completed: + raise ValueError("This remote authentication has already been completed") + + # The session will be created for the requesting device's host, not this device's + target_host = request.host + + # Generate authentication options (no credential restriction for remote auth) + options, challenge = passkey.instance.auth_generate_options(credential_ids=None) + await ws.send_json({"optionsJSON": options}) + + # Wait for client authentication response + credential = passkey.instance.auth_parse(await ws.receive_json()) + + # Fetch and verify credential + try: + stored_cred = await db.instance.get_credential_by_id(credential.raw_id) + except ValueError: + raise ValueError( + f"This passkey is no longer registered with {passkey.instance.rp_name}" + ) + + # Verify the credential + passkey.instance.auth_verify(credential, challenge, stored_cred, origin) + + # Update credential last_used + await db.instance.login(stored_cred.user_uuid, stored_cred) + + # Create a session for the REQUESTING device + assert stored_cred.uuid is not None + session_token = await create_session( + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + host=target_host, + ip=request.ip, + user_agent=request.user_agent, + ) + + # Complete the remote auth request (notifies the waiting device) + completed = await remoteauth.instance.complete_request( + token=token, + session_token=session_token, + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + ) + + if not completed: + raise ValueError("Failed to complete remote authentication") + + # Send confirmation to the authenticating device + await ws.send_json( + { + "status": "success", + "message": "Authentication successful. The other device is now logged in.", + } + ) + + +@app.websocket("/remote-auth/pair/{code}") +@websocket_error_handler +async def websocket_remote_auth_pair(ws: WebSocket, code: str): + """Complete a remote authentication request using a pairing code. + + This endpoint is called from the user's profile on the authenticating device. + The user enters the pairing code displayed on the requesting device. + + Flow: + 1. User on Device B (with passkey) enters pairing code from Device A + 2. Server looks up the remote auth request by pairing code + 3. Server sends WebAuthn options + 4. User authenticates with passkey + 5. Server creates session for Device A's host with Device A's metadata + 6. Server notifies Device A via the callback + 7. Server sends confirmation to Device B + """ + origin = _validate_origin(ws) + + if remoteauth.instance is None: + raise ValueError("Remote authentication is not available") + + # Look up the remote auth request by pairing code + request = await remoteauth.instance.get_request_by_pairing_code(code) + if request is None: + raise ValueError("Invalid or expired pairing code") + + if request.completed: + raise ValueError("This remote authentication has already been completed") + + # The session will be created for the requesting device's host + target_host = request.host + + # Generate authentication options (no credential restriction for remote auth) + options, challenge = passkey.instance.auth_generate_options(credential_ids=None) + await ws.send_json({"optionsJSON": options}) + + # Wait for client authentication response + credential = passkey.instance.auth_parse(await ws.receive_json()) + + # Fetch and verify credential + try: + stored_cred = await db.instance.get_credential_by_id(credential.raw_id) + except ValueError: + raise ValueError( + f"This passkey is no longer registered with {passkey.instance.rp_name}" + ) + + # Verify the credential + passkey.instance.auth_verify(credential, challenge, stored_cred, origin) + + # Update credential last_used + await db.instance.login(stored_cred.user_uuid, stored_cred) + + # Create a session for the REQUESTING device (with their IP/user-agent) + assert stored_cred.uuid is not None + session_token = await create_session( + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + host=target_host, + ip=request.ip, + user_agent=request.user_agent, + ) + + # Complete the remote auth request (notifies the waiting device) + completed = await remoteauth.instance.complete_request( + token=request.key, + session_token=session_token, + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + ) + + if not completed: + raise ValueError("Failed to complete remote authentication") + + # Send confirmation to the authenticating device + await ws.send_json( + { + "status": "success", + "message": "Authentication successful. The other device is now logged in.", + } + ) diff --git a/paskia/remoteauth.py b/paskia/remoteauth.py new file mode 100644 index 0000000..7607083 --- /dev/null +++ b/paskia/remoteauth.py @@ -0,0 +1,284 @@ +""" +Cross-device (remote) authentication support. + +This module manages the flow for authenticating from another device: +1. Device A (requesting) creates a remote auth request and displays QR/link +2. Device B (authenticating) opens the link and authenticates with passkey +3. Device A receives the session via WebSocket notification + +Alternative flow (initiated from profile/authenticating device): +1. Device A (requesting) creates request and displays short pairing code +2. Device B (authenticating) enters the pairing code in their profile +3. Device B authenticates, Device A receives the session + +The requests are stored in-memory with short expiration (5 minutes). +""" + +import asyncio +import logging +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Callable +from uuid import UUID + +from paskia.util import passphrase + +# Remote auth requests expire after this duration +REMOTE_AUTH_LIFETIME = timedelta(minutes=5) + +# Number of words for the short pairing code (easier to communicate than alphanumeric) +PAIRING_CODE_WORDS = 3 + + +@dataclass +class RemoteAuthRequest: + """A pending remote authentication request.""" + + key: str # The passphrase token + pairing_code: str # Short alphanumeric code for manual entry + created_at: datetime + host: str # The host where the session should be created + ip: str # IP of the requesting device + user_agent: str # User agent of the requesting device + # Callback to notify the requesting device when auth completes + # Takes (session_token, user_uuid, credential_uuid) or (None, None, None) on cancel/expire + notify: Callable[[str | None, UUID | None, UUID | None], None] | None = None + # Set when authentication completes + completed: bool = False + session_token: str | None = None + user_uuid: UUID | None = None + credential_uuid: UUID | None = None + + +def _generate_pairing_code() -> str: + """Generate a short, easy-to-communicate pairing code using words.""" + return passphrase.generate(n=PAIRING_CODE_WORDS) + + +class RemoteAuthManager: + """Manages pending remote authentication requests.""" + + def __init__(self): + self._requests: dict[str, RemoteAuthRequest] = {} # keyed by passphrase token + self._by_pairing_code: dict[str, str] = {} # pairing_code -> token + self._cleanup_task: asyncio.Task | None = None + self._lock = asyncio.Lock() + + async def start(self): + """Start the cleanup background task.""" + if self._cleanup_task is None: + self._cleanup_task = asyncio.create_task(self._cleanup_loop()) + + async def stop(self): + """Stop the cleanup background task.""" + if self._cleanup_task: + self._cleanup_task.cancel() + try: + await self._cleanup_task + except asyncio.CancelledError: + pass + self._cleanup_task = None + + async def _cleanup_loop(self): + """Periodically clean up expired requests.""" + while True: + try: + await asyncio.sleep(60) # Check every minute + await self._cleanup_expired() + except asyncio.CancelledError: + break + except Exception: + logging.exception("Error in remote auth cleanup loop") + + async def _cleanup_expired(self): + """Remove expired requests and notify waiting clients.""" + now = datetime.now(timezone.utc) + expired_keys = [] + async with self._lock: + for key, req in self._requests.items(): + if now > req.created_at + REMOTE_AUTH_LIFETIME: + expired_keys.append(key) + for key in expired_keys: + req = self._requests.pop(key) + # Also remove from pairing code index + self._by_pairing_code.pop(req.pairing_code, None) + if req.notify and not req.completed: + try: + req.notify(None, None, None) + except Exception: + pass + + async def create_request( + self, + host: str, + ip: str, + user_agent: str, + ) -> tuple[str, str, datetime]: + """Create a new remote auth request. + + Returns: + (token, pairing_code, expiry) - The passphrase token, short pairing code, and expiration time + """ + token = passphrase.generate() + pairing_code = _generate_pairing_code() + now = datetime.now(timezone.utc) + expiry = now + REMOTE_AUTH_LIFETIME + + request = RemoteAuthRequest( + key=token, + pairing_code=pairing_code, + created_at=now, + host=host, + ip=ip, + user_agent=user_agent, + ) + + async with self._lock: + # Ensure pairing code is unique (regenerate if collision) + while pairing_code in self._by_pairing_code: + pairing_code = _generate_pairing_code() + request.pairing_code = pairing_code + self._requests[token] = request + self._by_pairing_code[pairing_code] = token + + return token, pairing_code, expiry + + async def get_request(self, token: str) -> RemoteAuthRequest | None: + """Get a pending request by token, if valid and not expired.""" + if not passphrase.is_well_formed(token): + return None + async with self._lock: + req = self._requests.get(token) + if req is None: + return None + now = datetime.now(timezone.utc) + if now > req.created_at + REMOTE_AUTH_LIFETIME: + # Expired + del self._requests[token] + self._by_pairing_code.pop(req.pairing_code, None) + return None + return req + + async def get_request_by_pairing_code(self, code: str) -> RemoteAuthRequest | None: + """Get a pending request by pairing code, if valid and not expired.""" + # Normalize: lowercase, dot-separated words + normalized = code.lower().strip().replace(" ", ".") + # Validate it's a well-formed short passphrase + if not passphrase.is_well_formed(normalized, n=PAIRING_CODE_WORDS): + return None + async with self._lock: + token = self._by_pairing_code.get(normalized) + if token is None: + return None + req = self._requests.get(token) + if req is None: + self._by_pairing_code.pop(normalized, None) + return None + now = datetime.now(timezone.utc) + if now > req.created_at + REMOTE_AUTH_LIFETIME: + # Expired + del self._requests[token] + self._by_pairing_code.pop(normalized, None) + return None + return req + + async def set_notify_callback( + self, + token: str, + callback: Callable[[str | None, UUID | None, UUID | None], None], + ) -> bool: + """Set the notification callback for a request. + + Returns True if the request exists and callback was set. + """ + async with self._lock: + req = self._requests.get(token) + if req is None: + return False + req.notify = callback + # If already completed, notify immediately + if req.completed: + try: + callback(req.session_token, req.user_uuid, req.credential_uuid) + except Exception: + pass + return True + + async def complete_request( + self, + token: str, + session_token: str, + user_uuid: UUID, + credential_uuid: UUID, + ) -> bool: + """Mark a request as completed with the authentication result. + + Returns True if the request existed and was completed. + """ + async with self._lock: + req = self._requests.get(token) + if req is None: + return False + if req.completed: + return False # Already completed + req.completed = True + req.session_token = session_token + req.user_uuid = user_uuid + req.credential_uuid = credential_uuid + if req.notify: + try: + req.notify(session_token, user_uuid, credential_uuid) + except Exception: + pass + return True + + async def cancel_request(self, token: str) -> bool: + """Cancel and remove a request. + + Returns True if the request existed and was removed. + """ + async with self._lock: + req = self._requests.pop(token, None) + if req is None: + return False + self._by_pairing_code.pop(req.pairing_code, None) + if req.notify and not req.completed: + try: + req.notify(None, None, None) + except Exception: + pass + return True + + async def consume_request(self, token: str) -> RemoteAuthRequest | None: + """Get and remove a request (for use by the authenticating device).""" + if not passphrase.is_well_formed(token): + return None + async with self._lock: + req = self._requests.get(token) + if req is None: + return None + now = datetime.now(timezone.utc) + if now > req.created_at + REMOTE_AUTH_LIFETIME: + del self._requests[token] + return None + # Don't remove yet - wait until completion + return req + + +# Global instance +instance: RemoteAuthManager | None = None + + +async def init(): + """Initialize the global remote auth manager.""" + global instance + instance = RemoteAuthManager() + await instance.start() + + +async def shutdown(): + """Shutdown the global remote auth manager.""" + global instance + if instance: + await instance.stop() + instance = None -- 2.55.0 From c7d4907cf19c42ac77a6c82e2905caa3af647182 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 21:19:09 +0000 Subject: [PATCH 03/16] More robust dev mode script, with --caddy to run the proxy as well. --- scripts/dev.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 241 insertions(+), 12 deletions(-) diff --git a/scripts/dev.py b/scripts/dev.py index 24a3f8b..c8beb44 100755 --- a/scripts/dev.py +++ b/scripts/dev.py @@ -11,23 +11,33 @@ Usage: The optional host:port argument sets where the Vite frontend listens. All other options are forwarded to `paskia serve`. Backend always listens on localhost:4402. + +Options: + --caddy Run Caddy as HTTPS proxy on port 4443 + --rp-id HOST Relying Party ID (used as hostname for Caddy) + --origin URL Allowed origin(s), passed to backend + --auth-host H Dedicated auth host, passed to backend """ import argparse import atexit +import ipaddress +import json import os import shutil import signal import subprocess import sys +import time from pathlib import Path from sys import stderr from threading import Thread - -from paskia.fastapi.__main__ import parse_endpoint +from urllib.parse import urlparse DEFAULT_VITE_PORT = 4403 # overrides by CLI option BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts +CADDY_PORT = 4443 # HTTPS port for Caddy proxy +DEFAULT_HOST = "localhost" NO_FRONTEND_TOOL = """\ ┃ ⚠️ deno, npm or bunx needed to run the frontend server. @@ -51,6 +61,77 @@ NO_FRONTEND = """\ ┃ for various frontend assets, so make sure to start it manually. """ +# Global options to disable auto HTTPS redirect (requires port 80) +CADDYFILE_GLOBAL = """\ +{ + admin off + auto_https off + log { + level warn + } +} +""" + +CADDYFILE_SITE_BLOCK = """\ +SITE_ADDR { + # API requests go directly to backend + handle /auth/api/* { + reverse_proxy localhost:BACKEND_PORT + } + handle /auth/ws/* { + reverse_proxy localhost:BACKEND_PORT + } + # Everything else goes to Vite + handle { + reverse_proxy localhost:VITE_PORT + } +} +""" + + +def parse_endpoint( + value: str | None, default_port: int +) -> tuple[str | None, int | None, str | None, bool]: + """Parse an endpoint for Vite (simplified version for dev.py). + + Returns (host, port, uds_path, all_ifaces). + """ + if not value: + return DEFAULT_HOST, default_port, None, False + + # Port only (numeric) -> localhost:port + if value.isdigit(): + return DEFAULT_HOST, int(value), None, False + + # Leading colon :port -> bind all interfaces + if value.startswith(":") and value != ":": + port_part = value[1:] + if not port_part.isdigit(): + raise SystemExit(f"Invalid port in '{value}'") + return None, int(port_part), None, True + + # UNIX domain socket + if value.startswith("unix:"): + uds_path = value[5:] or None + if uds_path is None: + raise SystemExit("unix: path must not be empty") + return None, None, uds_path, False + + # Unbracketed IPv6 (cannot safely contain a port) + if value.count(":") > 1 and not value.startswith("["): + try: + ipaddress.IPv6Address(value) + except ValueError as e: + raise SystemExit(f"Invalid IPv6 address '{value}': {e}") + return value, default_port, None, False + + # Use urllib.parse for everything else + parsed = urlparse(f"//{value}") + host = parsed.hostname or DEFAULT_HOST + port = parsed.port or default_port + + return host, port, None, False + def run_vite(vite_url: str, vite_host: str | None, vite_port: int): """Spawn the frontend dev server (deno, npm, or bunx) as a background process.""" @@ -64,7 +145,7 @@ def run_vite(vite_url: str, vite_host: str | None, vite_port: int): options = [ ("deno", "run", "dev"), - ("npm", "run", "dev", "--"), + ("npm", "--silent", "run", "dev", "--"), ("bunx", "--bun", "vite"), ] cmd = None @@ -76,7 +157,7 @@ def run_vite(vite_url: str, vite_host: str | None, vite_port: int): break # Add Vite CLI args for host/port - vite_args = ["--port", str(vite_port)] + vite_args = ["--port", str(vite_port), "--logLevel", "silent"] if vite_host: vite_args.extend(["--host", vite_host]) @@ -115,10 +196,96 @@ def run_vite(vite_url: str, vite_host: str | None, vite_port: int): signal.signal(signal.SIGINT, lambda *_: cleanup()) +def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: + """Spawn Caddy as HTTPS reverse proxy for the given origins.""" + caddy_path = shutil.which("caddy") + if not caddy_path: + stderr.write("┃ ⚠️ Caddy not found. Install it to use --caddy option.\n") + return None + + # Build Caddyfile with a site block for each origin + caddyfile_parts = [] + for origin in origins: + parsed = urlparse(origin) + # Extract scheme://host:port from origin URL + scheme = parsed.scheme or "https" + host = parsed.hostname or parsed.path # handle case without scheme + port = parsed.port or (4480 if scheme == "http" else CADDY_PORT) + site_addr = f"{scheme}://{host}:{port}" + block = ( + CADDYFILE_SITE_BLOCK + .replace("SITE_ADDR", site_addr) + .replace("BACKEND_PORT", str(BACKEND_PORT)) + .replace("VITE_PORT", str(vite_port)) + ) + caddyfile_parts.append(block) + + caddyfile = CADDYFILE_GLOBAL + "\n".join(caddyfile_parts) + caddy_process = None + + try: + cmd = [caddy_path, "run", "--config", "-", "--adapter", "caddyfile"] + caddy_process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + caddy_process.stdin.write(caddyfile.encode()) + caddy_process.stdin.close() + except Exception as e: + stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n") + return None + + # Wait briefly to check if Caddy exits immediately (e.g., port binding failure) + time.sleep(0.2) + exit_code = caddy_process.poll() + if exit_code is not None: + # Read and display stderr output, filtering and formatting JSON log lines + caddy_stderr = caddy_process.stderr.read().decode() if caddy_process.stderr else "" + stderr.write(f"┃ ⚠️ Caddy startup failed\n") + if caddy_stderr: + for line in caddy_stderr.splitlines(): + if not line: + continue + try: + log = json.loads(line) + # Filter out info-level messages + if log.get("level") == "info": + continue + # Filter out expected "admin endpoint disabled" warning + if log.get("logger") == "admin" and "admin endpoint disabled" in log.get("msg", ""): + continue + # Format: [level] logger: msg + level = log.get("level", "?").upper() + logger = log.get("logger", "") + msg = log.get("msg", "") + if logger: + stderr.write(f"┃ [{level}] {logger}: {msg}\n") + else: + stderr.write(f"┃ [{level}] {msg}\n") + except json.JSONDecodeError: + # Not JSON, print as-is (e.g., "Error: ..." lines) + stderr.write(f"┃ {line}\n") + return None + + def cleanup(): + if caddy_process: + caddy_process.terminate() + caddy_process.wait() + + atexit.register(cleanup) + + return caddy_process + + def main(): # Parse optional hostport argument for Vite frontend parser = argparse.ArgumentParser(add_help=False) parser.add_argument("hostport", nargs="?", default=None) + parser.add_argument("--caddy", action="store_true", help="Run Caddy as HTTPS proxy") + parser.add_argument("--rp-id", default="localhost", help="Relying Party ID") + parser.add_argument("--origin", action="append", dest="origins", help="Allowed origin(s)") + parser.add_argument("--auth-host", help="Dedicated auth host") args, remaining = parser.parse_known_args() # Parse Vite endpoint @@ -137,19 +304,81 @@ def main(): # Build Vite URL for PASKIA_DEVMODE (always use localhost for URL) vite_url = f"http://localhost:{vite_port}" + # Compute origins for Caddy (user-specified or auto-generated) + caddy_origins = [] + if args.origins: + # User specified explicit origins - use those + caddy_origins = args.origins + elif args.caddy: + # Caddy mode without explicit origins: add https origin for the hostname + if args.auth_host: + # auth-host is the primary origin + auth_host = args.auth_host + if "://" not in auth_host: + auth_host = f"https://{auth_host}" + # Add port if not standard + parsed = urlparse(auth_host) + if not parsed.port: + caddy_origins.append(f"{auth_host}:{CADDY_PORT}") + else: + caddy_origins.append(auth_host) + else: + # Use rp-id as the hostname + caddy_origins.append(f"https://{args.rp_id}:{CADDY_PORT}") + + # Start Caddy if requested (after computing origins) + if args.caddy: + if not caddy_origins: + caddy_origins = [f"https://{args.rp_id}:{CADDY_PORT}"] + stderr.write(f">>> caddy @ {' '.join(caddy_origins)}/\n") + if not run_caddy(caddy_origins, vite_port): + raise SystemExit(1) + # Start Vite dev server run_vite(vite_url, vite_host, vite_port) - # Set dev mode with Vite URL - os.environ["PASKIA_DEVMODE"] = vite_url + # Set dev mode with Vite URL in environment for subprocess + env = os.environ.copy() + env["PASKIA_DEVMODE"] = vite_url - # Import CLI after environment is set up - from paskia.fastapi.__main__ import main as cli_main + # Build command with origin args + cmd = ["paskia", "serve", f"localhost:{BACKEND_PORT}"] - # Build argv for the main CLI in Dev mode - # Backend always listens on localhost only (Vite proxies API requests) - sys.argv = ["paskia", "serve", f"localhost:{BACKEND_PORT}"] + remaining - cli_main() + # Pass through rp-id (always pass, has default) + cmd.extend(["--rp-id", args.rp_id]) + + # Pass through auth-host if specified + if args.auth_host: + cmd.extend(["--auth-host", args.auth_host]) + + # Collect all origins: Caddy origins first (auth-host first), then user origins + # Use a set to track and avoid duplicates + all_origins = [] + seen_origins = set(args.origins) if args.origins else set() + + # Add Caddy origins first (they include auth-host origin if configured) + if args.caddy: + for origin in caddy_origins: + if origin not in seen_origins: + all_origins.append(origin) + seen_origins.add(origin) + + # Add user-specified origins + if args.origins: + for origin in args.origins: + if origin not in seen_origins: + all_origins.append(origin) + seen_origins.add(origin) + + # Pass all origins to backend + for origin in all_origins: + cmd.extend(["--origin", origin]) + + # Add remaining args (ones we didn't parse) + cmd.extend(remaining) + + stderr.write(f">>> (devmode) {' '.join(cmd)}\n") + subprocess.run(cmd, env=env) if __name__ == "__main__": -- 2.55.0 From eead72f0537f5144092abd7fd4e1c1068624ecd1 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 21:20:44 +0000 Subject: [PATCH 04/16] gitignore lockfiles. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d288a5e..94cdeef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ .* !.gitignore *.lock +package-lock.json paskia.sqlite /paskia/frontend-build /paskia/_version.py -- 2.55.0 From b9e1f76d94fa0166e68dbc678a3d45c69e174e75 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 21:21:48 +0000 Subject: [PATCH 05/16] Updated Vite config to allow all hosts, and proxy reset links to backend. --- frontend/vite.config.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c1c0563..1aca842 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -53,18 +53,26 @@ export default defineConfig(({ command }) => ({ base: '/', server: { port: 4403, + allowedHosts: true, fs: { allow: ['..'] }, proxy: { // Only proxy these two specific backend API paths '/auth/api': { - target: 'http://localhost:4402', - headers: { connection: 'close' } + target: 'http://localhost:4402' }, '/auth/ws': { target: 'http://localhost:4402', ws: true + }, + // Passphrase links: /auth/word1.word2.word3.word4.word5 + '^/auth/[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+$': { + target: 'http://localhost:4402' + }, + // Passphrase links: /word1.word2.word3.word4.word5 + '^/[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+$': { + target: 'http://localhost:4402' } } }, -- 2.55.0 From 9763ab37bc7239af5b7f9be6bc92a50d60bedee5 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 21:23:51 +0000 Subject: [PATCH 06/16] Fixed DEVMODE processing. --- paskia/fastapi/__main__.py | 4 +++- paskia/fastapi/mainapp.py | 5 +++++ paskia/util/frontend.py | 10 +++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/paskia/fastapi/__main__.py b/paskia/fastapi/__main__.py index 68394ea..f346031 100644 --- a/paskia/fastapi/__main__.py +++ b/paskia/fastapi/__main__.py @@ -273,7 +273,7 @@ def main(): } # Dev mode: enable reload when PASKIA_DEVMODE is set - devmode = os.environ.get("PASKIA_DEVMODE") == "1" + devmode = bool(os.environ.get("PASKIA_DEVMODE")) if devmode: # Security: dev mode must run on localhost:4402 to prevent # accidental public exposure of the Vite dev server @@ -281,6 +281,8 @@ def main(): raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}") run_kwargs["reload"] = True run_kwargs["reload_dirs"] = ["paskia"] + # Suppress uvicorn startup messages in dev mode + run_kwargs["log_level"] = "warning" if uds: run_kwargs["uds"] = uds diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index 5928645..e627875 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -45,6 +45,11 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path # Re-raise to fail fast raise + # Restore info level logging after startup (suppressed during uvicorn init in dev mode) + if frontend.is_dev_mode(): + logging.getLogger("uvicorn").setLevel(logging.INFO) + logging.getLogger("uvicorn.access").setLevel(logging.INFO) + yield # Shutdown cleanup diff --git a/paskia/util/frontend.py b/paskia/util/frontend.py index c560082..a32f1b0 100644 --- a/paskia/util/frontend.py +++ b/paskia/util/frontend.py @@ -8,7 +8,10 @@ import httpx __all__ = ["path", "file", "read", "is_dev_mode"] -DEV_SERVER = "http://localhost:4403" + +def _get_dev_server() -> str | None: + """Get the dev server URL from environment, or None if not in dev mode.""" + return os.environ.get("PASKIA_DEVMODE") or None def _resolve_static_dir() -> Path: @@ -34,7 +37,7 @@ def file(*parts: str) -> Path: def is_dev_mode() -> bool: """Check if we're running in dev mode (Vite frontend server).""" - return os.environ.get("PASKIA_DEVMODE") == "1" + return bool(_get_dev_server()) async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]: @@ -51,8 +54,9 @@ async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]: FastAPI Response(*args) or Sanic raw response. """ if is_dev_mode(): + dev_server = _get_dev_server() async with httpx.AsyncClient() as client: - resp = await client.get(f"{DEV_SERVER}{filepath}") + resp = await client.get(f"{dev_server}{filepath}") resp.raise_for_status() mime = resp.headers.get("content-type", "application/octet-stream") # Strip charset suffix if present -- 2.55.0 From f74b32c70b64d0453ec9a5b03a79dc2cd2a02b09 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 22:50:42 +0000 Subject: [PATCH 07/16] Devserver: run Caddy on standard ports such that it can obtain certs. --- scripts/{dev.py => devserver.py} | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) rename scripts/{dev.py => devserver.py} (92%) diff --git a/scripts/dev.py b/scripts/devserver.py similarity index 92% rename from scripts/dev.py rename to scripts/devserver.py index c8beb44..c413a6d 100755 --- a/scripts/dev.py +++ b/scripts/devserver.py @@ -13,7 +13,7 @@ All other options are forwarded to `paskia serve`. Backend always listens on localhost:4402. Options: - --caddy Run Caddy as HTTPS proxy on port 4443 + --caddy Run Caddy as HTTPS proxy on port 443 (requires sudo) --rp-id HOST Relying Party ID (used as hostname for Caddy) --origin URL Allowed origin(s), passed to backend --auth-host H Dedicated auth host, passed to backend @@ -36,7 +36,8 @@ from urllib.parse import urlparse DEFAULT_VITE_PORT = 4403 # overrides by CLI option BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts -CADDY_PORT = 4443 # HTTPS port for Caddy proxy +CADDY_PORT = 443 # HTTPS port for Caddy proxy +CADDY_HTTP_PORT = 80 # HTTP port for ACME challenges DEFAULT_HOST = "localhost" NO_FRONTEND_TOOL = """\ @@ -61,11 +62,10 @@ NO_FRONTEND = """\ ┃ for various frontend assets, so make sure to start it manually. """ -# Global options to disable auto HTTPS redirect (requires port 80) +# Global options for Caddy CADDYFILE_GLOBAL = """\ { admin off - auto_https off log { level warn } @@ -210,8 +210,12 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: # Extract scheme://host:port from origin URL scheme = parsed.scheme or "https" host = parsed.hostname or parsed.path # handle case without scheme - port = parsed.port or (4480 if scheme == "http" else CADDY_PORT) - site_addr = f"{scheme}://{host}:{port}" + port = parsed.port or (CADDY_HTTP_PORT if scheme == "http" else CADDY_PORT) + # Use standard ports without explicit port in address (cleaner URLs) + if port in (80, 443): + site_addr = f"{scheme}://{host}" + else: + site_addr = f"{scheme}://{host}:{port}" block = ( CADDYFILE_SITE_BLOCK .replace("SITE_ADDR", site_addr) @@ -224,7 +228,8 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: caddy_process = None try: - cmd = [caddy_path, "run", "--config", "-", "--adapter", "caddyfile"] + # Use sudo to bind to privileged ports (80/443) for ACME certificate fetching + cmd = ["sudo", caddy_path, "run", "--config", "-", "--adapter", "caddyfile"] caddy_process = subprocess.Popen( cmd, stdin=subprocess.PIPE, @@ -316,21 +321,16 @@ def main(): auth_host = args.auth_host if "://" not in auth_host: auth_host = f"https://{auth_host}" - # Add port if not standard - parsed = urlparse(auth_host) - if not parsed.port: - caddy_origins.append(f"{auth_host}:{CADDY_PORT}") - else: - caddy_origins.append(auth_host) + caddy_origins.append(auth_host) else: - # Use rp-id as the hostname - caddy_origins.append(f"https://{args.rp_id}:{CADDY_PORT}") + # Use rp-id as the hostname (standard port 443, no port in URL) + caddy_origins.append(f"https://{args.rp_id}") # Start Caddy if requested (after computing origins) if args.caddy: if not caddy_origins: - caddy_origins = [f"https://{args.rp_id}:{CADDY_PORT}"] - stderr.write(f">>> caddy @ {' '.join(caddy_origins)}/\n") + caddy_origins = [f"https://{args.rp_id}"] + stderr.write(f">>> sudo caddy @ {' '.join(caddy_origins)}\n") if not run_caddy(caddy_origins, vite_port): raise SystemExit(1) -- 2.55.0 From 82095fd7acbe29ff540c429965d713f7d436fd4d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 22:55:03 +0000 Subject: [PATCH 08/16] Wait until Caddy startup is actually complete, keep reading stderr in the background. --- scripts/devserver.py | 98 +++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/scripts/devserver.py b/scripts/devserver.py index c413a6d..b041162 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -28,7 +28,6 @@ import shutil import signal import subprocess import sys -import time from pathlib import Path from sys import stderr from threading import Thread @@ -241,37 +240,78 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n") return None - # Wait briefly to check if Caddy exits immediately (e.g., port binding failure) - time.sleep(0.2) - exit_code = caddy_process.poll() - if exit_code is not None: - # Read and display stderr output, filtering and formatting JSON log lines - caddy_stderr = caddy_process.stderr.read().decode() if caddy_process.stderr else "" - stderr.write(f"┃ ⚠️ Caddy startup failed\n") - if caddy_stderr: - for line in caddy_stderr.splitlines(): - if not line: - continue - try: - log = json.loads(line) - # Filter out info-level messages - if log.get("level") == "info": - continue - # Filter out expected "admin endpoint disabled" warning - if log.get("logger") == "admin" and "admin endpoint disabled" in log.get("msg", ""): - continue - # Format: [level] logger: msg - level = log.get("level", "?").upper() - logger = log.get("logger", "") + # Read stderr line by line until Caddy signals it's ready or exits + # Caddy outputs JSON logs; "serving initial configuration" means it's ready + ready = False + while True: + exit_code = caddy_process.poll() + if exit_code is not None: + # Process exited - read remaining stderr and report failure + remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else "" + stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n") + if remaining: + for line in remaining.splitlines(): + if line: + stderr.write(f"┃ {line}\n") + return None + + # Read one line from stderr (blocks until data available) + line = caddy_process.stderr.readline().decode() + if not line: + continue + + # Try to parse as JSON log + try: + log = json.loads(line) + msg = log.get("msg", "") + level = log.get("level", "") + + # Check for ready signal + if "serving initial configuration" in msg: + ready = True + break + + # Show errors/warnings to user + if level in ("error", "fatal"): + logger = log.get("logger", "") + if logger: + stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n") + else: + stderr.write(f"┃ [{level.upper()}] {msg}\n") + + except json.JSONDecodeError: + # Not JSON - might be sudo password prompt or other output + # Pass through to user + stderr.write(line) + stderr.flush() + + if ready: + break + + # Start a background thread to drain stderr and show errors + def drain_stderr(): + while True: + line = caddy_process.stderr.readline().decode() + if not line: + break + try: + log = json.loads(line) + level = log.get("level", "") + if level in ("error", "fatal", "warn"): msg = log.get("msg", "") + logger = log.get("logger", "") + # Skip expected warnings + if logger == "admin" and "admin endpoint disabled" in msg: + continue if logger: - stderr.write(f"┃ [{level}] {logger}: {msg}\n") + stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n") else: - stderr.write(f"┃ [{level}] {msg}\n") - except json.JSONDecodeError: - # Not JSON, print as-is (e.g., "Error: ..." lines) - stderr.write(f"┃ {line}\n") - return None + stderr.write(f"┃ [{level.upper()}] {msg}\n") + except json.JSONDecodeError: + stderr.write(line) + + drain_thread = Thread(target=drain_stderr, daemon=True) + drain_thread.start() def cleanup(): if caddy_process: -- 2.55.0 From 8f0cb8d80a44cba044c6e31d6df385f75709a7c6 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 23:13:12 +0000 Subject: [PATCH 09/16] Better caddy startup handling, fix hang waiting for message that never comes. --- scripts/devserver.py | 139 +++++++++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 44 deletions(-) diff --git a/scripts/devserver.py b/scripts/devserver.py index b041162..5840ff5 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -240,19 +240,73 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n") return None + # Helper to parse Caddy log line (JSON or plain text) into (level, logger, msg) + def parse_caddy_log(line: str) -> tuple[str, str, str] | None: + """Parse a Caddy log line, return (level, logger, msg) or None if unparseable.""" + line = line.rstrip("\n") + if not line: + return None + + # Try JSON format first + try: + log = json.loads(line) + return ( + log.get("level", ""), + log.get("logger", ""), + log.get("msg", ""), + ) + except json.JSONDecodeError: + pass + + # Plain text format: "2025/12/06 22:59:41.390 INFO logger msg..." + # or "2025/12/06 22:59:41.390 INFO msg..." (no logger) + parts = line.split("\t") + if len(parts) >= 2: + # First part is "timestamp LEVEL", rest are logger and/or message + first = parts[0].rsplit(None, 1) # split off the level from timestamp + if len(first) == 2: + level = first[1].lower() + if len(parts) == 2: + return (level, "", parts[1]) + else: + return (level, parts[1], "\t".join(parts[2:])) + + # Unparseable - return as-is with no level/logger + return ("", "", line) + + def strip_caddy_verbose(msg: str) -> str: + """Remove verbose prefixes from Caddy error messages.""" + return msg.replace("loading initial config: loading new config: ", "") + + def format_caddy_log(level: str, logger: str, msg: str) -> str: + """Format a parsed Caddy log for display.""" + msg = strip_caddy_verbose(msg) + if logger: + return f"┃ [{level.upper()}] {logger}: {msg}\n" + else: + return f"┃ [{level.upper()}] {msg}\n" + # Read stderr line by line until Caddy signals it's ready or exits - # Caddy outputs JSON logs; "serving initial configuration" means it's ready + # Caddy outputs logs; "serving initial configuration" means it's ready ready = False while True: exit_code = caddy_process.poll() if exit_code is not None: # Process exited - read remaining stderr and report failure remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else "" - stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n") if remaining: for line in remaining.splitlines(): if line: - stderr.write(f"┃ {line}\n") + parsed = parse_caddy_log(line) + if parsed: + level, logger, msg = parsed + if level: + stderr.write(format_caddy_log(level, logger, msg)) + else: + stderr.write(f"┃ {strip_caddy_verbose(msg)}\n") + else: + stderr.write(f"┃ {strip_caddy_verbose(line)}\n") + stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n") return None # Read one line from stderr (blocks until data available) @@ -260,55 +314,52 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: if not line: continue - # Try to parse as JSON log - try: - log = json.loads(line) - msg = log.get("msg", "") - level = log.get("level", "") - - # Check for ready signal - if "serving initial configuration" in msg: - ready = True - break - - # Show errors/warnings to user - if level in ("error", "fatal"): - logger = log.get("logger", "") - if logger: - stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n") - else: - stderr.write(f"┃ [{level.upper()}] {msg}\n") - - except json.JSONDecodeError: - # Not JSON - might be sudo password prompt or other output - # Pass through to user - stderr.write(line) - stderr.flush() - - if ready: + # Check for ready signal + if "serving initial configuration" in line: + ready = True break + parsed = parse_caddy_log(line) + if not parsed: + continue + + level, logger, msg = parsed + + # Filter out info-level and admin messages + if level == "info" or logger == "admin": + continue + + # Show errors/fatal to user + if level in ("error", "fatal"): + stderr.write(format_caddy_log(level, logger, msg)) + elif not level: + # Unparseable non-empty line (e.g., sudo prompt) - pass through with prefix + stderr.write(f"┃ {strip_caddy_verbose(msg)}\n") + stderr.flush() + # Start a background thread to drain stderr and show errors def drain_stderr(): while True: line = caddy_process.stderr.readline().decode() if not line: break - try: - log = json.loads(line) - level = log.get("level", "") - if level in ("error", "fatal", "warn"): - msg = log.get("msg", "") - logger = log.get("logger", "") - # Skip expected warnings - if logger == "admin" and "admin endpoint disabled" in msg: - continue - if logger: - stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n") - else: - stderr.write(f"┃ [{level.upper()}] {msg}\n") - except json.JSONDecodeError: - stderr.write(line) + + parsed = parse_caddy_log(line) + if not parsed: + continue + + level, logger, msg = parsed + + # Filter out info-level and admin messages + if level == "info" or logger == "admin": + continue + + # Show errors/warnings to user + if level in ("error", "fatal", "warn"): + stderr.write(format_caddy_log(level, logger, msg)) + elif not level: + # Unparseable line - pass through with prefix + stderr.write(f"┃ {strip_caddy_verbose(msg)}\n") drain_thread = Thread(target=drain_stderr, daemon=True) drain_thread.start() -- 2.55.0 From 269aefa9c801e1f8cc1120de22da748b5040769d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 23:32:24 +0000 Subject: [PATCH 10/16] Remove caddy global options that were causing error detecting startup complete message. --- scripts/devserver.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/scripts/devserver.py b/scripts/devserver.py index 5840ff5..59a5a78 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -61,26 +61,13 @@ NO_FRONTEND = """\ ┃ for various frontend assets, so make sure to start it manually. """ -# Global options for Caddy -CADDYFILE_GLOBAL = """\ -{ - admin off - log { - level warn - } -} -""" - CADDYFILE_SITE_BLOCK = """\ SITE_ADDR { - # API requests go directly to backend - handle /auth/api/* { - reverse_proxy localhost:BACKEND_PORT - } + # WebSockets bypass directly to backend (workaround for bun proxy bug) handle /auth/ws/* { reverse_proxy localhost:BACKEND_PORT } - # Everything else goes to Vite + # Everything else goes to or via Vite handle { reverse_proxy localhost:VITE_PORT } @@ -223,7 +210,7 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None: ) caddyfile_parts.append(block) - caddyfile = CADDYFILE_GLOBAL + "\n".join(caddyfile_parts) + caddyfile = "\n".join(caddyfile_parts) caddy_process = None try: -- 2.55.0 From dd49907c8d3202f94179b779f683206317395b5f Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 7 Dec 2025 00:15:56 +0000 Subject: [PATCH 11/16] Moved remote-link links to same location with reset links, avoiding vite routing issue. Realtime lookup of session when three words have been entered. --- frontend/auth/restricted/RestrictedApi.vue | 11 +- frontend/src/components/PairingCodeEntry.vue | 159 +++++++++++++++++-- paskia/fastapi/api.py | 65 +++++++- paskia/fastapi/mainapp.py | 32 ++-- paskia/fastapi/ws.py | 4 +- paskia/remoteauth.py | 57 ++++--- paskia/util/passphrase.py | 5 + 7 files changed, 280 insertions(+), 53 deletions(-) diff --git a/frontend/auth/restricted/RestrictedApi.vue b/frontend/auth/restricted/RestrictedApi.vue index e3eeeb3..2e2bf46 100644 --- a/frontend/auth/restricted/RestrictedApi.vue +++ b/frontend/auth/restricted/RestrictedApi.vue @@ -20,16 +20,19 @@ import { computed, onMounted, ref } from 'vue' import RestrictedAuth from '@/components/RestrictedAuth.vue' import RemoteAuthComplete from '@/components/RemoteAuthComplete.vue' -// Check if this is a remote auth URL: /remote/{token} or /auth/remote/{token} +// Check if this is a remote auth URL: /{token} or /auth/{token} +// The token is a 5-word passphrase like "word1.word2.word3.word4.word5" const remoteAuthToken = ref(null) function extractRemoteToken() { const path = window.location.pathname - const match = path.match(/\/(?:auth\/)?remote\/([^/]+)$/) + // Match /{token} or /auth/{token} where token is a passphrase with dots + const match = path.match(/\/(?:auth\/)?([^/]+)$/) if (match) { const token = match[1] - // Validate it looks like a passphrase (contains dots) - if (token.includes('.')) { + // Validate it looks like a passphrase (contains dots, likely 5 words) + const parts = token.split('.') + if (parts.length === 5 && parts.every(p => p.length > 0)) { return token } } diff --git a/frontend/src/components/PairingCodeEntry.vue b/frontend/src/components/PairingCodeEntry.vue index 54b8116..0ffb3b4 100644 --- a/frontend/src/components/PairingCodeEntry.vue +++ b/frontend/src/components/PairingCodeEntry.vue @@ -12,21 +12,44 @@ v-model="code" type="text" :placeholder="placeholder" - :disabled="loading" + :disabled="loading || completed" autocomplete="off" autocapitalize="characters" spellcheck="false" class="pairing-input" @input="handleInput" /> + + + +
+ + Looking up device… +
+ + +
+

📱 Device requesting login:

+
+
+ Site: + {{ deviceInfo.host }} +
+
+ Browser: + {{ deviceInfo.user_agent_pretty }} +
+
+

{{ error }}

@@ -38,15 +61,16 @@ @@ -243,15 +353,16 @@ defineExpose({ reset }) gap: 0.5rem; } -.input-group { +.input-wrapper { + position: relative; display: flex; - gap: 0.5rem; } .pairing-input { flex: 1; padding: 0.625rem 0.75rem; font-size: 1rem; + font-family: inherit; border: 1px solid var(--color-border); border-radius: var(--radius-sm, 4px); background: var(--color-surface); @@ -274,6 +385,19 @@ defineExpose({ reset }) opacity: 0.6; } +.autocomplete-hint { + position: absolute; + left: 0; + top: 0; + padding: 0.625rem 0.75rem; + font-size: 1rem; + font-family: inherit; + color: var(--color-text-muted); + opacity: 0.5; + pointer-events: none; + white-space: pre; +} + .lookup-status { display: flex; align-items: center; diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index be0be80..e97a7c3 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -209,10 +209,10 @@ async def get_settings(): @app.get("/token-info") async def api_token_info(token: str): """Get information about a token (remote auth or reset token). - + This endpoint allows the frontend to determine what type of token it is dealing with and get relevant information for display. - + Returns: - type: "remote_auth" or "reset" - For remote_auth: host, user_agent_pretty (requesting device info) @@ -220,7 +220,7 @@ async def api_token_info(token: str): """ if not passphrase.is_well_formed(token): raise HTTPException(status_code=404, detail="Invalid token") - + # Check if this is a remote auth token if remoteauth.instance is not None: request = await remoteauth.instance.get_request(token) @@ -232,7 +232,7 @@ async def api_token_info(token: str): "user_agent_pretty": useragent.compact_user_agent(request.user_agent), "ip": request.ip, } - + # Check if this is a reset token try: reset_token = await get_reset(token) @@ -249,17 +249,17 @@ async def api_token_info(token: str): @app.get("/remote-auth-info") async def api_remote_auth_info(code: str): """Get information about a remote auth request by pairing code (first 3 words). - + This is used for real-time lookup as the user types the pairing code. Returns info about the requesting device without initiating authentication. """ if remoteauth.instance is None: raise HTTPException(status_code=404, detail="Remote auth not available") - + request = await remoteauth.instance.get_request_by_pairing_code(code) if request is None: raise HTTPException(status_code=404, detail="Invalid or expired code") - + return { "host": request.host, "user_agent": request.user_agent, diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index d61e5cb..9aff44e 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -127,21 +127,21 @@ async def examples_page(): @app.get("/auth/{token}") async def token_link(token: str): """Serve the appropriate app based on token type. - + This endpoint handles both: - Remote auth tokens (cross-device login): serve restricted app - Reset tokens (password reset / device addition): serve reset app - + The frontend will detect the type by calling /auth/api/token-info. """ if not passphrase.is_well_formed(token): raise HTTPException(status_code=404) - + # Check if this is a remote auth token first (they're in-memory, fast lookup) if remoteauth.instance is not None: request = await remoteauth.instance.get_request(token) if request is not None: return Response(*await frontend.read("/auth/restricted/index.html")) - + # Otherwise, serve the reset app (it will validate the token via API) return Response(*await frontend.read("/int/reset/index.html")) diff --git a/paskia/remoteauth.py b/paskia/remoteauth.py index f0fbe7f..6de685e 100644 --- a/paskia/remoteauth.py +++ b/paskia/remoteauth.py @@ -55,7 +55,7 @@ class RemoteAuthRequest: def _generate_pairing_code() -> str: """Generate a short, easy-to-communicate pairing code using words. - + DEPRECATED: Now we use the first 3 words of the main token instead. """ return passphrase.generate(n=PAIRING_CODE_WORDS) -- 2.55.0 From 4bb64863f97ad317600f6da60368daa28adc8691 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 7 Dec 2025 04:11:43 +0000 Subject: [PATCH 13/16] Proper PoW, unified verification WS etc --- frontend/src/components/PairingCodeEntry.vue | 573 +++++++++++++++--- .../src/components/RemoteAuthComplete.vue | 13 + .../src/components/RemoteAuthLinkModal.vue | 13 + frontend/src/utils/pow-test.js | 46 ++ frontend/src/utils/pow.js | 142 +++++ frontend/src/utils/wordlist.js | 61 ++ paskia/fastapi/ws.py | 283 ++++++--- paskia/util/pow.py | 62 ++ 8 files changed, 1014 insertions(+), 179 deletions(-) create mode 100644 frontend/src/utils/pow-test.js create mode 100644 frontend/src/utils/pow.js create mode 100644 frontend/src/utils/wordlist.js create mode 100644 paskia/util/pow.py diff --git a/frontend/src/components/PairingCodeEntry.vue b/frontend/src/components/PairingCodeEntry.vue index 24ba9ca..1ca1c21 100644 --- a/frontend/src/components/PairingCodeEntry.vue +++ b/frontend/src/components/PairingCodeEntry.vue @@ -6,29 +6,35 @@
-
- - - {{ autocompleteHintDisplay }} -
- - -
- - Looking up device… +
+
+ + + + + +
+ +
+ {{ processingStatus === 'pow' ? '🔐' : '📡' }} + +
@@ -45,6 +51,7 @@
- + + + + + + + + +
+ +
+ + +
+ + + +

{{ remoteAuthRef.error }}

+ +
+ + - - - - +
- - - diff --git a/frontend/src/components/UserBasicInfo.vue b/frontend/src/components/UserBasicInfo.vue index 0e09005..9dc2f06 100644 --- a/frontend/src/components/UserBasicInfo.vue +++ b/frontend/src/components/UserBasicInfo.vue @@ -1,5 +1,5 @@ @@ -44,13 +47,50 @@ const userLoaded = computed(() => !!props.name) diff --git a/frontend/src/utils/awaitable-websocket.js b/frontend/src/utils/awaitable-websocket.js index f68b734..84a1740 100644 --- a/frontend/src/utils/awaitable-websocket.js +++ b/frontend/src/utils/awaitable-websocket.js @@ -18,12 +18,36 @@ class AwaitableWebSocket extends WebSocket { } this.onclose = e => { if (!this.#opened) { - reject(new Error(`WebSocket ${this.url} failed to connect, code ${e.code}`)) + reject(new Error(`Failed to connect to server (code ${e.code})`)) return } - this.#err = e.wasClean - ? new Error(`Websocket ${this.url} closed ${e.code}`) - : new Error(`WebSocket ${this.url} closed with error ${e.code}`) + // Create user-friendly close messages + let message + if (e.wasClean) { + // Standard close codes + switch (e.code) { + case 1000: message = 'Connection closed normally'; break + case 1001: message = 'Server is going away'; break + case 1002: message = 'Protocol error'; break + case 1003: message = 'Unsupported data received'; break + case 1006: message = 'Connection lost unexpectedly'; break + case 1007: message = 'Invalid data received'; break + case 1008: message = 'Policy violation'; break + case 1009: message = 'Message too large'; break + case 1010: message = 'Extension negotiation failed'; break + case 1011: message = 'Server encountered an error'; break + case 1012: message = 'Server is restarting'; break + case 1013: message = 'Server is overloaded, try again later'; break + case 1014: message = 'Bad gateway'; break + case 1015: message = 'TLS handshake failed'; break + default: message = `Connection closed (code ${e.code})` + } + } else { + message = e.code === 1006 + ? 'Connection lost unexpectedly' + : `Connection closed with error (code ${e.code})` + } + this.#err = new Error(message) this.#waiting.splice(0).forEach(p => p.reject(this.#err)) } } diff --git a/paskia/authsession.py b/paskia/authsession.py index 31dc6f3..f67a4ae 100644 --- a/paskia/authsession.py +++ b/paskia/authsession.py @@ -71,7 +71,7 @@ async def get_reset(token: str) -> ResetToken: record = await db.instance.get_reset_token(reset_key(token)) if record and record.expiry >= datetime.now(timezone.utc): return record - raise ValueError("This reset link is invalid or has expired") + raise ValueError("This authentication link is no longer valid.") async def get_session(token: str, host: str | None = None) -> Session: diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index e97a7c3..67dd8d3 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -201,7 +201,8 @@ async def get_settings(): "rp_id": pk.rp_id, "rp_name": pk.rp_name, "ui_base_path": base_path, - "auth_host": hostutil.configured_auth_host(), + "auth_host": hostutil.dedicated_auth_host(), + "auth_site_url": hostutil.auth_site_url(), "session_cookie": AUTH_COOKIE_NAME, } diff --git a/paskia/fastapi/auth_host.py b/paskia/fastapi/auth_host.py index 2cacc25..1a8da01 100644 --- a/paskia/fastapi/auth_host.py +++ b/paskia/fastapi/auth_host.py @@ -73,7 +73,7 @@ def redirect_to_root_on_auth_host(request: Request, cur: str, path: str) -> Resp async def redirect_middleware(request: Request, call_next): """Middleware to handle auth host redirects.""" - cfg = hostutil.configured_auth_host() + cfg = hostutil.dedicated_auth_host() if not cfg: return await call_next(request) diff --git a/paskia/fastapi/remote.py b/paskia/fastapi/remote.py index 4db4c63..7aba2c9 100644 --- a/paskia/fastapi/remote.py +++ b/paskia/fastapi/remote.py @@ -19,9 +19,9 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect from paskia import remoteauth from paskia.authsession import create_session from paskia.fastapi.session import infodict -from paskia.fastapi.wsutil import require_pow, validate_origin, websocket_error_handler +from paskia.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.globals import db, passkey -from paskia.util import hostutil, pow +from paskia.util import hostutil, passphrase, pow # Create a FastAPI subapp for remote auth WebSocket endpoints @@ -51,119 +51,196 @@ async def websocket_remote_auth_request(ws: WebSocket): if remoteauth.instance is None: raise ValueError("Remote authentication is not available") - # Require HARD PoW before creating the request (SECURITY) - await require_pow(ws, work=pow.HARD) - - metadata = infodict(ws, "remote-auth-request") - - # Create the remote auth request - token, pairing_code, expiry = await remoteauth.instance.create_request( - host=host, - ip=metadata.get("ip") or "", - user_agent=metadata.get("user_agent") or "", - ) - - # Build the URL for the authenticating device (same endpoint as reset tokens) - url = hostutil.auth_site_base_url() + token - - # Send the token, pairing code, and URL to the client - await ws.send_json( - { - "token": token, - "pairing_code": pairing_code, - "url": url, - "expires": expiry.isoformat().replace("+00:00", "Z"), - } - ) - - # Set up async notification - result_event = asyncio.Event() - result_data: dict = {} - - def on_complete( - session_token: str | None, - user_uuid: UUID | None, - credential_uuid: UUID | None, - ): - result_data["session_token"] = session_token - result_data["user_uuid"] = user_uuid - result_data["credential_uuid"] = credential_uuid - result_event.set() - - await remoteauth.instance.set_notify_callback(token, on_complete) - - # 5 minute timeout for the entire remote auth flow - timeout_seconds = 5 * 60 - + # Track this WebSocket connection for load-based PoW difficulty + remoteauth.instance.increment_connections() try: - # Wait for either: - # 1. Authentication to complete (result_event set) - # 2. Client to disconnect - # 3. Client to send a cancel message - # 4. Timeout after 5 minutes + # Send PoW challenge immediately with dynamic difficulty based on load + challenge = pow.generate_challenge() + work = remoteauth.instance.get_pow_difficulty() - async with asyncio.timeout(timeout_seconds): - while True: - # Use asyncio.wait to handle both event and websocket - receive_task = asyncio.create_task(ws.receive_json()) - event_task = asyncio.create_task(result_event.wait()) + await ws.send_json({ + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + }) - done, pending = await asyncio.wait( - [receive_task, event_task], - return_when=asyncio.FIRST_COMPLETED, - ) + # Receive client response with PoW solution and action + response = await ws.receive_json() - # Cancel pending tasks - for task in pending: - task.cancel() - try: - await task - except asyncio.CancelledError: - pass + # Verify PoW (required for this endpoint - SECURITY) + solution_b64 = response.get("pow") + if not solution_b64: + raise ValueError("PoW solution required") - if event_task in done: - # Authentication completed (or expired/cancelled) - if result_data.get("session_token"): - await ws.send_json( - { - "status": "authenticated", - "user_uuid": str(result_data["user_uuid"]), - "session_token": result_data["session_token"], - } - ) - else: - await ws.send_json( - { - "status": "expired", - "detail": "Remote authentication request expired or was cancelled", - } - ) - return + try: + solution = base64url.dec(solution_b64) + except Exception: + raise ValueError("Invalid PoW solution encoding") - if receive_task in done: - # Client sent a message - msg = receive_task.result() - if msg.get("action") == "cancel": - await remoteauth.instance.cancel_request(token) - await ws.send_json({"status": "cancelled"}) - return - # Ignore other messages + pow.verify_pow(challenge, solution, work) - except TimeoutError: - # 5 minute timeout reached - await remoteauth.instance.cancel_request(token) + # Extract action from the same message + action = response.get("action", "login") + if action not in ("login", "register"): + action = "login" + + metadata = infodict(ws, "remote-auth-request") + + # Create the remote auth request + token, pairing_code, expiry = await remoteauth.instance.create_request( + host=host, + ip=metadata.get("ip") or "", + user_agent=metadata.get("user_agent") or "", + action=action, + ) + + # Send the token and pairing code to the client (URL built in frontend) await ws.send_json( { - "status": "timeout", - "detail": "Remote authentication request timed out after 5 minutes", + "token": token, + "pairing_code": pairing_code, + "expires": expiry.isoformat().replace("+00:00", "Z"), } ) - except WebSocketDisconnect: - # Client disconnected, cancel the request - await remoteauth.instance.cancel_request(token) - except Exception: - await remoteauth.instance.cancel_request(token) - raise + + # Set up async notification for completion + result_event = asyncio.Event() + result_data: dict = {} + + def on_complete( + session_token: str | None, + user_uuid: UUID | None, + credential_uuid: UUID | None, + reset_token: str | None, + ): + # Check if this was an explicit denial (UUID(int=0) is the signal) + was_denied = user_uuid is not None and user_uuid == UUID(int=0) + result_data["session_token"] = session_token + result_data["user_uuid"] = user_uuid + result_data["credential_uuid"] = credential_uuid + result_data["reset_token"] = reset_token + result_data["was_denied"] = was_denied + result_event.set() + + await remoteauth.instance.set_notify_callback(token, on_complete) + + # Set up async notification for action lock + locked_event = asyncio.Event() + locked_data: dict = {} + + def on_action_locked(action: str): + locked_data["action"] = action + locked_event.set() + + await remoteauth.instance.set_action_locked_callback(token, on_action_locked) + + # 5 minute timeout for the entire remote auth flow + timeout_seconds = 5 * 60 + + try: + # Wait for either: + # 1. Authentication to complete (result_event set) + # 2. Action locked (locked_event set) + # 3. Client to disconnect + # 4. Client to send a cancel or update_action message + # 5. Timeout after 5 minutes + + async with asyncio.timeout(timeout_seconds): + while True: + # Use asyncio.wait to handle events and websocket + receive_task = asyncio.create_task(ws.receive_json()) + result_wait_task = asyncio.create_task(result_event.wait()) + locked_wait_task = asyncio.create_task(locked_event.wait()) + + tasks = [receive_task, result_wait_task] + # Only wait for locked event if not already locked + if not locked_event.is_set(): + tasks.append(locked_wait_task) + + done, pending = await asyncio.wait( + tasks, + return_when=asyncio.FIRST_COMPLETED, + ) + + # Cancel pending tasks + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + if result_wait_task in done: + # Authentication completed (or expired/cancelled/denied) + was_denied = result_data.get("was_denied", False) + if result_data.get("session_token") or result_data.get("reset_token"): + response = { + "status": "authenticated", + "user_uuid": str(result_data["user_uuid"]), + } + if result_data.get("session_token"): + response["session_token"] = result_data["session_token"] + if result_data.get("reset_token"): + response["reset_token"] = result_data["reset_token"] + await ws.send_json(response) + else: + # Check if it was explicitly denied + if was_denied: + await ws.send_json( + { + "status": "denied", + "detail": "Access denied", + } + ) + else: + await ws.send_json( + { + "status": "expired", + "detail": "Remote authentication request expired or was cancelled", + } + ) + return + + if locked_wait_task in done: + # Action was locked by the authenticating device + await ws.send_json({ + "status": "locked", + "action": locked_data.get("action", "login"), + }) + # Continue waiting for result + + if receive_task in done: + # Client sent a message + msg = receive_task.result() + if msg.get("action") == "cancel": + await remoteauth.instance.cancel_request(token) + await ws.send_json({"status": "cancelled"}) + return + elif msg.get("action") == "update_action": + # Update the action (login/register) if not locked + new_action = "register" if msg.get("register") else "login" + await remoteauth.instance.update_action(token, new_action) + # Ignore other messages + + except TimeoutError: + # 5 minute timeout reached + await remoteauth.instance.cancel_request(token) + await ws.send_json( + { + "status": "timeout", + "detail": "Remote authentication request timed out after 5 minutes", + } + ) + except WebSocketDisconnect: + # Client disconnected, cancel the request and mark as denied + await remoteauth.instance.cancel_request(token, denied=True) + except Exception: + await remoteauth.instance.cancel_request(token) + raise + finally: + # Decrement connection count + remoteauth.instance.decrement_connections() @app.websocket("/pair") @@ -197,7 +274,7 @@ async def websocket_remote_auth_pair(ws: WebSocket): if remoteauth.instance is None: raise ValueError("Remote authentication is not available") - # Generate initial PoW challenge + # Generate initial PoW challenge (always NORMAL for authenticated users) challenge = pow.generate_challenge() work = pow.NORMAL @@ -210,126 +287,203 @@ async def websocket_remote_auth_pair(ws: WebSocket): request = None webauthn_challenge = None + explicitly_denied = False - while True: - msg = await ws.receive_json() + try: + while True: + msg = await ws.receive_json() - # Check if this is a 5-word token (from link) - skip PoW validation - code = msg.get("code", "") - is_link_token = len(code.split(".")) == 5 - - if not is_link_token: - # Validate PoW for 3-word pairing codes - solution_b64 = msg.get("pow") - if not solution_b64: - raise ValueError("PoW solution required") - - try: - solution = base64url.dec(solution_b64) - except Exception: - raise ValueError("Invalid PoW solution encoding") - - try: - pow.verify_pow(challenge, solution, work) - except ValueError as e: - # Invalid PoW - send new challenge - challenge = pow.generate_challenge() + # Handle deny request first (no PoW needed - already validated during lookup) + if msg.get("deny") and request is not None: + # Cancel the request and mark it as denied + explicitly_denied = True + await remoteauth.instance.cancel_request(request.key, denied=True) await ws.send_json({ - "status": 400, - "detail": str(e), + "status": "denied", + "message": "Request denied", + }) + break + + # Handle authenticate request (no PoW needed - already validated during lookup) + if msg.get("authenticate") and request is not None: + # Generate authentication options + options, webauthn_challenge = passkey.instance.auth_generate_options( + credential_ids=None + ) + await ws.send_json({"optionsJSON": options}) + + # Wait for WebAuthn response + credential = passkey.instance.auth_parse(await ws.receive_json()) + + # Fetch and verify credential + try: + stored_cred = await db.instance.get_credential_by_id(credential.raw_id) + except ValueError: + raise ValueError( + f"This passkey is no longer registered with {passkey.instance.rp_name}" + ) + + # Verify the credential + passkey.instance.auth_verify( + credential, webauthn_challenge, stored_cred, origin + ) + + # Update credential last_used + await db.instance.login(stored_cred.user_uuid, stored_cred) + + # Create a session for the REQUESTING device + assert stored_cred.uuid is not None + + session_token = None + reset_token = None + + if request.action == "register": + # For registration, create a reset token for device addition + from paskia.authsession import expires + from paskia.util import tokens + + token_str = passphrase.generate() + expiry = expires() + await db.instance.create_reset_token( + user_uuid=stored_cred.user_uuid, + key=tokens.reset_key(token_str), + expiry=expiry, + token_type="device addition", + ) + reset_token = token_str + # Also create a session so the device is logged in? + # User requested: "We can make the flow always create a new session, but make additional tokens for other possibilities." + session_token = await create_session( + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + host=request.host, + ip=request.ip, + user_agent=request.user_agent, + ) + else: + # Default login action + session_token = await create_session( + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + host=request.host, + ip=request.ip, + user_agent=request.user_agent, + ) + + # Complete the remote auth request (notifies the waiting device) + completed = await remoteauth.instance.complete_request( + token=request.key, + session_token=session_token, + user_uuid=stored_cred.user_uuid, + credential_uuid=stored_cred.uuid, + reset_token=reset_token, + ) + + if not completed: + raise ValueError("Failed to complete remote authentication") + + msg = "Authentication successful." + if request.action == "register": + msg += " The other device can now register a passkey." + else: + msg += " The other device is now logged in." + + await ws.send_json({ + "status": "success", + "message": msg, + }) + break + + # Handle code lookup request - requires PoW validation + code = msg.get("code", "") + is_link_token = len(code.split(".")) == 5 + + if not is_link_token: + # Validate PoW for 3-word pairing codes + solution_b64 = msg.get("pow") + if not solution_b64: + raise ValueError("PoW solution required") + + try: + solution = base64url.dec(solution_b64) + except Exception: + raise ValueError("Invalid PoW solution encoding") + + try: + pow.verify_pow(challenge, solution, work) + except ValueError as e: + # Invalid PoW - send new challenge + challenge = pow.generate_challenge() + await ws.send_json({ + "status": 400, + "detail": str(e), + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + }) + continue + + if not code: + raise ValueError("Pairing code required") + + # Look up the remote auth request by pairing code or token + if is_link_token: + request = await remoteauth.instance.get_request(code) + else: + request = await remoteauth.instance.get_request_by_pairing_code(code) + + # Generate new challenge for next request (always NORMAL for authenticated users) + challenge = pow.generate_challenge() + + if request is None: + await ws.send_json({ + "status": 404, + "detail": "Code not found", "pow": { "challenge": base64url.enc(challenge), "work": work, } }) + request = None # Reset for next attempt continue - # Handle authenticate request (after successful lookup) - if msg.get("authenticate") and request is not None: - # Generate authentication options - options, webauthn_challenge = passkey.instance.auth_generate_options( - credential_ids=None - ) - await ws.send_json({"optionsJSON": options}) + # Valid code found - lock the action so it can't be changed anymore + # This also notifies the requesting device + locked_action = await remoteauth.instance.lock_action(request.key) + if locked_action is None: + # Already locked by another device + await ws.send_json({ + "status": 409, + "detail": "This request is already being processed in another window", + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + }) + request = None # Reset for next attempt + continue - # Wait for WebAuthn response - credential = passkey.instance.auth_parse(await ws.receive_json()) - - # Fetch and verify credential - try: - stored_cred = await db.instance.get_credential_by_id(credential.raw_id) - except ValueError: - raise ValueError( - f"This passkey is no longer registered with {passkey.instance.rp_name}" - ) - - # Verify the credential - passkey.instance.auth_verify( - credential, webauthn_challenge, stored_cred, origin - ) - - # Update credential last_used - await db.instance.login(stored_cred.user_uuid, stored_cred) - - # Create a session for the REQUESTING device - assert stored_cred.uuid is not None - session_token = await create_session( - user_uuid=stored_cred.user_uuid, - credential_uuid=stored_cred.uuid, - host=request.host, - ip=request.ip, - user_agent=request.user_agent, - ) - - # Complete the remote auth request (notifies the waiting device) - completed = await remoteauth.instance.complete_request( - token=request.key, - session_token=session_token, - user_uuid=stored_cred.user_uuid, - credential_uuid=stored_cred.uuid, - ) - - if not completed: - raise ValueError("Failed to complete remote authentication") + request.action = locked_action # Update local copy with locked value + # Send device info to the authenticating device await ws.send_json({ - "status": "success", - "message": "Authentication successful. The other device is now logged in.", - }) - break - - # Handle code lookup request - if not code: - raise ValueError("Pairing code required") - - # Look up the remote auth request by pairing code or token - if is_link_token: - request = await remoteauth.instance.get_request(code) - else: - request = await remoteauth.instance.get_request_by_pairing_code(code) - - # Generate new challenge for next request - challenge = pow.generate_challenge() - - if request is None: - await ws.send_json({ - "status": 404, - "detail": "Code not found", + "status": "found", + "host": request.host, + "user_agent_pretty": useragent.compact_user_agent(request.user_agent), + "client_ip": request.ip, + "action": request.action, "pow": { "challenge": base64url.enc(challenge), "work": work, } }) - request = None # Reset for next attempt - continue - - # Valid code found - send device info - await ws.send_json({ - "status": "found", - "host": request.host, - "user_agent_pretty": useragent.compact_user_agent(request.user_agent), - "pow": { - "challenge": base64url.enc(challenge), - "work": work, - } - }) + except Exception: + # If websocket disconnects without explicit denial, unlock the request + if request and not explicitly_denied: + # Unlock the request so the code can be used again + async with remoteauth.instance._lock: + req = remoteauth.instance._requests.get(request.key) + if req and req.locked: + req.locked = False + raise diff --git a/paskia/remoteauth.py b/paskia/remoteauth.py index b691a74..d39b933 100644 --- a/paskia/remoteauth.py +++ b/paskia/remoteauth.py @@ -43,14 +43,21 @@ class RemoteAuthRequest: host: str # The host where the session should be created ip: str # IP of the requesting device user_agent: str # User agent of the requesting device + action: str = "login" # "login" or "register" + locked: bool = False # True once the authenticating device has entered the code # Callback to notify the requesting device when auth completes - # Takes (session_token, user_uuid, credential_uuid) or (None, None, None) on cancel/expire - notify: Callable[[str | None, UUID | None, UUID | None], None] | None = None + # Takes (session_token, user_uuid, credential_uuid, reset_token) or (None, None, None, None) on cancel/expire + notify: Callable[[str | None, UUID | None, UUID | None, str | None], None] | None = None + # Callback to notify the requesting device when action is locked + # Takes (action) to confirm what action was locked + action_locked_notify: Callable[[str], None] | None = None # Set when authentication completes completed: bool = False + denied: bool = False # True if explicitly denied by the authenticating device session_token: str | None = None user_uuid: UUID | None = None credential_uuid: UUID | None = None + reset_token: str | None = None def _generate_pairing_code() -> str: @@ -110,7 +117,7 @@ class RemoteAuthManager: self._by_pairing_code.pop(req.pairing_code, None) if req.notify and not req.completed: try: - req.notify(None, None, None) + req.notify(None, None, None, None) except Exception: pass @@ -119,6 +126,7 @@ class RemoteAuthManager: host: str, ip: str, user_agent: str, + action: str = "login", ) -> tuple[str, str, datetime]: """Create a new remote auth request. @@ -151,6 +159,7 @@ class RemoteAuthManager: host=host, ip=ip, user_agent=user_agent, + action=action, ) self._requests[token] = request @@ -200,7 +209,7 @@ class RemoteAuthManager: async def set_notify_callback( self, token: str, - callback: Callable[[str | None, UUID | None, UUID | None], None], + callback: Callable[[str | None, UUID | None, UUID | None, str | None], None], ) -> bool: """Set the notification callback for a request. @@ -213,12 +222,72 @@ class RemoteAuthManager: req.notify = callback return True + async def set_action_locked_callback( + self, + token: str, + callback: Callable[[str], None], + ) -> bool: + """Set the callback for when the action is locked. + + Returns True if the request exists and callback was set. + """ + async with self._lock: + req = self._requests.get(token) + if req is None: + return False + req.action_locked_notify = callback + return True + + async def update_action( + self, + token: str, + action: str, + ) -> bool: + """Update the action for a request (only if not locked). + + Returns True if the request exists and was updated. + """ + if action not in ("login", "register"): + return False + async with self._lock: + req = self._requests.get(token) + if req is None or req.locked: + return False + req.action = action + return True + + async def lock_action( + self, + token: str, + ) -> str | None: + """Lock the action for a request (called when authenticating device enters code). + + Returns the locked action, or None if request doesn't exist or is already locked. + Notifies the requesting device via action_locked_notify callback. + """ + async with self._lock: + req = self._requests.get(token) + if req is None: + return None + if req.locked: + # Already locked by another authenticating device + return None + req.locked = True + action = req.action + if req.action_locked_notify: + try: + req.action_locked_notify(action) + except Exception: + pass + return action + async def complete_request( self, token: str, - session_token: str, + session_token: str | None, user_uuid: UUID, credential_uuid: UUID, + reset_token: str | None = None, ) -> bool: """Mark a request as completed with the authentication result. @@ -233,27 +302,67 @@ class RemoteAuthManager: self._by_pairing_code.pop(req.pairing_code, None) if req.notify: try: - req.notify(session_token, user_uuid, credential_uuid) + req.notify(session_token, user_uuid, credential_uuid, reset_token) except Exception: pass return True - async def cancel_request(self, token: str) -> bool: + async def cancel_request(self, token: str, *, denied: bool = False) -> RemoteAuthRequest | None: """Cancel and remove a request. - Returns True if the request existed and was removed. + Args: + token: The request token + denied: If True, marks this as an explicit denial (not just timeout/disconnect) + + Returns the removed request if it existed, None otherwise. """ async with self._lock: req = self._requests.pop(token, None) if req is None: - return False + return None self._by_pairing_code.pop(req.pairing_code, None) + if denied: + req.denied = True if req.notify and not req.completed: try: - req.notify(None, None, None) + # Pass denied status through a special UUID value (all zeros means denied) + if denied: + req.notify(None, UUID(int=0), None, None) + else: + req.notify(None, None, None, None) except Exception: pass - return True + return req + + def get_connection_count(self) -> int: + """Get the current count of open WebSocket connections. + + This is used to determine PoW difficulty based on load. + """ + # Count is maintained externally by the WebSocket endpoints + return getattr(self, '_ws_count', 0) + + def increment_connections(self) -> None: + """Increment the WebSocket connection counter.""" + self._ws_count = getattr(self, '_ws_count', 0) + 1 + + def decrement_connections(self) -> None: + """Decrement the WebSocket connection counter.""" + self._ws_count = max(0, getattr(self, '_ws_count', 0) - 1) + + def get_pow_difficulty(self) -> int: + """Get PoW difficulty based on current WebSocket connection count. + + Uses NORMAL difficulty with low load (< 10 connections), + HARD difficulty with high load (>= 10 connections). + + Returns: + PoW work units (pow.NORMAL or pow.HARD) + """ + from paskia.util import pow + + count = self.get_connection_count() + return pow.HARD if count >= 10 else pow.NORMAL async def consume_request(self, token: str) -> RemoteAuthRequest | None: """Get and remove a request (for use by the authenticating device).""" diff --git a/paskia/util/hostutil.py b/paskia/util/hostutil.py index 2d792c1..1a4fa1b 100644 --- a/paskia/util/hostutil.py +++ b/paskia/util/hostutil.py @@ -19,7 +19,7 @@ def is_root_mode() -> bool: return _load_config().get("auth_host") is not None -def configured_auth_host() -> str | None: +def dedicated_auth_host() -> str | None: """Return configured auth_host netloc, or None.""" auth_host = _load_config().get("auth_host") if not auth_host: @@ -34,7 +34,7 @@ def ui_base_path() -> str: return "/" if is_root_mode() else "/auth/" -def auth_site_base_url() -> str: +def auth_site_url() -> str: """Return the base URL for the auth site UI (computed at startup).""" cfg = _load_config() return cfg.get("site_url", "https://localhost") + cfg.get("site_path", "/auth/") @@ -42,7 +42,7 @@ def auth_site_base_url() -> str: def reset_link_url(token: str) -> str: """Generate a reset link URL for the given token.""" - return f"{auth_site_base_url()}{token}" + return f"{auth_site_url()}{token}" def normalize_origin(origin: str) -> str: -- 2.55.0 From f3118b7c1f0df456f7b3c62929df084e130e0883 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 8 Dec 2025 23:51:51 +0000 Subject: [PATCH 16/16] Cleanup, restore reset link functionality as it were, ruff and removed leftover remoteauth functionality. --- frontend/src/components/RemoteAuthPermit.vue | 89 +------- frontend/src/components/RemoteAuthRequest.vue | 171 +-------------- frontend/src/components/RestrictedAuth.vue | 47 +--- paskia/fastapi/api.py | 48 +---- paskia/fastapi/mainapp.py | 21 +- paskia/fastapi/remote.py | 203 ++++++++++-------- paskia/fastapi/ws.py | 7 +- paskia/fastapi/wsutil.py | 16 +- paskia/remoteauth.py | 96 +++------ paskia/util/passphrase.py | 6 +- paskia/util/pow.py | 5 +- pyproject.toml | 1 + scripts/devserver.py | 14 +- 13 files changed, 181 insertions(+), 543 deletions(-) diff --git a/frontend/src/components/RemoteAuthPermit.vue b/frontend/src/components/RemoteAuthPermit.vue index b7042fc..9bfd379 100644 --- a/frontend/src/components/RemoteAuthPermit.vue +++ b/frontend/src/components/RemoteAuthPermit.vue @@ -1,8 +1,8 @@