АРХ - NODEDC LAUNCHER: BFF OIDC и app access
This commit is contained in:
parent
5e86047a02
commit
de0a0d2948
|
|
@ -0,0 +1,8 @@
|
|||
# Optional: override the platform env file used by the local launcher BFF.
|
||||
# By default it auto-loads ../../NODEDC/platform/infra/.env from this repo.
|
||||
NODEDC_PLATFORM_ENV=../../NODEDC/platform/infra/.env
|
||||
|
||||
# Optional local overrides.
|
||||
PORT=5173
|
||||
LAUNCHER_BASE_URL=http://launcher.local.nodedc
|
||||
TASK_BASE_URL=http://task.local.nodedc
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^5.2.1",
|
||||
"jose": "^6.2.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^19.1.1",
|
||||
"react-day-picker": "^9.5.0",
|
||||
|
|
@ -1453,6 +1455,19 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
|
|
@ -1476,6 +1491,30 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
|
|
@ -1510,6 +1549,15 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
|
|
@ -1520,6 +1568,35 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001791",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
|
||||
|
|
@ -1568,6 +1645,28 @@
|
|||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
|
@ -1575,6 +1674,24 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
|
|
@ -1602,7 +1719,6 @@
|
|||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
|
|
@ -1626,6 +1742,35 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.348",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.348.tgz",
|
||||
|
|
@ -1633,6 +1778,33 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
|
|
@ -1640,6 +1812,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
|
|
@ -1692,6 +1876,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
|
|
@ -1702,6 +1892,15 @@
|
|||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
|
|
@ -1712,6 +1911,49 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
|
|
@ -1730,6 +1972,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -1745,6 +2026,15 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
|
@ -1755,6 +2045,145 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -1824,11 +2253,65 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
|
|
@ -1850,6 +2333,15 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
|
|
@ -1857,6 +2349,58 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
|
|
@ -1923,6 +2467,58 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
|
|
@ -2020,6 +2616,28 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
|
|
@ -2036,6 +2654,129 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
|
|
@ -2060,6 +2801,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
|
|
@ -2148,12 +2898,35 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
@ -2175,6 +2948,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
|
@ -2206,6 +2988,15 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
|
|
@ -2394,6 +3185,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -4,16 +4,19 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"dev": "node server/dev-server.mjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"dev:vite": "vite --host 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^5.2.1",
|
||||
"jose": "^6.2.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^19.1.1",
|
||||
"react-day-picker": "^9.5.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,659 @@
|
|||
import express from "express";
|
||||
import { createServer as createHttpServer } from "node:http";
|
||||
import { randomBytes, randomUUID, createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, extname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||
|
||||
const serverRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = resolve(serverRoot, "..");
|
||||
const maxStorageJsonBodyBytes = "260mb";
|
||||
const pendingLoginTtlMs = 10 * 60 * 1000;
|
||||
const sessionTtlMs = 12 * 60 * 60 * 1000;
|
||||
const oidcStateCookieName = "nodedc_oidc_state";
|
||||
const sessionCookieName = "nodedc_session";
|
||||
|
||||
loadEnvFiles([
|
||||
process.env.NODEDC_PLATFORM_ENV,
|
||||
resolve(projectRoot, ".env"),
|
||||
resolve(projectRoot, "..", "..", "NODEDC", "platform", "infra", ".env"),
|
||||
]);
|
||||
|
||||
const config = readConfig();
|
||||
const app = express();
|
||||
const httpServer = createHttpServer(app);
|
||||
const pendingLogins = new Map();
|
||||
const sessions = new Map();
|
||||
let discoveryCache = null;
|
||||
let jwksCache = null;
|
||||
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json({ limit: maxStorageJsonBodyBytes }));
|
||||
|
||||
app.get("/healthz", (_req, res) => {
|
||||
res.json({ ok: true, service: "nodedc-launcher-bff", oidcConfigured: config.oidcConfigured });
|
||||
});
|
||||
|
||||
app.get("/auth/login", asyncRoute(async (req, res) => {
|
||||
ensureOidcConfigured();
|
||||
|
||||
const discovery = await getOidcDiscovery();
|
||||
const state = randomBase64Url(32);
|
||||
const nonce = randomBase64Url(32);
|
||||
const codeVerifier = randomBase64Url(64);
|
||||
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
||||
const returnTo = sanitizeReturnTo(req.query.returnTo);
|
||||
|
||||
pruneExpiredState();
|
||||
pendingLogins.set(state, {
|
||||
codeVerifier,
|
||||
nonce,
|
||||
returnTo,
|
||||
expiresAt: Date.now() + pendingLoginTtlMs,
|
||||
});
|
||||
|
||||
res.cookie(oidcStateCookieName, state, cookieOptions(pendingLoginTtlMs));
|
||||
|
||||
const authorizationUrl = new URL(discovery.authorization_endpoint);
|
||||
authorizationUrl.searchParams.set("response_type", "code");
|
||||
authorizationUrl.searchParams.set("client_id", config.clientId);
|
||||
authorizationUrl.searchParams.set("redirect_uri", config.redirectUri);
|
||||
authorizationUrl.searchParams.set("scope", config.scope);
|
||||
authorizationUrl.searchParams.set("state", state);
|
||||
authorizationUrl.searchParams.set("nonce", nonce);
|
||||
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
||||
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
||||
|
||||
const prompt = sanitizePrompt(req.query.prompt);
|
||||
|
||||
if (prompt) {
|
||||
authorizationUrl.searchParams.set("prompt", prompt);
|
||||
}
|
||||
|
||||
res.redirect(authorizationUrl.toString());
|
||||
}));
|
||||
|
||||
app.get("/auth/callback", asyncRoute(async (req, res) => {
|
||||
ensureOidcConfigured();
|
||||
|
||||
const error = typeof req.query.error === "string" ? req.query.error : null;
|
||||
if (error) {
|
||||
throw new Error(`OIDC provider returned error: ${error}`);
|
||||
}
|
||||
|
||||
const code = typeof req.query.code === "string" ? req.query.code : null;
|
||||
const state = typeof req.query.state === "string" ? req.query.state : null;
|
||||
const cookieState = parseCookies(req.headers.cookie)[oidcStateCookieName];
|
||||
|
||||
if (!code || !state || state !== cookieState) {
|
||||
throw new Error("OIDC callback state validation failed");
|
||||
}
|
||||
|
||||
const pendingLogin = pendingLogins.get(state);
|
||||
pendingLogins.delete(state);
|
||||
res.clearCookie(oidcStateCookieName, clearCookieOptions());
|
||||
|
||||
if (!pendingLogin || pendingLogin.expiresAt < Date.now()) {
|
||||
throw new Error("OIDC login state expired");
|
||||
}
|
||||
|
||||
const discovery = await getOidcDiscovery();
|
||||
const tokenSet = await exchangeCodeForTokens(discovery, code, pendingLogin.codeVerifier);
|
||||
const claims = await verifyIdToken(discovery, tokenSet.id_token, pendingLogin.nonce);
|
||||
const sessionId = randomBase64Url(48);
|
||||
const session = {
|
||||
id: sessionId,
|
||||
user: normalizeUser(claims),
|
||||
tokenSet: {
|
||||
idToken: tokenSet.id_token,
|
||||
accessToken: tokenSet.access_token ?? null,
|
||||
expiresAt: tokenSet.expires_in ? Date.now() + Number(tokenSet.expires_in) * 1000 : null,
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + sessionTtlMs,
|
||||
};
|
||||
|
||||
pruneExpiredSessions();
|
||||
sessions.set(sessionId, session);
|
||||
res.cookie(sessionCookieName, sessionId, cookieOptions(sessionTtlMs));
|
||||
res.redirect(pendingLogin.returnTo);
|
||||
}));
|
||||
|
||||
app.get("/auth/logout", asyncRoute(async (req, res) => {
|
||||
const session = getCurrentSession(req);
|
||||
const returnTo = sanitizeReturnTo(req.query.returnTo);
|
||||
const globalLogout = req.query.global === "1" || req.query.global === "true";
|
||||
|
||||
if (session) {
|
||||
sessions.delete(session.id);
|
||||
}
|
||||
|
||||
res.clearCookie(sessionCookieName, clearCookieOptions());
|
||||
|
||||
if (!globalLogout || !config.oidcConfigured) {
|
||||
res.redirect(returnTo);
|
||||
return;
|
||||
}
|
||||
|
||||
const discovery = await getOidcDiscovery();
|
||||
const endSessionEndpoint = discovery.end_session_endpoint;
|
||||
|
||||
if (!endSessionEndpoint) {
|
||||
res.redirect(returnTo);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoutUrl = new URL(endSessionEndpoint);
|
||||
logoutUrl.searchParams.set("client_id", config.clientId);
|
||||
logoutUrl.searchParams.set("post_logout_redirect_uri", new URL(returnTo, config.appBaseUrl).toString());
|
||||
|
||||
if (session?.tokenSet.idToken) {
|
||||
logoutUrl.searchParams.set("id_token_hint", session.tokenSet.idToken);
|
||||
}
|
||||
|
||||
res.redirect(logoutUrl.toString());
|
||||
}));
|
||||
|
||||
app.get("/api/me", (req, res) => {
|
||||
const session = getCurrentSession(req);
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
authenticated: true,
|
||||
user: session.user,
|
||||
groups: session.user.groups,
|
||||
isSuperAdmin: session.user.groups.includes("nodedc:superadmin"),
|
||||
logoutUrl: "/auth/logout",
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/apps", (req, res) => {
|
||||
const session = getCurrentSession(req);
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ apps: getAppsForUser(session.user.groups) });
|
||||
});
|
||||
|
||||
app.post("/api/storage/upload", asyncRoute(async (req, res) => {
|
||||
const result = await saveUploadedFile(req.body);
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.post("/api/storage/data", asyncRoute(async (req, res) => {
|
||||
await saveLauncherData(req.body);
|
||||
res.json({ ok: true, url: "/storage/launcher-data.json" });
|
||||
}));
|
||||
|
||||
const vite = await createViteServer({
|
||||
root: projectRoot,
|
||||
appType: "spa",
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: { server: httpServer },
|
||||
},
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
|
||||
app.use((error, _req, res, _next) => {
|
||||
vite.ssrFixStacktrace(error);
|
||||
const message = error instanceof Error ? error.message : "Unexpected server error";
|
||||
res.status(500).json({ error: message });
|
||||
});
|
||||
|
||||
httpServer.listen(config.port, "0.0.0.0", () => {
|
||||
console.log(`NODE.DC launcher BFF listening on http://0.0.0.0:${config.port}`);
|
||||
});
|
||||
|
||||
function readConfig() {
|
||||
const issuer = process.env.LAUNCHER_OIDC_ISSUER ?? "";
|
||||
const clientId = process.env.LAUNCHER_OIDC_CLIENT_ID ?? "";
|
||||
const clientSecret = process.env.LAUNCHER_OIDC_CLIENT_SECRET ?? "";
|
||||
const launcherDomain = process.env.LAUNCHER_DOMAIN ?? "localhost:5173";
|
||||
const appBaseUrl = process.env.LAUNCHER_BASE_URL ?? `http://${launcherDomain}`;
|
||||
|
||||
return {
|
||||
port: Number(process.env.PORT ?? "5173"),
|
||||
issuer,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri: process.env.LAUNCHER_OIDC_REDIRECT_URI ?? `${appBaseUrl}/auth/callback`,
|
||||
appBaseUrl,
|
||||
scope: process.env.LAUNCHER_OIDC_SCOPE ?? "openid email profile groups offline_access",
|
||||
cookieDomain: process.env.LAUNCHER_COOKIE_DOMAIN || undefined,
|
||||
cookieSecure: process.env.COOKIE_SECURE === "true",
|
||||
oidcConfigured: Boolean(issuer && clientId && clientSecret),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureOidcConfigured() {
|
||||
if (!config.oidcConfigured) {
|
||||
throw new Error("Launcher OIDC is not configured. Set LAUNCHER_OIDC_ISSUER, LAUNCHER_OIDC_CLIENT_ID and LAUNCHER_OIDC_CLIENT_SECRET.");
|
||||
}
|
||||
}
|
||||
|
||||
async function getOidcDiscovery() {
|
||||
if (discoveryCache && discoveryCache.expiresAt > Date.now()) {
|
||||
return discoveryCache.discovery;
|
||||
}
|
||||
|
||||
const discoveryUrl = new URL("./.well-known/openid-configuration", ensureTrailingSlash(config.issuer));
|
||||
const response = await fetch(discoveryUrl, { headers: { Accept: "application/json" } });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load OIDC discovery from ${discoveryUrl}: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const discovery = await response.json();
|
||||
discoveryCache = { discovery, expiresAt: Date.now() + 5 * 60 * 1000 };
|
||||
return discovery;
|
||||
}
|
||||
|
||||
async function exchangeCodeForTokens(discovery, code, codeVerifier) {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: config.redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
});
|
||||
const response = await fetch(discovery.token_endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OIDC token exchange failed: HTTP ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const tokenSet = await response.json();
|
||||
|
||||
if (!tokenSet.id_token) {
|
||||
throw new Error("OIDC token response does not contain id_token");
|
||||
}
|
||||
|
||||
return tokenSet;
|
||||
}
|
||||
|
||||
async function verifyIdToken(discovery, idToken, nonce) {
|
||||
if (!jwksCache || jwksCache.uri !== discovery.jwks_uri) {
|
||||
jwksCache = {
|
||||
uri: discovery.jwks_uri,
|
||||
jwks: createRemoteJWKSet(new URL(discovery.jwks_uri)),
|
||||
};
|
||||
}
|
||||
|
||||
const { payload } = await jwtVerify(idToken, jwksCache.jwks, {
|
||||
issuer: discovery.issuer ?? config.issuer,
|
||||
audience: config.clientId,
|
||||
});
|
||||
|
||||
if (payload.nonce !== nonce) {
|
||||
throw new Error("OIDC nonce validation failed");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function normalizeUser(claims) {
|
||||
const groups = normalizeGroups(claims.groups);
|
||||
const email = typeof claims.email === "string" ? claims.email : "";
|
||||
const name =
|
||||
typeof claims.name === "string" && claims.name
|
||||
? claims.name
|
||||
: typeof claims.preferred_username === "string" && claims.preferred_username
|
||||
? claims.preferred_username
|
||||
: email || String(claims.sub);
|
||||
|
||||
return {
|
||||
sub: String(claims.sub),
|
||||
email,
|
||||
name,
|
||||
preferredUsername: typeof claims.preferred_username === "string" ? claims.preferred_username : null,
|
||||
groups,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGroups(groupsClaim) {
|
||||
if (Array.isArray(groupsClaim)) {
|
||||
return [...new Set(groupsClaim.filter((group) => typeof group === "string"))];
|
||||
}
|
||||
|
||||
if (typeof groupsClaim === "string" && groupsClaim) {
|
||||
return [groupsClaim];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getAppsForUser(userGroups) {
|
||||
const groupSet = new Set(userGroups);
|
||||
const catalog = getAppCatalog();
|
||||
|
||||
return catalog.map((app) => {
|
||||
const matchedGroups = app.requiredGroups.filter((group) => groupSet.has(group));
|
||||
const isSuperAdmin = groupSet.has("nodedc:superadmin");
|
||||
const isPublic = app.requiredGroups.length === 0;
|
||||
const hasAccess = isSuperAdmin || isPublic || matchedGroups.length > 0;
|
||||
|
||||
return {
|
||||
...app,
|
||||
matchedGroups: isSuperAdmin ? ["nodedc:superadmin", ...matchedGroups] : matchedGroups,
|
||||
hasAccess,
|
||||
accessReason: hasAccess ? "Доступ подтверждён" : "Нет доступа",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getAppCatalog() {
|
||||
const launcherData = readLauncherData();
|
||||
const services = Array.isArray(launcherData?.services) ? launcherData.services : [];
|
||||
const serviceCatalog = services.map((service) => {
|
||||
const specialGroups = specialRequiredGroups(service.slug);
|
||||
const requiredGroups = specialGroups.length
|
||||
? specialGroups
|
||||
: service.authentikGroupName
|
||||
? [service.authentikGroupName]
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: service.id,
|
||||
slug: service.slug,
|
||||
title: service.title,
|
||||
description: service.description,
|
||||
url: getServiceUrl(service),
|
||||
openUrl: getServiceUrl(service),
|
||||
status: service.status ?? "disabled",
|
||||
provider: "authentik",
|
||||
requiredGroups,
|
||||
media: {
|
||||
icon: service.iconUrl ?? null,
|
||||
coverImage: service.coverImageUrl ?? null,
|
||||
accentColor: service.accentColor ?? null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: "launcher",
|
||||
slug: "launcher",
|
||||
title: "NODE.DC Launcher",
|
||||
description: "Единая точка входа в приложения NODE.DC.",
|
||||
url: config.appBaseUrl,
|
||||
openUrl: config.appBaseUrl,
|
||||
status: "active",
|
||||
provider: "authentik",
|
||||
requiredGroups: ["nodedc:launcher:admin", "nodedc:launcher:user"],
|
||||
},
|
||||
...serviceCatalog.filter((service) => service.slug !== "launcher"),
|
||||
];
|
||||
}
|
||||
|
||||
function specialRequiredGroups(slug) {
|
||||
if (slug === "launcher" || slug === "nodedc") return ["nodedc:launcher:admin", "nodedc:launcher:user"];
|
||||
if (slug === "task-manager") return ["nodedc:taskmanager:admin", "nodedc:taskmanager:user"];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getServiceUrl(service) {
|
||||
if (service.slug === "task-manager") {
|
||||
const taskBaseUrl = process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`;
|
||||
return `${taskBaseUrl.replace(/\/$/, "")}/auth/oidc/login/`;
|
||||
}
|
||||
|
||||
return service.launchUrl || service.url || "#";
|
||||
}
|
||||
|
||||
function readLauncherData() {
|
||||
const dataPath = join(projectRoot, "public", "storage", "launcher-data.json");
|
||||
|
||||
try {
|
||||
if (!existsSync(dataPath)) return null;
|
||||
return JSON.parse(readFileSync(dataPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUploadedFile(payload) {
|
||||
if (!isUploadPayload(payload)) {
|
||||
throw new Error("Некорректный payload загрузки");
|
||||
}
|
||||
|
||||
const match = /^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/s.exec(payload.dataUrl);
|
||||
|
||||
if (!match) {
|
||||
throw new Error("Файл должен прийти data-url с base64");
|
||||
}
|
||||
|
||||
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
|
||||
const storedName = buildStoredFileName(payload.fileName, mimeType);
|
||||
const fileBuffer = Buffer.from(match[2], "base64");
|
||||
|
||||
await Promise.all(
|
||||
getWritableStorageRoots().map(async (storageRoot) => {
|
||||
const uploadDir = join(storageRoot, "uploads");
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
await writeFile(join(uploadDir, storedName), fileBuffer);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
url: `/storage/uploads/${storedName}`,
|
||||
fileName: storedName,
|
||||
originalFileName: payload.fileName,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
async function saveLauncherData(payload) {
|
||||
await Promise.all(
|
||||
getWritableStorageRoots().map(async (storageRoot) => {
|
||||
await mkdir(storageRoot, { recursive: true });
|
||||
await writeFile(join(storageRoot, "launcher-data.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getWritableStorageRoots() {
|
||||
const roots = [join(projectRoot, "public", "storage")];
|
||||
const distRoot = join(projectRoot, "dist");
|
||||
|
||||
if (existsSync(distRoot)) {
|
||||
roots.push(join(distRoot, "storage"));
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
function buildStoredFileName(fileName, mimeType) {
|
||||
const extension = extname(fileName) || extensionFromMimeType(mimeType);
|
||||
const rawBase = fileName.slice(0, extension ? -extension.length : undefined);
|
||||
const safeBase =
|
||||
rawBase
|
||||
.normalize("NFKD")
|
||||
.replace(/[^\w.-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 80) || "upload";
|
||||
|
||||
return `${Date.now()}-${randomUUID().slice(0, 8)}-${safeBase}${extension.toLowerCase()}`;
|
||||
}
|
||||
|
||||
function extensionFromMimeType(mimeType) {
|
||||
if (mimeType === "image/jpeg") return ".jpg";
|
||||
if (mimeType === "image/png") return ".png";
|
||||
if (mimeType === "image/gif") return ".gif";
|
||||
if (mimeType === "image/webp") return ".webp";
|
||||
if (mimeType === "video/mp4") return ".mp4";
|
||||
if (mimeType === "video/webm") return ".webm";
|
||||
if (mimeType === "video/quicktime") return ".mov";
|
||||
return "";
|
||||
}
|
||||
|
||||
function isUploadPayload(payload) {
|
||||
return Boolean(
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
typeof payload.fileName === "string" &&
|
||||
typeof payload.mimeType === "string" &&
|
||||
typeof payload.dataUrl === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function getCurrentSession(req) {
|
||||
const sessionId = parseCookies(req.headers.cookie)[sessionCookieName];
|
||||
|
||||
if (!sessionId) return null;
|
||||
|
||||
const session = sessions.get(sessionId);
|
||||
|
||||
if (!session || session.expiresAt < Date.now()) {
|
||||
sessions.delete(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function pruneExpiredSessions() {
|
||||
for (const [sessionId, session] of sessions) {
|
||||
if (session.expiresAt < Date.now()) {
|
||||
sessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pruneExpiredState() {
|
||||
for (const [state, pendingLogin] of pendingLogins) {
|
||||
if (pendingLogin.expiresAt < Date.now()) {
|
||||
pendingLogins.delete(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader) {
|
||||
if (!cookieHeader) return {};
|
||||
|
||||
return Object.fromEntries(
|
||||
cookieHeader.split(";").flatMap((part) => {
|
||||
const separatorIndex = part.indexOf("=");
|
||||
if (separatorIndex === -1) return [];
|
||||
const key = part.slice(0, separatorIndex).trim();
|
||||
const value = part.slice(separatorIndex + 1).trim();
|
||||
return [[key, decodeURIComponent(value)]];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function cookieOptions(maxAgeMs) {
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: config.cookieSecure,
|
||||
path: "/",
|
||||
maxAge: maxAgeMs,
|
||||
};
|
||||
|
||||
if (config.cookieDomain) {
|
||||
options.domain = config.cookieDomain;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function clearCookieOptions() {
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: config.cookieSecure,
|
||||
path: "/",
|
||||
};
|
||||
|
||||
if (config.cookieDomain) {
|
||||
options.domain = config.cookieDomain;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function randomBase64Url(size) {
|
||||
return randomBytes(size).toString("base64url");
|
||||
}
|
||||
|
||||
function sanitizeReturnTo(returnTo) {
|
||||
if (typeof returnTo !== "string" || !returnTo.startsWith("/") || returnTo.startsWith("//")) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return returnTo;
|
||||
}
|
||||
|
||||
function sanitizePrompt(prompt) {
|
||||
if (prompt === "login" || prompt === "none" || prompt === "consent" || prompt === "select_account") {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureTrailingSlash(value) {
|
||||
return value.endsWith("/") ? value : `${value}/`;
|
||||
}
|
||||
|
||||
function asyncRoute(handler) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(handler(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
function loadEnvFiles(candidates) {
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue;
|
||||
|
||||
const envPath = resolve(projectRoot, candidate);
|
||||
|
||||
if (!existsSync(envPath)) continue;
|
||||
|
||||
const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
||||
|
||||
const separatorIndex = trimmed.indexOf("=");
|
||||
const key = trimmed.slice(0, separatorIndex).trim();
|
||||
const value = stripEnvQuotes(trimmed.slice(separatorIndex + 1).trim());
|
||||
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stripEnvQuotes(value) {
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
profileOptions,
|
||||
type LauncherData,
|
||||
} from "../shared/api/mockApi";
|
||||
import { fetchAuthSession, fetchAvailableApps, type AuthSession, type LauncherAuthApp } from "../shared/api/authApi";
|
||||
import { loadPersistedLauncherData, persistLauncherData } from "../shared/api/storageApi";
|
||||
import { AdminOverlay, type SetUserServiceAccessCommand } from "../widgets/admin-overlay/AdminOverlay";
|
||||
import { ServiceRail } from "../widgets/service-rail/ServiceRail";
|
||||
|
|
@ -25,12 +26,78 @@ export function LauncherApp() {
|
|||
const [selectedServiceId, setSelectedServiceId] = useState<string | undefined>();
|
||||
const [adminOpen, setAdminOpen] = useState(false);
|
||||
const [storageHydrated, setStorageHydrated] = useState(false);
|
||||
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
||||
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
|
||||
const me = useMemo(() => buildMe(data, activeProfileId, activeClientId), [data, activeProfileId, activeClientId]);
|
||||
const runtimeMe = useMemo(() => {
|
||||
if (!authSession?.authenticated) return me;
|
||||
|
||||
return {
|
||||
...me,
|
||||
user: {
|
||||
...me.user,
|
||||
authentikUserId: authSession.user.sub,
|
||||
email: authSession.user.email || me.user.email,
|
||||
name: authSession.user.name || me.user.name,
|
||||
},
|
||||
mockAuthentikClaims: {
|
||||
...me.mockAuthentikClaims,
|
||||
sub: authSession.user.sub,
|
||||
email: authSession.user.email || me.mockAuthentikClaims.email,
|
||||
name: authSession.user.name || me.mockAuthentikClaims.name,
|
||||
groups: authSession.groups,
|
||||
},
|
||||
};
|
||||
}, [authSession, me]);
|
||||
const resolvedClientId = me.activeClientId;
|
||||
const authAppsBySlug = useMemo(() => new Map((authApps ?? []).map((app) => [app.slug, app])), [authApps]);
|
||||
const launcherServices = useMemo(
|
||||
() => buildLauncherServices(data, activeProfileId, resolvedClientId),
|
||||
[data, activeProfileId, resolvedClientId]
|
||||
() => {
|
||||
const services = buildLauncherServices(data, activeProfileId, resolvedClientId);
|
||||
|
||||
if (!authSession?.authenticated || authApps === null) {
|
||||
return services;
|
||||
}
|
||||
|
||||
return services.map((service) => {
|
||||
const app = authAppsBySlug.get(service.slug);
|
||||
|
||||
if (!app) {
|
||||
return {
|
||||
...service,
|
||||
userAccess: "denied" as const,
|
||||
openUrl: null,
|
||||
effectiveAccess: {
|
||||
...service.effectiveAccess,
|
||||
allowed: false,
|
||||
visible: true,
|
||||
openEnabled: false,
|
||||
reason: "Нет доступа",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const openEnabled = app.hasAccess && app.status === "active";
|
||||
|
||||
return {
|
||||
...service,
|
||||
title: app.title || service.title,
|
||||
description: app.description || service.description,
|
||||
openUrl: openEnabled ? app.openUrl || app.url || service.openUrl : null,
|
||||
userAccess: openEnabled ? ("allowed" as const) : ("denied" as const),
|
||||
effectiveAccess: {
|
||||
...service.effectiveAccess,
|
||||
allowed: app.hasAccess,
|
||||
visible: true,
|
||||
openEnabled,
|
||||
reason: app.accessReason || (app.hasAccess ? "Доступ подтверждён" : "Нет доступа"),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[authApps, authAppsBySlug, authSession, data, activeProfileId, resolvedClientId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -46,6 +113,55 @@ export function LauncherApp() {
|
|||
|
||||
const selectedService = launcherServices.find((service) => service.id === selectedServiceId);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
fetchAuthSession()
|
||||
.then(async (session) => {
|
||||
if (!isMounted) return;
|
||||
|
||||
setAuthSession(session);
|
||||
setAuthError(null);
|
||||
|
||||
if (!session.authenticated) {
|
||||
setAuthApps([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const apps = await fetchAvailableApps();
|
||||
|
||||
if (isMounted) {
|
||||
setAuthApps(apps);
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!isMounted) return;
|
||||
|
||||
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
|
||||
setAuthApps([]);
|
||||
setAuthError(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authSession?.authenticated) return;
|
||||
|
||||
const nextProfileId = authSession.isSuperAdmin ? "user_root" : "user_vasya";
|
||||
const nextProfile = profileOptions.find((profile) => profile.userId === nextProfileId);
|
||||
|
||||
if (activeProfileId !== nextProfileId) {
|
||||
setActiveProfileId(nextProfileId);
|
||||
}
|
||||
|
||||
if (nextProfile && activeClientId !== nextProfile.defaultClientId) {
|
||||
setActiveClientId(nextProfile.defaultClientId);
|
||||
}
|
||||
}, [activeClientId, activeProfileId, authSession]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
|
|
@ -178,7 +294,7 @@ export function LauncherApp() {
|
|||
{
|
||||
...invite,
|
||||
id: `invite_mock_${Date.now()}`,
|
||||
invitedByUserId: me.user.id,
|
||||
invitedByUserId: runtimeMe.user.id,
|
||||
token: `mock-${Date.now()}`,
|
||||
expiresAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "created",
|
||||
|
|
@ -476,10 +592,18 @@ export function LauncherApp() {
|
|||
setSelectedServiceId((current) => (current === serviceId ? undefined : current));
|
||||
}
|
||||
|
||||
if (!authSession) {
|
||||
return <AuthStateScreen title="Проверяем сессию NODE.DC" description="Платформа подготавливает рабочую область и список приложений." />;
|
||||
}
|
||||
|
||||
if (!authSession.authenticated) {
|
||||
return <AuthStateScreen title="Вход на платформу NODE.DC" description="Войдите, чтобы открыть рабочую область и доступы." error={authError} loginUrl={authSession.loginUrl} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="launcher-app">
|
||||
<TopBar
|
||||
me={me}
|
||||
me={runtimeMe}
|
||||
clients={data.clients}
|
||||
profileOptions={profileOptions}
|
||||
activeProfileId={activeProfileId}
|
||||
|
|
@ -489,6 +613,7 @@ export function LauncherApp() {
|
|||
onClientChange={setActiveClientId}
|
||||
onToggleAdmin={() => setAdminOpen((current) => !current)}
|
||||
onOpenShowcase={() => setAdminOpen(false)}
|
||||
onLogout={() => window.location.assign(authSession.logoutUrl)}
|
||||
/>
|
||||
|
||||
<main className="launcher-main">
|
||||
|
|
@ -502,7 +627,7 @@ export function LauncherApp() {
|
|||
{adminOpen && me.permissions.canOpenAdmin ? (
|
||||
<AdminOverlay
|
||||
data={data}
|
||||
me={me}
|
||||
me={runtimeMe}
|
||||
activeClientId={resolvedClientId}
|
||||
onClose={() => setAdminOpen(false)}
|
||||
onSetUserServiceAccess={handleSetUserServiceAccess}
|
||||
|
|
@ -537,3 +662,50 @@ function syncLauncherServiceLinks(data: LauncherData): LauncherData {
|
|||
services: data.services.map(syncServiceLaunchLink),
|
||||
};
|
||||
}
|
||||
|
||||
function AuthStateScreen({
|
||||
title,
|
||||
description,
|
||||
error,
|
||||
loginUrl,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
error?: string | null;
|
||||
loginUrl?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="launcher-app">
|
||||
<main
|
||||
style={{
|
||||
display: "grid",
|
||||
minHeight: "100vh",
|
||||
placeItems: "center",
|
||||
padding: "2rem",
|
||||
}}
|
||||
>
|
||||
<section
|
||||
style={{
|
||||
display: "grid",
|
||||
width: "min(34rem, 100%)",
|
||||
gap: "1rem",
|
||||
padding: "2rem",
|
||||
borderRadius: "1.75rem",
|
||||
background: "rgba(255, 255, 255, 0.08)",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<img src="/nodedc-logo.svg" alt="NODE.DC" style={{ justifySelf: "center", width: "11rem" }} />
|
||||
<h1 style={{ margin: 0 }}>{title}</h1>
|
||||
<p style={{ margin: 0, color: "var(--text-secondary)", lineHeight: 1.5 }}>{description}</p>
|
||||
{error ? <p style={{ margin: 0, color: "var(--warning)", lineHeight: 1.45 }}>{error}</p> : null}
|
||||
{loginUrl ? (
|
||||
<button className="button button--primary" type="button" onClick={() => window.location.assign(loginUrl)}>
|
||||
Войти
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
export interface AuthUser {
|
||||
sub: string;
|
||||
email: string;
|
||||
name: string;
|
||||
preferredUsername: string | null;
|
||||
groups: string[];
|
||||
}
|
||||
|
||||
export interface AuthenticatedSession {
|
||||
authenticated: true;
|
||||
user: AuthUser;
|
||||
groups: string[];
|
||||
isSuperAdmin: boolean;
|
||||
logoutUrl: string;
|
||||
}
|
||||
|
||||
export interface UnauthenticatedSession {
|
||||
authenticated: false;
|
||||
loginUrl: string;
|
||||
}
|
||||
|
||||
export type AuthSession = AuthenticatedSession | UnauthenticatedSession;
|
||||
|
||||
export interface LauncherAuthApp {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
openUrl: string;
|
||||
status: string;
|
||||
provider: string;
|
||||
requiredGroups: string[];
|
||||
matchedGroups: string[];
|
||||
hasAccess: boolean;
|
||||
accessReason: string;
|
||||
media?: {
|
||||
icon?: string | null;
|
||||
coverImage?: string | null;
|
||||
accentColor?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchAuthSession(): Promise<AuthSession> {
|
||||
const response = await fetch("/api/me", { cache: "no-store" });
|
||||
|
||||
if (response.status === 401) {
|
||||
return (await response.json()) as UnauthenticatedSession;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, "Не удалось получить сессию платформы"));
|
||||
}
|
||||
|
||||
return (await response.json()) as AuthenticatedSession;
|
||||
}
|
||||
|
||||
export async function fetchAvailableApps(): Promise<LauncherAuthApp[]> {
|
||||
const response = await fetch("/api/apps", { cache: "no-store" });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, "Не удалось получить список приложений"));
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as { apps?: LauncherAuthApp[] };
|
||||
return payload.apps ?? [];
|
||||
}
|
||||
|
||||
async function readErrorMessage(response: Response, fallback: string) {
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
return payload.error ?? fallback;
|
||||
} catch {
|
||||
return response.statusText || fallback;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,11 @@ interface NodeDcProfileMenuProps {
|
|||
coverUrl?: string;
|
||||
trigger: (api: { open: boolean; toggle: () => void; setTriggerRef: (node: HTMLElement | null) => void }) => ReactNode;
|
||||
className?: string;
|
||||
onLogout?: () => void;
|
||||
onSettings?: () => void;
|
||||
}
|
||||
|
||||
export function NodeDcProfileMenu({ user, coverUrl = "/storage/default.gif", trigger, className }: NodeDcProfileMenuProps) {
|
||||
export function NodeDcProfileMenu({ user, coverUrl = "/storage/default.gif", trigger, className, onLogout, onSettings }: NodeDcProfileMenuProps) {
|
||||
return (
|
||||
<NodeDcDropdown
|
||||
className={className}
|
||||
|
|
@ -32,11 +34,11 @@ export function NodeDcProfileMenu({ user, coverUrl = "/storage/default.gif", tri
|
|||
<strong>{user.name}</strong>
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
<button className="nodedc-ui-profile-card__row" type="button">
|
||||
<button className="nodedc-ui-profile-card__row" type="button" onClick={onSettings}>
|
||||
<Settings size={15} strokeWidth={1.7} />
|
||||
<span>Настройки</span>
|
||||
</button>
|
||||
<button className="nodedc-ui-profile-card__row" type="button">
|
||||
<button className="nodedc-ui-profile-card__row" type="button" onClick={onLogout}>
|
||||
<LogOut size={15} strokeWidth={1.7} />
|
||||
<span>Выйти</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function TopBar({
|
|||
onClientChange,
|
||||
onToggleAdmin,
|
||||
onOpenShowcase,
|
||||
onLogout,
|
||||
}: {
|
||||
me: MeResponse;
|
||||
clients: Client[];
|
||||
|
|
@ -26,6 +27,7 @@ export function TopBar({
|
|||
onClientChange: (clientId: string) => void;
|
||||
onToggleAdmin: () => void;
|
||||
onOpenShowcase: () => void;
|
||||
onLogout?: () => void;
|
||||
}) {
|
||||
const availableClientIds = new Set(me.memberships.map((membership) => membership.clientId));
|
||||
const availableClients = clients.filter((client) => availableClientIds.has(client.id));
|
||||
|
|
@ -112,6 +114,7 @@ export function TopBar({
|
|||
<div className="nodedc-expanded-toolbar-right">
|
||||
<NodeDcProfileMenu
|
||||
user={me.user}
|
||||
onLogout={onLogout}
|
||||
trigger={({ open, toggle, setTriggerRef }) => (
|
||||
<div
|
||||
ref={setTriggerRef}
|
||||
|
|
|
|||
Loading…
Reference in New Issue