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
+2 -2
View File
@@ -1,4 +1,4 @@
DATABASE_URL=postgres://postgres:v1sq3vD5faXL@194.26.138.94:5432/stream
JWT_SECRET=b5cf2bd3894fb24191f13dc9dddaeecccc92d0ee298e7ee41c2d0aab51c28fa1
PORT=6000
SOCKET_PORT=6001
PORT=3000
SOCKET_PORT=3001
+38 -1
View File
@@ -13,6 +13,7 @@
"got": "^14.4.8",
"jose": "^6.1.0",
"pg": "^8.16.3",
"socket.io": "^4.8.1",
"uuid": "^11.1.0",
"zod": "^4.1.11",
},
@@ -133,8 +134,12 @@
"@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=="],
"@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/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=="],
"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=="],
"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-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=="],
"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=="],
"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=="],
@@ -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=="],
"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-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=="],
"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=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"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=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"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=="],
"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-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=="],
"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=="],
"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=="],
"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=="],
"@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(
"/",
async ({ body, currentUser, status }) => {
async ({ body, currentUser, guestId, status }) => {
const { appId, mode, serverId, tier } = body as {
appId: string;
mode: "stream" | "local";
@@ -116,6 +116,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
// Если пользователь не авторизован
if (!currentUser) {
// Проверяем наличие guestId для неавторизованных пользователей
if (!guestId) {
return status(400, "Guest ID is required for unauthorized users");
}
// Проверяем, что режим - stream (только stream поддерживает demo)
if (mode !== "stream") {
return status(401, "Authorization required for local sessions");
@@ -141,11 +146,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
);
}
// Создаем сессию без userId (для неавторизованных пользователей)
// Создаем сессию с guestId для неавторизованных пользователей
try {
const newSession = await serverSessionService.create({
appId,
// userId не передаем - будет undefined для неавторизованных пользователей
guestId, // UUID v4 от клиента (только для неавторизованных)
mode,
serverId,
tier: "demo", // Всегда demo для неавторизованных
@@ -181,11 +186,11 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
}
}
// Создать сессию
// Создать сессию для авторизованного пользователя (используем userId, не guestId)
try {
const newSession = await serverSessionService.create({
appId,
userId: currentUser.id,
userId: currentUser.id, // Для авторизованных используем userId
mode,
serverId,
tier,
+3 -1
View File
@@ -21,7 +21,9 @@ export const serverSessions = pgTable("server_sessions", {
appId: uuid("app_id")
.notNull()
.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(),
endAt: timestamp("end_at", { withTimezone: true }), // Default 30 minutes from start_at
appPid: integer("app_pid"),
+156 -129
View File
@@ -6,9 +6,132 @@ import { jwtVerify } from "jose";
import { userService } from "../services/auth/user";
import { protectedRoutes } from "../db/schema";
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 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 - проверяет авторизацию если токен предоставлен,
@@ -16,157 +139,61 @@ const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
*/
export const optionalAuthMiddleware = new Elysia().derive(
{ as: "scoped" },
async ({ request }) => {
async ({ request }): Promise<AuthContext> => {
const { headers } = request;
const authHeader = headers.get("Authorization");
// Если нет заголовка авторизации, продолжаем без пользователя
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return {
authSession: null,
currentUser: null,
};
}
const accessToken = authHeader.split(" ")[1];
const guestId = headers.get(HEADER_GUEST_ID);
const authHeader = headers.get(HEADER_AUTHORIZATION);
// 1. Извлекаем токен
const accessToken = extractAccessToken(authHeader);
if (!accessToken) {
return {
authSession: null,
currentUser: null,
};
return createUnauthenticatedContext(guestId);
}
// 2. Верифицировать JWT (проверка подписи и срока действия)
let sessionId: string;
try {
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,
};
// 2. Верифицируем JWT
const sessionId = await verifyJwtToken(accessToken);
if (!sessionId) {
return createUnauthenticatedContext(guestId);
}
// 3. Получить сессию из БД и проверить её валидность
let authSession: AuthSession;
try {
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,
};
// 3. Получаем активную сессию
const authSession = await fetchActiveSession(sessionId);
if (!authSession) {
return createUnauthenticatedContext(guestId);
}
// 4. Проверить срок действия сессии
if (authSession.expiresAt && authSession.expiresAt < new Date()) {
return {
authSession: null,
currentUser: null,
};
// 4. Проверяем срок действия сессии
if (isSessionExpired(authSession)) {
return createUnauthenticatedContext(guestId);
}
// 5. Верифицировать bcrypt hash токена
try {
const verified = await Bun.password.verify(
accessToken,
authSession.accessTokenHash
);
if (!verified) {
return {
authSession: null,
currentUser: null,
};
}
} catch (err) {
console.error("Token verification error:", err);
return {
authSession: null,
currentUser: null,
};
// 5. Верифицируем hash токена
const isValidToken = await verifyTokenHash(
accessToken,
authSession.accessTokenHash
);
if (!isValidToken) {
return createUnauthenticatedContext(guestId);
}
// 6. Получить пользователя
// 6. Получаем пользователя
const user = await userService.findById(authSession.userId);
if (!user) {
return {
authSession: null,
currentUser: null,
};
return createUnauthenticatedContext(guestId);
}
// 7. Проверить доступ к маршруту на основе ролей (если маршрут защищен)
// 7. Проверяем доступ к маршруту
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// Получить маршрут из БД
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,
};
}
}
const hasAccess = await checkRouteAccess(user, url.pathname, request.method);
if (!hasAccess) {
return createUnauthenticatedContext(guestId);
}
// 8. Всё ОК - вернуть сессию и санитизированного пользователя (без пароля)
// 8. Возвращаем успешный контекст
return {
authSession,
currentUser: userService.sanitize(user),
guestId,
};
}
);
+11 -3
View File
@@ -9,7 +9,8 @@ export type SessionStatus = "starting" | "started" | "ending" | "ended";
export interface CreateSessionParams {
appId: string;
userId?: string; // Optional для неавторизованных пользователей на demo-серверах
userId?: string; // Для авторизованных пользователей
guestId?: string; // UUID v4 от клиента для неавторизованных пользователей
mode: SessionMode;
serverId?: string;
tier?: "demo" | "prod"; // Предпочитаемый tier для stream-сессий
@@ -245,7 +246,12 @@ export const serverSessionService = {
* Создать новую сессию
*/
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-сессий выбираем сервер сразу
// Для stream-сессий сервер будет назначен динамически при запуске
@@ -262,12 +268,14 @@ export const serverSessionService = {
endAt.setMinutes(endAt.getMinutes() + 30);
// Создать сессию
// Если пользователь авторизован - используем userId, если нет - guestId
const [newSession] = await db
.insert(serverSessions)
.values({
serverId: selectedServerId, // Может быть null для stream-сессий
appId,
userId,
userId: userId || null, // Для авторизованных пользователей
guestId: userId ? null : guestId, // Для неавторизованных пользователей
mode,
tier, // Предпочитаемый tier (для stream-сессий)
status: "starting",