From c605926c3004db2ae6b6a488313c55268d6f04b6 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 8 Dec 2025 23:56:48 +0000 Subject: [PATCH] Implement code word based remote authentication (#1) Add comprehensive remote authentication system allowing users to log in from one device by authenticating from another trusted device. Features include: - Proof of Work (PoW) protection using PBKDF2-SHA512 to prevent abuse - Simple pairing codes (3 words) protected by dynamic PoW difficulty - Autocomplete pairing code input with error checking - Real-time WebSocket communication between devices Unlike device addition links and reset links with QR codes that only allow adding an authentication method, and that work offline over the duration of several days, this mechanism is strictly online, with 5 minute time limit. --- .gitignore | 1 + frontend/auth/admin/AdminApp.vue | 1 - frontend/auth/restricted/RestrictedApi.vue | 25 +- frontend/int/reset/ResetApp.vue | 6 +- frontend/src/admin/AdminUserDetail.vue | 1 - frontend/src/assets/style.css | 6 +- frontend/src/components/DeviceLinkView.vue | 20 +- frontend/src/components/ProfileView.vue | 42 +- frontend/src/components/QRCodeDisplay.vue | 161 ++++ .../src/components/RegistrationLinkModal.vue | 180 ++-- frontend/src/components/RemoteAuthPermit.vue | 894 ++++++++++++++++++ frontend/src/components/RemoteAuthRequest.vue | 535 +++++++++++ frontend/src/components/RestrictedAuth.vue | 141 ++- frontend/src/components/UserBasicInfo.vue | 65 +- frontend/src/utils/awaitable-websocket.js | 32 +- frontend/src/utils/base64url.js | 33 + frontend/src/utils/pow-test.js | 37 + frontend/src/utils/pow.js | 68 ++ frontend/src/utils/wordlist.js | 61 ++ frontend/vite.config.js | 12 +- paskia/authsession.py | 2 +- paskia/fastapi/__main__.py | 4 +- paskia/fastapi/api.py | 28 +- paskia/fastapi/auth_host.py | 2 +- paskia/fastapi/mainapp.py | 20 +- paskia/fastapi/remote.py | 504 ++++++++++ paskia/fastapi/ws.py | 48 +- paskia/fastapi/wsutil.py | 91 ++ 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 - paskia/remoteauth.py | 359 +++++++ paskia/util/frontend.py | 10 +- paskia/util/hostutil.py | 6 +- paskia/util/passphrase.py | 1 + paskia/util/pow.py | 45 + paskia/util/tokens.py | 10 +- pyproject.toml | 1 + scripts/dev.py | 156 --- scripts/devserver.py | 463 +++++++++ 57 files changed, 3668 insertions(+), 503 deletions(-) create mode 100644 frontend/src/components/QRCodeDisplay.vue create mode 100644 frontend/src/components/RemoteAuthPermit.vue create mode 100644 frontend/src/components/RemoteAuthRequest.vue create mode 100644 frontend/src/utils/base64url.js 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/fastapi/remote.py create mode 100644 paskia/fastapi/wsutil.py 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 create mode 100644 paskia/remoteauth.py create mode 100644 paskia/util/pow.py delete mode 100755 scripts/dev.py create mode 100755 scripts/devserver.py 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 diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index fb42321..ab436ca 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -3,7 +3,6 @@ import { ref, onMounted, onUnmounted, computed, watch } from 'vue' import Breadcrumbs from '@/components/Breadcrumbs.vue' import CredentialList from '@/components/CredentialList.vue' import UserBasicInfo from '@/components/UserBasicInfo.vue' -import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue' import StatusMessage from '@/components/StatusMessage.vue' import LoadingView from '@/components/LoadingView.vue' import AuthRequiredMessage from '@/components/AccessDenied.vue' diff --git a/frontend/auth/restricted/RestrictedApi.vue b/frontend/auth/restricted/RestrictedApi.vue index 950aa2e..056632a 100644 --- a/frontend/auth/restricted/RestrictedApi.vue +++ b/frontend/auth/restricted/RestrictedApi.vue @@ -1,15 +1,35 @@ + + diff --git a/frontend/src/components/RegistrationLinkModal.vue b/frontend/src/components/RegistrationLinkModal.vue index 0dd9dfc..9858b26 100644 --- a/frontend/src/components/RegistrationLinkModal.vue +++ b/frontend/src/components/RegistrationLinkModal.vue @@ -1,147 +1,109 @@ + diff --git a/frontend/src/components/RemoteAuthPermit.vue b/frontend/src/components/RemoteAuthPermit.vue new file mode 100644 index 0000000..9bfd379 --- /dev/null +++ b/frontend/src/components/RemoteAuthPermit.vue @@ -0,0 +1,894 @@ + + + + + diff --git a/frontend/src/components/RemoteAuthRequest.vue b/frontend/src/components/RemoteAuthRequest.vue new file mode 100644 index 0000000..fef5568 --- /dev/null +++ b/frontend/src/components/RemoteAuthRequest.vue @@ -0,0 +1,535 @@ + + + + + diff --git a/frontend/src/components/RestrictedAuth.vue b/frontend/src/components/RestrictedAuth.vue index 9018f5c..420dd43 100644 --- a/frontend/src/components/RestrictedAuth.vue +++ b/frontend/src/components/RestrictedAuth.vue @@ -11,27 +11,41 @@

{{ headingTitle }}

👤 {{ userDisplayName }}

-

{{ headerMessage }}

+

-
- - - - - - - + +
+
+ + + + + + + +
+
+ + +
+
@@ -41,10 +55,11 @@ 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/frontend/src/utils/base64url.js b/frontend/src/utils/base64url.js new file mode 100644 index 0000000..097ba63 --- /dev/null +++ b/frontend/src/utils/base64url.js @@ -0,0 +1,33 @@ +/** + * URL-safe Base64 encoding/decoding utilities. + * + * These functions handle base64url format (RFC 4648) which uses: + * - '-' instead of '+' + * - '_' instead of '/' + * - No padding '=' characters + */ + +/** + * Decode a base64url string to Uint8Array. + * Handles both standard base64 and URL-safe base64 (with or without padding). + * @param {string} str - Base64url encoded string + * @returns {Uint8Array} - Decoded bytes + */ +export function dec(str) { + // Convert URL-safe characters to standard base64 + const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + // Add padding if needed + const padded = base64 + '='.repeat((4 - base64.length % 4) % 4) + return Uint8Array.from(atob(padded), c => c.charCodeAt(0)) +} + +/** + * Encode a Uint8Array to base64url string. + * @param {Uint8Array} bytes - Bytes to encode + * @returns {string} - Base64url encoded string (no padding) + */ +export function enc(bytes) { + const base64 = btoa(String.fromCharCode(...bytes)) + // Convert to URL-safe and remove padding + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} diff --git a/frontend/src/utils/pow-test.js b/frontend/src/utils/pow-test.js new file mode 100644 index 0000000..4d5cab2 --- /dev/null +++ b/frontend/src/utils/pow-test.js @@ -0,0 +1,37 @@ +import { solvePoW, verifyPoW } from './pow.js' + +const TRIALS = 5 +const WORK = 10 + +async function test() { + console.log(`Running ${TRIALS} trials with ${WORK} work units...\n`) + + const times = [] + + for (let trial = 1; trial <= TRIALS; trial++) { + const challenge = crypto.getRandomValues(new Uint8Array(8)) + + const start = performance.now() + const solution = await solvePoW(challenge, WORK) + const elapsed = performance.now() - start + + const valid = await verifyPoW(challenge, solution, WORK) + + times.push(elapsed) + + console.log(`Trial ${trial.toString().padStart(2)}: ${(elapsed / 1000).toFixed(3)}s, valid=${valid}`) + } + + const avgTime = times.reduce((a, b) => a + b, 0) / times.length + const minTime = Math.min(...times) + const maxTime = Math.max(...times) + + console.log('\n--- Summary ---') + console.log(`Trials: ${TRIALS}`) + console.log(`Work units: ${WORK}`) + console.log(`Avg time: ${(avgTime / 1000).toFixed(3)}s`) + console.log(`Min time: ${(minTime / 1000).toFixed(3)}s`) + console.log(`Max time: ${(maxTime / 1000).toFixed(3)}s`) +} + +test() diff --git a/frontend/src/utils/pow.js b/frontend/src/utils/pow.js new file mode 100644 index 0000000..b1b33f3 --- /dev/null +++ b/frontend/src/utils/pow.js @@ -0,0 +1,68 @@ +/** + * Proof of Work utility using PBKDF2-SHA512 + * + * The PoW requires finding nonces where PBKDF2(challenge, nonce) produces + * output with a zero first byte. Each work unit requires finding one such nonce. + * All valid nonces are concatenated into a solution for server verification. + */ + +/** + * Solve a Proof of Work challenge + * + * @param {Uint8Array|ArrayBuffer} challenge - 8-byte server-provided challenge + * @param {number} work - Number of PBKDF2 work units required + * @param {object} [options] - Optional parameters + * @param {AbortSignal} [options.signal] - AbortSignal to cancel the operation + * @returns {Promise} Solution: concatenated 8-byte nonces (8 * work bytes) + * @throws {Error} If challenge is invalid or operation is aborted + */ +export async function solvePoW(challenge, work, options = {}) { + const { signal } = options + const startTime = performance.now() + + // Validate inputs + const challengeBytes = challenge instanceof ArrayBuffer + ? new Uint8Array(challenge) + : challenge + + if (!(challengeBytes instanceof Uint8Array) || challengeBytes.length !== 8) { + throw new Error('Challenge must be exactly 8 bytes') + } + + // Import challenge as PBKDF2 key material + const baseKey = await crypto.subtle.importKey('raw', challengeBytes, 'PBKDF2', false, ['deriveBits']) + + // Build solution from found nonces + const solution = new Uint8Array(8 * work) + let totalIterations = 0 + const mask = 0x7FF // The client must work 2048x harder than the server + + // Sequential nonce starting at zero (little-endian, using Uint32Array for efficient increment) + const nonce = new Uint32Array(2) + + for (let i = 0; i < work; i++) { + if (signal?.aborted) { + throw new DOMException('PoW operation aborted', 'AbortError') + } + + // Find a nonce where PBKDF2 output passes the mask check + let result + do { + totalIterations++ + if (++nonce[0] === 0x100000000) ++nonce[1] // Increment 64-bit little-endian nonce + result = new Uint32Array(await crypto.subtle.deriveBits( + { name: 'PBKDF2', salt: nonce, iterations: 128, hash: 'SHA-512'}, + baseKey, + 32 + )) + } while (result[0] & mask) + solution.set(new Uint8Array(nonce.buffer), i * 8) + } + + const elapsed = (performance.now() - startTime) / 1000 + const expectedIterations = work * (mask + 1) + const luckRatio = (totalIterations / expectedIterations).toFixed(1) + const bench = totalIterations / ((mask + 1) * elapsed) + console.log(`PoW work=${work} solved in ${elapsed.toFixed(2)}s (${luckRatio}x expected ${bench.toFixed(1)} work/s)`) + return solution +} diff --git a/frontend/src/utils/wordlist.js b/frontend/src/utils/wordlist.js new file mode 100644 index 0000000..2a8592f --- /dev/null +++ b/frontend/src/utils/wordlist.js @@ -0,0 +1,61 @@ +// Auto-generated from paskia/util/wordlist.py - do not edit manually +// A custom list of 1024 common 3-6 letter words, with unique 3-prefixes and no prefix words + +export const words = ["able", "about", "absent", "abuse", "access", "acid", "across", "act", "adapt", "add", "adjust", "admit", "adult", "advice", "affair", "afraid", "again", "age", "agree", "ahead", "aim", "air", "aisle", "alarm", "album", "alert", "alien", "all", "almost", "alone", "alpha", "also", "alter", "always", "amazed", "among", "amused", "anchor", "angle", "animal", "ankle", "annual", "answer", "any", "apart", "appear", "april", "arch", "are", "argue", "army", "around", "array", "art", "ascent", "ash", "ask", "aspect", "assume", "asthma", "atom", "attack", "audit", "august", "aunt", "author", "avoid", "away", "awful", "axis", "baby", "back", "bad", "bag", "ball", "bamboo", "bank", "bar", "base", "battle", "beach", "become", "beef", "before", "begin", "behind", "below", "bench", "best", "better", "beyond", "bid", "bike", "bind", "bio", "birth", "bitter", "black", "bleak", "blind", "blood", "blue", "board", "body", "boil", "bomb", "bone", "book", "border", "boss", "bottom", "bounce", "bowl", "box", "boy", "brain", "bread", "bring", "brown", "brush", "bubble", "buck", "budget", "build", "bulk", "bundle", "burden", "bus", "but", "buyer", "buzz", "cable", "cache", "cage", "cake", "call", "came", "can", "car", "case", "catch", "cause", "cave", "celery", "cement", "census", "cereal", "change", "check", "child", "choice", "chunk", "cigar", "circle", "city", "civil", "class", "clean", "client", "close", "club", "coast", "code", "coffee", "coil", "cold", "come", "cool", "copy", "core", "cost", "cotton", "couch", "cover", "coyote", "craft", "cream", "crime", "cross", "cruel", "cry", "cube", "cue", "cult", "cup", "curve", "custom", "cute", "cycle", "dad", "damage", "danger", "daring", "dash", "dawn", "day", "deal", "debate", "decide", "deer", "define", "degree", "deity", "delay", "demand", "denial", "depth", "derive", "design", "detail", "device", "dial", "dice", "die", "differ", "dim", "dinner", "direct", "dish", "divert", "dizzy", "doctor", "dog", "dollar", "domain", "donate", "door", "dose", "double", "dove", "draft", "dream", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "dwarf", "eager", "early", "east", "echo", "eco", "edge", "edit", "effort", "egg", "eight", "either", "elbow", "elder", "elite", "else", "embark", "emerge", "emily", "employ", "enable", "end", "enemy", "engine", "enjoy", "enlist", "enough", "enrich", "ensure", "entire", "envy", "equal", "era", "erode", "error", "erupt", "escape", "essay", "estate", "ethics", "evil", "evoke", "exact", "excess", "exist", "exotic", "expect", "extent", "eye", "fabric", "face", "fade", "faith", "fall", "family", "fan", "far", "father", "fault", "feel", "female", "fence", "fetch", "fever", "few", "fiber", "field", "figure", "file", "find", "first", "fish", "fit", "fix", "flat", "flesh", "flight", "float", "fluid", "fly", "foam", "focus", "fog", "foil", "follow", "food", "force", "fossil", "found", "fox", "frame", "fresh", "friend", "frog", "fruit", "fuel", "fun", "fury", "future", "gadget", "gain", "galaxy", "game", "gap", "garden", "gas", "gate", "gauge", "gaze", "genius", "ghost", "giant", "gift", "giggle", "ginger", "girl", "give", "glass", "glide", "globe", "glue", "goal", "god", "gold", "good", "gospel", "govern", "gown", "grant", "great", "grid", "group", "grunt", "guard", "guess", "guide", "gulf", "gun", "gym", "habit", "hair", "half", "hammer", "hand", "happy", "hard", "hat", "have", "hawk", "hay", "hazard", "head", "hedge", "height", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "hobby", "hockey", "hold", "home", "honey", "hood", "hope", "horse", "host", "hotel", "hour", "hover", "how", "hub", "huge", "human", "hungry", "hurt", "hybrid", "ice", "icon", "idea", "idle", "ignore", "ill", "image", "immune", "impact", "income", "index", "infant", "inhale", "inject", "inmate", "inner", "input", "inside", "into", "invest", "iron", "island", "issue", "italy", "item", "ivory", "jacket", "jaguar", "james", "jar", "jazz", "jeans", "jelly", "jewel", "job", "joe", "joke", "joy", "judge", "juice", "july", "jump", "june", "just", "kansas", "kate", "keep", "kernel", "key", "kick", "kid", "kind", "kiss", "kit", "kiwi", "knee", "knife", "know", "labor", "lady", "lag", "lake", "lamp", "laptop", "large", "later", "laugh", "lava", "law", "layer", "lazy", "leader", "left", "legal", "lemon", "length", "lesson", "letter", "level", "liar", "libya", "lid", "life", "light", "like", "limit", "line", "lion", "liquid", "list", "little", "live", "lizard", "load", "local", "logic", "long", "loop", "lost", "loud", "love", "low", "loyal", "lucky", "lumber", "lunch", "lust", "luxury", "lyrics", "mad", "magic", "main", "major", "make", "male", "mammal", "man", "map", "market", "mass", "matter", "maze", "mccoy", "meadow", "media", "meet", "melt", "member", "men", "mercy", "mesh", "method", "middle", "milk", "mimic", "mind", "mirror", "miss", "mix", "mobile", "model", "mom", "monkey", "moon", "more", "mother", "mouse", "move", "much", "muffin", "mule", "must", "mutual", "myself", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "near", "neck", "need", "nephew", "nerve", "nest", "net", "never", "news", "next", "nice", "night", "noble", "noise", "noodle", "normal", "nose", "note", "novel", "now", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obtain", "occur", "ocean", "odor", "off", "often", "oil", "okay", "old", "olive", "omit", "once", "one", "onion", "online", "open", "opium", "oppose", "option", "orange", "orbit", "order", "organ", "orient", "orphan", "other", "outer", "oval", "oven", "own", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "panel", "paper", "parade", "past", "path", "pause", "pave", "paw", "pay", "peace", "pen", "people", "pepper", "permit", "pet", "philip", "phone", "phrase", "piano", "pick", "piece", "pig", "pilot", "pink", "pipe", "pistol", "pitch", "pizza", "place", "please", "pluck", "poem", "point", "polar", "pond", "pool", "post", "pot", "pound", "powder", "praise", "prefer", "price", "profit", "public", "pull", "punch", "pupil", "purity", "push", "put", "puzzle", "qatar", "quasi", "queen", "quite", "quoted", "rabbit", "race", "radio", "rail", "rally", "ramp", "range", "rapid", "rare", "rather", "raven", "raw", "razor", "real", "rebel", "recall", "red", "reform", "region", "reject", "relief", "remain", "rent", "reopen", "report", "result", "return", "review", "reward", "rhythm", "rib", "rich", "ride", "rifle", "right", "ring", "riot", "ripple", "risk", "ritual", "river", "road", "robot", "rocket", "room", "rose", "rotate", "round", "row", "royal", "rubber", "rude", "rug", "rule", "run", "rural", "sad", "safe", "sage", "sail", "salad", "same", "santa", "sauce", "save", "say", "scale", "scene", "school", "scope", "screen", "scuba", "sea", "second", "seed", "self", "semi", "sense", "series", "settle", "seven", "shadow", "she", "ship", "shock", "shrimp", "shy", "sick", "side", "siege", "sign", "silver", "simple", "since", "siren", "sister", "six", "size", "skate", "sketch", "ski", "skull", "slab", "sleep", "slight", "slogan", "slush", "small", "smile", "smooth", "snake", "sniff", "snow", "soap", "soccer", "soda", "soft", "solid", "son", "soon", "sort", "south", "space", "speak", "sphere", "spirit", "split", "spoil", "spring", "spy", "square", "state", "step", "still", "story", "strong", "stuff", "style", "submit", "such", "sudden", "suffer", "sugar", "suit", "summer", "sun", "supply", "sure", "swamp", "sweet", "switch", "sword", "symbol", "syntax", "syria", "system", "table", "tackle", "tag", "tail", "talk", "tank", "tape", "target", "task", "tattoo", "taxi", "team", "tell", "ten", "term", "test", "text", "that", "theme", "this", "three", "thumb", "tibet", "ticket", "tide", "tight", "tilt", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "today", "toe", "toilet", "token", "tomato", "tone", "tool", "top", "torch", "toss", "total", "toward", "toy", "trade", "tree", "trial", "trophy", "true", "try", "tube", "tumble", "tunnel", "turn", "twenty", "twice", "two", "type", "ugly", "unable", "uncle", "under", "unfair", "unique", "unlock", "until", "unveil", "update", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "usual", "vacuum", "vague", "valid", "van", "vapor", "vast", "vault", "vein", "velvet", "vendor", "very", "vessel", "viable", "video", "view", "villa", "violin", "virus", "visit", "vital", "vivid", "vocal", "voice", "volume", "vote", "voyage", "wage", "wait", "wall", "want", "war", "wash", "water", "wave", "way", "wealth", "web", "weird", "were", "west", "wet", "what", "when", "whip", "wide", "wife", "will", "window", "wire", "wish", "wolf", "woman", "wonder", "wood", "work", "wrap", "wreck", "write", "wrong", "xander", "xbox", "xerox", "xray", "yang", "yard", "year", "yellow", "yes", "yin", "york", "you", "zane", "zara", "zebra", "zen", "zero", "zippo", "zone", "zoo", "zorro", "zulu"]; + +// Pre-computed prefix map for autocomplete (maps 1-3 char prefixes to matching words) +const prefixMap = new Map(); + +// Build prefix map on module load +for (const word of words) { + for (let i = 1; i <= Math.min(word.length, 6); i++) { + const prefix = word.slice(0, i); + if (!prefixMap.has(prefix)) { + prefixMap.set(prefix, []); + } + prefixMap.get(prefix).push(word); + } +} + +/** + * Find words matching the given prefix. + * @param {string} prefix - The prefix to match (case-insensitive) + * @returns {string[]} Array of matching words + */ +export function findMatches(prefix) { + if (!prefix) return []; + const normalized = prefix.toLowerCase(); + return prefixMap.get(normalized) || []; +} + +/** + * Get unique autocomplete suggestion if prefix matches exactly one word. + * @param {string} prefix - The prefix to match (case-insensitive) + * @returns {string|null} The matching word if unique, null otherwise + */ +export function getUniqueMatch(prefix) { + const matches = findMatches(prefix); + return matches.length === 1 ? matches[0] : null; +} + +/** + * Check if a word is valid (exists in the wordlist). + * @param {string} word - The word to check (case-insensitive) + * @returns {boolean} True if the word is valid + */ +export function isValidWord(word) { + if (!word) return false; + const normalized = word.toLowerCase(); + return words.includes(normalized); +} + +/** + * Check if a string is a valid prefix (could be the start of a valid word). + * @param {string} prefix - The prefix to check (case-insensitive) + * @returns {boolean} True if the prefix matches at least one word + */ +export function isValidPrefix(prefix) { + if (!prefix) return true; // Empty is always valid + const normalized = prefix.toLowerCase(); + return prefixMap.has(normalized); +} 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' } } }, 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/__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/api.py b/paskia/fastapi/api.py index ebc020d..6a489ea 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -200,11 +200,37 @@ 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, } +@app.get("/token-info") +async def api_token_info(token: str): + """Get information about a reset token. + + Returns: + - type: "reset" + - user_name: display name of the user + - token_type: type of reset token + """ + if not passphrase.is_well_formed(token): + raise HTTPException(status_code=404, detail="Invalid token") + + # Check if this is a reset token + try: + reset_token = await get_reset(token) + user = await db.instance.get_user_by_uuid(reset_token.user_uuid) + return { + "type": "reset", + "user_name": user.display_name, + "token_type": reset_token.token_type, + } + except (ValueError, Exception): + raise HTTPException(status_code=404, detail="Token not found or expired") + + @app.post("/user-info") async def api_user_info( request: Request, 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/mainapp.py b/paskia/fastapi/mainapp.py index 95c159f..75803b2 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -42,8 +42,12 @@ 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 - # (Optional) add shutdown cleanup here later app = FastAPI(lifespan=lifespan) @@ -113,10 +117,14 @@ async def examples_page(): # Note: this catch-all handler must be the last route defined -@app.get("/{reset}") -@app.get("/auth/{reset}") -async def reset_link(reset: str): - """Serve the reset app directly with an injected reset token.""" - if not passphrase.is_well_formed(reset): +@app.get("/{token}") +@app.get("/auth/{token}") +async def token_link(token: str): + """Serve the reset app for reset tokens (password reset / device addition). + + The frontend will validate the token via /auth/api/token-info. + """ + if not passphrase.is_well_formed(token): raise HTTPException(status_code=404) + return Response(*await frontend.read("/int/reset/index.html")) diff --git a/paskia/fastapi/remote.py b/paskia/fastapi/remote.py new file mode 100644 index 0000000..e29d81e --- /dev/null +++ b/paskia/fastapi/remote.py @@ -0,0 +1,504 @@ +""" +Remote authentication WebSocket endpoints. + +This module handles cross-device authentication where one device (requesting) +wants to log in and another device (authenticating) provides the passkey. + +Endpoints: +- /request: Called by the device wanting to be authenticated +- /pair: Called by the authenticating device to complete the request +""" + +import asyncio +from uuid import UUID + +import base64url +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 validate_origin, websocket_error_handler +from paskia.globals import db, passkey +from paskia.util import passphrase, pow + +# Create a FastAPI subapp for remote auth WebSocket endpoints +app = FastAPI() + + +@app.websocket("/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 sends HARD PoW challenge, client solves and responds + 3. Server creates a 3-word pairing code and sends it with expiry + 4. Server waits for another device to authenticate via /remote-auth/pair + 5. When auth completes, server sends session_token to this client + 6. Client can then use the session token to set a cookie + 7. Connection times out after 5 minutes with explicit timeout message + """ + origin = validate_origin(ws) + host = origin.split("://", 1)[1] + + if remoteauth.instance is None: + raise ValueError("Remote authentication is not available") + + # Track this WebSocket connection for load-based PoW difficulty + remoteauth.instance.increment_connections() + try: + # Send PoW challenge immediately with dynamic difficulty based on load + challenge = pow.generate_challenge() + work = remoteauth.instance.get_pow_difficulty() + + await ws.send_json( + { + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + } + ) + + # Receive client response with PoW solution and action + response = await ws.receive_json() + + # Verify PoW (required for this endpoint - SECURITY) + solution_b64 = response.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") + + pow.verify_pow(challenge, solution, work) + + # 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 + 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 pairing code to the client + await ws.send_json( + { + "pairing_code": pairing_code, + "expires": expiry.isoformat().replace("+00:00", "Z"), + } + ) + + # 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(pairing_code, 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( + pairing_code, 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(pairing_code) + 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( + pairing_code, new_action + ) + # Ignore other messages + + except TimeoutError: + # 5 minute timeout reached + await remoteauth.instance.cancel_request(pairing_code) + 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(pairing_code, denied=True) + except Exception: + await remoteauth.instance.cancel_request(pairing_code) + raise + finally: + # Decrement connection count + remoteauth.instance.decrement_connections() + + +@app.websocket("/pair") +@websocket_error_handler +async def websocket_remote_auth_pair(ws: WebSocket): + """Complete a remote authentication request using a 3-word 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. + + Protocol: + 1. Server sends PoW challenge immediately on connect + 2. Client sends {code: "word.word.word", pow: ""} for 3-word pairing code + 3. Server validates PoW and code: + - If invalid code/PoW: {status: 4xx, detail: "...", pow: {challenge, work}} + - If valid: {status: "found", host: "...", user_agent_pretty: "...", pow: {challenge, work}} + 4. Client can then send {authenticate: true} to start WebAuthn + 5. Server sends {optionsJSON: ...} + 6. Client sends WebAuthn response + 7. Server sends {status: "success", message: "..."} + """ + from paskia.util import useragent + + origin = validate_origin(ws) + + if remoteauth.instance is None: + raise ValueError("Remote authentication is not available") + + # Generate initial PoW challenge (always NORMAL for authenticated users) + challenge = pow.generate_challenge() + work = pow.NORMAL + + await ws.send_json( + { + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + } + ) + + request = None + webauthn_challenge = None + explicitly_denied = False + + try: + while True: + msg = await ws.receive_json() + + # 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": "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", "") + + # Validate PoW for 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 + request = await remoteauth.instance.get_request(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 + + # 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 + + request.action = locked_action # Update local copy with locked value + + # Send device info to the authenticating device + await ws.send_json( + { + "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, + }, + } + ) + 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/fastapi/ws.py b/paskia/fastapi/ws.py index 4efca50..0745cc6 100644 --- a/paskia/fastapi/ws.py +++ b/paskia/fastapi/ws.py @@ -1,59 +1,19 @@ -import logging -from functools import wraps from uuid import UUID -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from webauthn.helpers.exceptions import InvalidAuthenticationResponse +from fastapi import FastAPI, WebSocket 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.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.globals import db, passkey from paskia.util import passphrase from paskia.util.tokens import create_token, session_key - -# WebSocket error handling decorator -def websocket_error_handler(func): - @wraps(func) - async def wrapper(ws: WebSocket, *args, **kwargs): - try: - await ws.accept() - return await func(ws, *args, **kwargs) - except WebSocketDisconnect: - pass - except authz.AuthException as e: - await ws.send_json( - { - "status": e.status_code, - **(await authz.auth_error_content(e)), - } - ) - except (ValueError, InvalidAuthenticationResponse) as e: - await ws.send_json({"status": 401, "detail": str(e)}) - except Exception: - logging.exception("Internal Server Error") - await ws.send_json({"status": 500, "detail": "Internal Server Error"}) - - return wrapper - - # Create a FastAPI subapp for WebSocket endpoints app = FastAPI() -def _validate_origin(ws: WebSocket) -> str: - """Extract and validate origin from WebSocket request headers. - - Raises: - ValueError: If origin header is missing or not in allowed list - """ - origin = ws.headers.get("origin") - if not origin: - raise ValueError("Origin header is required for WebSocket connections") - return passkey.instance.validate_origin(origin) - - async def register_chat( ws: WebSocket, user_uuid: UUID, @@ -86,7 +46,7 @@ async def websocket_register_add( - Normal session via auth cookie (requires recent authentication) - Reset token supplied as ?reset=... (auth cookie ignored) """ - origin = _validate_origin(ws) + origin = validate_origin(ws) host = origin.split("://", 1)[1] if reset is not None: if not passphrase.is_well_formed(reset): @@ -142,7 +102,7 @@ async def websocket_register_add( @app.websocket("/authenticate") @websocket_error_handler async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE): - origin = _validate_origin(ws) + origin = validate_origin(ws) host = origin.split("://", 1)[1] # If there's an existing session, restrict to that user's credentials (reauth) diff --git a/paskia/fastapi/wsutil.py b/paskia/fastapi/wsutil.py new file mode 100644 index 0000000..a2c2028 --- /dev/null +++ b/paskia/fastapi/wsutil.py @@ -0,0 +1,91 @@ +""" +Shared WebSocket utilities for FastAPI endpoints. +""" + +import logging +from functools import wraps + +import base64url +from fastapi import WebSocket, WebSocketDisconnect +from webauthn.helpers.exceptions import InvalidAuthenticationResponse + +from paskia.fastapi import authz +from paskia.globals import passkey +from paskia.util import pow + + +def websocket_error_handler(func): + """Decorator for WebSocket endpoints that handles common errors.""" + + @wraps(func) + async def wrapper(ws: WebSocket, *args, **kwargs): + try: + await ws.accept() + return await func(ws, *args, **kwargs) + except WebSocketDisconnect: + pass + except authz.AuthException as e: + await ws.send_json( + { + "status": e.status_code, + **(await authz.auth_error_content(e)), + } + ) + except (ValueError, InvalidAuthenticationResponse) as e: + await ws.send_json({"status": 401, "detail": str(e)}) + except Exception: + logging.exception("Internal Server Error") + await ws.send_json({"status": 500, "detail": "Internal Server Error"}) + + return wrapper + + +async def require_pow(ws: WebSocket, work: int | None = None) -> None: + """Send a PoW challenge and verify the client's solution. + + Sends: {"pow": {"challenge": "", "work": 10}} + Expects: {"pow": ""} + + Args: + ws: WebSocket connection + work: PoW difficulty level (default: pow.DEFAULT_WORK) + + Raises: + ValueError: If the PoW solution is invalid + """ + challenge = pow.generate_challenge() + if work is None: + work = pow.DEFAULT_WORK + + await ws.send_json( + { + "pow": { + "challenge": base64url.enc(challenge), + "work": work, + } + } + ) + + response = await ws.receive_json() + solution_b64 = response.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") + + pow.verify_pow(challenge, solution, work) + + +def validate_origin(ws: WebSocket) -> str: + """Extract and validate origin from WebSocket request headers. + + Raises: + ValueError: If origin header is missing or not in allowed list + """ + origin = ws.headers.get("origin") + if not origin: + raise ValueError("Origin header is required for WebSocket connections") + return passkey.instance.validate_origin(origin) 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 - - - - - - -
- - diff --git a/paskia/remoteauth.py b/paskia/remoteauth.py new file mode 100644 index 0000000..d65d2bf --- /dev/null +++ b/paskia/remoteauth.py @@ -0,0 +1,359 @@ +""" +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). +The link uses the same /{token} endpoint as reset tokens, but the server +distinguishes between them by checking if the token exists in remoteauth first. +The first 3 words of the token serve as the pairing code for manual entry. +""" + +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) + + +@dataclass +class RemoteAuthRequest: + """A pending remote authentication request.""" + + key: str # The 3-word passphrase code + 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 + 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, 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 + + +class RemoteAuthManager: + """Manages pending remote authentication requests.""" + + def __init__(self): + self._requests: dict[str, RemoteAuthRequest] = {} # keyed by 3-word code + 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) + if req.notify and not req.completed: + try: + req.notify(None, None, None, None) + except Exception: + pass + + async def create_request( + self, + host: str, + ip: str, + user_agent: str, + action: str = "login", + ) -> tuple[str, datetime]: + """Create a new remote auth request. + + The code is a 3-word passphrase. + We ensure uniqueness across concurrent requests. + + Returns: + (code, expiry) - The 3-word passphrase code and expiration time + """ + now = datetime.now(timezone.utc) + expiry = now + REMOTE_AUTH_LIFETIME + + async with self._lock: + # Generate unique 3-word code + max_attempts = 100 + for _ in range(max_attempts): + code = passphrase.generate(n=passphrase.N_WORDS_SHORT) + if code not in self._requests: + break + else: + # Extremely unlikely but handle gracefully + raise ValueError("Unable to generate unique code") + + request = RemoteAuthRequest( + key=code, + created_at=now, + host=host, + ip=ip, + user_agent=user_agent, + action=action, + ) + + self._requests[code] = request + + return code, expiry + + async def get_request(self, code: str) -> RemoteAuthRequest | None: + """Get a pending request by code, if valid and not expired.""" + # Normalize: lowercase, dot-separated words + normalized = code.lower().strip().replace(" ", ".") + if not passphrase.is_well_formed(normalized, n=passphrase.N_WORDS_SHORT): + return None + async with self._lock: + req = self._requests.get(normalized) + if req is None: + return None + now = datetime.now(timezone.utc) + if now > req.created_at + REMOTE_AUTH_LIFETIME: + # Expired + del self._requests[normalized] + return None + return req + + async def set_notify_callback( + self, + token: str, + callback: Callable[[str | None, UUID | None, UUID | None, str | 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 + 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 | None, + user_uuid: UUID, + credential_uuid: UUID, + reset_token: str | None = None, + ) -> bool: + """Mark a request as completed with the authentication result. + + The request is removed after notifying the waiting client. + Returns True if the request existed and was completed. + """ + async with self._lock: + req = self._requests.pop(token, None) + if req is None: + return False + if req.notify: + try: + req.notify(session_token, user_uuid, credential_uuid, reset_token) + except Exception: + pass + return True + + async def cancel_request( + self, token: str, *, denied: bool = False + ) -> RemoteAuthRequest | None: + """Cancel and remove a request. + + 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 None + if denied: + req.denied = True + if req.notify and not req.completed: + try: + # 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 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).""" + if not passphrase.is_well_formed(token, n=passphrase.N_WORDS_SHORT): + 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 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 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: diff --git a/paskia/util/passphrase.py b/paskia/util/passphrase.py index e51b2ab..86be828 100644 --- a/paskia/util/passphrase.py +++ b/paskia/util/passphrase.py @@ -3,6 +3,7 @@ import secrets from paskia.util.wordlist import words N_WORDS = 5 +N_WORDS_SHORT = 3 wset = set(words) diff --git a/paskia/util/pow.py b/paskia/util/pow.py new file mode 100644 index 0000000..f8c7e60 --- /dev/null +++ b/paskia/util/pow.py @@ -0,0 +1,45 @@ +""" +Proof of Work utility using PBKDF2-SHA512. + +The PoW requires finding nonces where PBKDF2(challenge, nonce) produces +output with a zero first byte. Each work unit requires finding one such nonce. +All valid nonces are concatenated into a solution for server verification. +""" + +import hashlib +import secrets + +EASY = 2 # Around 0.25s +NORMAL = 8 # Around 1s +HARD = 32 # Around 4s + + +def generate_challenge() -> bytes: + """Generate a random 8-byte challenge.""" + return secrets.token_bytes(8) + + +def verify_pow(challenge: bytes, solution: bytes, work: int = NORMAL) -> None: + """Verify a Proof of Work solution. + + Args: + challenge: 8-byte server-provided challenge + solution: Concatenated 8-byte nonces (8 * work bytes) + work: Number of work units expected + + Raises: + ValueError: If the solution is invalid + """ + if len(challenge) != 8: + raise ValueError("Invalid challenge length") + + if len(solution) != 8 * work: + raise ValueError("Invalid solution length") + + # Verify each work unit - check that PBKDF2 output starts with 0x00 + for i in range(work): + nonce = solution[i * 8 : (i + 1) * 8] + # Require first byte of PBKDF2-SHA512 to be zero + result = hashlib.pbkdf2_hmac("sha512", challenge, nonce, 128, 2) + if result[0] or result[1] & 0x07: + raise ValueError("Invalid PoW solution") diff --git a/paskia/util/tokens.py b/paskia/util/tokens.py index 2126448..a702819 100644 --- a/paskia/util/tokens.py +++ b/paskia/util/tokens.py @@ -1,7 +1,8 @@ -import base64 import hashlib import secrets +import base64url + from paskia.util.passphrase import is_well_formed @@ -12,21 +13,20 @@ def create_token() -> str: def session_key(token: str) -> bytes: if len(token) != 16: raise ValueError("Session token must be exactly 16 characters long") - return b"sess" + base64.urlsafe_b64decode(token) + return b"sess" + base64url.dec(token) def encode_session_key(key: bytes) -> str: """Encode an opaque session key for external representation.""" - return base64.urlsafe_b64encode(key).decode().rstrip("=") + return base64url.enc(key) def decode_session_key(encoded: str) -> bytes: """Decode an opaque session key from its public representation.""" if not encoded: raise ValueError("Invalid session identifier") - padding = "=" * (-len(encoded) % 4) try: - raw = base64.urlsafe_b64decode(encoded + padding) + raw = base64url.dec(encoded) except Exception as exc: # pragma: no cover - defensive raise ValueError("Invalid session identifier") from exc if not raw.startswith(b"sess"): diff --git a/pyproject.toml b/pyproject.toml index fd1cad6..fb7326b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ dev = [ "pytest>=9.0.1", "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", + "ruff>=0.14.8", ] [project.scripts] diff --git a/scripts/dev.py b/scripts/dev.py deleted file mode 100755 index 24a3f8b..0000000 --- a/scripts/dev.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env -S uv run -"""Run Vite development server for frontend and FastAPI backend with auto-reload. - -This script is only available when running from the git repository source, -not from the installed package. It starts both the Vite frontend dev server -and the FastAPI backend with auto-reload enabled. - -Usage: - uv run scripts/dev.py [host:port] [options...] - -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. -""" - -import argparse -import atexit -import os -import shutil -import signal -import subprocess -import sys -from pathlib import Path -from sys import stderr -from threading import Thread - -from paskia.fastapi.__main__ import parse_endpoint - -DEFAULT_VITE_PORT = 4403 # overrides by CLI option -BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts - -NO_FRONTEND_TOOL = """\ -┃ ⚠️ deno, npm or bunx needed to run the frontend server. -""" - -BUN_BUG = """\ -┃ ⚠️ Bun cannot correctly proxy API requests to the backend. -┃ Bug report: https://github.com/oven-sh/bun/issues/9882 -┃ -┃ Options: -┃ - sudo caddy run --config caddy/Caddyfile.dev -┃ - Install deno or npm instead -┃ -┃ Caddy will skip the Vite for API calls and serve everything at port 443. -┃ Otherwise Vite serves at port 8077 and proxies to backend (broken with bun). -""" - -NO_FRONTEND = """\ -┃ -┃ The backend will still try reaching Vite at {vite_url} -┃ for various frontend assets, so make sure to start it manually. -""" - - -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.""" - devpath = Path(__file__).parent.parent / "frontend" - if not (devpath / "package.json").exists(): - stderr.write( - f"┃ ⚠️ Frontend source not found at {devpath}\n" - + NO_FRONTEND.format(vite_url=vite_url) - ) - return - - options = [ - ("deno", "run", "dev"), - ("npm", "run", "dev", "--"), - ("bunx", "--bun", "vite"), - ] - cmd = None - tool_name = None - for option in options: - if tool := shutil.which(option[0]): - cmd = [tool, *option[1:]] - tool_name = option[0] - break - - # Add Vite CLI args for host/port - vite_args = ["--port", str(vite_port)] - if vite_host: - vite_args.extend(["--host", vite_host]) - - vite_process = None - - def start_vite(): - nonlocal vite_process - if cmd is None: - stderr.write(NO_FRONTEND_TOOL + NO_FRONTEND.format(vite_url=vite_url)) - return - assert tool_name is not None - try: - if tool_name == "bunx": - stderr.write(BUN_BUG) - - full_cmd = cmd + vite_args - stderr.write(f">>> {' '.join([tool_name, *full_cmd[1:]])}\n") - vite_process = subprocess.Popen(full_cmd, cwd=str(devpath), shell=False) - except Exception as e: - stderr.write( - f"┃ ⚠️ Vite couldn't start: {e}\n" - + NO_FRONTEND.format(vite_url=vite_url) - ) - - def cleanup(): - if vite_process: - vite_process.terminate() - vite_process.wait() - - # Start Vite in a separate thread - vite_thread = Thread(target=start_vite, daemon=True) - vite_thread.start() - - atexit.register(cleanup) - signal.signal(signal.SIGTERM, lambda *_: cleanup()) - signal.signal(signal.SIGINT, lambda *_: cleanup()) - - -def main(): - # Parse optional hostport argument for Vite frontend - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("hostport", nargs="?", default=None) - args, remaining = parser.parse_known_args() - - # Parse Vite endpoint - vite_host, vite_port, vite_uds, all_ifaces = parse_endpoint( - args.hostport, DEFAULT_VITE_PORT - ) - - if vite_uds: - raise SystemExit("┃ ⚠️ Unix sockets are not supported for Vite frontend") - - # Handle all-interfaces case (:port syntax) - # Vite uses 0.0.0.0 to listen on all interfaces (IPv4 only, sufficient for dev) - if all_ifaces: - vite_host = "0.0.0.0" - - # Build Vite URL for PASKIA_DEVMODE (always use localhost for URL) - vite_url = f"http://localhost:{vite_port}" - - # Start Vite dev server - run_vite(vite_url, vite_host, vite_port) - - # Set dev mode with Vite URL - os.environ["PASKIA_DEVMODE"] = vite_url - - # Import CLI after environment is set up - from paskia.fastapi.__main__ import main as cli_main - - # 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() - - -if __name__ == "__main__": - main() diff --git a/scripts/devserver.py b/scripts/devserver.py new file mode 100755 index 0000000..86573a9 --- /dev/null +++ b/scripts/devserver.py @@ -0,0 +1,463 @@ +#!/usr/bin/env -S uv run +"""Run Vite development server for frontend and FastAPI backend with auto-reload. + +This script is only available when running from the git repository source, +not from the installed package. It starts both the Vite frontend dev server +and the FastAPI backend with auto-reload enabled. + +Usage: + uv run scripts/dev.py [host:port] [options...] + +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 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 +""" + +import argparse +import atexit +import ipaddress +import json +import os +import shutil +import signal +import subprocess +from pathlib import Path +from sys import stderr +from threading import Thread +from urllib.parse import urlparse + +DEFAULT_VITE_PORT = 4403 # overrides by CLI option +BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts +CADDY_PORT = 443 # HTTPS port for Caddy proxy +CADDY_HTTP_PORT = 80 # HTTP port for ACME challenges +DEFAULT_HOST = "localhost" + +NO_FRONTEND_TOOL = """\ +┃ ⚠️ deno, npm or bunx needed to run the frontend server. +""" + +BUN_BUG = """\ +┃ ⚠️ Bun cannot correctly proxy API requests to the backend. +┃ Bug report: https://github.com/oven-sh/bun/issues/9882 +┃ +┃ Options: +┃ - sudo caddy run --config caddy/Caddyfile.dev +┃ - Install deno or npm instead +┃ +┃ Caddy will skip the Vite for API calls and serve everything at port 443. +┃ Otherwise Vite serves at port 8077 and proxies to backend (broken with bun). +""" + +NO_FRONTEND = """\ +┃ +┃ The backend will still try reaching Vite at {vite_url} +┃ for various frontend assets, so make sure to start it manually. +""" + +CADDYFILE_SITE_BLOCK = """\ +SITE_ADDR { + # WebSockets bypass directly to backend (workaround for bun proxy bug) + handle /auth/ws/* { + reverse_proxy localhost:BACKEND_PORT + } + # Everything else goes to or via 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.""" + devpath = Path(__file__).parent.parent / "frontend" + if not (devpath / "package.json").exists(): + stderr.write( + f"┃ ⚠️ Frontend source not found at {devpath}\n" + + NO_FRONTEND.format(vite_url=vite_url) + ) + return + + options = [ + ("deno", "run", "dev"), + ("npm", "--silent", "run", "dev", "--"), + ("bunx", "--bun", "vite"), + ] + cmd = None + tool_name = None + for option in options: + if tool := shutil.which(option[0]): + cmd = [tool, *option[1:]] + tool_name = option[0] + break + + # Add Vite CLI args for host/port + vite_args = ["--port", str(vite_port), "--logLevel", "silent"] + if vite_host: + vite_args.extend(["--host", vite_host]) + + vite_process = None + + def start_vite(): + nonlocal vite_process + if cmd is None: + stderr.write(NO_FRONTEND_TOOL + NO_FRONTEND.format(vite_url=vite_url)) + return + assert tool_name is not None + try: + if tool_name == "bunx": + stderr.write(BUN_BUG) + + full_cmd = cmd + vite_args + stderr.write(f">>> {' '.join([tool_name, *full_cmd[1:]])}\n") + vite_process = subprocess.Popen(full_cmd, cwd=str(devpath), shell=False) + except Exception as e: + stderr.write( + f"┃ ⚠️ Vite couldn't start: {e}\n" + + NO_FRONTEND.format(vite_url=vite_url) + ) + + def cleanup(): + if vite_process: + vite_process.terminate() + vite_process.wait() + + # Start Vite in a separate thread + vite_thread = Thread(target=start_vite, daemon=True) + vite_thread.start() + + atexit.register(cleanup) + signal.signal(signal.SIGTERM, lambda *_: cleanup()) + 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 (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) + .replace("BACKEND_PORT", str(BACKEND_PORT)) + .replace("VITE_PORT", str(vite_port)) + ) + caddyfile_parts.append(block) + + caddyfile = "\n".join(caddyfile_parts) + caddy_process = None + + try: + # 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, + 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 + + # 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 logs; "serving initial configuration" means it's ready + 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 "" + ) + if remaining: + for line in remaining.splitlines(): + if line: + 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) + line = caddy_process.stderr.readline().decode() + if not line: + continue + + # Check for ready signal + if "serving initial configuration" in line: + 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 + + 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() + + 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 + vite_host, vite_port, vite_uds, all_ifaces = parse_endpoint( + args.hostport, DEFAULT_VITE_PORT + ) + + if vite_uds: + raise SystemExit("┃ ⚠️ Unix sockets are not supported for Vite frontend") + + # Handle all-interfaces case (:port syntax) + # Vite uses 0.0.0.0 to listen on all interfaces (IPv4 only, sufficient for dev) + if all_ifaces: + vite_host = "0.0.0.0" + + # 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}" + caddy_origins.append(auth_host) + else: + # 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}"] + stderr.write(f">>> sudo 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 in environment for subprocess + env = os.environ.copy() + env["PASKIA_DEVMODE"] = vite_url + + # Build command with origin args + cmd = ["paskia", "serve", f"localhost:{BACKEND_PORT}"] + + # 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__": + main()