Update environment configurations for local development, add socket.io and uuid dependencies, and refactor session management to support guest IDs for unauthorized users. Enhance ParticipantsPopup and UserCamera components to handle local media permissions and improve user session handling. Update optional authentication middleware to manage guest IDs and session validation.

This commit is contained in:
2025-10-28 16:58:38 +05:00
parent 2378ed1ff4
commit 4b81b22a1d
17 changed files with 375 additions and 203 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
# VITE_API_URL=http://192.168.1.23:3000 VITE_API_URL=http://localhost:3000
# VITE_API_URL=http://192.168.1.224:3000 VITE_WEBRTC_URL=http://localhost:3001
VITE_API_URL=https://stream.graff.estate/api # VITE_API_URL=https://stream.graff.estate/api
VITE_WEBRTC_URL=https://stream.graff.estate # VITE_WEBRTC_URL=https://stream.graff.estate
+27
View File
@@ -14,6 +14,8 @@
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-qr-code": "^2.0.18", "react-qr-code": "^2.0.18",
"react-router": "^7.9.3", "react-router": "^7.9.3",
"socket.io-client": "^4.8.1",
"uuid": "^13.0.0",
"zustand": "^5.0.8", "zustand": "^5.0.8",
}, },
"devDependencies": { "devDependencies": {
@@ -21,6 +23,7 @@
"@types/node": "^24.6.0", "@types/node": "^24.6.0",
"@types/react": "^19.1.16", "@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@types/uuid": "^11.0.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.36.0", "eslint": "^9.36.0",
@@ -186,6 +189,8 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="],
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="], "@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
@@ -226,6 +231,8 @@
"@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="], "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="],
@@ -326,6 +333,10 @@
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -568,6 +579,10 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="],
"socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -616,6 +631,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
"vite": ["vite@7.1.8", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ=="], "vite": ["vite@7.1.8", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -628,6 +645,8 @@
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
@@ -644,6 +663,10 @@
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -652,6 +675,10 @@
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -72,6 +72,7 @@ function SessionUsersPanel({
isControlDisabled={true} isControlDisabled={true}
isAdmin={true} // Локальный пользователь - админ своей сессии isAdmin={true} // Локальный пользователь - админ своей сессии
mediaStream={participant.stream} mediaStream={participant.stream}
hasLocalMediaPermission={hasLocalStream}
onMute={() => console.log(`Mute user ${participant.id}`)} onMute={() => console.log(`Mute user ${participant.id}`)}
onVideoOff={() => console.log(`Video off user ${participant.id}`)} onVideoOff={() => console.log(`Video off user ${participant.id}`)}
onCanControl={() => onCanControl={() =>
@@ -12,11 +12,24 @@ import { Fragment, useRef } from "react";
import DraggableContainer from "../DraggableContainer"; import DraggableContainer from "../DraggableContainer";
import { useWebRTC } from "../../hooks/useWebRTC"; import { useWebRTC } from "../../hooks/useWebRTC";
import type { Participant } from "../../lib/webrtc"; import type { Participant } from "../../lib/webrtc";
import type { Session } from "../../types/Session";
import { getGuestId } from "../../lib/guestId";
import { useMe } from "../../hooks/useAuth";
export default function ParticipantsPopup() { interface ParticipantsPopupProps {
session: Session;
}
export default function ParticipantsPopup({ session }: ParticipantsPopupProps) {
const { participants, currentUserId, localStream } = useWebRTC(); const { participants, currentUserId, localStream } = useWebRTC();
const { data: user } = useMe();
const headerRef = useRef<HTMLDivElement>(null); const headerRef = useRef<HTMLDivElement>(null);
// Определяем, является ли текущий пользователь организатором
const isOrganizer =
!!(session.userId && user?.id === session.userId) ||
!!(session.guestId && getGuestId() === session.guestId);
// Добавляем локального пользователя в начало списка // Добавляем локального пользователя в начало списка
const allParticipants: (Participant & { isLocal?: boolean })[] = [ const allParticipants: (Participant & { isLocal?: boolean })[] = [
{ {
@@ -39,7 +52,7 @@ export default function ParticipantsPopup() {
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 2xl:-mt-[1.389vw] -mt-5"> <div className="flex flex-col 2xl:gap-[1.667vw] gap-6 2xl:-mt-[1.389vw] -mt-5">
<div className="flex flex-col gap-4 2xl:gap-[1.111vw] 2xl:max-h-[calc(11.944vw+1.389vw)] max-h-[73.75dvh] overflow-y-auto 2xl:pt-[1.389vw] pt-5"> <div className="flex flex-col gap-4 2xl:gap-[1.111vw] 2xl:max-h-[calc(11.944vw+1.389vw)] max-h-[73.75dvh] overflow-y-auto 2xl:pt-[1.389vw] pt-5">
{allParticipants.length === 0 ? ( {allParticipants.length === 0 ? (
<div className="text-center text-gray-500 py-8"> <div className="py-8 text-center text-gray-500">
Нет участников Нет участников
</div> </div>
) : ( ) : (
@@ -48,6 +61,8 @@ export default function ParticipantsPopup() {
<ParticipantItem <ParticipantItem
participant={participant} participant={participant}
isLocal={participant.isLocal || false} isLocal={participant.isLocal || false}
isOrganizer={isOrganizer}
session={session}
/> />
<hr className="w-full 2xl:h-[0.69vw] h-px border-[#F6F6F6] last:hidden" /> <hr className="w-full 2xl:h-[0.69vw] h-px border-[#F6F6F6] last:hidden" />
</Fragment> </Fragment>
@@ -81,21 +96,36 @@ export default function ParticipantsPopup() {
function ParticipantItem({ function ParticipantItem({
participant, participant,
isLocal isLocal,
isOrganizer,
session,
}: { }: {
participant: Participant & { isLocal?: boolean }; participant: Participant & { isLocal?: boolean };
isLocal: boolean; isLocal: boolean;
isOrganizer: boolean;
session: Session;
}) { }) {
const parentRef = useRef<HTMLDivElement>(null); const parentRef = useRef<HTMLDivElement>(null);
// Проверяем наличие аудио/видео треков // Проверяем наличие аудио/видео треков
const hasAudio = participant.stream?.getAudioTracks().some(track => track.enabled) ?? false; const hasAudio =
const hasVideo = participant.stream?.getVideoTracks().some(track => track.enabled) ?? false; participant.stream?.getAudioTracks().some((track) => track.enabled) ??
false;
const hasVideo =
participant.stream?.getVideoTracks().some((track) => track.enabled) ??
false;
const isMuted = !hasAudio; const isMuted = !hasAudio;
const isVideoOff = !hasVideo; const isVideoOff = !hasVideo;
// Определяем статус участника // Определяем, является ли этот конкретный участник организатором сессии
const status: "admin" | "caution" | undefined = participant.stream ? "admin" : "caution"; const isThisParticipantOrganizer =
(session.userId && participant.id === session.userId) ||
(session.guestId && participant.id === session.guestId);
// Определяем статус участника для аватара
const status: "admin" | "caution" | undefined = isThisParticipantOrganizer
? "admin"
: undefined;
return ( return (
<div ref={parentRef} className="flex justify-between items-center w-full"> <div ref={parentRef} className="flex justify-between items-center w-full">
@@ -103,10 +133,12 @@ function ParticipantItem({
<Avatar size="medium" status={status} /> <Avatar size="medium" status={status} />
<div className="flex flex-col 2xl:gap-[0.278vw] gap-1"> <div className="flex flex-col 2xl:gap-[0.278vw] gap-1">
<span className="button-m"> <span className="button-m">
{isLocal ? "Вы" : participant.name || `Участник ${participant.id.slice(0, 8)}`} {isLocal
? "Вы"
: participant.name || `Участник ${participant.id.slice(0, 8)}`}
</span> </span>
<span className="caption-s text-[#CCCCCC]"> <span className="caption-s text-[#CCCCCC]">
{isLocal ? "Организатор" : "Участник"} {isThisParticipantOrganizer ? "Организатор" : "Участник"}
</span> </span>
</div> </div>
</div> </div>
@@ -123,8 +155,8 @@ function ParticipantItem({
</div> </div>
)} )}
{/* Действия только для удаленных участников и только для администратора */} {/* Действия только для удаленных участников и только для организатора */}
{!isLocal && ( {!isLocal && isOrganizer && (
<ActionsPopover <ActionsPopover
options={[ options={[
{ {
+12 -25
View File
@@ -35,6 +35,7 @@ interface UserCameraProps {
isLocal?: boolean; isLocal?: boolean;
isSpeaking?: boolean; // Для удаленных участников - получаем по Socket.IO isSpeaking?: boolean; // Для удаленных участников - получаем по Socket.IO
onSpeakingChange?: (isSpeaking: boolean) => void; // Для локального - отправляем изменения onSpeakingChange?: (isSpeaking: boolean) => void; // Для локального - отправляем изменения
hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа
} }
export default function UserCamera({ export default function UserCamera({
@@ -50,9 +51,11 @@ export default function UserCamera({
isLocal = false, isLocal = false,
isSpeaking: remoteSpeaking, isSpeaking: remoteSpeaking,
onSpeakingChange, onSpeakingChange,
hasLocalMediaPermission = false,
}: UserCameraProps) { }: UserCameraProps) {
const ref = useRef<HTMLVideoElement>(null); const ref = useRef<HTMLVideoElement>(null);
const [isAudioMuted, setIsAudioMuted] = useState(true); // Для удаленных участников - начинаем с muted // Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay)
const [isAudioMuted, setIsAudioMuted] = useState(!hasLocalMediaPermission);
// Детекция голосовой активности (только для локального пользователя) // Детекция голосовой активности (только для локального пользователя)
const { isSpeaking: isVoiceActive } = useVoiceActivity( const { isSpeaking: isVoiceActive } = useVoiceActivity(
@@ -62,7 +65,7 @@ export default function UserCamera({
// Для локального - используем локальную детекцию // Для локального - используем локальную детекцию
// Для удаленных - используем полученное состояние через Socket.IO // Для удаленных - используем полученное состояние через Socket.IO
const localSpeaking = !isMuted && isVoiceActive; const localSpeaking = !isMuted && isVoiceActive;
const isSpeaking = isLocal ? localSpeaking : (remoteSpeaking || false); const isSpeaking = isLocal ? localSpeaking : remoteSpeaking || false;
// Отправляем изменения состояния для локального пользователя // Отправляем изменения состояния для локального пользователя
useEffect(() => { useEffect(() => {
@@ -78,7 +81,11 @@ export default function UserCamera({
// Логируем для отладки // Логируем для отладки
useEffect(() => { useEffect(() => {
console.log( console.log(
`[${name}${isLocal ? " (local)" : ""}] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed(2)}, isMuted: ${isMuted}` `[${name}${
isLocal ? " (local)" : ""
}] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed(
2
)}, isMuted: ${isMuted}`
); );
}, [isSpeaking, ringOpacity, name, isMuted, isLocal]); }, [isSpeaking, ringOpacity, name, isMuted, isLocal]);
@@ -90,12 +97,6 @@ export default function UserCamera({
); );
ref.current.srcObject = mediaStream; ref.current.srcObject = mediaStream;
// Убеждаемся что видео muted для autoplay
if (!isLocal) {
ref.current.muted = true;
console.log(`[UserCamera] Set muted=true for remote video ${name}`);
}
// Принудительно запускаем воспроизведение // Принудительно запускаем воспроизведение
ref.current.play().catch((error) => { ref.current.play().catch((error) => {
console.error(`[UserCamera] Failed to play video for ${name}:`, error); console.error(`[UserCamera] Failed to play video for ${name}:`, error);
@@ -237,14 +238,13 @@ export default function UserCamera({
}, [name]); }, [name]);
const toggleRemoteAudio = () => { const toggleRemoteAudio = () => {
if (!isLocal && ref.current) { if (!isLocal) {
const newMutedState = !isAudioMuted; const newMutedState = !isAudioMuted;
ref.current.muted = newMutedState;
setIsAudioMuted(newMutedState); setIsAudioMuted(newMutedState);
console.log( console.log(
`[UserCamera] ${name} audio ${ `[UserCamera] ${name} audio ${
newMutedState ? "muted" : "unmuted" newMutedState ? "muted" : "unmuted"
}, video element muted: ${ref.current.muted}` }`
); );
} }
}; };
@@ -305,15 +305,6 @@ export default function UserCamera({
</div> </div>
)} )}
{/* Подсказка для запуска видео */}
{!isLocal && mediaStream && !isVideoOff && (
<div className="flex absolute inset-0 justify-center items-center opacity-0 transition-opacity duration-300 pointer-events-none bg-black/50 group-hover:opacity-100">
<div className="px-2 py-1 text-xs text-white rounded bg-black/70">
Кликните для запуска видео
</div>
</div>
)}
<video <video
ref={ref} ref={ref}
className={clsx( className={clsx(
@@ -324,10 +315,6 @@ export default function UserCamera({
autoPlay autoPlay
muted={isLocal ? isMuted : isAudioMuted} muted={isLocal ? isMuted : isAudioMuted}
playsInline playsInline
webkit-playsinline="true"
controls={false}
preload="metadata"
loop={false}
onLoadedData={() => { onLoadedData={() => {
if (!isLocal && ref.current) { if (!isLocal && ref.current) {
console.log( console.log(
+8 -10
View File
@@ -28,7 +28,7 @@ export function useVoiceActivity(
const [audioLevel, setAudioLevel] = useState(0); const [audioLevel, setAudioLevel] = useState(0);
const audioContextRef = useRef<AudioContext | null>(null); const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null); const analyserRef = useRef<AnalyserNode | null>(null);
const animationFrameRef = useRef<number | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null);
const lastSpeakingTimeRef = useRef<number>(0); const lastSpeakingTimeRef = useRef<number>(0);
const speakingTimeoutRef = useRef<NodeJS.Timeout | null>(null); const speakingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -132,7 +132,7 @@ export function useVoiceActivity(
} }
} }
// Логируем каждые 30 кадров (~500ms при 60fps) // Логируем каждые 30 вызовов (~500ms при частоте 60 Hz)
frameCount++; frameCount++;
if (frameCount % 30 === 0) { if (frameCount % 30 === 0) {
console.log( console.log(
@@ -145,13 +145,11 @@ export function useVoiceActivity(
}` }`
); );
} }
// Запланировать следующую проверку
animationFrameRef.current = requestAnimationFrame(checkVoiceActivity);
}; };
// Запускаем проверку // Запускаем проверку с интервалом ~16ms (приблизительно 60 FPS)
checkVoiceActivity(); // setInterval работает стабильно даже когда окно неактивно
intervalRef.current = setInterval(checkVoiceActivity, 16);
console.log( console.log(
`[useVoiceActivity] Started voice activity detection - Threshold: ${threshold}, FFT: ${fftSize}, Smoothing: ${smoothingTimeConstant}, Debounce: ${debounceTime}ms` `[useVoiceActivity] Started voice activity detection - Threshold: ${threshold}, FFT: ${fftSize}, Smoothing: ${smoothingTimeConstant}, Debounce: ${debounceTime}ms`
@@ -167,9 +165,9 @@ export function useVoiceActivity(
// Cleanup // Cleanup
return () => { return () => {
if (animationFrameRef.current !== null) { if (intervalRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current); clearInterval(intervalRef.current);
animationFrameRef.current = null; intervalRef.current = null;
} }
if (speakingTimeoutRef.current !== null) { if (speakingTimeoutRef.current !== null) {
+5
View File
@@ -1,4 +1,5 @@
import ky from "ky"; import ky from "ky";
import { getOrCreateGuestId } from "./guestId";
// Базовый API клиент // Базовый API клиент
export const api = ky.create({ export const api = ky.create({
@@ -15,6 +16,10 @@ export const api = ky.create({
if (token) { if (token) {
request.headers.set("Authorization", `Bearer ${token}`); request.headers.set("Authorization", `Bearer ${token}`);
} }
// Автоматически добавляем guestId для всех запросов
const guestId = getOrCreateGuestId();
request.headers.set("X-Guest-Id", guestId);
}, },
], ],
}, },
+42
View File
@@ -0,0 +1,42 @@
import { v4 as uuidv4 } from "uuid";
const GUEST_ID_KEY = "guestId";
/**
* Получает или создает guestId для неавторизованного пользователя
* guestId генерируется один раз и сохраняется в localStorage
* @returns UUID v4 строка
*/
export function getOrCreateGuestId(): string {
// Пытаемся получить существующий guestId
let guestId = localStorage.getItem(GUEST_ID_KEY);
// Если нет - генерируем новый UUID v4
if (!guestId) {
guestId = uuidv4();
localStorage.setItem(GUEST_ID_KEY, guestId);
console.log("Generated new guestId:", guestId);
} else {
console.log("Using existing guestId:", guestId);
}
return guestId;
}
/**
* Очищает guestId из localStorage
* Используется при логине пользователя, если нужно
*/
export function clearGuestId(): void {
localStorage.removeItem(GUEST_ID_KEY);
console.log("Cleared guestId from localStorage");
}
/**
* Получает текущий guestId без создания нового
* @returns UUID v4 строка или null если не существует
*/
export function getGuestId(): string | null {
return localStorage.getItem(GUEST_ID_KEY);
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { v4 as uuidv4 } from "uuid"; import { getOrCreateGuestId } from "./guestId";
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
@@ -121,8 +121,8 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) {
const socket = io(serverUrl, { const socket = io(serverUrl, {
transports: ["websocket", "polling"], transports: ["websocket", "polling"],
}); });
const userId = uuidv4(); const userId = getOrCreateGuestId();
console.log("Generated user ID:", userId); console.log("Using guest ID:", userId);
state = { state = {
socket, socket,
+1 -1
View File
@@ -81,7 +81,7 @@ function SessionPage() {
} }
function handleParticipantsOpen() { function handleParticipantsOpen() {
setPopup(<ParticipantsPopup />); setPopup(<ParticipantsPopup session={session} />);
} }
function handleShareOpen() { function handleShareOpen() {
+1
View File
@@ -2,6 +2,7 @@ export interface Session {
id: string; id: string;
appId: string; appId: string;
userId: string | null; userId: string | null;
guestId: string | null;
mode: "stream" | "local"; mode: "stream" | "local";
status: "starting" | "started" | "ending" | "ended"; status: "starting" | "started" | "ending" | "ended";
tier: "demo" | "prod" | null; tier: "demo" | "prod" | null;
+2 -2
View File
@@ -1,4 +1,4 @@
DATABASE_URL=postgres://postgres:v1sq3vD5faXL@194.26.138.94:5432/stream DATABASE_URL=postgres://postgres:v1sq3vD5faXL@194.26.138.94:5432/stream
JWT_SECRET=b5cf2bd3894fb24191f13dc9dddaeecccc92d0ee298e7ee41c2d0aab51c28fa1 JWT_SECRET=b5cf2bd3894fb24191f13dc9dddaeecccc92d0ee298e7ee41c2d0aab51c28fa1
PORT=6000 PORT=3000
SOCKET_PORT=6001 SOCKET_PORT=3001
+38 -1
View File
@@ -13,6 +13,7 @@
"got": "^14.4.8", "got": "^14.4.8",
"jose": "^6.1.0", "jose": "^6.1.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"socket.io": "^4.8.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^4.1.11", "zod": "^4.1.11",
}, },
@@ -133,8 +134,12 @@
"@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="], "@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="],
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
"@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
@@ -145,8 +150,12 @@
"@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
"better-auth": ["better-auth@1.3.24", "", { "dependencies": { "@better-auth/core": "1.3.24", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-LyxIbnB2FExhjqQ/J1G8S8EAbmTBDFOz6CjqHNNu15Gux+c4fF0Si1YNLprROEb4EGNuGUfslurW0Q6nZ+Dobg=="], "better-auth": ["better-auth@1.3.24", "", { "dependencies": { "@better-auth/core": "1.3.24", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-LyxIbnB2FExhjqQ/J1G8S8EAbmTBDFOz6CjqHNNu15Gux+c4fF0Si1YNLprROEb4EGNuGUfslurW0Q6nZ+Dobg=="],
"better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="],
@@ -161,11 +170,13 @@
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
@@ -181,6 +192,10 @@
"elysia": ["elysia@1.4.9", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["file-type", "typescript"] }, "sha512-BWNhA8DoKQvlQTjAUkMAmNeso24U+ibZxY/8LN96qSDK/6eevaX59r3GISow699JPxSnFY3gLMUzJzCLYVtbvg=="], "elysia": ["elysia@1.4.9", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["file-type", "typescript"] }, "sha512-BWNhA8DoKQvlQTjAUkMAmNeso24U+ibZxY/8LN96qSDK/6eevaX59r3GISow699JPxSnFY3gLMUzJzCLYVtbvg=="],
"engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="],
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
@@ -213,14 +228,22 @@
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="], "normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="], "p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="],
@@ -267,6 +290,12 @@
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="],
"socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="],
"socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
@@ -285,6 +314,10 @@
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
@@ -293,6 +326,10 @@
"decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"esbuild-register/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
+10 -5
View File
@@ -97,7 +97,7 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
// POST /sessions - создать новую сессию (с optional auth для demo серверов) // POST /sessions - создать новую сессию (с optional auth для demo серверов)
.post( .post(
"/", "/",
async ({ body, currentUser, status }) => { async ({ body, currentUser, guestId, status }) => {
const { appId, mode, serverId, tier } = body as { const { appId, mode, serverId, tier } = body as {
appId: string; appId: string;
mode: "stream" | "local"; mode: "stream" | "local";
@@ -116,6 +116,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
// Если пользователь не авторизован // Если пользователь не авторизован
if (!currentUser) { if (!currentUser) {
// Проверяем наличие guestId для неавторизованных пользователей
if (!guestId) {
return status(400, "Guest ID is required for unauthorized users");
}
// Проверяем, что режим - stream (только stream поддерживает demo) // Проверяем, что режим - stream (только stream поддерживает demo)
if (mode !== "stream") { if (mode !== "stream") {
return status(401, "Authorization required for local sessions"); return status(401, "Authorization required for local sessions");
@@ -141,11 +146,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
); );
} }
// Создаем сессию без userId (для неавторизованных пользователей) // Создаем сессию с guestId для неавторизованных пользователей
try { try {
const newSession = await serverSessionService.create({ const newSession = await serverSessionService.create({
appId, appId,
// userId не передаем - будет undefined для неавторизованных пользователей guestId, // UUID v4 от клиента (только для неавторизованных)
mode, mode,
serverId, serverId,
tier: "demo", // Всегда demo для неавторизованных tier: "demo", // Всегда demo для неавторизованных
@@ -181,11 +186,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
} }
} }
// Создать сессию // Создать сессию для авторизованного пользователя (используем userId, не guestId)
try { try {
const newSession = await serverSessionService.create({ const newSession = await serverSessionService.create({
appId, appId,
userId: currentUser.id, userId: currentUser.id, // Для авторизованных используем userId
mode, mode,
serverId, serverId,
tier, tier,
+3 -1
View File
@@ -21,7 +21,9 @@ export const serverSessions = pgTable("server_sessions", {
appId: uuid("app_id") appId: uuid("app_id")
.notNull() .notNull()
.references(() => apps.id), .references(() => apps.id),
userId: uuid("user_id").references(() => users.id), // Nullable - для неавторизованных пользователей на demo-серверах userId: uuid("user_id")
.references(() => users.id), // для авторизованных пользователей (nullable - если пользователь не авторизован)
guestId: uuid("guest_id"), // UUID v4 генерируется на клиенте для неавторизованных пользователей (nullable - если пользователь авторизован)
startAt: timestamp("start_at", { withTimezone: true }).defaultNow().notNull(), startAt: timestamp("start_at", { withTimezone: true }).defaultNow().notNull(),
endAt: timestamp("end_at", { withTimezone: true }), // Default 30 minutes from start_at endAt: timestamp("end_at", { withTimezone: true }), // Default 30 minutes from start_at
appPid: integer("app_pid"), appPid: integer("app_pid"),
+156 -129
View File
@@ -6,9 +6,132 @@ import { jwtVerify } from "jose";
import { userService } from "../services/auth/user"; import { userService } from "../services/auth/user";
import { protectedRoutes } from "../db/schema"; import { protectedRoutes } from "../db/schema";
import { RoleName } from "../services/auth"; import { RoleName } from "../services/auth";
import type { User } from "../db/schema/users";
// JWT секрет (должен совпадать с session.service.ts) // Константы
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET); const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
const BEARER_PREFIX = "Bearer ";
const HEADER_AUTHORIZATION = "Authorization";
const HEADER_GUEST_ID = "X-Guest-Id";
// Типы
interface AuthContext extends Record<string, unknown> {
authSession: AuthSession | null;
currentUser: ReturnType<typeof userService.sanitize> | null;
guestId: string | null;
}
interface JwtPayload {
id: string;
}
/**
* Создает контекст без авторизации
*/
const createUnauthenticatedContext = (guestId: string | null): AuthContext => ({
authSession: null,
currentUser: null,
guestId,
});
/**
* Извлекает токен из заголовка Authorization
*/
const extractAccessToken = (authHeader: string | null): string | null => {
if (!authHeader?.startsWith(BEARER_PREFIX)) {
return null;
}
const token = authHeader.slice(BEARER_PREFIX.length);
return token || null;
};
/**
* Верифицирует JWT и возвращает sessionId
*/
const verifyJwtToken = async (token: string): Promise<string | null> => {
try {
const { payload } = await jwtVerify<JwtPayload>(token, JWT_SECRET);
return payload.id || null;
} catch (error) {
return null;
}
};
/**
* Получает активную сессию из БД
*/
const fetchActiveSession = async (
sessionId: string
): Promise<AuthSession | null> => {
try {
const [session] = await db
.select()
.from(authSessions)
.where(
and(
eq(authSessions.id, sessionId),
isNull(authSessions.revokedAt)
)
)
.limit(1);
return session || null;
} catch (error) {
console.error("Database error while fetching session:", error);
return null;
}
};
/**
* Проверяет, не истекла ли сессия
*/
const isSessionExpired = (session: AuthSession): boolean => {
return !!(session.expiresAt && session.expiresAt < new Date());
};
/**
* Верифицирует токен через bcrypt hash
*/
const verifyTokenHash = async (
token: string,
hash: string
): Promise<boolean> => {
try {
return await Bun.password.verify(token, hash);
} catch (error) {
console.error("Token hash verification error:", error);
return false;
}
};
/**
* Проверяет права доступа пользователя к маршруту
*/
const checkRouteAccess = async (
user: User,
path: string,
method: string
): Promise<boolean> => {
const [route] = await db
.select({
methods: protectedRoutes.methods,
roles: protectedRoutes.roles,
})
.from(protectedRoutes)
.where(eq(protectedRoutes.path, path))
.limit(1);
// Если маршрут не защищен, доступ разрешен
if (!route) {
return true;
}
const { methods, roles } = route;
// Проверяем, что метод разрешен и роль пользователя подходит
return methods.includes(method) && roles.includes(user.role as RoleName);
};
/** /**
* Optional auth middleware - проверяет авторизацию если токен предоставлен, * Optional auth middleware - проверяет авторизацию если токен предоставлен,
@@ -16,157 +139,61 @@ const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
*/ */
export const optionalAuthMiddleware = new Elysia().derive( export const optionalAuthMiddleware = new Elysia().derive(
{ as: "scoped" }, { as: "scoped" },
async ({ request }) => { async ({ request }): Promise<AuthContext> => {
const { headers } = request; const { headers } = request;
const authHeader = headers.get("Authorization"); const guestId = headers.get(HEADER_GUEST_ID);
const authHeader = headers.get(HEADER_AUTHORIZATION);
// Если нет заголовка авторизации, продолжаем без пользователя
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return {
authSession: null,
currentUser: null,
};
}
const accessToken = authHeader.split(" ")[1];
// 1. Извлекаем токен
const accessToken = extractAccessToken(authHeader);
if (!accessToken) { if (!accessToken) {
return { return createUnauthenticatedContext(guestId);
authSession: null,
currentUser: null,
};
} }
// 2. Верифицировать JWT (проверка подписи и срока действия) // 2. Верифицируем JWT
let sessionId: string; const sessionId = await verifyJwtToken(accessToken);
if (!sessionId) {
try { return createUnauthenticatedContext(guestId);
const { payload } = await jwtVerify<{ id: string }>(
accessToken,
JWT_SECRET
);
sessionId = payload.id;
if (!sessionId) {
return {
authSession: null,
currentUser: null,
};
}
} catch (err) {
return {
authSession: null,
currentUser: null,
};
} }
// 3. Получить сессию из БД и проверить её валидность // 3. Получаем активную сессию
let authSession: AuthSession; const authSession = await fetchActiveSession(sessionId);
if (!authSession) {
try { return createUnauthenticatedContext(guestId);
authSession = (
await db
.select()
.from(authSessions)
.where(
and(
eq(authSessions.id, sessionId),
isNull(authSessions.revokedAt) // Сессия не отозвана
)
)
.limit(1)
)[0];
if (!authSession) {
return {
authSession: null,
currentUser: null,
};
}
} catch (err) {
console.error("Database error in optional auth middleware:", err);
return {
authSession: null,
currentUser: null,
};
} }
// 4. Проверить срок действия сессии // 4. Проверяем срок действия сессии
if (authSession.expiresAt && authSession.expiresAt < new Date()) { if (isSessionExpired(authSession)) {
return { return createUnauthenticatedContext(guestId);
authSession: null,
currentUser: null,
};
} }
// 5. Верифицировать bcrypt hash токена // 5. Верифицируем hash токена
try { const isValidToken = await verifyTokenHash(
const verified = await Bun.password.verify( accessToken,
accessToken, authSession.accessTokenHash
authSession.accessTokenHash );
); if (!isValidToken) {
return createUnauthenticatedContext(guestId);
if (!verified) {
return {
authSession: null,
currentUser: null,
};
}
} catch (err) {
console.error("Token verification error:", err);
return {
authSession: null,
currentUser: null,
};
} }
// 6. Получить пользователя // 6. Получаем пользователя
const user = await userService.findById(authSession.userId); const user = await userService.findById(authSession.userId);
if (!user) { if (!user) {
return { return createUnauthenticatedContext(guestId);
authSession: null,
currentUser: null,
};
} }
// 7. Проверить доступ к маршруту на основе ролей (если маршрут защищен) // 7. Проверяем доступ к маршруту
const url = new URL(request.url); const url = new URL(request.url);
const path = url.pathname; const hasAccess = await checkRouteAccess(user, url.pathname, request.method);
const method = request.method; if (!hasAccess) {
return createUnauthenticatedContext(guestId);
// Получить маршрут из БД
const route = await db
.select({
methods: protectedRoutes.methods,
roles: protectedRoutes.roles,
})
.from(protectedRoutes)
.where(eq(protectedRoutes.path, path))
.limit(1);
// Если маршрут защищен, проверить права доступа
if (route.length > 0) {
const allowedMethods = route[0].methods;
const allowedRoles = route[0].roles;
// Проверить, что метод входит в список разрешенных
if (allowedMethods.includes(method)) {
// Проверить, есть ли роль пользователя среди разрешенных
if (!allowedRoles.includes(user.role as RoleName)) {
return {
authSession: null,
currentUser: null,
};
}
}
} }
// 8. Всё ОК - вернуть сессию и санитизированного пользователя (без пароля) // 8. Возвращаем успешный контекст
return { return {
authSession, authSession,
currentUser: userService.sanitize(user), currentUser: userService.sanitize(user),
guestId,
}; };
} }
); );
+11 -3
View File
@@ -9,7 +9,8 @@ export type SessionStatus = "starting" | "started" | "ending" | "ended";
export interface CreateSessionParams { export interface CreateSessionParams {
appId: string; appId: string;
userId?: string; // Optional для неавторизованных пользователей на demo-серверах userId?: string; // Для авторизованных пользователей
guestId?: string; // UUID v4 от клиента для неавторизованных пользователей
mode: SessionMode; mode: SessionMode;
serverId?: string; serverId?: string;
tier?: "demo" | "prod"; // Предпочитаемый tier для stream-сессий tier?: "demo" | "prod"; // Предпочитаемый tier для stream-сессий
@@ -245,7 +246,12 @@ export const serverSessionService = {
* Создать новую сессию * Создать новую сессию
*/ */
async create(params: CreateSessionParams) { async create(params: CreateSessionParams) {
const { appId, userId, mode, serverId, tier } = params; const { appId, userId, guestId, mode, serverId, tier } = params;
// Валидация: должен быть указан либо userId, либо guestId
if (!userId && !guestId) {
throw new Error("Either userId or guestId must be provided");
}
// Для local-сессий выбираем сервер сразу // Для local-сессий выбираем сервер сразу
// Для stream-сессий сервер будет назначен динамически при запуске // Для stream-сессий сервер будет назначен динамически при запуске
@@ -262,12 +268,14 @@ export const serverSessionService = {
endAt.setMinutes(endAt.getMinutes() + 30); endAt.setMinutes(endAt.getMinutes() + 30);
// Создать сессию // Создать сессию
// Если пользователь авторизован - используем userId, если нет - guestId
const [newSession] = await db const [newSession] = await db
.insert(serverSessions) .insert(serverSessions)
.values({ .values({
serverId: selectedServerId, // Может быть null для stream-сессий serverId: selectedServerId, // Может быть null для stream-сессий
appId, appId,
userId, userId: userId || null, // Для авторизованных пользователей
guestId: userId ? null : guestId, // Для неавторизованных пользователей
mode, mode,
tier, // Предпочитаемый tier (для stream-сессий) tier, // Предпочитаемый tier (для stream-сессий)
status: "starting", status: "starting",