logto implementation
This commit is contained in:
parent
bbda602fac
commit
b59fa5e267
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
6
.env.example
Normal file
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
LOGTO_ENDPOINT=http://localhost:3001
|
||||||
|
LOGTO_APP_ID=vffsu10ig54nvytbf5r4j
|
||||||
|
LOGTO_APP_SECRET=RquAjwLFXlukWEYTmsf9KXPlyBMNr090
|
||||||
|
LOGTO_RESOURCE=https://default.logto.app/api
|
||||||
|
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
4
.env.litellm.example
Normal file
4
.env.litellm.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
LITELLM_MASTER_KEY="sk-1234" # Change this if you like
|
||||||
|
LITELLM_SALT_KEY="sk-1234" # Change this if you like but can't be changed after the first run
|
||||||
|
DATABASE_URL="postgres://postgres:p0stgr3s@postgres:5432/litellm"
|
||||||
|
STORE_MODEL_IN_DB='True'
|
||||||
34
.env.logto.example
Normal file
34
.env.logto.example
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Logto Configuration for Development
|
||||||
|
DB_URL=postgres://postgres:p0stgr3s@postgres:5432/logto
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Core endpoints
|
||||||
|
# Pour la communication container-to-container, utilisez le nom du service
|
||||||
|
ENDPOINT=http://localhost:3001
|
||||||
|
ADMIN_ENDPOINT=http://localhost:3002
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
PORT=3001
|
||||||
|
ADMIN_PORT=3002
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_POOL_SIZE=20
|
||||||
|
DB_CONNECTION_TIMEOUT=30000
|
||||||
|
|
||||||
|
# Security settings (development only)
|
||||||
|
COOKIE_KEYS=your-secret-key-change-in-production-12345678901234567890
|
||||||
|
JWT_SECRET=your-jwt-secret-change-in-production-12345678901234567890
|
||||||
|
|
||||||
|
# CORS settings for development
|
||||||
|
# Permettre les requêtes depuis l'application Next.js
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://jaimes:3000
|
||||||
|
|
||||||
|
# Admin settings
|
||||||
|
ADMIN_DISABLE_LOCALHOST_RESTRICTION=true
|
||||||
|
|
||||||
|
# Feature flags
|
||||||
|
ENABLE_WELLKNOWN=true
|
||||||
|
|
||||||
|
# Database initialization
|
||||||
|
LOGTO_DATABASE_URL=postgres://postgres:p0stgr3s@postgres:5432/logto
|
||||||
|
AUTO_MIGRATE=true
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,6 +32,9 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
!.env.litellm.example
|
||||||
|
!.env.logto.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
51
.prettierignore
Normal file
51
.prettierignore
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.vercel/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# GraphQL generated
|
||||||
|
*.generated.*
|
||||||
15
.prettierrc.json
Normal file
15
.prettierrc.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
25
.stylelintrc.json
Normal file
25
.stylelintrc.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": ["stylelint-config-standard", "stylelint-config-tailwindcss", "stylelint-config-prettier"],
|
||||||
|
"plugins": ["stylelint-config-tailwindcss"],
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen", "layer"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"declaration-block-trailing-semicolon": null,
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
"function-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreFunctions": ["theme", "screen"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selector-class-pattern": null,
|
||||||
|
"custom-property-pattern": null,
|
||||||
|
"property-no-vendor-prefix": null,
|
||||||
|
"value-no-vendor-prefix": null
|
||||||
|
},
|
||||||
|
"ignoreFiles": ["node_modules/**/*", ".next/**/*", "out/**/*", "dist/**/*", "build/**/*", "coverage/**/*"]
|
||||||
|
}
|
||||||
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"ms-vscode.vscode-typescript-next",
|
||||||
|
"GraphQL.vscode-graphql",
|
||||||
|
"GraphQL.vscode-graphql-syntax"
|
||||||
|
]
|
||||||
|
}
|
||||||
85
.vscode/settings.json
vendored
Normal file
85
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.stylelint": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
"stylelint.validate": [
|
||||||
|
"css",
|
||||||
|
"scss",
|
||||||
|
"postcss"
|
||||||
|
],
|
||||||
|
"typescript.preferences.organizeImportsIgnoreCase": false,
|
||||||
|
"typescript.preferences.organizeImportsCollation": "ordinal",
|
||||||
|
"typescript.preferences.organizeImportsNumericCollation": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
},
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"javascript": "javascriptreact",
|
||||||
|
"typescript": "typescriptreact"
|
||||||
|
},
|
||||||
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"typescript": "javascript",
|
||||||
|
"typescriptreact": "javascript"
|
||||||
|
},
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
[
|
||||||
|
"cva\\(([^)]*)\\)",
|
||||||
|
"[\"'`]([^\"'`]*).*?[\"'`]"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"cx\\(([^)]*)\\)",
|
||||||
|
"(?:'|\"|`)([^']*)(?:'|\"|`)"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
341
bun.lock
341
bun.lock
@ -4,18 +4,43 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "next-logto-graphql",
|
"name": "next-logto-graphql",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@graphql-yoga/plugin-csrf-prevention": "^3.15.1",
|
||||||
|
"@graphql-yoga/plugin-disable-introspection": "^2.16.1",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"graphql": "^16.11.0",
|
||||||
|
"graphql-depth-limit": "^1.1.0",
|
||||||
|
"graphql-request": "^7.2.0",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
|
"graphql-yoga": "^5.15.1",
|
||||||
"next": "15.4.3",
|
"next": "15.4.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"sequelize": "^6.37.7",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/graphql-depth-limit": "^1.1.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||||
|
"@typescript-eslint/parser": "^8.38.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.3",
|
"eslint-config-next": "15.4.3",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-prettier": "^5.5.3",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"stylelint": "^16.22.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
@ -26,12 +51,32 @@
|
|||||||
|
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
|
||||||
|
|
||||||
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||||
|
|
||||||
|
"@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@4.0.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ=="],
|
||||||
|
|
||||||
|
"@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="],
|
||||||
|
|
||||||
|
"@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.1.0", "", {}, "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
||||||
|
|
||||||
|
"@envelop/core": ["@envelop/core@5.3.0", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-xvUkOWXI8JsG2OOnqiI2tOkEc52wbmIqWORr7yGc8B8E53Oh1MMGGGck4mbR80s25LnHVzfNIiIlNkuDgZRuuA=="],
|
||||||
|
|
||||||
|
"@envelop/instrumentation": ["@envelop/instrumentation@1.0.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.1", "tslib": "^2.5.0" } }, "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw=="],
|
||||||
|
|
||||||
|
"@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||||
@ -50,6 +95,28 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="],
|
||||||
|
|
||||||
|
"@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="],
|
||||||
|
|
||||||
|
"@graphql-tools/executor": ["@graphql-tools/executor@1.4.9", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w=="],
|
||||||
|
|
||||||
|
"@graphql-tools/merge": ["@graphql-tools/merge@9.1.1", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w=="],
|
||||||
|
|
||||||
|
"@graphql-tools/schema": ["@graphql-tools/schema@10.0.25", "", { "dependencies": { "@graphql-tools/merge": "^9.1.1", "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw=="],
|
||||||
|
|
||||||
|
"@graphql-tools/utils": ["@graphql-tools/utils@10.9.1", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "dset": "^3.1.4", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw=="],
|
||||||
|
|
||||||
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
|
|
||||||
|
"@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="],
|
||||||
|
|
||||||
|
"@graphql-yoga/plugin-csrf-prevention": ["@graphql-yoga/plugin-csrf-prevention@3.15.1", "", { "peerDependencies": { "graphql-yoga": "^5.15.1" } }, "sha512-sx6OM49iv6M3U62uo3RRATL7PK8aXlWC0BrEo/W1X2cd8KtlY0Jd2mqlvcb9gHTJFe+WUvh7EJOCfPjzPf717g=="],
|
||||||
|
|
||||||
|
"@graphql-yoga/plugin-disable-introspection": ["@graphql-yoga/plugin-disable-introspection@2.16.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.4" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0", "graphql-yoga": "^5.15.1" } }, "sha512-sL62lPhFd380eP8SrMbYhz2zaeegHBD3f0JBVHKOf8dJApnswAvRMo6UVfghwFfDFO5rEsEOIHGlIFiWDA79zg=="],
|
||||||
|
|
||||||
|
"@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="],
|
||||||
|
|
||||||
|
"@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||||
@ -112,6 +179,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
|
"@keyv/serialize": ["@keyv/serialize@1.1.0", "", {}, "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@15.4.3", "", {}, "sha512-lKJ9KJAvaWzqurIsz6NWdQOLj96mdhuDMusLSYHw9HBe2On7BjUwU1WeRvq19x7NrEK3iOgMeSBV5qEhVH1cMw=="],
|
"@next/env": ["@next/env@15.4.3", "", {}, "sha512-lKJ9KJAvaWzqurIsz6NWdQOLj96mdhuDMusLSYHw9HBe2On7BjUwU1WeRvq19x7NrEK3iOgMeSBV5qEhVH1cMw=="],
|
||||||
@ -142,6 +211,10 @@
|
|||||||
|
|
||||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||||
|
|
||||||
|
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
||||||
|
|
||||||
|
"@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="],
|
||||||
|
|
||||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||||
|
|
||||||
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
|
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
|
||||||
@ -180,18 +253,26 @@
|
|||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||||
|
|
||||||
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/graphql-depth-limit": ["@types/graphql-depth-limit@1.1.6", "", { "dependencies": { "graphql": "^14.5.3" } }, "sha512-WU4bjoKOzJ8CQE32Pbyq+YshTMcLJf2aJuvVtSLv1BQPwDUGa38m2Vr8GGxf0GZ0luCQcfxlhZeHKu6nmTBvrw=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||||
|
|
||||||
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
|
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||||
|
|
||||||
|
"@types/validator": ["@types/validator@13.15.2", "", {}, "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="],
|
||||||
@ -250,12 +331,26 @@
|
|||||||
|
|
||||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||||
|
|
||||||
|
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
||||||
|
|
||||||
|
"@whatwg-node/events": ["@whatwg-node/events@0.1.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ=="],
|
||||||
|
|
||||||
|
"@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.9", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.7.22", "urlpattern-polyfill": "^10.0.0" } }, "sha512-2TaXKmjy53cybNtaAtzbPOzwIPkjXbzvZcimnaJxQwYXKSC8iYnWoZOyT4+CFt8w0KDieg5J5dIMNzUrW/UZ5g=="],
|
||||||
|
|
||||||
|
"@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.7.22", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-h4GGjGF2vH3kGJ/fEOeg9Xfu4ncoyRwFcjGIxr/5dTBgZNVwq888byIsZ+XXRDJnNnRlzVVVQDcqrZpY2yctGA=="],
|
||||||
|
|
||||||
|
"@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="],
|
||||||
|
|
||||||
|
"@whatwg-node/server": ["@whatwg-node/server@0.10.10", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/fetch": "^0.10.8", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-GwpdMgUmwIp0jGjP535YtViP/nnmETAyHpGPWPZKdX++Qht/tSLbGXgFUMSsQvEACmZAR1lAPNu2CnYL1HpBgg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
@ -266,6 +361,8 @@
|
|||||||
|
|
||||||
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||||
|
|
||||||
|
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||||
|
|
||||||
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
|
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
|
||||||
|
|
||||||
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
||||||
@ -278,22 +375,32 @@
|
|||||||
|
|
||||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||||
|
|
||||||
|
"arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="],
|
||||||
|
|
||||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||||
|
|
||||||
|
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
|
||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
|
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="],
|
||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@2.0.0", "", {}, "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"cacheable": ["cacheable@1.10.3", "", { "dependencies": { "hookified": "^1.10.0", "keyv": "^5.4.0" } }, "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
@ -318,10 +425,24 @@
|
|||||||
|
|
||||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||||
|
|
||||||
|
"colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
|
||||||
|
|
||||||
|
"cross-inspect": ["cross-inspect@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
|
||||||
|
|
||||||
|
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
||||||
@ -340,16 +461,28 @@
|
|||||||
|
|
||||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||||
|
|
||||||
|
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
|
"dottie": ["dottie@2.0.6", "", {}, "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="],
|
||||||
|
|
||||||
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||||
|
|
||||||
|
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
|
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||||
|
|
||||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
@ -372,6 +505,8 @@
|
|||||||
|
|
||||||
"eslint-config-next": ["eslint-config-next@15.4.3", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-blytVMTpdqqlLBvYOvwT51m5eqRHNofKR/pfBSeeHiQMSY33kCph31hAK3DiAsL/RamVJRQzHwTRbbNr+7c/sw=="],
|
"eslint-config-next": ["eslint-config-next@15.4.3", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-blytVMTpdqqlLBvYOvwT51m5eqRHNofKR/pfBSeeHiQMSY33kCph31hAK3DiAsL/RamVJRQzHwTRbbNr+7c/sw=="],
|
||||||
|
|
||||||
|
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
||||||
|
|
||||||
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
|
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
|
||||||
@ -382,10 +517,14 @@
|
|||||||
|
|
||||||
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w=="],
|
||||||
|
|
||||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||||
|
|
||||||
|
"eslint-plugin-tailwindcss": ["eslint-plugin-tailwindcss@3.18.2", "", { "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4" }, "peerDependencies": { "tailwindcss": "^3.4.0" } }, "sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||||
@ -402,12 +541,18 @@
|
|||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||||
|
|
||||||
|
"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||||
@ -422,8 +567,12 @@
|
|||||||
|
|
||||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||||
@ -440,16 +589,34 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
|
"global-modules": ["global-modules@2.0.0", "", { "dependencies": { "global-prefix": "^3.0.0" } }, "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A=="],
|
||||||
|
|
||||||
|
"global-prefix": ["global-prefix@3.0.0", "", { "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } }, "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg=="],
|
||||||
|
|
||||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
|
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
||||||
|
|
||||||
|
"globjoin": ["globjoin@0.1.4", "", {}, "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg=="],
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
|
"graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="],
|
||||||
|
|
||||||
|
"graphql-depth-limit": ["graphql-depth-limit@1.1.0", "", { "dependencies": { "arrify": "^1.0.1" }, "peerDependencies": { "graphql": "*" } }, "sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw=="],
|
||||||
|
|
||||||
|
"graphql-request": ["graphql-request@7.2.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-0GR7eQHBFYz372u9lxS16cOtEekFlZYB2qOyq8wDvzRmdRSJ0mgUVX1tzNcIzk3G+4NY+mGtSz411wZdeDF/+A=="],
|
||||||
|
|
||||||
|
"graphql-tag": ["graphql-tag@2.12.6", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg=="],
|
||||||
|
|
||||||
|
"graphql-yoga": ["graphql-yoga@5.15.1", "", { "dependencies": { "@envelop/core": "^5.3.0", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.4.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.6.2", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.2.4", "@whatwg-node/server": "^0.10.5", "dset": "^3.1.4", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-wCSnviFFGC4CF9lyeRNMW1p55xVWkMRLPu9iHYbBd8WCJEjduDTo3nh91sVktpbJdUQ6rxNBN6hhpTYMFZuMwg=="],
|
||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
@ -464,17 +631,25 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"hookified": ["hookified@1.10.0", "", {}, "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA=="],
|
||||||
|
|
||||||
|
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
|
"inflection": ["inflection@1.13.4", "", {}, "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw=="],
|
||||||
|
|
||||||
|
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||||
|
|
||||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||||
|
|
||||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||||
|
|
||||||
@ -496,6 +671,8 @@
|
|||||||
|
|
||||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
|
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
|
||||||
|
|
||||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
@ -508,6 +685,8 @@
|
|||||||
|
|
||||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||||
|
|
||||||
|
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
|
||||||
|
|
||||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||||
|
|
||||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||||
@ -530,6 +709,8 @@
|
|||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"iterall": ["iterall@1.3.0", "", {}, "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="],
|
||||||
|
|
||||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.5.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-NWDAhdnATItTnRhip9VTd8oXDjVcbhetRN6YzckApnXGxpGUooKMAaf0KVvlZG0+KlJMGkeLElVn4M1ReuxKUQ=="],
|
"jiti": ["jiti@2.5.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-NWDAhdnATItTnRhip9VTd8oXDjVcbhetRN6YzckApnXGxpGUooKMAaf0KVvlZG0+KlJMGkeLElVn4M1ReuxKUQ=="],
|
||||||
@ -540,6 +721,8 @@
|
|||||||
|
|
||||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
@ -550,6 +733,10 @@
|
|||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
|
|
||||||
|
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
|
||||||
|
|
||||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||||
|
|
||||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||||
@ -578,20 +765,38 @@
|
|||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||||
|
|
||||||
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mathml-tag-names": ["mathml-tag-names@2.1.3", "", {}, "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg=="],
|
||||||
|
|
||||||
|
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||||
|
|
||||||
|
"meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
@ -602,6 +807,10 @@
|
|||||||
|
|
||||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||||
|
|
||||||
|
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||||
|
|
||||||
|
"moment-timezone": ["moment-timezone@0.5.48", "", { "dependencies": { "moment": "^2.29.4" } }, "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
@ -612,6 +821,8 @@
|
|||||||
|
|
||||||
"next": ["next@15.4.3", "", { "dependencies": { "@next/env": "15.4.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.3", "@next/swc-darwin-x64": "15.4.3", "@next/swc-linux-arm64-gnu": "15.4.3", "@next/swc-linux-arm64-musl": "15.4.3", "@next/swc-linux-x64-gnu": "15.4.3", "@next/swc-linux-x64-musl": "15.4.3", "@next/swc-win32-arm64-msvc": "15.4.3", "@next/swc-win32-x64-msvc": "15.4.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-uW7Qe6poVasNIE1X382nI29oxSdFJzjQzTgJFLD43MxyPfGKKxCMySllhBpvqr48f58Om+tLMivzRwBpXEytvA=="],
|
"next": ["next@15.4.3", "", { "dependencies": { "@next/env": "15.4.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.3", "@next/swc-darwin-x64": "15.4.3", "@next/swc-linux-arm64-gnu": "15.4.3", "@next/swc-linux-arm64-musl": "15.4.3", "@next/swc-linux-x64-gnu": "15.4.3", "@next/swc-linux-x64-musl": "15.4.3", "@next/swc-win32-arm64-msvc": "15.4.3", "@next/swc-win32-x64-msvc": "15.4.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-uW7Qe6poVasNIE1X382nI29oxSdFJzjQzTgJFLD43MxyPfGKKxCMySllhBpvqr48f58Om+tLMivzRwBpXEytvA=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
@ -638,24 +849,46 @@
|
|||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
|
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-resolve-nested-selector": ["postcss-resolve-nested-selector@0.1.6", "", {}, "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw=="],
|
||||||
|
|
||||||
|
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
|
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||||
|
|
||||||
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
@ -670,12 +903,16 @@
|
|||||||
|
|
||||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||||
|
|
||||||
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"retry-as-promised": ["retry-as-promised@7.1.1", "", {}, "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw=="],
|
||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
@ -690,6 +927,10 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"sequelize": ["sequelize@6.37.7", "", { "dependencies": { "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", "debug": "^4.3.4", "dottie": "^2.0.6", "inflection": "^1.13.4", "lodash": "^4.17.21", "moment": "^2.29.4", "moment-timezone": "^0.5.43", "pg-connection-string": "^2.6.1", "retry-as-promised": "^7.0.4", "semver": "^7.5.4", "sequelize-pool": "^7.1.0", "toposort-class": "^1.0.1", "uuid": "^8.3.2", "validator": "^13.9.0", "wkx": "^0.5.0" } }, "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA=="],
|
||||||
|
|
||||||
|
"sequelize-pool": ["sequelize-pool@7.1.0", "", {}, "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
@ -710,14 +951,22 @@
|
|||||||
|
|
||||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
|
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
|
"slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
|
|
||||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||||
@ -730,16 +979,36 @@
|
|||||||
|
|
||||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
|
"stylelint": ["stylelint@16.22.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.1.1", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.5", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-SVEMTdjKNV4ollUrIY9ordZ36zHv2/PHzPjfPMau370MlL2VYXeLgSNMMiEbLGRO8RmD2R8/BVUeF2DfnfkC0w=="],
|
||||||
|
|
||||||
|
"stylelint-config-prettier": ["stylelint-config-prettier@9.0.5", "", { "peerDependencies": { "stylelint": ">= 11.x < 15" }, "bin": { "stylelint-config-prettier": "bin/check.js", "stylelint-config-prettier-check": "bin/check.js" } }, "sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA=="],
|
||||||
|
|
||||||
|
"stylelint-config-recommended": ["stylelint-config-recommended@16.0.0", "", { "peerDependencies": { "stylelint": "^16.16.0" } }, "sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA=="],
|
||||||
|
|
||||||
|
"stylelint-config-standard": ["stylelint-config-standard@38.0.0", "", { "dependencies": { "stylelint-config-recommended": "^16.0.0" }, "peerDependencies": { "stylelint": "^16.18.0" } }, "sha512-uj3JIX+dpFseqd/DJx8Gy3PcRAJhlEZ2IrlFOc4LUxBX/PNMEQ198x7LCOE2Q5oT9Vw8nyc4CIL78xSqPr6iag=="],
|
||||||
|
|
||||||
|
"stylelint-config-tailwindcss": ["stylelint-config-tailwindcss@1.0.0", "", { "peerDependencies": { "stylelint": ">=13.13.1", "tailwindcss": ">=2.2.16" } }, "sha512-e6WUBJeLdOZ0sy8FZ1jk5Zy9iNGqqJbrMwnnV0Hpaw/yin6QO3gVv/zvyqSty8Yg6nEB5gqcyJbN387TPhEa7Q=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="],
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"svg-tags": ["svg-tags@1.0.0", "", {}, "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA=="],
|
||||||
|
|
||||||
|
"synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
|
||||||
|
|
||||||
|
"table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||||
@ -750,6 +1019,8 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"toposort-class": ["toposort-class@1.0.1", "", {}, "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||||
|
|
||||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||||
@ -776,6 +1047,14 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
|
|
||||||
|
"validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
@ -786,8 +1065,12 @@
|
|||||||
|
|
||||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||||
|
|
||||||
|
"wkx": ["wkx@0.5.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg=="],
|
||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
@ -796,6 +1079,8 @@
|
|||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
|
|
||||||
|
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||||
@ -808,36 +1093,64 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@types/graphql-depth-limit/graphql": ["graphql@14.7.0", "", { "dependencies": { "iterall": "^1.2.2" } }, "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"cacheable/keyv": ["keyv@5.4.0", "", { "dependencies": { "@keyv/serialize": "^1.1.0" } }, "sha512-TMckyVjEoacG5IteUpUrOBsFORtheqziVyyY2dLUwg1jwTb8u48LX4TgmtogkNl9Y9unaEJ1luj10fGyjMGFOQ=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|
||||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||||
|
|
||||||
|
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
|
||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
|
"sequelize/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||||
|
|
||||||
|
"string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"stylelint/file-entry-cache": ["file-entry-cache@10.1.3", "", { "dependencies": { "flat-cache": "^6.1.12" } }, "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg=="],
|
||||||
|
|
||||||
|
"stylelint/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
|
"table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
|
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"stylelint/file-entry-cache/flat-cache": ["flat-cache@6.1.12", "", { "dependencies": { "cacheable": "^1.10.3", "flatted": "^3.3.3", "hookified": "^1.10.0" } }, "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q=="],
|
||||||
|
|
||||||
|
"table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
docker-compose.yml
Normal file
64
docker-compose.yml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./initdb:/docker-entrypoint-initdb.d
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: logto
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: p0stgr3s
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d logto"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
logto:
|
||||||
|
image: svhd/logto:latest
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
- "3002:3002"
|
||||||
|
env_file:
|
||||||
|
- .env.logto
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nc -z localhost 3001 || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 120s
|
||||||
|
volumes:
|
||||||
|
- logto-data:/etc/logto
|
||||||
|
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer:latest
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: postgres
|
||||||
|
|
||||||
|
libretranslate:
|
||||||
|
image: libretranslate/libretranslate:latest
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
|
||||||
|
litellm:
|
||||||
|
image: ghcr.io/berriai/litellm:main-latest
|
||||||
|
env_file:
|
||||||
|
- .env.litellm
|
||||||
|
ports:
|
||||||
|
- 4000:4000
|
||||||
|
init: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
logto-data:
|
||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@ -10,7 +10,32 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
|
||||||
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
|
"no-debugger": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"dist/**",
|
||||||
|
"build/**",
|
||||||
|
"node_modules/**",
|
||||||
|
"*.config.js",
|
||||||
|
"*.config.mjs",
|
||||||
|
"*.config.ts",
|
||||||
|
".env*",
|
||||||
|
"coverage/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
1
general.d.ts
vendored
Normal file
1
general.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="./src/types/graphql.d.ts" />
|
||||||
7
initdb/create-multiple-dbs.sh
Normal file
7
initdb/create-multiple-dbs.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
|
||||||
|
CREATE DATABASE jaimes;
|
||||||
|
CREATE DATABASE litellm;
|
||||||
|
EOSQL
|
||||||
@ -2,6 +2,14 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
webpack: config => {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.(graphql|gql)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: "graphql-tag/loader",
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
46
package.json
46
package.json
@ -3,25 +3,57 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"lint:fix": "next lint --fix",
|
||||||
|
"lint:styles": "stylelint \"**/*.css\" \"**/*.scss\"",
|
||||||
|
"lint:styles:fix": "stylelint \"**/*.css\" \"**/*.scss\" --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"check-all": "bun run type-check && bun run lint && bun run lint:styles && bun run format:check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@graphql-yoga/plugin-csrf-prevention": "^3.15.1",
|
||||||
|
"@graphql-yoga/plugin-disable-introspection": "^2.16.1",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"graphql": "^16.11.0",
|
||||||
|
"graphql-depth-limit": "^1.1.0",
|
||||||
|
"graphql-request": "^7.2.0",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
|
"graphql-yoga": "^5.15.1",
|
||||||
|
"next": "15.4.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.4.3"
|
"sequelize": "^6.37.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/graphql-depth-limit": "^1.1.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||||
"tailwindcss": "^4",
|
"@typescript-eslint/parser": "^8.38.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.3",
|
"eslint-config-next": "15.4.3",
|
||||||
"@eslint/eslintrc": "^3"
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-prettier": "^5.5.3",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"stylelint": "^16.22.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
scripts/debug-roles.ts
Normal file
0
scripts/debug-roles.ts
Normal file
3
src/app/api/graphql/route.ts
Normal file
3
src/app/api/graphql/route.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { yoga } from "@/graphql";
|
||||||
|
|
||||||
|
export { yoga as GET, yoga as POST, yoga as OPTIONS };
|
||||||
114
src/app/callback/page.tsx
Normal file
114
src/app/callback/page.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function CallbackPage() {
|
||||||
|
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCallback = async () => {
|
||||||
|
try {
|
||||||
|
const code = searchParams.get("code");
|
||||||
|
const state = searchParams.get("state");
|
||||||
|
const error = searchParams.get("error");
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setStatus("error");
|
||||||
|
setMessage(`Erreur d'authentification: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code || !state) {
|
||||||
|
setStatus("error");
|
||||||
|
setMessage("Paramètres de callback manquants");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Décoder le state pour récupérer les informations du type d'authentification
|
||||||
|
const stateData = JSON.parse(decodeURIComponent(state));
|
||||||
|
const { type, connector } = stateData;
|
||||||
|
|
||||||
|
// Ici vous pouvez traiter le code d'autorisation pour obtenir les tokens
|
||||||
|
// et créer ou connecter l'utilisateur avec votre API
|
||||||
|
|
||||||
|
setStatus("success");
|
||||||
|
setMessage(
|
||||||
|
`Authentification ${type === "login" ? "connexion" : "inscription"} réussie avec le connecteur ${connector}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rediriger vers la page appropriée après un délai
|
||||||
|
setTimeout(() => {
|
||||||
|
if (type === "login") {
|
||||||
|
window.location.href = "/profile";
|
||||||
|
} else {
|
||||||
|
window.location.href = "/profile";
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors du traitement du callback:", err);
|
||||||
|
setStatus("error");
|
||||||
|
setMessage("Erreur lors du traitement de l'authentification");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCallback();
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div className="rounded-lg bg-white p-8 text-center shadow-lg">
|
||||||
|
{status === "loading" && (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto h-12 w-12 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
|
||||||
|
<h2 className="mt-4 text-xl font-semibold text-gray-900">Authentification en cours...</h2>
|
||||||
|
<p className="mt-2 text-gray-600">Veuillez patienter pendant que nous traitons votre authentification.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "success" && (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||||
|
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-4 text-xl font-semibold text-green-900">Authentification réussie !</h2>
|
||||||
|
<p className="mt-2 text-gray-600">{message}</p>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">Redirection en cours...</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "error" && (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||||
|
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-4 text-xl font-semibold text-red-900">Erreur d'authentification</h2>
|
||||||
|
<p className="mt-2 text-gray-600">{message}</p>
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
<a
|
||||||
|
href="/login"
|
||||||
|
className="inline-flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
Retour à la connexion
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/signup"
|
||||||
|
className="inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
Retour à l'inscription
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
621
src/app/connectors/page.tsx
Normal file
621
src/app/connectors/page.tsx
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const GET_CONNECTORS_QUERY = gql`
|
||||||
|
query GetConnectors {
|
||||||
|
connectors {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connectors {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
enabled
|
||||||
|
config
|
||||||
|
metadata {
|
||||||
|
id
|
||||||
|
target
|
||||||
|
platform
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
availableConnectors {
|
||||||
|
id
|
||||||
|
target
|
||||||
|
platform
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
configTemplate
|
||||||
|
formItems {
|
||||||
|
key
|
||||||
|
type
|
||||||
|
label
|
||||||
|
placeholder
|
||||||
|
required
|
||||||
|
defaultValue
|
||||||
|
description
|
||||||
|
}
|
||||||
|
isAdded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CREATE_CONNECTOR_MUTATION = gql`
|
||||||
|
mutation CreateConnector($input: CreateConnectorInput!) {
|
||||||
|
createConnector(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connector {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
enabled
|
||||||
|
metadata {
|
||||||
|
name
|
||||||
|
platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UPDATE_CONNECTOR_MUTATION = gql`
|
||||||
|
mutation UpdateConnector($input: UpdateConnectorInput!) {
|
||||||
|
updateConnector(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connector {
|
||||||
|
id
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DELETE_CONNECTOR_MUTATION = gql`
|
||||||
|
mutation DeleteConnector($id: ID!) {
|
||||||
|
deleteConnector(id: $id) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TOGGLE_CONNECTOR_MUTATION = gql`
|
||||||
|
mutation ToggleConnector($id: ID!, $enabled: Boolean!) {
|
||||||
|
toggleConnector(id: $id, enabled: $enabled) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connector {
|
||||||
|
id
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TEST_CONNECTOR_MUTATION = gql`
|
||||||
|
mutation TestConnector($id: ID!) {
|
||||||
|
testConnector(id: $id) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Connector {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
enabled: boolean;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
target: string;
|
||||||
|
platform?: string;
|
||||||
|
name: string | Record<string, string>;
|
||||||
|
description: string | Record<string, string>;
|
||||||
|
logo: string;
|
||||||
|
logoDark: string | null;
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FactoryConnector {
|
||||||
|
id: string;
|
||||||
|
target: string;
|
||||||
|
platform?: string;
|
||||||
|
name: string | Record<string, string>;
|
||||||
|
description: string | Record<string, string>;
|
||||||
|
logo: string;
|
||||||
|
logoDark: string | null;
|
||||||
|
configTemplate: Record<string, unknown> | null;
|
||||||
|
formItems: FormItem[];
|
||||||
|
isAdded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormItem {
|
||||||
|
key: string;
|
||||||
|
type: string;
|
||||||
|
label: string | Record<string, string>;
|
||||||
|
placeholder?: string | Record<string, string>;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: string | boolean | number | string[];
|
||||||
|
description?: string | Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectorsResponse {
|
||||||
|
connectors: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connectors?: Connector[];
|
||||||
|
availableConnectors?: FactoryConnector[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConnectorsPage() {
|
||||||
|
const [connectors, setConnectors] = useState<Connector[]>([]);
|
||||||
|
const [availableConnectors, setAvailableConnectors] = useState<FactoryConnector[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedFactory, setSelectedFactory] = useState<FactoryConnector | null>(null);
|
||||||
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [configForm, setConfigForm] = useState<Record<string, string | boolean | number | string[]>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConnectors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchConnectors = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) {
|
||||||
|
setError("Aucun token trouvé. Veuillez vous connecter.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authenticatedClient.request<ConnectorsResponse>(GET_CONNECTORS_QUERY);
|
||||||
|
|
||||||
|
if (response.connectors.success) {
|
||||||
|
setConnectors(response.connectors.connectors || []);
|
||||||
|
setAvailableConnectors(response.connectors.availableConnectors || []);
|
||||||
|
} else {
|
||||||
|
setError(response.connectors.message || "Erreur lors du chargement");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors du chargement des connecteurs:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateConnector = async () => {
|
||||||
|
if (!selectedFactory) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) {
|
||||||
|
setError("Aucun token trouvé. Veuillez vous connecter.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = (await authenticatedClient.request(CREATE_CONNECTOR_MUTATION, {
|
||||||
|
input: {
|
||||||
|
connectorId: selectedFactory.id,
|
||||||
|
config: configForm,
|
||||||
|
},
|
||||||
|
})) as {
|
||||||
|
createConnector: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connector?: {
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
metadata: {
|
||||||
|
name: string | Record<string, string>;
|
||||||
|
platform?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.createConnector.success) {
|
||||||
|
await fetchConnectors();
|
||||||
|
setShowConfig(false);
|
||||||
|
setSelectedFactory(null);
|
||||||
|
setConfigForm({});
|
||||||
|
} else {
|
||||||
|
setError(response.createConnector.message || "Erreur lors de la création");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleConnector = async (id: string, enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) return;
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await authenticatedClient.request(TOGGLE_CONNECTOR_MUTATION, {
|
||||||
|
id,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchConnectors();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error("Erreur lors du basculement:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConnector = async (id: string) => {
|
||||||
|
if (!confirm("Êtes-vous sûr de vouloir supprimer ce connecteur ?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) return;
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await authenticatedClient.request(DELETE_CONNECTOR_MUTATION, { id });
|
||||||
|
await fetchConnectors();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error("Erreur lors de la suppression:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnector = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) return;
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = (await authenticatedClient.request(TEST_CONNECTOR_MUTATION, { id })) as {
|
||||||
|
testConnector: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
alert(response.testConnector.message || "Test terminé");
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error("Erreur lors du test:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openConfigDialog = (factory: FactoryConnector) => {
|
||||||
|
setSelectedFactory(factory);
|
||||||
|
setShowConfig(true);
|
||||||
|
// Initialiser le formulaire avec les valeurs par défaut
|
||||||
|
const initialForm: Record<string, string | boolean | number | string[]> = {};
|
||||||
|
factory.formItems.forEach(item => {
|
||||||
|
if (item.defaultValue !== undefined) {
|
||||||
|
initialForm[item.key] = item.defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setConfigForm(initialForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectorName = (nameObj: string | Record<string, string>): string => {
|
||||||
|
if (typeof nameObj === "object" && nameObj !== null) {
|
||||||
|
return nameObj.en || nameObj.fr || Object.values(nameObj)[0] || "Connecteur";
|
||||||
|
}
|
||||||
|
return nameObj || "Connecteur";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectorDescription = (descObj: string | Record<string, string>): string => {
|
||||||
|
if (typeof descObj === "object" && descObj !== null) {
|
||||||
|
return descObj.en || descObj.fr || Object.values(descObj)[0] || "";
|
||||||
|
}
|
||||||
|
return descObj || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">Gestion des Connecteurs Logto</h1>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<p className="text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<p>Chargement des connecteurs...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Connecteurs configurés */}
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-gray-800">
|
||||||
|
Connecteurs configurés ({connectors.length})
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{connectors.length === 0 ? (
|
||||||
|
<p className="text-gray-500">Aucun connecteur configuré</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{connectors.map(connector => (
|
||||||
|
<div key={connector.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Image
|
||||||
|
src={connector.metadata.logo}
|
||||||
|
alt={getConnectorName(connector.metadata.name)}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-2 py-1 text-xs ${
|
||||||
|
connector.enabled ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{connector.enabled ? "Activé" : "Désactivé"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="mt-2 font-medium text-gray-900">{getConnectorName(connector.metadata.name)}</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600">{connector.metadata.platform || connector.type}</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleConnector(connector.id, !connector.enabled)}
|
||||||
|
className={`rounded px-3 py-1 text-xs ${
|
||||||
|
connector.enabled
|
||||||
|
? "bg-red-100 text-red-700 hover:bg-red-200"
|
||||||
|
: "bg-green-100 text-green-700 hover:bg-green-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{connector.enabled ? "Désactiver" : "Activer"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleTestConnector(connector.id)}
|
||||||
|
className="rounded bg-blue-100 px-3 py-1 text-xs text-blue-700 hover:bg-blue-200"
|
||||||
|
>
|
||||||
|
Tester
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteConnector(connector.id)}
|
||||||
|
className="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connecteurs disponibles */}
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-gray-800">Connecteurs disponibles</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{availableConnectors.map(factory => (
|
||||||
|
<div
|
||||||
|
key={factory.id}
|
||||||
|
className={`rounded-lg border p-4 shadow-sm ${
|
||||||
|
factory.isAdded ? "border-gray-300 bg-gray-50" : "border-gray-200 bg-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Image
|
||||||
|
src={factory.logo}
|
||||||
|
alt={getConnectorName(factory.name)}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="h-8 w-8"
|
||||||
|
/>
|
||||||
|
{factory.isAdded && (
|
||||||
|
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800">Ajouté</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="mt-2 font-medium text-gray-900">{getConnectorName(factory.name)}</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600">{getConnectorDescription(factory.description)}</p>
|
||||||
|
|
||||||
|
{!factory.isAdded && (
|
||||||
|
<button
|
||||||
|
onClick={() => openConfigDialog(factory)}
|
||||||
|
className="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Configurer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dialog de configuration */}
|
||||||
|
{showConfig && selectedFactory && (
|
||||||
|
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
||||||
|
<div className="max-h-[80vh] w-full max-w-md overflow-y-auto rounded-lg bg-white p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium">Configurer {getConnectorName(selectedFactory.name)}</h3>
|
||||||
|
<button onClick={() => setShowConfig(false)} className="text-gray-400 hover:text-gray-600">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCreateConnector();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{selectedFactory.formItems.map(item => (
|
||||||
|
<div key={item.key}>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{typeof item.label === "object" && item.label !== null
|
||||||
|
? item.label.en || item.label.fr
|
||||||
|
: item.label}
|
||||||
|
{item.required && <span className="text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{item.type === "Password" ? (
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required={item.required}
|
||||||
|
value={String(configForm[item.key] || "")}
|
||||||
|
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.value })}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
) : item.type === "Switch" ? (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(configForm[item.key])}
|
||||||
|
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.checked })}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
) : item.type === "MultiSelect" ? (
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
required={item.required}
|
||||||
|
value={Array.isArray(configForm[item.key]) ? (configForm[item.key] as string[]) : []}
|
||||||
|
onChange={e => {
|
||||||
|
const values = Array.from(e.target.selectedOptions, option => option.value);
|
||||||
|
setConfigForm({ ...configForm, [item.key]: values });
|
||||||
|
}}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
{/* Options would come from the form item configuration */}
|
||||||
|
</select>
|
||||||
|
) : item.type === "Number" ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
required={item.required}
|
||||||
|
value={Number(configForm[item.key] || 0)}
|
||||||
|
onChange={e => setConfigForm({ ...configForm, [item.key]: Number(e.target.value) })}
|
||||||
|
placeholder={
|
||||||
|
typeof item.placeholder === "object" && item.placeholder !== null
|
||||||
|
? item.placeholder.en || item.placeholder.fr
|
||||||
|
: item.placeholder || ""
|
||||||
|
}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required={item.required}
|
||||||
|
value={String(configForm[item.key] || "")}
|
||||||
|
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.value })}
|
||||||
|
placeholder={
|
||||||
|
typeof item.placeholder === "object" && item.placeholder !== null
|
||||||
|
? item.placeholder.en || item.placeholder.fr
|
||||||
|
: item.placeholder || ""
|
||||||
|
}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.description && (
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
{typeof item.description === "object" && item.description !== null
|
||||||
|
? item.description.en || item.description.fr
|
||||||
|
: item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex space-x-4 pt-4">
|
||||||
|
<button type="submit" className="flex-1 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
|
||||||
|
Créer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfig(false)}
|
||||||
|
className="flex-1 rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Page de connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
📝 Page d'inscription
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Voir votre profil
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,11 +24,7 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
402
src/app/login/page.tsx
Normal file
402
src/app/login/page.tsx
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
const LOGIN_MUTATION = gql`
|
||||||
|
mutation Login($input: LoginInput!) {
|
||||||
|
login(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
primaryPhone
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
accessToken
|
||||||
|
tokenExpiry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_SOCIAL_CONNECTORS_QUERY = gql`
|
||||||
|
query GetSocialConnectors {
|
||||||
|
connectors {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connectors {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
enabled
|
||||||
|
target
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
platform
|
||||||
|
metadata {
|
||||||
|
id
|
||||||
|
target
|
||||||
|
platform
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
login: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
user?: User;
|
||||||
|
accessToken?: string;
|
||||||
|
tokenExpiry?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocialConnector {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
// Propriétés disponibles au niveau racine
|
||||||
|
target?: string;
|
||||||
|
name?: string | Record<string, string>;
|
||||||
|
description?: string | Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
|
platform?: string;
|
||||||
|
|
||||||
|
// Metadata (pour compatibilité)
|
||||||
|
metadata: {
|
||||||
|
id?: string;
|
||||||
|
target?: string;
|
||||||
|
platform?: string;
|
||||||
|
name?: string | Record<string, string>;
|
||||||
|
description?: string | Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectorsResponse {
|
||||||
|
connectors: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connectors?: SocialConnector[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
primaryEmail: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [result, setResult] = useState<LoginResponse["login"] | null>(null);
|
||||||
|
const [socialConnectors, setSocialConnectors] = useState<SocialConnector[]>([]);
|
||||||
|
const [connectorsLoading, setConnectorsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSocialConnectors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSocialConnectors = async () => {
|
||||||
|
setConnectorsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<ConnectorsResponse>(GET_SOCIAL_CONNECTORS_QUERY);
|
||||||
|
if (response.connectors.success && response.connectors.connectors) {
|
||||||
|
// Filtrer uniquement les connecteurs sociaux activés
|
||||||
|
const socialConnectors = response.connectors.connectors.filter(
|
||||||
|
connector => connector.type === "Social" && connector.enabled !== false
|
||||||
|
);
|
||||||
|
|
||||||
|
setSocialConnectors(socialConnectors);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors du chargement des connecteurs sociaux:", err);
|
||||||
|
} finally {
|
||||||
|
setConnectorsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour obtenir le nom d'affichage du connecteur
|
||||||
|
const getDisplayName = (connector: SocialConnector): string => {
|
||||||
|
// Essayer d'obtenir le nom depuis les propriétés racine ou metadata
|
||||||
|
const nameSource = connector.name || connector.metadata?.name;
|
||||||
|
if (nameSource) {
|
||||||
|
if (typeof nameSource === "object" && nameSource !== null) {
|
||||||
|
const name = nameSource.en || nameSource.fr || Object.values(nameSource)[0];
|
||||||
|
if (name && name.trim()) return name;
|
||||||
|
}
|
||||||
|
if (typeof nameSource === "string" && nameSource.trim()) {
|
||||||
|
return nameSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser target avec une capitalisation propre
|
||||||
|
const target = connector.target || connector.metadata?.target;
|
||||||
|
if (target && target.trim()) {
|
||||||
|
return target.charAt(0).toUpperCase() + target.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Connecteur Social";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSocialLogin = (connectorId: string, _platform: string) => {
|
||||||
|
// Rediriger vers l'URL d'authentification sociale de Logto
|
||||||
|
const socialLoginUrl = `${process.env.NEXT_PUBLIC_LOGTO_ENDPOINT}/oidc/auth?client_id=${process.env.NEXT_PUBLIC_LOGTO_APP_ID}&response_type=code&scope=openid%20profile%20email&redirect_uri=${encodeURIComponent(window.location.origin + "/callback")}&state=${encodeURIComponent(JSON.stringify({ type: "login", connector: connectorId }))}&prompt=consent&connector_id=${connectorId}`;
|
||||||
|
window.location.href = socialLoginUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<LoginResponse>(LOGIN_MUTATION, {
|
||||||
|
input: {
|
||||||
|
primaryEmail: formData.primaryEmail,
|
||||||
|
password: formData.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setResult(response.login);
|
||||||
|
|
||||||
|
// Sauvegarder automatiquement le token d'accès dans localStorage
|
||||||
|
if (response.login.success && response.login.accessToken) {
|
||||||
|
localStorage.setItem("accessToken", response.login.accessToken);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors de la connexion:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-md">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">Connexion</h1>
|
||||||
|
|
||||||
|
{/* Connecteurs sociaux */}
|
||||||
|
{!connectorsLoading && socialConnectors.length > 0 && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="bg-white px-2 text-gray-500">Connexion avec</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{socialConnectors.map(connector => (
|
||||||
|
<button
|
||||||
|
key={connector.id}
|
||||||
|
onClick={() =>
|
||||||
|
handleSocialLogin(connector.id, connector.platform || connector.metadata?.platform || "")
|
||||||
|
}
|
||||||
|
className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
{(connector.logo || connector.metadata?.logo) && (
|
||||||
|
<Image
|
||||||
|
src={connector.logo || connector.metadata?.logo || ""}
|
||||||
|
alt={getDisplayName(connector)}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
Continuer avec {getDisplayName(connector)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="bg-white px-2 text-gray-500">Ou continuer avec email</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="primaryEmail" className="block text-sm font-medium text-gray-700">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="primaryEmail"
|
||||||
|
name="primaryEmail"
|
||||||
|
required
|
||||||
|
value={formData.primaryEmail}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Mot de passe *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Connexion en cours..." : "Se connecter"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Affichage des erreurs */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Erreur de connexion</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Affichage des résultats */}
|
||||||
|
{result && (
|
||||||
|
<div
|
||||||
|
className={`mt-6 rounded-md border p-4 ${result.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"}`}
|
||||||
|
>
|
||||||
|
<h3 className={`text-sm font-medium ${result.success ? "text-green-800" : "text-red-800"}`}>
|
||||||
|
Résultat de la connexion
|
||||||
|
</h3>
|
||||||
|
<div className={`mt-2 text-sm ${result.success ? "text-green-700" : "text-red-700"}`}>
|
||||||
|
<p>
|
||||||
|
<strong>Succès:</strong> {result.success ? "Oui" : "Non"}
|
||||||
|
</p>
|
||||||
|
{result.message && (
|
||||||
|
<p>
|
||||||
|
<strong>Message:</strong> {result.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result.accessToken && (
|
||||||
|
<div className="mt-2 rounded border border-blue-200 bg-blue-50 p-3">
|
||||||
|
<p className="font-medium text-blue-800">🔑 Token d'accès créé et sauvegardé !</p>
|
||||||
|
<p className="mt-1 text-xs break-all text-blue-600">
|
||||||
|
<strong>Token:</strong> {result.accessToken.substring(0, 50)}...
|
||||||
|
</p>
|
||||||
|
{result.tokenExpiry && (
|
||||||
|
<p className="text-xs text-blue-600">
|
||||||
|
<strong>Expire le:</strong> {new Date(parseInt(result.tokenExpiry)).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-xs font-medium text-green-600">
|
||||||
|
✅ Token automatiquement sauvegardé dans votre navigateur
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<a
|
||||||
|
href="/profile"
|
||||||
|
className="inline-flex items-center text-xs text-blue-600 underline hover:text-blue-500"
|
||||||
|
>
|
||||||
|
🚀 Voir votre profil (token utilisé automatiquement) →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.user && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p>
|
||||||
|
<strong>Utilisateur connecté:</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="ml-4 list-inside list-disc">
|
||||||
|
<li>ID: {result.user.id}</li>
|
||||||
|
<li>Email: {result.user.primaryEmail}</li>
|
||||||
|
{result.user.username && <li>Username: {result.user.username}</li>}
|
||||||
|
{result.user.name && <li>Nom: {result.user.name}</li>}
|
||||||
|
{result.user.primaryPhone && <li>Téléphone: {result.user.primaryPhone}</li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
📝 Page d'inscription
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Voir votre profil
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/organizations" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🏢 Organisations
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
src/app/logout/page.tsx
Normal file
183
src/app/logout/page.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const LOGOUT_MUTATION = gql`
|
||||||
|
mutation Logout {
|
||||||
|
logout {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface LogoutResponse {
|
||||||
|
logout: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogoutPage() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [result, setResult] = useState<LogoutResponse["logout"] | null>(null);
|
||||||
|
const [hasToken, setHasToken] = useState(false);
|
||||||
|
|
||||||
|
// Vérifier si un token existe
|
||||||
|
useEffect(() => {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
setHasToken(!!storedToken);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) {
|
||||||
|
setError("Aucun token trouvé. Vous n'êtes pas connecté.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un client GraphQL avec le token d'authentification
|
||||||
|
const graphqlUrl = process.env.NEXT_PUBLIC_APP_URL
|
||||||
|
? `${process.env.NEXT_PUBLIC_APP_URL}/api/graphql`
|
||||||
|
: window.location.origin + "/api/graphql";
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(graphqlUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${storedToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authenticatedClient.request<LogoutResponse>(LOGOUT_MUTATION);
|
||||||
|
setResult(response.logout);
|
||||||
|
|
||||||
|
// Supprimer le token du localStorage
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
setHasToken(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors de la déconnexion:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLocalData = () => {
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
setHasToken(false);
|
||||||
|
setResult({ success: true, message: "Données locales supprimées" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-md">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">Déconnexion</h1>
|
||||||
|
|
||||||
|
{/* Statut de connexion */}
|
||||||
|
<div
|
||||||
|
className={`mb-6 rounded-md border p-4 ${hasToken ? "border-blue-200 bg-blue-50" : "border-gray-200 bg-gray-50"}`}
|
||||||
|
>
|
||||||
|
<h3 className={`text-sm font-medium ${hasToken ? "text-blue-800" : "text-gray-800"}`}>
|
||||||
|
{hasToken ? "✅ Connecté" : "❌ Non connecté"}
|
||||||
|
</h3>
|
||||||
|
<p className={`mt-2 text-sm ${hasToken ? "text-blue-700" : "text-gray-700"}`}>
|
||||||
|
{hasToken
|
||||||
|
? "Vous avez un token d'accès actif. Vous pouvez vous déconnecter."
|
||||||
|
: "Aucun token trouvé. Vous n'êtes pas connecté."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Boutons d'action */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{hasToken && (
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Déconnexion en cours..." : "Se déconnecter"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={clearLocalData}
|
||||||
|
className="w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
Effacer les données locales
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Affichage des erreurs */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Erreur de déconnexion</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Affichage des résultats */}
|
||||||
|
{result && (
|
||||||
|
<div
|
||||||
|
className={`mt-6 rounded-md border p-4 ${result.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"}`}
|
||||||
|
>
|
||||||
|
<h3 className={`text-sm font-medium ${result.success ? "text-green-800" : "text-red-800"}`}>
|
||||||
|
Résultat de la déconnexion
|
||||||
|
</h3>
|
||||||
|
<div className={`mt-2 text-sm ${result.success ? "text-green-700" : "text-red-700"}`}>
|
||||||
|
<p>
|
||||||
|
<strong>Succès:</strong> {result.success ? "Oui" : "Non"}
|
||||||
|
</p>
|
||||||
|
{result.message && (
|
||||||
|
<p>
|
||||||
|
<strong>Message:</strong> {result.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result.success && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-green-600">✅ Token supprimé du cache serveur et du navigateur</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Page de connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
📝 Page d'inscription
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Voir votre profil
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
757
src/app/organizations/page.tsx
Normal file
757
src/app/organizations/page.tsx
Normal file
@ -0,0 +1,757 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
const GET_ORGANIZATIONS_QUERY = gql`
|
||||||
|
query GetOrganizations($page: Int, $pageSize: Int) {
|
||||||
|
organizations(page: $page, pageSize: $pageSize) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
totalCount
|
||||||
|
organizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
customData
|
||||||
|
isMfaRequired
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_MY_ORGANIZATIONS_QUERY = gql`
|
||||||
|
query GetMyOrganizations($page: Int, $pageSize: Int) {
|
||||||
|
myOrganizations(page: $page, pageSize: $pageSize) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
totalCount
|
||||||
|
organizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
customData
|
||||||
|
isMfaRequired
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CREATE_ORGANIZATION_MUTATION = gql`
|
||||||
|
mutation CreateOrganization($input: CreateOrganizationInput!) {
|
||||||
|
createOrganization(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
customData
|
||||||
|
isMfaRequired
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_ORGANIZATION_USERS_QUERY = gql`
|
||||||
|
query GetOrganizationUsers($organizationId: ID!, $page: Int, $pageSize: Int) {
|
||||||
|
organizationUsers(organizationId: $organizationId, page: $page, pageSize: $pageSize) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
totalCount
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
organizationRoles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_ORGANIZATION_ROLES_QUERY = gql`
|
||||||
|
query GetOrganizationRoles($organizationId: ID!, $page: Int, $pageSize: Int) {
|
||||||
|
organizationRoles(organizationId: $organizationId, page: $page, pageSize: $pageSize) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
totalCount
|
||||||
|
roles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TEST_ORGANIZATIONS_API_QUERY = gql`
|
||||||
|
query TestOrganizationsAPI {
|
||||||
|
testOrganizationsAPI {
|
||||||
|
available
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Organization {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
isMfaRequired?: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationUser {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
organizationRoles: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationRole {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationsResponse {
|
||||||
|
organizations: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
totalCount?: number;
|
||||||
|
organizations?: Organization[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MyOrganizationsResponse {
|
||||||
|
myOrganizations: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
totalCount?: number;
|
||||||
|
organizations?: Organization[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateOrganizationResponse {
|
||||||
|
createOrganization: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
organization?: Organization;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationUsersResponse {
|
||||||
|
organizationUsers: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
totalCount?: number;
|
||||||
|
users?: OrganizationUser[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationRolesResponse {
|
||||||
|
organizationRoles: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
totalCount?: number;
|
||||||
|
roles?: OrganizationRole[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestOrganizationsAPIResponse {
|
||||||
|
testOrganizationsAPI: {
|
||||||
|
available: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrganizationsTestPage() {
|
||||||
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
|
const [myOrganizations, setMyOrganizations] = useState<Organization[]>([]);
|
||||||
|
const [selectedOrganization, setSelectedOrganization] = useState<Organization | null>(null);
|
||||||
|
const [organizationUsers, setOrganizationUsers] = useState<OrganizationUser[]>([]);
|
||||||
|
const [organizationRoles, setOrganizationRoles] = useState<OrganizationRole[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<"all" | "mine" | "details">("all");
|
||||||
|
|
||||||
|
// Form data for creating organization
|
||||||
|
const [createFormData, setCreateFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
isMfaRequired: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchAllOrganizations = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<OrganizationsResponse>(GET_ORGANIZATIONS_QUERY, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.organizations.success) {
|
||||||
|
const orgs = response.organizations.organizations || [];
|
||||||
|
setOrganizations(orgs);
|
||||||
|
} else {
|
||||||
|
setError(response.organizations.message || "Erreur lors du chargement des organisations");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Récupérer le token depuis localStorage
|
||||||
|
const token = localStorage.getItem("accessToken");
|
||||||
|
setAccessToken(token);
|
||||||
|
|
||||||
|
// Auto-loading des organisations au démarrage de la page
|
||||||
|
const loadOrganizations = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<OrganizationsResponse>(GET_ORGANIZATIONS_QUERY, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.organizations.success) {
|
||||||
|
const orgs = response.organizations.organizations || [];
|
||||||
|
setOrganizations(orgs);
|
||||||
|
} else {
|
||||||
|
setError(response.organizations.message || "Erreur lors du chargement des organisations");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadOrganizations();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const testOrganizationsAPI = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<TestOrganizationsAPIResponse>(TEST_ORGANIZATIONS_API_QUERY);
|
||||||
|
|
||||||
|
if (response.testOrganizationsAPI.available) {
|
||||||
|
// Si l'API est disponible, on peut essayer de charger les organisations
|
||||||
|
setError(null);
|
||||||
|
// On ne recharge pas automatiquement, on laisse l'utilisateur choisir
|
||||||
|
alert(
|
||||||
|
`✅ ${response.testOrganizationsAPI.message}\n\nVous pouvez maintenant utiliser les fonctionnalités d'organisations.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setError(response.testOrganizationsAPI.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue lors du test");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMyOrganizations = async (token: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const clientWithAuth = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await clientWithAuth.request<MyOrganizationsResponse>(GET_MY_ORGANIZATIONS_QUERY, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.myOrganizations.success) {
|
||||||
|
setMyOrganizations(response.myOrganizations.organizations || []);
|
||||||
|
} else {
|
||||||
|
setError(response.myOrganizations.message || "Erreur lors du chargement de vos organisations");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOrganizationDetails = async (organization: Organization) => {
|
||||||
|
setSelectedOrganization(organization);
|
||||||
|
setActiveTab("details");
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer les utilisateurs
|
||||||
|
const usersResponse = await graphQLClient.request<OrganizationUsersResponse>(GET_ORGANIZATION_USERS_QUERY, {
|
||||||
|
organizationId: organization.id,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (usersResponse.organizationUsers.success) {
|
||||||
|
setOrganizationUsers(usersResponse.organizationUsers.users || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les rôles
|
||||||
|
const rolesResponse = await graphQLClient.request<OrganizationRolesResponse>(GET_ORGANIZATION_ROLES_QUERY, {
|
||||||
|
organizationId: organization.id,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rolesResponse.organizationRoles.success) {
|
||||||
|
setOrganizationRoles(rolesResponse.organizationRoles.roles || []);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateOrganization = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validation simple côté client
|
||||||
|
if (!createFormData.name.trim()) {
|
||||||
|
setError("Le nom de l'organisation est requis");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreateLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<CreateOrganizationResponse>(CREATE_ORGANIZATION_MUTATION, {
|
||||||
|
input: {
|
||||||
|
name: createFormData.name,
|
||||||
|
description: createFormData.description || undefined,
|
||||||
|
isMfaRequired: createFormData.isMfaRequired,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.createOrganization.success) {
|
||||||
|
alert("Organisation créée avec succès!");
|
||||||
|
setCreateFormData({ name: "", description: "", isMfaRequired: false });
|
||||||
|
// Recharger la liste si on est sur l'onglet "all"
|
||||||
|
if (activeTab === "all") {
|
||||||
|
await fetchAllOrganizations();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(response.createOrganization.message || "Erreur lors de la création");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||||||
|
} finally {
|
||||||
|
setCreateLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-3xl font-bold text-gray-900">🏢 Test des Organisations Logto</h1>
|
||||||
|
|
||||||
|
{/* Info Section */}
|
||||||
|
<div className="mb-6 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">ℹ️ À propos des organisations Logto</h3>
|
||||||
|
<div className="mt-2 text-sm text-blue-700">
|
||||||
|
<p>
|
||||||
|
Les organisations permettent de gérer des environnements multi-tenant (B2B/SaaS). Cette fonctionnalité
|
||||||
|
peut nécessiter :
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 ml-4 list-disc space-y-1">
|
||||||
|
<li>Un plan Logto qui inclut les organisations</li>
|
||||||
|
<li>L'activation dans votre console Logto</li>
|
||||||
|
<li>La configuration d'un template d'organisation</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2">
|
||||||
|
<a
|
||||||
|
href="https://docs.logto.io/docs/recipes/organizations/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:no-underline"
|
||||||
|
>
|
||||||
|
📚 Documentation Logto sur les organisations
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Tabs */}
|
||||||
|
<div className="mb-6 border-b border-gray-200">
|
||||||
|
<nav className="-mb-px flex space-x-8">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("all");
|
||||||
|
fetchAllOrganizations();
|
||||||
|
}}
|
||||||
|
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||||||
|
activeTab === "all"
|
||||||
|
? "border-blue-500 text-blue-600"
|
||||||
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Toutes les organisations
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("mine");
|
||||||
|
if (accessToken) {
|
||||||
|
fetchMyOrganizations(accessToken);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||||||
|
activeTab === "mine"
|
||||||
|
? "border-blue-500 text-blue-600"
|
||||||
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
disabled={!accessToken || loading}
|
||||||
|
>
|
||||||
|
Mes organisations
|
||||||
|
{!accessToken && " (Connexion requise)"}
|
||||||
|
</button>
|
||||||
|
{selectedOrganization && (
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("details")}
|
||||||
|
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||||||
|
activeTab === "details"
|
||||||
|
? "border-blue-500 text-blue-600"
|
||||||
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Détails: {selectedOrganization.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Test Section */}
|
||||||
|
<div className="mb-8 rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<h3 className="mb-4 text-lg font-medium text-gray-800">🔬 Test de l'API des organisations</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={testOrganizationsAPI}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "⏳ Test..." : "Tester l'API des organisations"}
|
||||||
|
</button>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Cliquez pour vérifier si l'API des organisations est disponible sur votre instance Logto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Organization Form */}
|
||||||
|
<div className="mb-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="mb-4 text-lg font-medium text-blue-800">Créer une nouvelle organisation</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateOrganization} className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
|
Nom (requis)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
value={createFormData.name}
|
||||||
|
onChange={e => setCreateFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
placeholder="Nom de l'organisation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
value={createFormData.description}
|
||||||
|
onChange={e => setCreateFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
placeholder="Description de l'organisation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={createFormData.isMfaRequired}
|
||||||
|
onChange={e => setCreateFormData(prev => ({ ...prev, isMfaRequired: e.target.checked }))}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">MFA requis</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={createLoading}
|
||||||
|
className="ml-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{createLoading ? "⏳ Création..." : "Créer"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Erreur</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
|
||||||
|
{error.includes("n'est pas disponible") && (
|
||||||
|
<div className="mt-4 rounded border border-yellow-200 bg-yellow-50 p-3">
|
||||||
|
<h4 className="text-sm font-medium text-yellow-800">💡 Comment activer les organisations</h4>
|
||||||
|
<div className="mt-2 text-sm text-yellow-700">
|
||||||
|
<p>Les organisations Logto peuvent nécessiter :</p>
|
||||||
|
<ul className="mt-1 ml-4 list-disc space-y-1">
|
||||||
|
<li>Un plan Logto qui inclut cette fonctionnalité</li>
|
||||||
|
<li>L'activation des organisations dans votre console Logto</li>
|
||||||
|
<li>La configuration d'un template d'organisation</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2">
|
||||||
|
Consultez la{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.logto.io/docs/recipes/organizations/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:no-underline"
|
||||||
|
>
|
||||||
|
documentation Logto sur les organisations
|
||||||
|
</a>{" "}
|
||||||
|
pour plus d'informations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading */}
|
||||||
|
{loading && (
|
||||||
|
<div className="mb-6 text-center">
|
||||||
|
<p className="text-gray-600">Chargement...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content based on active tab */}
|
||||||
|
{activeTab === "all" && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-gray-900">
|
||||||
|
Toutes les organisations ({organizations.length})
|
||||||
|
</h2>
|
||||||
|
{organizations.length === 0 ? (
|
||||||
|
<p className="text-gray-600">Aucune organisation trouvée.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{organizations.map(org => (
|
||||||
|
<div
|
||||||
|
key={org.id}
|
||||||
|
className="cursor-pointer rounded-lg border border-gray-200 p-4 hover:bg-gray-50"
|
||||||
|
onClick={() => fetchOrganizationDetails(org)}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900">{org.name}</h3>
|
||||||
|
{org.description && <p className="mt-1 text-sm text-gray-600">{org.description}</p>}
|
||||||
|
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span>ID: {org.id.substring(0, 8)}...</span>
|
||||||
|
{org.isMfaRequired && (
|
||||||
|
<span className="rounded bg-yellow-100 px-2 py-1 text-yellow-800">MFA</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
Créée: {new Date(parseInt(org.createdAt)).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "mine" && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-gray-900">Mes organisations ({myOrganizations.length})</h2>
|
||||||
|
{!accessToken ? (
|
||||||
|
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-4">
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
Veuillez vous connecter pour voir vos organisations.{" "}
|
||||||
|
<a href="/login" className="underline hover:no-underline">
|
||||||
|
Se connecter
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : myOrganizations.length === 0 ? (
|
||||||
|
<p className="text-gray-600">Vous n'êtes membre d'aucune organisation.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{myOrganizations.map(org => (
|
||||||
|
<div
|
||||||
|
key={org.id}
|
||||||
|
className="cursor-pointer rounded-lg border border-gray-200 p-4 hover:bg-gray-50"
|
||||||
|
onClick={() => fetchOrganizationDetails(org)}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900">{org.name}</h3>
|
||||||
|
{org.description && <p className="mt-1 text-sm text-gray-600">{org.description}</p>}
|
||||||
|
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span>ID: {org.id.substring(0, 8)}...</span>
|
||||||
|
{org.isMfaRequired && (
|
||||||
|
<span className="rounded bg-yellow-100 px-2 py-1 text-yellow-800">MFA</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "details" && selectedOrganization && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-gray-900">Détails: {selectedOrganization.name}</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Organization Users */}
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-3 font-medium text-gray-900">Utilisateurs ({organizationUsers.length})</h3>
|
||||||
|
{organizationUsers.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-600">Aucun utilisateur.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{organizationUsers.map(user => (
|
||||||
|
<div key={user.id} className="rounded bg-gray-50 p-3">
|
||||||
|
<p className="text-sm font-medium">{user.name || user.username || "Utilisateur"}</p>
|
||||||
|
{user.primaryEmail && <p className="text-xs text-gray-600">{user.primaryEmail}</p>}
|
||||||
|
{user.organizationRoles.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{user.organizationRoles.map(role => (
|
||||||
|
<span key={role.id} className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800">
|
||||||
|
{role.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organization Roles */}
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-3 font-medium text-gray-900">Rôles ({organizationRoles.length})</h3>
|
||||||
|
{organizationRoles.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-600">Aucun rôle défini.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{organizationRoles.map(role => (
|
||||||
|
<div key={role.id} className="rounded bg-gray-50 p-3">
|
||||||
|
<p className="text-sm font-medium">{role.name}</p>
|
||||||
|
{role.description && <p className="text-xs text-gray-600">{role.description}</p>}
|
||||||
|
<span className="mt-1 inline-block rounded bg-gray-200 px-2 py-1 text-xs text-gray-700">
|
||||||
|
{role.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organization Info */}
|
||||||
|
<div className="mt-6 rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-3 font-medium text-gray-900">Informations</h3>
|
||||||
|
<dl className="grid grid-cols-1 gap-2 text-sm md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">ID:</dt>
|
||||||
|
<dd className="text-gray-600">{selectedOrganization.id}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">Nom:</dt>
|
||||||
|
<dd className="text-gray-600">{selectedOrganization.name}</dd>
|
||||||
|
</div>
|
||||||
|
{selectedOrganization.description && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<dt className="font-medium text-gray-700">Description:</dt>
|
||||||
|
<dd className="text-gray-600">{selectedOrganization.description}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">MFA requis:</dt>
|
||||||
|
<dd className="text-gray-600">{selectedOrganization.isMfaRequired ? "Oui" : "Non"}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">Créée le:</dt>
|
||||||
|
<dd className="text-gray-600">
|
||||||
|
{new Date(parseInt(selectedOrganization.createdAt)).toLocaleString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Se connecter
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Profil utilisateur
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,47 +2,32 @@ import Image from "next/image";
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
<div className="grid min-h-screen grid-rows-[20px_1fr_20px] items-center justify-items-center gap-16 p-8 pb-20 font-sans sm:p-20">
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
<main className="row-start-2 flex flex-col items-center gap-[32px] sm:items-start">
|
||||||
<Image
|
<Image className="dark:invert" src="/next.svg" alt="Next.js logo" width={180} height={38} priority />
|
||||||
className="dark:invert"
|
<ol className="list-inside list-decimal text-center font-mono text-sm/6 sm:text-left">
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
<li className="mb-2 tracking-[-.01em]">
|
||||||
Get started by editing{" "}
|
Get started by editing{" "}
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
<code className="rounded bg-black/[.05] px-1 py-0.5 font-mono font-semibold dark:bg-white/[.06]">
|
||||||
src/app/page.tsx
|
src/app/page.tsx
|
||||||
</code>
|
</code>
|
||||||
.
|
.
|
||||||
</li>
|
</li>
|
||||||
<li className="tracking-[-.01em]">
|
<li className="tracking-[-.01em]">Save and see your changes instantly.</li>
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
<div className="flex flex-col items-center gap-4 sm:flex-row">
|
||||||
<a
|
<a
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
className="bg-foreground text-background flex h-10 items-center justify-center gap-2 rounded-full border border-solid border-transparent px-4 text-sm font-medium transition-colors hover:bg-[#383838] sm:h-12 sm:w-auto sm:px-5 sm:text-base dark:hover:bg-[#ccc]"
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image className="dark:invert" src="/vercel.svg" alt="Vercel logomark" width={20} height={20} />
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
Deploy now
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
className="flex h-10 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-4 text-sm font-medium transition-colors hover:border-transparent hover:bg-[#f2f2f2] sm:h-12 sm:w-auto sm:px-5 sm:text-base md:w-[158px] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@ -51,20 +36,14 @@ export default function Home() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
<footer className="row-start-3 flex flex-wrap items-center justify-center gap-[24px]">
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src="/file.svg" alt="File icon" width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
Learn
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@ -73,13 +52,7 @@ export default function Home() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src="/window.svg" alt="Window icon" width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
Examples
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@ -88,13 +61,7 @@ export default function Home() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
Go to nextjs.org →
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
308
src/app/profile/page.tsx
Normal file
308
src/app/profile/page.tsx
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const GET_ME_QUERY = gql`
|
||||||
|
query GetMe {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
primaryPhone
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_CACHE_STATS_QUERY = gql`
|
||||||
|
query GetPATCacheStats {
|
||||||
|
patCacheStats {
|
||||||
|
cacheSize
|
||||||
|
totalEntries
|
||||||
|
timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetMeResponse {
|
||||||
|
me: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheStats {
|
||||||
|
cacheSize: number;
|
||||||
|
totalEntries: number;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCacheStatsResponse {
|
||||||
|
patCacheStats: CacheStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [hasToken, setHasToken] = useState(false);
|
||||||
|
const [cacheStats, setCacheStats] = useState<CacheStats | null>(null);
|
||||||
|
|
||||||
|
// Charger automatiquement le token depuis localStorage au montage du composant
|
||||||
|
useEffect(() => {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (storedToken) {
|
||||||
|
setHasToken(true);
|
||||||
|
// Charger automatiquement le profil si un token est disponible
|
||||||
|
fetchProfileWithToken(storedToken);
|
||||||
|
} else {
|
||||||
|
setError("Aucun token trouvé. Veuillez d'abord vous inscrire pour obtenir un token d'accès.");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchProfileWithToken = async (token: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setUser(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Créer un client GraphQL avec le token d'authentification
|
||||||
|
const graphqlUrl = process.env.NEXT_PUBLIC_APP_URL
|
||||||
|
? `${process.env.NEXT_PUBLIC_APP_URL}/api/graphql`
|
||||||
|
: window.location.origin + "/api/graphql";
|
||||||
|
|
||||||
|
const authenticatedClient = new GraphQLClient(graphqlUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authenticatedClient.request<GetMeResponse>(GET_ME_QUERY);
|
||||||
|
setUser(response.me);
|
||||||
|
|
||||||
|
// Récupérer les statistiques du cache
|
||||||
|
const statsResponse = await authenticatedClient.request<GetCacheStatsResponse>(GET_CACHE_STATS_QUERY);
|
||||||
|
setCacheStats(statsResponse.patCacheStats);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors de la récupération du profil:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
const storedToken = localStorage.getItem("accessToken");
|
||||||
|
if (!storedToken) {
|
||||||
|
setError("Aucun token trouvé. Veuillez d'abord vous inscrire pour obtenir un token d'accès.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchProfileWithToken(storedToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearData = () => {
|
||||||
|
setUser(null);
|
||||||
|
setError(null);
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
setHasToken(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">
|
||||||
|
Test de la query "me" - Profil utilisateur
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Section de statut du token */}
|
||||||
|
<div
|
||||||
|
className={`mb-6 rounded-md border p-4 ${hasToken ? "border-green-200 bg-green-50" : "border-yellow-200 bg-yellow-50"}`}
|
||||||
|
>
|
||||||
|
<h3 className={`text-sm font-medium ${hasToken ? "text-green-800" : "text-yellow-800"}`}>
|
||||||
|
{hasToken ? "✅ Token trouvé" : "⚠️ Aucun token disponible"}
|
||||||
|
</h3>
|
||||||
|
<p className={`mt-2 text-sm ${hasToken ? "text-green-700" : "text-yellow-700"}`}>
|
||||||
|
{hasToken
|
||||||
|
? "Token d'accès automatiquement chargé et associé dans le contexte GraphQL."
|
||||||
|
: "Aucun token trouvé. Inscrivez-vous d'abord pour obtenir un token d'accès."}
|
||||||
|
</p>
|
||||||
|
{hasToken && (
|
||||||
|
<div className="mt-3 flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={fetchProfile}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Chargement..." : "Actualiser le profil"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={clearData}
|
||||||
|
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
Effacer les données
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cacheStats && (
|
||||||
|
<div className="mt-3 rounded border border-blue-200 bg-blue-50 p-2">
|
||||||
|
<p className="text-xs text-blue-700">
|
||||||
|
<strong>Cache PAT:</strong> {cacheStats.totalEntries} entrée(s) | Mis à jour:{" "}
|
||||||
|
{new Date(cacheStats.timestamp).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Affichage des erreurs */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">❌ Erreur</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
{!hasToken && (
|
||||||
|
<div className="mt-2 text-xs text-red-600">
|
||||||
|
<p>Pour obtenir un token d'accès :</p>
|
||||||
|
<ul className="ml-4 list-inside list-disc">
|
||||||
|
<li>Allez sur la page d'inscription</li>
|
||||||
|
<li>Créez un nouveau compte</li>
|
||||||
|
<li>Le token sera automatiquement sauvegardé</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message de chargement initial */}
|
||||||
|
{loading && !user && (
|
||||||
|
<div className="mb-6 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">🔄 Chargement en cours...</h3>
|
||||||
|
<p className="mt-2 text-sm text-blue-700">Récupération de votre profil utilisateur...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Affichage du profil utilisateur */}
|
||||||
|
{user && (
|
||||||
|
<div className="mb-6 rounded-md border border-green-200 bg-green-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-green-800">✅ Profil récupéré avec succès</h3>
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">ID Utilisateur</label>
|
||||||
|
<p className="mt-1 font-mono text-sm break-all text-green-900">{user.id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Email</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{user.primaryEmail}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Nom d'utilisateur</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{user.username || "Non défini"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Nom complet</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{user.name || "Non défini"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Téléphone</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{user.primaryPhone || "Non défini"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Avatar</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{user.avatar || "Aucun avatar"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Créé le</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{new Date(parseInt(user.createdAt)).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-green-700">Modifié le</label>
|
||||||
|
<p className="mt-1 text-sm text-green-900">{new Date(parseInt(user.updatedAt)).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instructions d'utilisation */}
|
||||||
|
<div className="mb-6 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">💡 Comment fonctionne le nouveau système</h3>
|
||||||
|
<div className="mt-2 text-sm text-blue-700">
|
||||||
|
<ol className="ml-4 list-inside list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Allez sur la{" "}
|
||||||
|
<a href="/signup" className="font-medium underline hover:text-blue-600">
|
||||||
|
page d'inscription
|
||||||
|
</a>{" "}
|
||||||
|
pour créer un utilisateur
|
||||||
|
</li>
|
||||||
|
<li>Le token PAT est automatiquement créé et associé à votre userId dans le contexte GraphQL</li>
|
||||||
|
<li>Plus besoin de rechercher dans tous les utilisateurs - le cache PAT est intégré !</li>
|
||||||
|
<li>Revenez sur cette page - votre profil se chargera automatiquement via le contexte</li>
|
||||||
|
<li>Les statistiques du cache s'affichent en temps réel</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Exemple de requête GraphQL */}
|
||||||
|
<div className="mb-6 rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-800">🔍 Requête GraphQL utilisée</h3>
|
||||||
|
<pre className="mt-2 overflow-x-auto text-xs text-gray-600">
|
||||||
|
{`query GetMe {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
primaryPhone
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
|
<p>
|
||||||
|
Headers requis: <code>Authorization: Bearer <your-token></code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-800">🧭 Navigation</h3>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-4">
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Page de connexion
|
||||||
|
</a>
|
||||||
|
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
📝 Page d'inscription
|
||||||
|
</a>
|
||||||
|
<a href="/organizations" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🏢 Organisations
|
||||||
|
</a>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
<a href="/logout" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🚪 Page de déconnexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
224
src/app/resources/page.tsx
Normal file
224
src/app/resources/page.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
"use client";
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Resource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
indicator: string;
|
||||||
|
scopes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
export default function ResourcesPage() {
|
||||||
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [form, setForm] = useState<{
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
indicator: string;
|
||||||
|
description?: string;
|
||||||
|
scopes: string;
|
||||||
|
}>({ name: "", indicator: "", description: "", scopes: "" });
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client
|
||||||
|
.request<{ resources: Resource[] }>(gql`
|
||||||
|
{
|
||||||
|
resources {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
indicator
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
.then(data => {
|
||||||
|
setResources(data.resources);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError("Erreur lors du chargement des ressources");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setForm({ ...form, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (resource: Resource) => {
|
||||||
|
setForm({ ...resource, scopes: resource.scopes.join(",") });
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await client.request(
|
||||||
|
gql`
|
||||||
|
mutation DeleteResource($id: ID!) {
|
||||||
|
deleteResource(id: $id)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
setResources(resources.filter(r => r.id !== id));
|
||||||
|
} catch {
|
||||||
|
setError("Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
let data: { createResource?: Resource; updateResource?: Resource };
|
||||||
|
if (editMode) {
|
||||||
|
data = await client.request<{ updateResource: Resource }>(
|
||||||
|
gql`
|
||||||
|
mutation UpdateResource(
|
||||||
|
$id: ID!
|
||||||
|
$name: String!
|
||||||
|
$indicator: String!
|
||||||
|
$scopes: [String!]!
|
||||||
|
$description: String
|
||||||
|
) {
|
||||||
|
updateResource(id: $id, name: $name, indicator: $indicator, scopes: $scopes, description: $description) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
indicator
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
id: form.id!,
|
||||||
|
name: form.name,
|
||||||
|
indicator: form.indicator,
|
||||||
|
scopes: form.scopes.split(",").map(s => s.trim()),
|
||||||
|
description: form.description,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setResources(resources.map(r => (r.id === form.id ? data.updateResource! : r)));
|
||||||
|
} else {
|
||||||
|
data = await client.request<{ createResource: Resource }>(
|
||||||
|
gql`
|
||||||
|
mutation CreateResource($name: String!, $indicator: String!, $scopes: [String!]!, $description: String) {
|
||||||
|
createResource(name: $name, indicator: $indicator, scopes: $scopes, description: $description) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
indicator
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
name: form.name,
|
||||||
|
indicator: form.indicator,
|
||||||
|
scopes: form.scopes.split(",").map(s => s.trim()),
|
||||||
|
description: form.description,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setResources([...resources, data.createResource!]);
|
||||||
|
}
|
||||||
|
setForm({ name: "", indicator: "", description: "", scopes: "" });
|
||||||
|
setEditMode(false);
|
||||||
|
} catch {
|
||||||
|
setError("Erreur lors de l'enregistrement");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl p-4">
|
||||||
|
<h1 className="mb-4 text-2xl font-bold">Gestion des ressources</h1>
|
||||||
|
{error && <div className="mb-2 text-red-500">{error}</div>}
|
||||||
|
<form onSubmit={handleSubmit} className="mb-6 space-y-2">
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Nom"
|
||||||
|
className="w-full border p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="indicator"
|
||||||
|
value={form.indicator}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="URI (indicator)"
|
||||||
|
className="w-full border p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="scopes"
|
||||||
|
value={form.scopes}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Scopes (séparés par des virgules)"
|
||||||
|
className="w-full border p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
value={form.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Description"
|
||||||
|
className="w-full border p-2"
|
||||||
|
/>
|
||||||
|
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
|
||||||
|
{editMode ? "Modifier" : "Créer"}
|
||||||
|
</button>
|
||||||
|
{editMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-2 rounded border px-4 py-2"
|
||||||
|
onClick={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setForm({ name: "", indicator: "", description: "", scopes: "" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
{loading ? (
|
||||||
|
<div>Chargement...</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full border">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th className="p-2">Nom</th>
|
||||||
|
<th className="p-2">URI</th>
|
||||||
|
<th className="p-2">Scopes</th>
|
||||||
|
<th className="p-2">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{resources.map(resource => (
|
||||||
|
<tr key={resource.id} className="border-t">
|
||||||
|
<td className="p-2">{resource.name}</td>
|
||||||
|
<td className="p-2">{resource.indicator}</td>
|
||||||
|
<td className="p-2">{resource.scopes.join(", ")}</td>
|
||||||
|
<td className="space-x-2 p-2">
|
||||||
|
<button className="text-blue-600" onClick={() => handleEdit(resource)}>
|
||||||
|
Éditer
|
||||||
|
</button>
|
||||||
|
<button className="text-red-600" onClick={() => handleDelete(resource.id)}>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
src/app/roles/page.tsx
Normal file
205
src/app/roles/page.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"use client";
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import PermissionsManager from "./permissions";
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
permissions?: { id: string; name: string; description?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
export default function RolesPage() {
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [form, setForm] = useState<{ id?: string; name: string; description?: string }>({ name: "", description: "" });
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Fetch roles
|
||||||
|
useEffect(() => {
|
||||||
|
client
|
||||||
|
.request(gql`
|
||||||
|
{
|
||||||
|
roles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
permissions {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setRoles(data.roles);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response?.errors?.[0]?.message) {
|
||||||
|
setError("Erreur lors du chargement des rôles : " + err.response.errors[0].message);
|
||||||
|
} else {
|
||||||
|
setError("Erreur lors du chargement des rôles");
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setForm({ ...form, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (role: Role) => {
|
||||||
|
setForm(role);
|
||||||
|
setEditMode(true);
|
||||||
|
setSelectedPermissions(role.permissions?.map(p => p.id) || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
let mutation;
|
||||||
|
if (editMode) {
|
||||||
|
mutation = `mutation { updateRole(id: "${form.id}", name: "${form.name}"${form.description ? `, description: "${form.description}"` : ""}, permissionIds: [${selectedPermissions.map(id => `\"${id}\"`).join(",")}]) { id name description permissions { id name description } } }`;
|
||||||
|
} else {
|
||||||
|
mutation = `mutation { createRole(name: "${form.name}"${form.description ? `, description: "${form.description}"` : ""}, permissionIds: [${selectedPermissions.map(id => `\"${id}\"`).join(",")}]) { id name description permissions { id name description } } }`;
|
||||||
|
}
|
||||||
|
const res = await fetch("/api/graphql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ query: mutation }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.errors) {
|
||||||
|
setError(data.errors[0].message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editMode) {
|
||||||
|
setRoles(roles.map(r => (r.id === form.id ? data.data.updateRole : r)));
|
||||||
|
} else {
|
||||||
|
setRoles([...roles, data.data.createRole]);
|
||||||
|
}
|
||||||
|
setForm({ name: "", description: "" });
|
||||||
|
setEditMode(false);
|
||||||
|
setSelectedPermissions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
setError(null);
|
||||||
|
const res = await fetch("/api/graphql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ query: `mutation { deleteRole(id: "${id}") }` }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.errors) {
|
||||||
|
setError(data.errors[0].message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRoles(roles.filter(r => r.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <div className="flex h-64 items-center justify-center text-lg font-medium text-gray-500">Chargement...</div>;
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-8 max-w-2xl rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-3xl font-bold text-gray-800">Gestion des rôles</h1>
|
||||||
|
{error && <div className="mb-4 rounded bg-red-100 px-4 py-2 text-red-700">{error}</div>}
|
||||||
|
<form onSubmit={handleSubmit} className="mb-8 flex flex-col gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-700">Nom du rôle</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Nom du rôle"
|
||||||
|
className="w-full rounded border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-700">Description</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
value={form.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Description"
|
||||||
|
className="w-full rounded border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-700">Permissions</label>
|
||||||
|
<PermissionsManager selectedPermissions={selectedPermissions} onChange={setSelectedPermissions} />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded bg-blue-600 px-5 py-2 font-semibold text-white shadow transition hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{editMode ? "Modifier" : "Créer"}
|
||||||
|
</button>
|
||||||
|
{editMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded border border-gray-300 bg-gray-100 px-5 py-2 font-semibold text-gray-700 transition hover:bg-gray-200"
|
||||||
|
onClick={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setForm({ name: "", description: "" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="overflow-x-auto rounded border border-gray-200 bg-gray-50">
|
||||||
|
<table className="w-full min-w-[400px] text-left text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 text-gray-700">
|
||||||
|
<th className="border-b p-3 font-semibold">Nom</th>
|
||||||
|
<th className="border-b p-3 font-semibold">Description</th>
|
||||||
|
<th className="border-b p-3 font-semibold">Permissions</th>
|
||||||
|
<th className="border-b p-3 font-semibold">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{roles.map(role => (
|
||||||
|
<tr key={role.id} className="border-b last:border-b-0 hover:bg-blue-50">
|
||||||
|
<td className="p-3 align-middle font-medium text-gray-900">{role.name}</td>
|
||||||
|
<td className="p-3 align-middle text-gray-700">{role.description}</td>
|
||||||
|
<td className="p-3 align-middle text-xs text-gray-600">
|
||||||
|
{role.permissions && role.permissions.length > 0 ? (
|
||||||
|
role.permissions.map(p => p.name).join(", ")
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 italic">Aucune</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 align-middle">
|
||||||
|
<button
|
||||||
|
className="mr-2 rounded bg-blue-100 px-3 py-1 text-blue-700 transition hover:bg-blue-200"
|
||||||
|
onClick={() => handleEdit(role)}
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded bg-red-100 px-3 py-1 text-red-700 transition hover:bg-red-200"
|
||||||
|
onClick={() => handleDelete(role.id)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
src/app/roles/permissions.tsx
Normal file
184
src/app/roles/permissions.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ResourcesSelector from "./resources";
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GraphQLErrorResponse = { response?: { errors?: { message?: string }[] } };
|
||||||
|
|
||||||
|
const client = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
export default function PermissionsManager({
|
||||||
|
selectedPermissions,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedPermissions: string[];
|
||||||
|
onChange: (ids: string[]) => void;
|
||||||
|
}) {
|
||||||
|
const [selectedResource, setSelectedResource] = useState<string | null>(null);
|
||||||
|
const [scopes, setScopes] = useState<Permission[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [form, setForm] = useState<{ name: string; description?: string }>({ name: "", description: "" });
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
|
// Charge les scopes de la ressource sélectionnée
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedResource) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
client
|
||||||
|
.request<{ resourceScopes: Permission[] }>(
|
||||||
|
gql`
|
||||||
|
query ResourceScopes($resourceId: ID!) {
|
||||||
|
resourceScopes(resourceId: $resourceId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ resourceId: selectedResource }
|
||||||
|
)
|
||||||
|
.then(data => {
|
||||||
|
setScopes(data.resourceScopes);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError("Erreur lors du chargement des permissions de la ressource");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [selectedResource]);
|
||||||
|
|
||||||
|
const handleCreate = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); // Ajouté pour empêcher tout rechargement ou propagation
|
||||||
|
setError(null);
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation CreateResourceScope($resourceId: ID!, $name: String!, $description: String) {
|
||||||
|
createResourceScope(resourceId: $resourceId, name: $name, description: $description) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
console.log("[DEBUG] createResourceScope called", {
|
||||||
|
resourceId: selectedResource,
|
||||||
|
name: form.name,
|
||||||
|
description: form.description,
|
||||||
|
});
|
||||||
|
const variables = { resourceId: selectedResource, name: form.name, description: form.description || undefined };
|
||||||
|
const data: { createResourceScope: Permission } = await client.request(mutation, variables);
|
||||||
|
setScopes([...scopes, data.createResourceScope]);
|
||||||
|
setForm({ name: "", description: "" });
|
||||||
|
} catch (err) {
|
||||||
|
const errorObj = err as GraphQLErrorResponse;
|
||||||
|
if (errorObj.response?.errors?.[0]?.message) {
|
||||||
|
setError(errorObj.response.errors[0].message);
|
||||||
|
} else {
|
||||||
|
setError("Erreur lors de la création");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation DeleteResourceScope($resourceId: ID!, $scopeId: ID!) {
|
||||||
|
deleteResourceScope(resourceId: $resourceId, scopeId: $scopeId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await client.request(mutation, { resourceId: selectedResource, scopeId: id });
|
||||||
|
setScopes(scopes.filter(p => p.id !== id));
|
||||||
|
onChange(selectedPermissions.filter(pid => pid !== id));
|
||||||
|
} catch (err) {
|
||||||
|
const errorObj = err as GraphQLErrorResponse;
|
||||||
|
if (errorObj.response?.errors?.[0]?.message) {
|
||||||
|
setError(errorObj.response.errors[0].message);
|
||||||
|
} else {
|
||||||
|
setError("Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ResourcesSelector selectedResource={selectedResource} onChange={setSelectedResource} />
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div>Chargement des permissions...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-red-500">{error}</div>
|
||||||
|
) : selectedResource ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-2 flex gap-2">
|
||||||
|
<input
|
||||||
|
className="rounded border p-1 text-sm"
|
||||||
|
placeholder="Nom de la permission"
|
||||||
|
value={form.name}
|
||||||
|
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded border p-1 text-sm"
|
||||||
|
placeholder="Description"
|
||||||
|
value={form.description}
|
||||||
|
onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded bg-blue-600 px-3 py-1 text-sm text-white"
|
||||||
|
disabled={creating}
|
||||||
|
onClick={handleCreate}
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{scopes.map(perm => (
|
||||||
|
<label
|
||||||
|
key={perm.id}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded border bg-gray-50 px-2 py-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedPermissions.includes(perm.id)}
|
||||||
|
onChange={e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
onChange([...selectedPermissions, perm.id]);
|
||||||
|
} else {
|
||||||
|
onChange(selectedPermissions.filter(id => id !== perm.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-gray-700">{perm.name}</span>
|
||||||
|
{perm.description && <span className="text-xs text-gray-500">({perm.description})</span>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 rounded bg-red-100 px-2 py-0.5 text-xs text-red-700 hover:bg-red-200"
|
||||||
|
onClick={() => handleDelete(perm.id)}
|
||||||
|
title="Supprimer la permission"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500">Sélectionnez une ressource pour gérer ses permissions.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/app/roles/resources.tsx
Normal file
63
src/app/roles/resources.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Resource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
indicator: string;
|
||||||
|
scopes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
export default function ResourcesSelector({
|
||||||
|
selectedResource,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedResource: string | null;
|
||||||
|
onChange: (id: string | null) => void;
|
||||||
|
}) {
|
||||||
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client
|
||||||
|
.request<{ resources: Resource[] }>(gql`
|
||||||
|
{
|
||||||
|
resources {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
.then(data => {
|
||||||
|
setResources(data.resources);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError("Erreur lors du chargement des ressources");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div>Chargement des ressources...</div>;
|
||||||
|
if (error) return <div className="text-red-500">{error}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="w-full rounded border p-2"
|
||||||
|
value={selectedResource || ""}
|
||||||
|
onChange={e => onChange(e.target.value || null)}
|
||||||
|
>
|
||||||
|
<option value="">Aucune ressource</option>
|
||||||
|
{resources.map(resource => (
|
||||||
|
<option key={resource.id} value={resource.id}>
|
||||||
|
{resource.name} ({resource.indicator})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
490
src/app/signup/page.tsx
Normal file
490
src/app/signup/page.tsx
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
const SIGNUP_MUTATION = gql`
|
||||||
|
mutation SignUp($input: RegisterInput!) {
|
||||||
|
signUp(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
accessToken
|
||||||
|
tokenExpiry
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
primaryPhone
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_SOCIAL_CONNECTORS_QUERY = gql`
|
||||||
|
query GetSocialConnectors {
|
||||||
|
connectors {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
connectors {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
enabled
|
||||||
|
target
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
platform
|
||||||
|
metadata {
|
||||||
|
id
|
||||||
|
target
|
||||||
|
platform
|
||||||
|
name
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logoDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface RegisterFormData {
|
||||||
|
primaryEmail: string;
|
||||||
|
password: string;
|
||||||
|
username?: string;
|
||||||
|
name?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
customInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignUpResponse {
|
||||||
|
signUp: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
tokenExpiry?: string;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocialConnector {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
// Propriétés disponibles au niveau racine
|
||||||
|
target?: string;
|
||||||
|
name?: string | Record<string, string>;
|
||||||
|
description?: string | Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
|
platform?: string;
|
||||||
|
|
||||||
|
// Metadata (pour compatibilité)
|
||||||
|
metadata: {
|
||||||
|
id?: string;
|
||||||
|
target?: string;
|
||||||
|
platform?: string;
|
||||||
|
name?: string | Record<string, string>;
|
||||||
|
description?: string | Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectorsResponse {
|
||||||
|
connectors: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connectors?: SocialConnector[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TestSignUpPage() {
|
||||||
|
const [formData, setFormData] = useState<RegisterFormData>({
|
||||||
|
primaryEmail: "",
|
||||||
|
password: "",
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
primaryPhone: "",
|
||||||
|
customInfo: "",
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<SignUpResponse["signUp"] | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [socialConnectors, setSocialConnectors] = useState<SocialConnector[]>([]);
|
||||||
|
const [connectorsLoading, setConnectorsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSocialConnectors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSocialConnectors = async () => {
|
||||||
|
setConnectorsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<ConnectorsResponse>(GET_SOCIAL_CONNECTORS_QUERY);
|
||||||
|
if (response.connectors.success && response.connectors.connectors) {
|
||||||
|
// Filtrer uniquement les connecteurs sociaux activés
|
||||||
|
const socialConnectors = response.connectors.connectors.filter(
|
||||||
|
connector => connector.type === "Social" && connector.enabled !== false
|
||||||
|
);
|
||||||
|
|
||||||
|
setSocialConnectors(socialConnectors);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors du chargement des connecteurs sociaux:", err);
|
||||||
|
} finally {
|
||||||
|
setConnectorsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour obtenir le nom d'affichage du connecteur
|
||||||
|
const getDisplayName = (connector: SocialConnector): string => {
|
||||||
|
// Essayer d'obtenir le nom depuis les propriétés racine ou metadata
|
||||||
|
const nameSource = connector.name || connector.metadata?.name;
|
||||||
|
if (nameSource) {
|
||||||
|
if (typeof nameSource === "object" && nameSource !== null) {
|
||||||
|
const name = nameSource.en || nameSource.fr || Object.values(nameSource)[0];
|
||||||
|
if (name && name.trim()) return name;
|
||||||
|
}
|
||||||
|
if (typeof nameSource === "string" && nameSource.trim()) {
|
||||||
|
return nameSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser target avec une capitalisation propre
|
||||||
|
const target = connector.target || connector.metadata?.target;
|
||||||
|
if (target && target.trim()) {
|
||||||
|
return target.charAt(0).toUpperCase() + target.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Connecteur Social";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSocialSignup = (connectorId: string, _platform: string) => {
|
||||||
|
// Rediriger vers l'URL d'authentification sociale de Logto pour l'inscription
|
||||||
|
const socialSignupUrl = `${process.env.NEXT_PUBLIC_LOGTO_ENDPOINT}/oidc/auth?client_id=${process.env.NEXT_PUBLIC_LOGTO_APP_ID}&response_type=code&scope=openid%20profile%20email&redirect_uri=${encodeURIComponent(window.location.origin + "/callback")}&state=${encodeURIComponent(JSON.stringify({ type: "register", connector: connectorId }))}&prompt=consent&connector_id=${connectorId}`;
|
||||||
|
window.location.href = socialSignupUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<SignUpResponse>(SIGNUP_MUTATION, {
|
||||||
|
input: {
|
||||||
|
primaryEmail: formData.primaryEmail,
|
||||||
|
password: formData.password,
|
||||||
|
username: formData.username || undefined,
|
||||||
|
name: formData.name || undefined,
|
||||||
|
primaryPhone: formData.primaryPhone || undefined,
|
||||||
|
customInfo: formData.customInfo || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setResult(response.signUp);
|
||||||
|
|
||||||
|
// Sauvegarder automatiquement le token d'accès dans localStorage
|
||||||
|
if (response.signUp.success && response.signUp.accessToken) {
|
||||||
|
localStorage.setItem("accessToken", response.signUp.accessToken);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors de l'inscription:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-md">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">Test d'inscription Logto</h1>
|
||||||
|
|
||||||
|
{/* Connecteurs sociaux */}
|
||||||
|
{!connectorsLoading && socialConnectors.length > 0 && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="bg-white px-2 text-gray-500">S'inscrire avec</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{socialConnectors.map(connector => (
|
||||||
|
<button
|
||||||
|
key={connector.id}
|
||||||
|
onClick={() =>
|
||||||
|
handleSocialSignup(connector.id, connector.platform || connector.metadata?.platform || "")
|
||||||
|
}
|
||||||
|
className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
{(connector.logo || connector.metadata?.logo) && (
|
||||||
|
<Image
|
||||||
|
src={connector.logo || connector.metadata?.logo || ""}
|
||||||
|
alt={getDisplayName(connector)}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
Continuer avec {getDisplayName(connector)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="bg-white px-2 text-gray-500">Ou créer un compte avec email</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="primaryEmail" className="block text-sm font-medium text-gray-700">
|
||||||
|
Email (requis)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="primaryEmail"
|
||||||
|
name="primaryEmail"
|
||||||
|
value={formData.primaryEmail}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Mot de passe (requis)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||||
|
Nom d'utilisateur
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
|
Nom complet
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="primaryPhone" className="block text-sm font-medium text-gray-700">
|
||||||
|
Téléphone
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="primaryPhone"
|
||||||
|
name="primaryPhone"
|
||||||
|
value={formData.primaryPhone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="customInfo" className="block text-sm font-medium text-gray-700">
|
||||||
|
Information personnalisée
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="customInfo"
|
||||||
|
name="customInfo"
|
||||||
|
value={formData.customInfo}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Ex: Département, fonction, préférences..."
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Inscription en cours..." : "S'inscrire"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Affichage des résultats */}
|
||||||
|
{result && (
|
||||||
|
<div className="mt-6 rounded-md border border-green-200 bg-green-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-green-800">Résultat de l'inscription</h3>
|
||||||
|
<div className="mt-2 text-sm text-green-700">
|
||||||
|
<p>
|
||||||
|
<strong>Succès:</strong> {result.success ? "Oui" : "Non"}
|
||||||
|
</p>
|
||||||
|
{result.message && (
|
||||||
|
<p>
|
||||||
|
<strong>Message:</strong> {result.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result.accessToken && (
|
||||||
|
<div className="mt-2 rounded border border-blue-200 bg-blue-50 p-3">
|
||||||
|
<p className="font-medium text-blue-800">🔑 Token d'accès personnel créé et sauvegardé !</p>
|
||||||
|
<p className="mt-1 text-xs break-all text-blue-600">
|
||||||
|
<strong>Token:</strong> {result.accessToken.substring(0, 50)}...
|
||||||
|
</p>
|
||||||
|
{result.tokenExpiry && (
|
||||||
|
<p className="text-xs text-blue-600">
|
||||||
|
<strong>Expire le:</strong> {new Date(parseInt(result.tokenExpiry)).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-xs font-medium text-green-600">
|
||||||
|
✅ Token automatiquement sauvegardé dans votre navigateur
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<a
|
||||||
|
href="/profile"
|
||||||
|
className="inline-flex items-center text-xs text-blue-600 underline hover:text-blue-500"
|
||||||
|
>
|
||||||
|
🚀 Voir votre profil (token utilisé automatiquement) →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.user && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p>
|
||||||
|
<strong>Utilisateur créé:</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="ml-4 list-inside list-disc">
|
||||||
|
<li>ID: {result.user.id}</li>
|
||||||
|
<li>Email: {result.user.primaryEmail}</li>
|
||||||
|
{result.user.username && <li>Username: {result.user.username}</li>}
|
||||||
|
{result.user.name && <li>Nom: {result.user.name}</li>}
|
||||||
|
{result.user.primaryPhone && <li>Téléphone: {result.user.primaryPhone}</li>}
|
||||||
|
{result.user.avatar && <li>Avatar: {result.user.avatar}</li>}
|
||||||
|
<li>Créé le: {new Date(parseInt(result.user.createdAt)).toLocaleString()}</li>
|
||||||
|
<li>Modifié le: {new Date(parseInt(result.user.updatedAt)).toLocaleString()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Erreur</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Informations de debug */}
|
||||||
|
<div className="mt-8 rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-800">Configuration</h3>
|
||||||
|
<div className="mt-2 text-xs text-gray-600">
|
||||||
|
<p>Endpoint GraphQL: /api/graphql</p>
|
||||||
|
<p>Variables d'environnement requises:</p>
|
||||||
|
<ul className="ml-4 list-inside list-disc">
|
||||||
|
<li>LOGTO_ENDPOINT</li>
|
||||||
|
<li>LOGTO_APP_ID</li>
|
||||||
|
<li>LOGTO_APP_SECRET</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Page de connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Tester la query "me" (Profil)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/organizations" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🏢 Organisations
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👥 Liste des utilisateurs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/logout" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🚪 Page de déconnexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/app/test-signup/page.tsx
Normal file
0
src/app/test-signup/page.tsx
Normal file
303
src/app/test-users/page.tsx
Normal file
303
src/app/test-users/page.tsx
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GraphQLClient, gql } from "graphql-request";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||||||
|
|
||||||
|
const GET_USERS_QUERY = gql`
|
||||||
|
query GetUsers {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
primaryEmail
|
||||||
|
primaryPhone
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_ROLES_QUERY = gql`
|
||||||
|
query GetRoles {
|
||||||
|
roles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
type
|
||||||
|
permissions {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ASSIGN_ROLE_MUTATION = gql`
|
||||||
|
mutation AssignRoleToUser($userId: ID!, $roleId: ID!) {
|
||||||
|
assignRoleToUser(userId: $userId, roleId: $roleId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GET_USER_ROLES_QUERY = gql`
|
||||||
|
query GetUserRoles($userId: ID!) {
|
||||||
|
userRoles(userId: $userId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetUsersResponse {
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TestUsersPage() {
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [assigning, setAssigning] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [assignError, setAssignError] = useState<string | null>(null);
|
||||||
|
const [userRolesMap, setUserRolesMap] = useState<Record<string, Role[]>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRoles = async () => {
|
||||||
|
try {
|
||||||
|
const data = await graphQLClient.request<{ roles: Role[] }>(GET_ROLES_QUERY);
|
||||||
|
setRoles(data.roles.filter(r => r.type === "User"));
|
||||||
|
} catch {
|
||||||
|
setRoles([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchRoles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Récupère les rôles pour chaque utilisateur après fetchUsers
|
||||||
|
const fetchUserRolesForAll = async (users: User[]) => {
|
||||||
|
const rolesMap: Record<string, Role[]> = {};
|
||||||
|
await Promise.all(
|
||||||
|
users.map(async user => {
|
||||||
|
try {
|
||||||
|
const data = await graphQLClient.request<{ userRoles: Role[] }>(GET_USER_ROLES_QUERY, { userId: user.id });
|
||||||
|
rolesMap[user.id] = data.userRoles;
|
||||||
|
} catch {
|
||||||
|
rolesMap[user.id] = [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setUserRolesMap(rolesMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await graphQLClient.request<GetUsersResponse>(GET_USERS_QUERY);
|
||||||
|
setUsers(response.users);
|
||||||
|
await fetchUserRolesForAll(response.users);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Erreur lors de la récupération des utilisateurs:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignRole = async (userId: string, roleId: string) => {
|
||||||
|
setAssigning(userId + roleId);
|
||||||
|
setAssignError(null);
|
||||||
|
try {
|
||||||
|
await graphQLClient.request(ASSIGN_ROLE_MUTATION, { userId, roleId });
|
||||||
|
// Recharge les rôles de l'utilisateur après assignation
|
||||||
|
const data = await graphQLClient.request<{ userRoles: Role[] }>(GET_USER_ROLES_QUERY, { userId });
|
||||||
|
setUserRolesMap(prev => ({ ...prev, [userId]: data.userRoles }));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
let errorMessage = "Erreur lors de l'assignation du rôle";
|
||||||
|
if (
|
||||||
|
err &&
|
||||||
|
typeof err === "object" &&
|
||||||
|
"response" in err &&
|
||||||
|
err.response &&
|
||||||
|
typeof err.response === "object" &&
|
||||||
|
"errors" in err.response
|
||||||
|
) {
|
||||||
|
const response = err.response as { errors?: { message?: string }[] };
|
||||||
|
if (Array.isArray(response.errors) && response.errors[0]?.message) {
|
||||||
|
errorMessage = response.errors[0].message;
|
||||||
|
}
|
||||||
|
} else if (err) {
|
||||||
|
errorMessage += ": " + JSON.stringify(err);
|
||||||
|
}
|
||||||
|
// Ajoute un log détaillé pour debug
|
||||||
|
|
||||||
|
console.error("Erreur assignRole:", err);
|
||||||
|
setAssignError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setAssigning(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-4xl">
|
||||||
|
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Liste des utilisateurs Logto</h1>
|
||||||
|
<button
|
||||||
|
onClick={fetchUsers}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Chargement..." : "Récupérer les utilisateurs"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Erreur</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{users.length > 0 && (
|
||||||
|
<div className="ring-opacity-5 overflow-hidden shadow ring-1 ring-black md:rounded-lg">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Nom
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Téléphone
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Créé le
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Rôles actuels
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
|
||||||
|
Rôle à assigner
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
{users.map(user => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td className="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900">{user.id}</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">{user.primaryEmail}</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">{user.username || "-"}</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">{user.name || "-"}</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||||
|
{user.primaryPhone || "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||||
|
{new Date(parseInt(user.createdAt)).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||||
|
{userRolesMap[user.id]?.length ? (
|
||||||
|
userRolesMap[user.id].map(r => r.name).join(", ")
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 italic">Aucun</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||||
|
<select
|
||||||
|
className="rounded border p-1 text-sm"
|
||||||
|
defaultValue=""
|
||||||
|
onChange={e => {
|
||||||
|
if (e.target.value) assignRole(user.id, e.target.value);
|
||||||
|
}}
|
||||||
|
disabled={assigning !== null}
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un rôle</option>
|
||||||
|
{roles.map(role => (
|
||||||
|
<option key={role.id} value={role.id}>
|
||||||
|
{role.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{assigning === user.id && <span className="ml-2 text-xs text-blue-600">Assignation...</span>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{assignError && <div className="mt-2 text-sm text-red-600">{assignError}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{users.length === 0 && !loading && !error && (
|
||||||
|
<div className="py-8 text-center">
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Aucun utilisateur trouvé. Cliquez sur "Récupérer les utilisateurs" pour charger la liste.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🔐 Page de connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
📝 Page d'inscription
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
👤 Tester la query "me" (Profil)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/logout" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||||||
|
🚪 Page de déconnexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/db/models/index.ts
Normal file
0
src/db/models/index.ts
Normal file
20
src/graphql/context/context.ts
Normal file
20
src/graphql/context/context.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import initGuard from "@/graphql/context/features/auth";
|
||||||
|
import setAuth from "@/graphql/context/features/guard";
|
||||||
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
|
import { GraphQLContext } from "@/graphql/context/types";
|
||||||
|
import { YogaInitialContext } from "graphql-yoga";
|
||||||
|
|
||||||
|
export async function context({ request }: YogaInitialContext): Promise<GraphQLContext> {
|
||||||
|
const auth = await setAuth(request);
|
||||||
|
const guard = initGuard(auth);
|
||||||
|
|
||||||
|
const context: GraphQLContext = {
|
||||||
|
db: {},
|
||||||
|
request,
|
||||||
|
...auth,
|
||||||
|
guard,
|
||||||
|
patCache: patContextCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
9
src/graphql/context/features/auth.ts
Normal file
9
src/graphql/context/features/auth.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { GuardFunction } from "@/graphql/context/types";
|
||||||
|
|
||||||
|
const initGuard: GuardFunction = auth => {
|
||||||
|
return {
|
||||||
|
isAuthenticated: !!auth?.accessToken,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initGuard;
|
||||||
105
src/graphql/context/features/guard.ts
Normal file
105
src/graphql/context/features/guard.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { AuthTypes } from "@/graphql/context";
|
||||||
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
|
|
||||||
|
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
||||||
|
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
||||||
|
try {
|
||||||
|
const parts = token.split(".");
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
|
||||||
|
const payload = parts[1];
|
||||||
|
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
||||||
|
return JSON.parse(decoded) as { sub?: string; [key: string]: unknown };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Impossible de décoder le JWT:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAuth = async (
|
||||||
|
req:
|
||||||
|
| Request
|
||||||
|
| {
|
||||||
|
headers?: Headers | Record<string, string>;
|
||||||
|
get?: (header: string) => string | undefined;
|
||||||
|
header?: (header: string) => string | undefined;
|
||||||
|
}
|
||||||
|
): Promise<AuthTypes> => {
|
||||||
|
// Extraire le token d'autorisation des headers
|
||||||
|
let authHeader: string | null = null;
|
||||||
|
|
||||||
|
// GraphQL Yoga utilise la structure Request Web API
|
||||||
|
if (req.headers) {
|
||||||
|
// Si c'est un objet Headers (Web API)
|
||||||
|
if (typeof req.headers.get === "function") {
|
||||||
|
authHeader = (req.headers as Headers).get("Authorization");
|
||||||
|
}
|
||||||
|
// Si c'est un objet record (GraphQL Yoga parfois)
|
||||||
|
else if (typeof req.headers === "object") {
|
||||||
|
authHeader =
|
||||||
|
(req.headers as Record<string, string>)["authorization"] ||
|
||||||
|
(req.headers as Record<string, string>)["Authorization"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Essayer d'accéder aux headers via une autre méthode
|
||||||
|
try {
|
||||||
|
// Tenter d'utiliser req comme un objet Request ou custom
|
||||||
|
if (typeof (req as { get?: (header: string) => string | undefined }).get === "function") {
|
||||||
|
const val = (req as { get: (header: string) => string | undefined }).get("Authorization");
|
||||||
|
authHeader = val ?? null;
|
||||||
|
} else if (typeof (req as { header?: (header: string) => string | undefined }).header === "function") {
|
||||||
|
const val = (req as { header: (header: string) => string | undefined }).header("Authorization");
|
||||||
|
authHeader = val ?? null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs d'accès aux headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authHeader.substring(7); // Retirer "Bearer "
|
||||||
|
|
||||||
|
// Décoder le JWT pour récupérer l'userId (seulement si c'est un JWT)
|
||||||
|
let userId: string | undefined = undefined;
|
||||||
|
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (token.includes(".") && token.split(".").length === 3) {
|
||||||
|
// C'est un JWT
|
||||||
|
const decoded = decodeJWT(token);
|
||||||
|
userId = decoded?.sub as string | undefined;
|
||||||
|
if (userId) {
|
||||||
|
user = {
|
||||||
|
id: userId,
|
||||||
|
name: typeof decoded?.name === "string" ? decoded.name : undefined,
|
||||||
|
email: typeof decoded?.email === "string" ? decoded.email : undefined,
|
||||||
|
lastName: typeof decoded?.family_name === "string" ? decoded.family_name : undefined,
|
||||||
|
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// PAT : tenter de récupérer l'userId via le cache
|
||||||
|
userId = patContextCache.getUserId(token) || undefined;
|
||||||
|
if (userId) {
|
||||||
|
user = { id: userId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: token,
|
||||||
|
personalAccessToken: token,
|
||||||
|
userId: userId,
|
||||||
|
user: user, // <-- Correction ici
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas d'authentification
|
||||||
|
return {
|
||||||
|
accessToken: undefined,
|
||||||
|
personalAccessToken: undefined,
|
||||||
|
userId: undefined,
|
||||||
|
user: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setAuth;
|
||||||
82
src/graphql/context/features/patCache.ts
Normal file
82
src/graphql/context/features/patCache.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Cache pour les associations PAT -> userId intégré au contexte GraphQL
|
||||||
|
export interface PATCacheEntry {
|
||||||
|
userId: string;
|
||||||
|
expiry: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PATContextCache {
|
||||||
|
private cache: Map<string, PATCacheEntry> = new Map();
|
||||||
|
|
||||||
|
// Enregistrer une association PAT -> userId
|
||||||
|
register(pat: string, userId: string, ttlHours: number = 24): void {
|
||||||
|
this.cache.set(pat, {
|
||||||
|
userId: userId,
|
||||||
|
expiry: Date.now() + ttlHours * 60 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'userId depuis un PAT
|
||||||
|
getUserId(pat: string): string | null {
|
||||||
|
const entry = this.cache.get(pat);
|
||||||
|
if (entry && Date.now() < entry.expiry) {
|
||||||
|
return entry.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer l'entrée expirée
|
||||||
|
if (entry) {
|
||||||
|
this.cache.delete(pat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer une entrée du cache
|
||||||
|
remove(pat: string): void {
|
||||||
|
this.cache.delete(pat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer les entrées expirées
|
||||||
|
cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [pat, entry] of this.cache.entries()) {
|
||||||
|
if (now >= entry.expiry) {
|
||||||
|
this.cache.delete(pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les statistiques du cache
|
||||||
|
getStats(): { size: number; entries: number } {
|
||||||
|
this.cleanup(); // Nettoyer avant de retourner les stats
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
entries: this.cache.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si un PAT existe dans le cache
|
||||||
|
has(pat: string): boolean {
|
||||||
|
const entry = this.cache.get(pat);
|
||||||
|
if (entry && Date.now() < entry.expiry) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer l'entrée expirée
|
||||||
|
if (entry) {
|
||||||
|
this.cache.delete(pat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton du cache PAT
|
||||||
|
export const patContextCache = new PATContextCache();
|
||||||
|
|
||||||
|
// Nettoyer le cache toutes les heures
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
patContextCache.cleanup();
|
||||||
|
},
|
||||||
|
60 * 60 * 1000
|
||||||
|
); // 1 heure
|
||||||
2
src/graphql/context/index.ts
Normal file
2
src/graphql/context/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "@/graphql/context/context";
|
||||||
|
export * from "@/graphql/context/types";
|
||||||
48
src/graphql/context/types.ts
Normal file
48
src/graphql/context/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export interface DB {
|
||||||
|
// Define your database types here
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Guard {}
|
||||||
|
|
||||||
|
export interface GuardFunction {
|
||||||
|
(auth?: AuthTypes): Guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthTypes {
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
lastName?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
accessToken?: string;
|
||||||
|
personalAccessToken?: string; // Token d'accès personnel Logto
|
||||||
|
userId?: string; // ID utilisateur pour les requêtes authentifiées
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PATCache {
|
||||||
|
register(pat: string, userId: string, ttlHours?: number): void;
|
||||||
|
getUserId(pat: string): string | null;
|
||||||
|
remove(pat: string): void;
|
||||||
|
cleanup(): void;
|
||||||
|
has(pat: string): boolean;
|
||||||
|
getStats(): { size: number; entries: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphQLContext {
|
||||||
|
request: Request;
|
||||||
|
db: DB;
|
||||||
|
guard?: Guard;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
lastName?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
accessToken?: string;
|
||||||
|
personalAccessToken?: string;
|
||||||
|
userId?: string;
|
||||||
|
patCache: PATCache; // Cache PAT intégré au contexte
|
||||||
|
}
|
||||||
159
src/graphql/features/Connector/Connector.graphql
Normal file
159
src/graphql/features/Connector/Connector.graphql
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
enum ConnectorType {
|
||||||
|
Email
|
||||||
|
Sms
|
||||||
|
Social
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectorPlatform {
|
||||||
|
# Social connectors
|
||||||
|
Google
|
||||||
|
Facebook
|
||||||
|
GitHub
|
||||||
|
Discord
|
||||||
|
WeChat
|
||||||
|
Alipay
|
||||||
|
Kakao
|
||||||
|
Naver
|
||||||
|
AzureAd
|
||||||
|
OIDC
|
||||||
|
SAML
|
||||||
|
|
||||||
|
# Email connectors
|
||||||
|
SendGrid
|
||||||
|
SES
|
||||||
|
Mailgun
|
||||||
|
SMTP
|
||||||
|
|
||||||
|
# SMS connectors
|
||||||
|
Twilio
|
||||||
|
AliCloudSms
|
||||||
|
TencentSms
|
||||||
|
|
||||||
|
# Other platforms
|
||||||
|
Native
|
||||||
|
Web
|
||||||
|
Universal
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FormItemType {
|
||||||
|
Text
|
||||||
|
MultilineText
|
||||||
|
Password
|
||||||
|
Switch
|
||||||
|
Select
|
||||||
|
Json
|
||||||
|
MultiSelect
|
||||||
|
Number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connector {
|
||||||
|
id: ID!
|
||||||
|
type: ConnectorType!
|
||||||
|
enabled: Boolean
|
||||||
|
config: JSON
|
||||||
|
|
||||||
|
# Propriétés disponibles au niveau racine dans l'API Logto
|
||||||
|
target: String
|
||||||
|
name: JSON
|
||||||
|
description: JSON
|
||||||
|
logo: String
|
||||||
|
logoDark: String
|
||||||
|
platform: String
|
||||||
|
|
||||||
|
# Metadata (pour compatibilité)
|
||||||
|
metadata: ConnectorMetadata!
|
||||||
|
createdAt: String
|
||||||
|
updatedAt: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectorMetadata {
|
||||||
|
id: ID
|
||||||
|
target: String
|
||||||
|
platform: ConnectorPlatform
|
||||||
|
name: JSON
|
||||||
|
description: JSON
|
||||||
|
logo: String
|
||||||
|
logoDark: String
|
||||||
|
readme: String
|
||||||
|
configTemplate: JSON
|
||||||
|
formItems: [FormItem!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItem {
|
||||||
|
key: String!
|
||||||
|
type: FormItemType!
|
||||||
|
label: JSON!
|
||||||
|
placeholder: JSON
|
||||||
|
required: Boolean
|
||||||
|
defaultValue: JSON
|
||||||
|
description: JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type FactoryConnector {
|
||||||
|
id: ID!
|
||||||
|
target: String!
|
||||||
|
platform: ConnectorPlatform
|
||||||
|
name: JSON!
|
||||||
|
description: JSON!
|
||||||
|
logo: String!
|
||||||
|
logoDark: String
|
||||||
|
readme: String!
|
||||||
|
configTemplate: JSON
|
||||||
|
formItems: [FormItem!]!
|
||||||
|
isAdded: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateConnectorInput {
|
||||||
|
connectorId: String!
|
||||||
|
config: JSON!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateConnectorInput {
|
||||||
|
id: ID!
|
||||||
|
config: JSON
|
||||||
|
enabled: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectorResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
connector: Connector
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectorsListResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
connectors: [Connector!]
|
||||||
|
availableConnectors: [FactoryConnector!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
# Get all configured connectors
|
||||||
|
connectors: ConnectorsListResponse!
|
||||||
|
|
||||||
|
# Get a specific connector by ID
|
||||||
|
connector(id: ID!): ConnectorResponse!
|
||||||
|
|
||||||
|
# Get all available connector factories
|
||||||
|
connectorFactories: [FactoryConnector!]!
|
||||||
|
|
||||||
|
# Get a specific connector factory
|
||||||
|
connectorFactory(id: ID!): FactoryConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
# Create a new connector from factory
|
||||||
|
createConnector(input: CreateConnectorInput!): ConnectorResponse!
|
||||||
|
|
||||||
|
# Update an existing connector
|
||||||
|
updateConnector(input: UpdateConnectorInput!): ConnectorResponse!
|
||||||
|
|
||||||
|
# Delete a connector
|
||||||
|
deleteConnector(id: ID!): ConnectorResponse!
|
||||||
|
|
||||||
|
# Enable/disable a connector
|
||||||
|
toggleConnector(id: ID!, enabled: Boolean!): ConnectorResponse!
|
||||||
|
|
||||||
|
# Test a connector configuration
|
||||||
|
testConnector(id: ID!): ConnectorResponse!
|
||||||
|
}
|
||||||
267
src/graphql/features/Connector/connectorService.ts
Normal file
267
src/graphql/features/Connector/connectorService.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { ConnectorMetadata, CreateConnectorInput, LogtoConnector, UpdateConnectorInput } from "@/types/Connector";
|
||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
|
||||||
|
async function getLogtoAccessToken(): Promise<string> {
|
||||||
|
const client_id = process.env.LOGTO_APP_ID;
|
||||||
|
const client_secret = process.env.LOGTO_APP_SECRET;
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/oidc/token`,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: client_id || "",
|
||||||
|
client_secret: client_secret || "",
|
||||||
|
resource: process.env.LOGTO_RESOURCE || "",
|
||||||
|
scope: "all",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new GraphQLError(`Erreur d'authentification HTTP ${response.status}`, {
|
||||||
|
extensions: { code: "LOGTO_HTTP_ERROR" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConnectors(): Promise<LogtoConnector[]> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/connectors`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des connecteurs:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération des connecteurs", {
|
||||||
|
extensions: { code: "CONNECTORS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConnector(id: string): Promise<LogtoConnector | null> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/connectors/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erreur lors de la récupération du connecteur:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération du connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConnectorFactories(): Promise<ConnectorMetadata[]> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/connector-factories`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des factories de connecteurs:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération des factories de connecteurs", {
|
||||||
|
extensions: { code: "CONNECTOR_FACTORIES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConnectorFactory(id: string): Promise<ConnectorMetadata | null> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/connector-factories/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erreur lors de la récupération de la factory de connecteur:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération de la factory de connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_FACTORY_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createConnector(input: CreateConnectorInput): Promise<LogtoConnector> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/connectors`,
|
||||||
|
{
|
||||||
|
connectorId: input.connectorId,
|
||||||
|
config: input.config,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la création du connecteur:", error);
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
const errorCode = error.response?.data?.code;
|
||||||
|
if (errorCode === "connector.connector_id_in_use") {
|
||||||
|
throw new GraphQLError("Ce connecteur est déjà configuré", {
|
||||||
|
extensions: { code: "CONNECTOR_ALREADY_EXISTS" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
const errorMessage = error.response?.data?.message || "Configuration invalide";
|
||||||
|
throw new GraphQLError(`Erreur de configuration: ${errorMessage}`, {
|
||||||
|
extensions: { code: "CONNECTOR_CONFIG_INVALID" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Échec de la création du connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_CREATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConnector(input: UpdateConnectorInput): Promise<LogtoConnector> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
if (input.config !== undefined) updateData.config = input.config;
|
||||||
|
if (input.enabled !== undefined) updateData.enabled = input.enabled;
|
||||||
|
|
||||||
|
const response = await axios.patch(`${process.env.LOGTO_ENDPOINT}/api/connectors/${input.id}`, updateData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la mise à jour du connecteur:", error);
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new GraphQLError("Connecteur non trouvé", {
|
||||||
|
extensions: { code: "CONNECTOR_NOT_FOUND" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
const errorMessage = error.response?.data?.message || "Configuration invalide";
|
||||||
|
throw new GraphQLError(`Erreur de configuration: ${errorMessage}`, {
|
||||||
|
extensions: { code: "CONNECTOR_CONFIG_INVALID" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Échec de la mise à jour du connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_UPDATE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteConnector(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/connectors/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression du connecteur:", error);
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new GraphQLError("Connecteur non trouvé", {
|
||||||
|
extensions: { code: "CONNECTOR_NOT_FOUND" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Échec de la suppression du connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_DELETE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testConnector(id: string): Promise<{ success: boolean; message?: string }> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/connectors/${id}/test`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Test du connecteur réussi",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du test du connecteur:", error);
|
||||||
|
|
||||||
|
let message = "Test du connecteur échoué";
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
message = "Connecteur non trouvé";
|
||||||
|
} else if (error.response?.data?.message) {
|
||||||
|
message = error.response.data.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/graphql/features/Connector/index.ts
Normal file
8
src/graphql/features/Connector/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export * from "./connectorService";
|
||||||
|
import typeDefs from "./Connector.graphql";
|
||||||
|
import { connectorResolvers } from "./resolver";
|
||||||
|
|
||||||
|
export const Connector = {
|
||||||
|
typeDefs,
|
||||||
|
resolvers: connectorResolvers,
|
||||||
|
};
|
||||||
224
src/graphql/features/Connector/resolver.ts
Normal file
224
src/graphql/features/Connector/resolver.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import {
|
||||||
|
ConnectorResponse,
|
||||||
|
ConnectorsListResponse,
|
||||||
|
CreateConnectorInput,
|
||||||
|
FactoryConnector,
|
||||||
|
LogtoConnector,
|
||||||
|
UpdateConnectorInput,
|
||||||
|
} from "../../../types/Connector";
|
||||||
|
import {
|
||||||
|
createConnector,
|
||||||
|
deleteConnector,
|
||||||
|
getConnector,
|
||||||
|
getConnectorFactories,
|
||||||
|
getConnectorFactory,
|
||||||
|
getConnectors,
|
||||||
|
testConnector,
|
||||||
|
updateConnector,
|
||||||
|
} from "./connectorService";
|
||||||
|
|
||||||
|
export const connectorResolvers = {
|
||||||
|
// Resolver pour le type Connector pour mapper les propriétés correctement
|
||||||
|
Connector: {
|
||||||
|
// S'assurer que les propriétés racine sont utilisées quand metadata est null
|
||||||
|
target: (parent: LogtoConnector) => parent.target || parent.metadata?.target || null,
|
||||||
|
name: (parent: LogtoConnector) => parent.name || parent.metadata?.name || null,
|
||||||
|
description: (parent: LogtoConnector) => parent.description || parent.metadata?.description || null,
|
||||||
|
logo: (parent: LogtoConnector) => parent.logo || parent.metadata?.logo || null,
|
||||||
|
logoDark: (parent: LogtoConnector) => parent.logoDark || parent.metadata?.logoDark || null,
|
||||||
|
platform: (parent: LogtoConnector) => parent.platform || parent.metadata?.platform || null,
|
||||||
|
|
||||||
|
// Assurer que metadata existe même si certaines propriétés sont nulles
|
||||||
|
metadata: (parent: LogtoConnector) => ({
|
||||||
|
id: parent.metadata?.id || parent.id,
|
||||||
|
target: parent.target || parent.metadata?.target || null,
|
||||||
|
platform: parent.platform || parent.metadata?.platform || null,
|
||||||
|
name: parent.name || parent.metadata?.name || null,
|
||||||
|
description: parent.description || parent.metadata?.description || null,
|
||||||
|
logo: parent.logo || parent.metadata?.logo || null,
|
||||||
|
logoDark: parent.logoDark || parent.metadata?.logoDark || null,
|
||||||
|
readme: parent.metadata?.readme || null,
|
||||||
|
configTemplate: parent.metadata?.configTemplate || null,
|
||||||
|
formItems: parent.metadata?.formItems || [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
Query: {
|
||||||
|
async connectors(): Promise<ConnectorsListResponse> {
|
||||||
|
try {
|
||||||
|
const [connectors, factories] = await Promise.all([getConnectors(), getConnectorFactories()]);
|
||||||
|
|
||||||
|
// Marquer les factories qui sont déjà ajoutées
|
||||||
|
const availableConnectors: FactoryConnector[] = factories.map(factory => ({
|
||||||
|
...factory,
|
||||||
|
isAdded: connectors.some(connector => connector.metadata.id === factory.id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
connectors,
|
||||||
|
availableConnectors,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des connecteurs:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
connectors: [],
|
||||||
|
availableConnectors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async connector(_: unknown, { id }: { id: string }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
const connector = await getConnector(id);
|
||||||
|
|
||||||
|
if (!connector) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Connecteur non trouvé",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
connector,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async connectorFactories(): Promise<FactoryConnector[]> {
|
||||||
|
try {
|
||||||
|
const [factories, connectors] = await Promise.all([getConnectorFactories(), getConnectors()]);
|
||||||
|
|
||||||
|
return factories.map(factory => ({
|
||||||
|
...factory,
|
||||||
|
isAdded: connectors.some(connector => connector.metadata.id === factory.id),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des factories:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération des factories de connecteurs", {
|
||||||
|
extensions: { code: "CONNECTOR_FACTORIES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async connectorFactory(_: unknown, { id }: { id: string }): Promise<FactoryConnector | null> {
|
||||||
|
try {
|
||||||
|
const [factory, connectors] = await Promise.all([getConnectorFactory(id), getConnectors()]);
|
||||||
|
|
||||||
|
if (!factory) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...factory,
|
||||||
|
isAdded: connectors.some(connector => connector.metadata.id === factory.id),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de la factory:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération de la factory de connecteur", {
|
||||||
|
extensions: { code: "CONNECTOR_FACTORY_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
async createConnector(_: unknown, { input }: { input: CreateConnectorInput }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
const connector = await createConnector(input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Connecteur créé avec succès",
|
||||||
|
connector,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la création du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateConnector(_: unknown, { input }: { input: UpdateConnectorInput }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
const connector = await updateConnector(input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Connecteur mis à jour avec succès",
|
||||||
|
connector,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la mise à jour du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteConnector(_: unknown, { id }: { id: string }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
await deleteConnector(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Connecteur supprimé avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleConnector(_: unknown, { id, enabled }: { id: string; enabled: boolean }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
const connector = await updateConnector({ id, enabled });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Connecteur ${enabled ? "activé" : "désactivé"} avec succès`,
|
||||||
|
connector,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du basculement du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async testConnector(_: unknown, { id }: { id: string }): Promise<ConnectorResponse> {
|
||||||
|
try {
|
||||||
|
const testResult = await testConnector(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: testResult.success,
|
||||||
|
message: testResult.message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du test du connecteur:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur inconnue",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
179
src/graphql/features/Organization/Organization.graphql
Normal file
179
src/graphql/features/Organization/Organization.graphql
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
type Organization {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
customData: JSON
|
||||||
|
isMfaRequired: Boolean
|
||||||
|
createdAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationRole {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
type: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationPermission {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
resourceId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationUser {
|
||||||
|
id: ID!
|
||||||
|
username: String
|
||||||
|
primaryEmail: String
|
||||||
|
primaryPhone: String
|
||||||
|
name: String
|
||||||
|
avatar: String
|
||||||
|
organizationRoles: [OrganizationRole!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationInvitation {
|
||||||
|
id: ID!
|
||||||
|
inviterId: String!
|
||||||
|
invitee: String!
|
||||||
|
organizationId: String!
|
||||||
|
status: String!
|
||||||
|
organizationRoleIds: [String!]!
|
||||||
|
expiresAt: String!
|
||||||
|
createdAt: String!
|
||||||
|
acceptedAt: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input types for mutations
|
||||||
|
input CreateOrganizationInput {
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
customData: JSON
|
||||||
|
isMfaRequired: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateOrganizationInput {
|
||||||
|
id: ID!
|
||||||
|
name: String
|
||||||
|
description: String
|
||||||
|
customData: JSON
|
||||||
|
isMfaRequired: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input AddUserToOrganizationInput {
|
||||||
|
organizationId: ID!
|
||||||
|
userId: ID!
|
||||||
|
organizationRoleIds: [ID!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input RemoveUserFromOrganizationInput {
|
||||||
|
organizationId: ID!
|
||||||
|
userId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserOrganizationRolesInput {
|
||||||
|
organizationId: ID!
|
||||||
|
userId: ID!
|
||||||
|
organizationRoleIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateOrganizationInvitationInput {
|
||||||
|
organizationId: ID!
|
||||||
|
invitee: String!
|
||||||
|
organizationRoleIds: [ID!]
|
||||||
|
messagePayload: JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response types
|
||||||
|
type OrganizationResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
organization: Organization
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationsListResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
organizations: [Organization!]
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationUsersResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
users: [OrganizationUser!]
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationRolesResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
roles: [OrganizationRole!]
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationPermissionsResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
permissions: [OrganizationPermission!]
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationInvitationsResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
invitations: [OrganizationInvitation!]
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationInvitationResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
invitation: OrganizationInvitation
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationAPITestResponse {
|
||||||
|
available: Boolean!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
# Test API availability
|
||||||
|
testOrganizationsAPI: OrganizationAPITestResponse!
|
||||||
|
|
||||||
|
# Organizations
|
||||||
|
organizations(page: Int, pageSize: Int): OrganizationsListResponse!
|
||||||
|
organization(id: ID!): OrganizationResponse!
|
||||||
|
|
||||||
|
# Organization Users
|
||||||
|
organizationUsers(organizationId: ID!, page: Int, pageSize: Int): OrganizationUsersResponse!
|
||||||
|
|
||||||
|
# Organization Roles
|
||||||
|
organizationRoles(organizationId: ID!, page: Int, pageSize: Int): OrganizationRolesResponse!
|
||||||
|
|
||||||
|
# Organization Permissions
|
||||||
|
organizationPermissions(organizationId: ID!, page: Int, pageSize: Int): OrganizationPermissionsResponse!
|
||||||
|
|
||||||
|
# Organization Invitations
|
||||||
|
organizationInvitations(organizationId: ID!, page: Int, pageSize: Int): OrganizationInvitationsResponse!
|
||||||
|
|
||||||
|
# User Organizations (organizations where the current user is a member)
|
||||||
|
myOrganizations(page: Int, pageSize: Int): OrganizationsListResponse!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
# Organization management
|
||||||
|
createOrganization(input: CreateOrganizationInput!): OrganizationResponse!
|
||||||
|
updateOrganization(input: UpdateOrganizationInput!): OrganizationResponse!
|
||||||
|
deleteOrganization(id: ID!): OrganizationResponse!
|
||||||
|
|
||||||
|
# Organization membership
|
||||||
|
addUserToOrganization(input: AddUserToOrganizationInput!): OrganizationResponse!
|
||||||
|
removeUserFromOrganization(input: RemoveUserFromOrganizationInput!): OrganizationResponse!
|
||||||
|
updateUserOrganizationRoles(input: UpdateUserOrganizationRolesInput!): OrganizationResponse!
|
||||||
|
|
||||||
|
# Organization invitations
|
||||||
|
createOrganizationInvitation(input: CreateOrganizationInvitationInput!): OrganizationInvitationResponse!
|
||||||
|
resendOrganizationInvitation(invitationId: ID!): OrganizationInvitationResponse!
|
||||||
|
revokeOrganizationInvitation(invitationId: ID!): OrganizationInvitationResponse!
|
||||||
|
acceptOrganizationInvitation(invitationId: ID!): OrganizationInvitationResponse!
|
||||||
|
}
|
||||||
7
src/graphql/features/Organization/index.ts
Normal file
7
src/graphql/features/Organization/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import typeDefs from "./Organization.graphql";
|
||||||
|
import { organizationResolvers } from "./resolver";
|
||||||
|
|
||||||
|
export const Organization = {
|
||||||
|
typeDefs,
|
||||||
|
resolvers: organizationResolvers,
|
||||||
|
};
|
||||||
597
src/graphql/features/Organization/organizationService.ts
Normal file
597
src/graphql/features/Organization/organizationService.ts
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import {
|
||||||
|
AddUserToOrganizationInput,
|
||||||
|
CreateOrganizationInput,
|
||||||
|
CreateOrganizationInvitationInput,
|
||||||
|
LogtoOrganization,
|
||||||
|
OrganizationInvitation,
|
||||||
|
OrganizationPermission,
|
||||||
|
OrganizationRole,
|
||||||
|
OrganizationUser,
|
||||||
|
RemoveUserFromOrganizationInput,
|
||||||
|
UpdateOrganizationInput,
|
||||||
|
UpdateUserOrganizationRolesInput,
|
||||||
|
} from "../../../types/Organization";
|
||||||
|
|
||||||
|
// Cache pour le token d'accès (réutilise la même logique que les autres services)
|
||||||
|
let accessTokenCache: { token: string; expiry: number } | null = null;
|
||||||
|
|
||||||
|
async function getLogtoAccessToken(): Promise<string> {
|
||||||
|
if (accessTokenCache && Date.now() < accessTokenCache.expiry) {
|
||||||
|
return accessTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_id = process.env.LOGTO_APP_ID;
|
||||||
|
const client_secret = process.env.LOGTO_APP_SECRET;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/oidc/token`,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: client_id || "",
|
||||||
|
client_secret: client_secret || "",
|
||||||
|
resource: process.env.LOGTO_RESOURCE || "",
|
||||||
|
scope: "all",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
validateStatus: status => status < 500,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error("Erreur HTTP lors de l'authentification:", {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
data: response.data,
|
||||||
|
});
|
||||||
|
throw new GraphQLError("Échec de l'authentification avec Logto", {
|
||||||
|
extensions: { code: "LOGTO_AUTH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { access_token, expires_in } = response.data;
|
||||||
|
if (!access_token) {
|
||||||
|
throw new GraphQLError("Aucun token d'accès reçu de Logto", {
|
||||||
|
extensions: { code: "LOGTO_TOKEN_MISSING" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache le token avec une marge de sécurité de 60 secondes
|
||||||
|
accessTokenCache = {
|
||||||
|
token: access_token,
|
||||||
|
expiry: Date.now() + (expires_in - 60) * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
return access_token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'authentification Logto:", error);
|
||||||
|
throw new GraphQLError("Impossible de s'authentifier avec Logto", {
|
||||||
|
extensions: { code: "LOGTO_AUTH_ERROR" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test function to check if organizations API is available
|
||||||
|
export async function testOrganizationsAPI(): Promise<{ available: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
// Test with a simple GET request to see if the endpoint exists
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: "1",
|
||||||
|
page_size: "1",
|
||||||
|
},
|
||||||
|
timeout: 5000,
|
||||||
|
validateStatus: () => true, // Don't throw for any status code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message:
|
||||||
|
"L'API des organisations n'est pas disponible sur cette instance Logto. Cette fonctionnalité peut nécessiter un plan premium ou une activation spécifique dans votre console Logto.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 403) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message:
|
||||||
|
"Accès refusé à l'API des organisations. Vérifiez les permissions de votre application Logto ou votre plan.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
message: "L'API des organisations est disponible et accessible.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: `L'API des organisations a retourné un statut inattendu: ${response.status}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.code === "ECONNREFUSED") {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: "Impossible de se connecter au serveur Logto. Vérifiez votre configuration LOGTO_ENDPOINT.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: "L'API des organisations n'est pas disponible sur cette instance Logto.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: `Erreur lors du test de l'API: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organizations CRUD
|
||||||
|
export async function getOrganizations(
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ organizations: LogtoOrganization[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${process.env.LOGTO_ENDPOINT}/api/organizations`;
|
||||||
|
const params = {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
organizations: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// Vérifier si c'est une erreur 404 (fonctionnalité non disponible)
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
throw new GraphQLError(
|
||||||
|
"L'API des organisations n'est pas disponible sur cette instance Logto. Vérifiez que cette fonctionnalité est activée dans votre console Logto.",
|
||||||
|
{
|
||||||
|
extensions: { code: "ORGANIZATIONS_NOT_AVAILABLE" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Impossible de récupérer les organisations", {
|
||||||
|
extensions: { code: "ORGANIZATIONS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrganization(id: string): Promise<LogtoOrganization | null> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new GraphQLError("Impossible de récupérer l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrganization(input: CreateOrganizationInput): Promise<LogtoOrganization> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${process.env.LOGTO_ENDPOINT}/api/organizations`, input, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// Vérifier si c'est une erreur 404 (fonctionnalité non disponible)
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
throw new GraphQLError(
|
||||||
|
"L'API des organisations n'est pas disponible sur cette instance Logto. Vérifiez que cette fonctionnalité est activée dans votre console Logto.",
|
||||||
|
{
|
||||||
|
extensions: { code: "ORGANIZATIONS_NOT_AVAILABLE" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Impossible de créer l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_CREATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOrganization(input: UpdateOrganizationInput): Promise<LogtoOrganization> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
const { id, ...updateData } = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${id}`, updateData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de mettre à jour l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_UPDATE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteOrganization(id: string): Promise<void> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/organizations/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de supprimer l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_DELETION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization Users
|
||||||
|
export async function getOrganizationUsers(
|
||||||
|
organizationId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ users: OrganizationUser[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations/${organizationId}/users`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de récupérer les utilisateurs de l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_USERS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addUserToOrganization(input: AddUserToOrganizationInput): Promise<void> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organizations/${input.organizationId}/users`,
|
||||||
|
{
|
||||||
|
userIds: [input.userId],
|
||||||
|
organizationRoleIds: input.organizationRoleIds || [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible d'ajouter l'utilisateur à l'organisation", {
|
||||||
|
extensions: { code: "ADD_USER_TO_ORGANIZATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeUserFromOrganization(input: RemoveUserFromOrganizationInput): Promise<void> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organizations/${input.organizationId}/users/${input.userId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de supprimer l'utilisateur de l'organisation", {
|
||||||
|
extensions: { code: "REMOVE_USER_FROM_ORGANIZATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserOrganizationRoles(input: UpdateUserOrganizationRolesInput): Promise<void> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organizations/${input.organizationId}/users/${input.userId}/roles`,
|
||||||
|
{
|
||||||
|
organizationRoleIds: input.organizationRoleIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de mettre à jour les rôles de l'utilisateur dans l'organisation", {
|
||||||
|
extensions: { code: "UPDATE_USER_ORGANIZATION_ROLES_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization Roles
|
||||||
|
export async function getOrganizationRoles(
|
||||||
|
organizationId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ roles: OrganizationRole[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations/${organizationId}/roles`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
roles: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de récupérer les rôles de l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_ROLES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization Permissions
|
||||||
|
export async function getOrganizationPermissions(
|
||||||
|
organizationId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ permissions: OrganizationPermission[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations/${organizationId}/scopes`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de récupérer les permissions de l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_PERMISSIONS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization Invitations
|
||||||
|
export async function getOrganizationInvitations(
|
||||||
|
organizationId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ invitations: OrganizationInvitation[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/organizations/${organizationId}/invitations`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitations: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de récupérer les invitations de l'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_INVITATIONS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrganizationInvitation(
|
||||||
|
input: CreateOrganizationInvitationInput
|
||||||
|
): Promise<OrganizationInvitation> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organizations/${input.organizationId}/invitations`,
|
||||||
|
{
|
||||||
|
invitee: input.invitee,
|
||||||
|
organizationRoleIds: input.organizationRoleIds || [],
|
||||||
|
messagePayload: input.messagePayload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de créer l'invitation d'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_INVITATION_CREATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resendOrganizationInvitation(invitationId: string): Promise<OrganizationInvitation> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organization-invitations/${invitationId}/message`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de renvoyer l'invitation d'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_INVITATION_RESEND_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revokeOrganizationInvitation(invitationId: string): Promise<void> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/organization-invitations/${invitationId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de révoquer l'invitation d'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_INVITATION_REVOKE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acceptOrganizationInvitation(invitationId: string): Promise<OrganizationInvitation> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/organization-invitations/${invitationId}/status`,
|
||||||
|
{
|
||||||
|
status: "Accepted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible d'accepter l'invitation d'organisation", {
|
||||||
|
extensions: { code: "ORGANIZATION_INVITATION_ACCEPT_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get organizations for current user
|
||||||
|
export async function getUserOrganizations(
|
||||||
|
userId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
): Promise<{ organizations: LogtoOrganization[]; totalCount: number }> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/organizations`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizations: response.data || [],
|
||||||
|
totalCount: parseInt(response.headers["total-number"] || "0"),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Impossible de récupérer les organisations de l'utilisateur", {
|
||||||
|
extensions: { code: "USER_ORGANIZATIONS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
424
src/graphql/features/Organization/resolver.ts
Normal file
424
src/graphql/features/Organization/resolver.ts
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import {
|
||||||
|
AddUserToOrganizationInput,
|
||||||
|
CreateOrganizationInput,
|
||||||
|
CreateOrganizationInvitationInput,
|
||||||
|
OrganizationAPITestResponse,
|
||||||
|
OrganizationInvitationResponse,
|
||||||
|
OrganizationInvitationsResponse,
|
||||||
|
OrganizationPermissionsResponse,
|
||||||
|
OrganizationResponse,
|
||||||
|
OrganizationRolesResponse,
|
||||||
|
OrganizationsListResponse,
|
||||||
|
OrganizationUsersResponse,
|
||||||
|
RemoveUserFromOrganizationInput,
|
||||||
|
UpdateOrganizationInput,
|
||||||
|
UpdateUserOrganizationRolesInput,
|
||||||
|
} from "../../../types/Organization";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import {
|
||||||
|
acceptOrganizationInvitation,
|
||||||
|
addUserToOrganization,
|
||||||
|
createOrganization,
|
||||||
|
createOrganizationInvitation,
|
||||||
|
deleteOrganization,
|
||||||
|
getOrganization,
|
||||||
|
getOrganizationInvitations,
|
||||||
|
getOrganizationPermissions,
|
||||||
|
getOrganizationRoles,
|
||||||
|
getOrganizations,
|
||||||
|
getOrganizationUsers,
|
||||||
|
getUserOrganizations,
|
||||||
|
removeUserFromOrganization,
|
||||||
|
resendOrganizationInvitation,
|
||||||
|
revokeOrganizationInvitation,
|
||||||
|
testOrganizationsAPI,
|
||||||
|
updateOrganization,
|
||||||
|
updateUserOrganizationRoles,
|
||||||
|
} from "./organizationService";
|
||||||
|
|
||||||
|
export const organizationResolvers = {
|
||||||
|
Query: {
|
||||||
|
async testOrganizationsAPI(): Promise<OrganizationAPITestResponse> {
|
||||||
|
try {
|
||||||
|
const result = await testOrganizationsAPI();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans testOrganizationsAPI resolver:", error);
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: error instanceof Error ? error.message : "Erreur lors du test de l'API",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organizations(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { page?: number; pageSize?: number }
|
||||||
|
): Promise<OrganizationsListResponse> {
|
||||||
|
try {
|
||||||
|
const { page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getOrganizations(page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
organizations: result.organizations,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organizations resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération des organisations",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organization(_parent: unknown, args: { id: string }): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
const organization = await getOrganization(args.id);
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Organisation non trouvée",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
organization,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération de l'organisation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organizationUsers(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { organizationId: string; page?: number; pageSize?: number }
|
||||||
|
): Promise<OrganizationUsersResponse> {
|
||||||
|
try {
|
||||||
|
const { organizationId, page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getOrganizationUsers(organizationId, page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
users: result.users,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organizationUsers resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération des utilisateurs",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organizationRoles(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { organizationId: string; page?: number; pageSize?: number }
|
||||||
|
): Promise<OrganizationRolesResponse> {
|
||||||
|
try {
|
||||||
|
const { organizationId, page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getOrganizationRoles(organizationId, page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
roles: result.roles,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organizationRoles resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération des rôles",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organizationPermissions(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { organizationId: string; page?: number; pageSize?: number }
|
||||||
|
): Promise<OrganizationPermissionsResponse> {
|
||||||
|
try {
|
||||||
|
const { organizationId, page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getOrganizationPermissions(organizationId, page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
permissions: result.permissions,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organizationPermissions resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération des permissions",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async organizationInvitations(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { organizationId: string; page?: number; pageSize?: number }
|
||||||
|
): Promise<OrganizationInvitationsResponse> {
|
||||||
|
try {
|
||||||
|
const { organizationId, page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getOrganizationInvitations(organizationId, page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invitations: result.invitations,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans organizationInvitations resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la récupération des invitations",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async myOrganizations(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { page?: number; pageSize?: number },
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<OrganizationsListResponse> {
|
||||||
|
try {
|
||||||
|
if (!context.user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Authentification requise",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page = 1, pageSize = 10 } = args;
|
||||||
|
const result = await getUserOrganizations(context.user.id, page, pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
organizations: result.organizations,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans myOrganizations resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
error instanceof GraphQLError ? error.message : "Erreur lors de la récupération de vos organisations",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
async createOrganization(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: CreateOrganizationInput }
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
const organization = await createOrganization(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
organization,
|
||||||
|
message: "Organisation créée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans createOrganization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la création de l'organisation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateOrganization(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: UpdateOrganizationInput }
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
const organization = await updateOrganization(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
organization,
|
||||||
|
message: "Organisation mise à jour avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans updateOrganization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la mise à jour de l'organisation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteOrganization(_parent: unknown, args: { id: string }): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
await deleteOrganization(args.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Organisation supprimée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans deleteOrganization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la suppression de l'organisation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addUserToOrganization(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: AddUserToOrganizationInput }
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
await addUserToOrganization(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Utilisateur ajouté à l'organisation avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans addUserToOrganization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de l'ajout de l'utilisateur",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeUserFromOrganization(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: RemoveUserFromOrganizationInput }
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
await removeUserFromOrganization(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Utilisateur supprimé de l'organisation avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans removeUserFromOrganization resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la suppression de l'utilisateur",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUserOrganizationRoles(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: UpdateUserOrganizationRolesInput }
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
try {
|
||||||
|
await updateUserOrganizationRoles(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Rôles de l'utilisateur mis à jour avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans updateUserOrganizationRoles resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la mise à jour des rôles",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createOrganizationInvitation(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { input: CreateOrganizationInvitationInput }
|
||||||
|
): Promise<OrganizationInvitationResponse> {
|
||||||
|
try {
|
||||||
|
const invitation = await createOrganizationInvitation(args.input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invitation,
|
||||||
|
message: "Invitation créée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans createOrganizationInvitation resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la création de l'invitation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async resendOrganizationInvitation(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { invitationId: string }
|
||||||
|
): Promise<OrganizationInvitationResponse> {
|
||||||
|
try {
|
||||||
|
const invitation = await resendOrganizationInvitation(args.invitationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invitation,
|
||||||
|
message: "Invitation renvoyée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans resendOrganizationInvitation resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors du renvoi de l'invitation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async revokeOrganizationInvitation(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { invitationId: string }
|
||||||
|
): Promise<OrganizationInvitationResponse> {
|
||||||
|
try {
|
||||||
|
await revokeOrganizationInvitation(args.invitationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Invitation révoquée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans revokeOrganizationInvitation resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de la révocation de l'invitation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async acceptOrganizationInvitation(
|
||||||
|
_parent: unknown,
|
||||||
|
args: { invitationId: string }
|
||||||
|
): Promise<OrganizationInvitationResponse> {
|
||||||
|
try {
|
||||||
|
const invitation = await acceptOrganizationInvitation(args.invitationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invitation,
|
||||||
|
message: "Invitation acceptée avec succès",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans acceptOrganizationInvitation resolver:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof GraphQLError ? error.message : "Erreur lors de l'acceptation de l'invitation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
20
src/graphql/features/Resource/Resource.graphql
Normal file
20
src/graphql/features/Resource/Resource.graphql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
type Resource {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
indicator: String!
|
||||||
|
scopes: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
resources: [Resource!]!
|
||||||
|
resource(id: ID!): Resource!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createResource(name: String!, indicator: String!, scopes: [String!]!, description: String): Resource!
|
||||||
|
updateResource(id: ID!, name: String!, indicator: String!, scopes: [String!]!, description: String): Resource!
|
||||||
|
deleteResource(id: ID!): Boolean!
|
||||||
|
assignScopeToResource(resourceId: ID!, scope: String!): Boolean!
|
||||||
|
removeScopeFromResource(resourceId: ID!, scope: String!): Boolean!
|
||||||
|
}
|
||||||
16
src/graphql/features/Resource/ResourceScope.graphql
Normal file
16
src/graphql/features/Resource/ResourceScope.graphql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
type ResourceScope {
|
||||||
|
id: ID!
|
||||||
|
resourceId: String!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
createdAt: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
resourceScopes(resourceId: ID!): [ResourceScope!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createResourceScope(resourceId: ID!, name: String!, description: String): ResourceScope!
|
||||||
|
deleteResourceScope(resourceId: ID!, scopeId: ID!): Boolean!
|
||||||
|
}
|
||||||
21
src/graphql/features/Resource/index.ts
Normal file
21
src/graphql/features/Resource/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { resourceResolvers } from "./resolver";
|
||||||
|
import typeDefs from "./Resource.graphql";
|
||||||
|
import resourceScopeTypeDefs from "./ResourceScope.graphql";
|
||||||
|
import { resourceScopeResolvers } from "./scopeResolver";
|
||||||
|
|
||||||
|
// Fusionne les Query et Mutation des deux resolvers
|
||||||
|
export const Resource = {
|
||||||
|
typeDefs: [typeDefs, resourceScopeTypeDefs],
|
||||||
|
resolvers: [
|
||||||
|
{
|
||||||
|
Query: {
|
||||||
|
...resourceResolvers.Query,
|
||||||
|
...resourceScopeResolvers.Query,
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
...resourceResolvers.Mutation,
|
||||||
|
...resourceScopeResolvers.Mutation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
58
src/graphql/features/Resource/resolver.ts
Normal file
58
src/graphql/features/Resource/resolver.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { assignScopeToResource, removeScopeFromResource } from "../Role/permissionService";
|
||||||
|
import { createResource, deleteResource, getResource, listResources, updateResource } from "./resourceService";
|
||||||
|
|
||||||
|
export const resourceResolvers = {
|
||||||
|
Query: {
|
||||||
|
resources: async (_parent: unknown, _args: unknown, context: GraphQLContext) => {
|
||||||
|
return listResources(context);
|
||||||
|
},
|
||||||
|
resource: async (_parent: unknown, { id }: { id: string }, context: GraphQLContext) => {
|
||||||
|
return getResource(id, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
createResource: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
indicator,
|
||||||
|
scopes,
|
||||||
|
description,
|
||||||
|
}: { name: string; indicator: string; scopes: string[]; description?: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return createResource(name, indicator, scopes, description ?? null, context);
|
||||||
|
},
|
||||||
|
updateResource: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
indicator,
|
||||||
|
scopes,
|
||||||
|
description,
|
||||||
|
}: { id: string; name: string; indicator: string; scopes: string[]; description?: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return updateResource(id, name, indicator, scopes, description ?? null, context);
|
||||||
|
},
|
||||||
|
deleteResource: async (_parent: unknown, { id }: { id: string }, context: GraphQLContext) => {
|
||||||
|
return deleteResource(id, context);
|
||||||
|
},
|
||||||
|
assignScopeToResource: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ resourceId, scope }: { resourceId: string; scope: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return assignScopeToResource(resourceId, scope, context);
|
||||||
|
},
|
||||||
|
removeScopeFromResource: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ resourceId, scope }: { resourceId: string; scope: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return removeScopeFromResource(resourceId, scope, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
103
src/graphql/features/Resource/resourceService.ts
Normal file
103
src/graphql/features/Resource/resourceService.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import { Resource } from "../../../types/Resource";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { getLogtoAccessToken } from "../User/resolver";
|
||||||
|
|
||||||
|
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||||
|
return context.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listResources(context: GraphQLContext): Promise<Resource[]> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/resources`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return (response.data as Resource[]).map(resource => ({
|
||||||
|
...resource,
|
||||||
|
scopes: Array.isArray(resource.scopes) ? resource.scopes : [],
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération des ressources", {
|
||||||
|
extensions: { code: "RESOURCES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResource(id: string, context: GraphQLContext): Promise<Resource> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/resources/${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const resource = response.data as Resource;
|
||||||
|
return {
|
||||||
|
...resource,
|
||||||
|
scopes: Array.isArray(resource.scopes) ? resource.scopes : [],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération de la ressource", {
|
||||||
|
extensions: { code: "RESOURCE_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createResource(
|
||||||
|
name: string,
|
||||||
|
indicator: string,
|
||||||
|
_scopes: string[], // ignoré
|
||||||
|
description: string | null,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<Resource> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/resources`,
|
||||||
|
{ name, indicator, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
return response.data as Resource;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la création de la ressource", {
|
||||||
|
extensions: { code: "CREATE_RESOURCE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateResource(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
indicator: string,
|
||||||
|
_scopes: string[], // ignoré
|
||||||
|
description: string | null,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<Resource> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/resources/${id}`,
|
||||||
|
{ name, indicator, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
return response.data as Resource;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la mise à jour de la ressource", {
|
||||||
|
extensions: { code: "UPDATE_RESOURCE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteResource(id: string, context: GraphQLContext): Promise<boolean> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/resources/${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression de la ressource", {
|
||||||
|
extensions: { code: "DELETE_RESOURCE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/graphql/features/Resource/scopeResolver.ts
Normal file
28
src/graphql/features/Resource/scopeResolver.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { createResourceScope, deleteResourceScope, listResourceScopes } from "./scopeService";
|
||||||
|
|
||||||
|
export const resourceScopeResolvers = {
|
||||||
|
Query: {
|
||||||
|
resourceScopes: async (_parent: unknown, { resourceId }: { resourceId: string }, context: GraphQLContext) => {
|
||||||
|
return listResourceScopes(resourceId, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
createResourceScope: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ resourceId, name, description }: { resourceId: string; name: string; description?: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[DEBUG resolver] createResourceScope called", { resourceId, name, description });
|
||||||
|
return createResourceScope(resourceId, name, description ?? null, context);
|
||||||
|
},
|
||||||
|
deleteResourceScope: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ resourceId, scopeId }: { resourceId: string; scopeId: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return deleteResourceScope(resourceId, scopeId, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
67
src/graphql/features/Resource/scopeService.ts
Normal file
67
src/graphql/features/Resource/scopeService.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import { ResourceScope } from "../../../types/ResourceScope";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { getLogtoAccessToken } from "../User/resolver";
|
||||||
|
|
||||||
|
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||||
|
return context.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listResourceScopes(resourceId: string, context: GraphQLContext): Promise<ResourceScope[]> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/resources/${resourceId}/scopes`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération des permissions de la ressource", {
|
||||||
|
extensions: { code: "RESOURCE_SCOPES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createResourceScope(
|
||||||
|
resourceId: string,
|
||||||
|
name: string,
|
||||||
|
description: string | null,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<any> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
// LOG AVANT REQUETE
|
||||||
|
console.error("[DEBUG resolver] createResourceScope called", { resourceId, name, description });
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/resources/${resourceId}/scopes`,
|
||||||
|
{ name, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
// LOG APRES REQUETE
|
||||||
|
console.error("[DEBUG createResourceScope] Logto response:", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("[ERROR createResourceScope]", e?.response?.data || e);
|
||||||
|
throw new GraphQLError("Erreur lors de la création de la permission de ressource", {
|
||||||
|
extensions: { code: "CREATE_RESOURCE_SCOPE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteResourceScope(
|
||||||
|
resourceId: string,
|
||||||
|
scopeId: string,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<boolean> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/resources/${resourceId}/scopes/${scopeId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression de la permission de ressource", {
|
||||||
|
extensions: { code: "DELETE_RESOURCE_SCOPE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/graphql/features/Role/Role.graphql
Normal file
33
src/graphql/features/Role/Role.graphql
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Role GraphQL schema
|
||||||
|
|
||||||
|
type Permission {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Role {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
permissions: [Permission!]!
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
roles: [Role!]!
|
||||||
|
userRoles(userId: ID!): [Role!]!
|
||||||
|
permissions: [Permission!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
assignRoleToUser(userId: ID!, roleId: ID!): Boolean!
|
||||||
|
removeRoleFromUser(userId: ID!, roleId: ID!): Boolean!
|
||||||
|
createRole(name: String!, description: String, permissionIds: [ID!]): Role!
|
||||||
|
updateRole(id: ID!, name: String, description: String, permissionIds: [ID!]): Role!
|
||||||
|
deleteRole(id: ID!): Boolean!
|
||||||
|
assignPermissionToRole(roleId: ID!, permissionId: ID!): Boolean!
|
||||||
|
removePermissionFromRole(roleId: ID!, permissionId: ID!): Boolean!
|
||||||
|
createPermission(name: String!, description: String): Permission!
|
||||||
|
deletePermission(id: ID!): Boolean!
|
||||||
|
}
|
||||||
7
src/graphql/features/Role/index.ts
Normal file
7
src/graphql/features/Role/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import typeDefs from "./Role.graphql";
|
||||||
|
import { resolvers } from "./resolver";
|
||||||
|
|
||||||
|
export const Role = {
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
};
|
||||||
131
src/graphql/features/Role/permissionService.ts
Normal file
131
src/graphql/features/Role/permissionService.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { getLogtoAccessToken } from "../User/resolver";
|
||||||
|
|
||||||
|
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||||
|
return context.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPermissions(context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/permissions`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération des permissions", {
|
||||||
|
extensions: { code: "PERMISSIONS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPermission(name: string, description: string | null, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/permissions`,
|
||||||
|
{ name, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new GraphQLError("Erreur lors de la création de la permission", {
|
||||||
|
extensions: { code: "CREATE_PERMISSION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePermission(id: string, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/scopes/${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression de la permission", {
|
||||||
|
extensions: { code: "DELETE_PERMISSION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignPermissionToRole(roleId: string, permissionId: string, context: GraphQLContext) {
|
||||||
|
// permissionId est un scopeId (ResourceScope)
|
||||||
|
// Pour compatibilité avec l'API Logto actuelle, on envoie scopeIds
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/roles/${roleId}/scopes`,
|
||||||
|
{ scopeIds: [permissionId] },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[DEBUG assignPermissionToRole] Logto response:", res.data);
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Si l'erreur est que la permission existe déjà, on considère comme succès
|
||||||
|
if (e?.response?.data?.code === "role.scope_exists") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.error("[ERROR assignPermissionToRole]", e?.response?.data || e);
|
||||||
|
throw new GraphQLError("Erreur lors de l'assignation du scope au rôle", {
|
||||||
|
extensions: { code: "ASSIGN_SCOPE_TO_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePermissionFromRole(roleId: string, permissionId: string, context: GraphQLContext) {
|
||||||
|
// permissionId est en fait un scopeId (ResourceScope)
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/roles/${roleId}/scopes/${permissionId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression du scope du rôle", {
|
||||||
|
extensions: { code: "REMOVE_SCOPE_FROM_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignScopeToResource(
|
||||||
|
resourceId: string,
|
||||||
|
name: string,
|
||||||
|
description: string | null,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<boolean> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/resources/${resourceId}/scopes`,
|
||||||
|
{ name, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de l'assignation du scope à la ressource", {
|
||||||
|
extensions: { code: "ASSIGN_SCOPE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeScopeFromResource(
|
||||||
|
resourceId: string,
|
||||||
|
scope: string,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<boolean> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/resources/${resourceId}/scopes/${scope}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression du scope de la ressource", {
|
||||||
|
extensions: { code: "REMOVE_SCOPE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/graphql/features/Role/resolver.ts
Normal file
138
src/graphql/features/Role/resolver.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import {
|
||||||
|
assignPermissionToRole,
|
||||||
|
createPermission,
|
||||||
|
deletePermission,
|
||||||
|
listPermissions,
|
||||||
|
removePermissionFromRole,
|
||||||
|
} from "./permissionService";
|
||||||
|
import {
|
||||||
|
assignRoleToUser,
|
||||||
|
createRole,
|
||||||
|
deleteRole,
|
||||||
|
getUserRoles,
|
||||||
|
listRoles,
|
||||||
|
removeRoleFromUser,
|
||||||
|
updateRole,
|
||||||
|
} from "./roleService";
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
interface Role {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
permissions?: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolvers = {
|
||||||
|
Query: {
|
||||||
|
roles: async (_parent: unknown, _args: unknown, context: GraphQLContext) => {
|
||||||
|
const roles = await listRoles(context);
|
||||||
|
return (roles as Role[]).map(role => ({
|
||||||
|
...role,
|
||||||
|
permissions: Array.isArray(role.permissions) ? role.permissions : [],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
userRoles: async (_parent: unknown, { userId }: { userId: string }, context: GraphQLContext) => {
|
||||||
|
const roles = await getUserRoles(userId, context);
|
||||||
|
return (roles as Role[]).map(role => ({
|
||||||
|
...role,
|
||||||
|
permissions: Array.isArray(role.permissions) ? role.permissions : [],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
permissions: async (_parent: unknown, _args: unknown, context: GraphQLContext) => {
|
||||||
|
return listPermissions(context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
assignRoleToUser: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ userId, roleId }: { userId: string; roleId: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return assignRoleToUser(userId, roleId, context);
|
||||||
|
},
|
||||||
|
removeRoleFromUser: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ userId, roleId }: { userId: string; roleId: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return removeRoleFromUser(userId, roleId, context);
|
||||||
|
},
|
||||||
|
createRole: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ name, description, permissionIds }: { name: string; description?: string; permissionIds?: string[] },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
// Crée le rôle
|
||||||
|
const role = await createRole(name, description ?? null, context);
|
||||||
|
// Assigne les permissions si fourni
|
||||||
|
if (permissionIds && permissionIds.length > 0) {
|
||||||
|
for (const permissionId of permissionIds) {
|
||||||
|
await assignPermissionToRole(role.id, permissionId, context);
|
||||||
|
}
|
||||||
|
// Recharge le rôle pour inclure les permissions à jour
|
||||||
|
// (optionnel, dépend de l'API Logto)
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
},
|
||||||
|
updateRole: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissionIds,
|
||||||
|
}: { id: string; name?: string; description?: string; permissionIds?: string[] },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
const role = await updateRole(id, context, name, description);
|
||||||
|
if (permissionIds) {
|
||||||
|
const current = await listRoles(context);
|
||||||
|
const found = (current as Role[]).find(r => r.id === id);
|
||||||
|
if (found && found.permissions) {
|
||||||
|
for (const perm of found.permissions) {
|
||||||
|
if (!permissionIds.includes(perm.id)) {
|
||||||
|
await removePermissionFromRole(id, perm.id, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const permissionId of permissionIds) {
|
||||||
|
await assignPermissionToRole(id, permissionId, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
},
|
||||||
|
deleteRole: async (_parent: unknown, { id }: { id: string }, context: GraphQLContext) => {
|
||||||
|
return deleteRole(id, context);
|
||||||
|
},
|
||||||
|
createPermission: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ name, description }: { name: string; description?: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return createPermission(name, description ?? null, context);
|
||||||
|
},
|
||||||
|
deletePermission: async (_parent: unknown, { id }: { id: string }, context: GraphQLContext) => {
|
||||||
|
return deletePermission(id, context);
|
||||||
|
},
|
||||||
|
assignPermissionToRole: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ roleId, permissionId }: { roleId: string; permissionId: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return assignPermissionToRole(roleId, permissionId, context);
|
||||||
|
},
|
||||||
|
removePermissionFromRole: async (
|
||||||
|
_parent: unknown,
|
||||||
|
{ roleId, permissionId }: { roleId: string; permissionId: string },
|
||||||
|
context: GraphQLContext
|
||||||
|
) => {
|
||||||
|
return removePermissionFromRole(roleId, permissionId, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
168
src/graphql/features/Role/roleService.ts
Normal file
168
src/graphql/features/Role/roleService.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import { Role } from "../../../types/Role";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
import { getLogtoAccessToken } from "../User/resolver";
|
||||||
|
|
||||||
|
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||||
|
return context.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRolePermissions(
|
||||||
|
roleId: string,
|
||||||
|
token: string
|
||||||
|
): Promise<{ id: string; name: string; description?: string }[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/roles/${roleId}/scopes`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
// Les scopes sont des permissions
|
||||||
|
return Array.isArray(response.data)
|
||||||
|
? response.data.map((scope: any) => ({
|
||||||
|
id: scope.id,
|
||||||
|
name: scope.name,
|
||||||
|
description: scope.description,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRoles(context: GraphQLContext): Promise<Role[]> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/roles`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const roles = response.data as Role[];
|
||||||
|
// Enrichit chaque rôle avec ses permissions (scopes)
|
||||||
|
return await Promise.all(
|
||||||
|
roles.map(async role => ({
|
||||||
|
...role,
|
||||||
|
permissions: await fetchRolePermissions(role.id, token),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération des rôles", {
|
||||||
|
extensions: { code: "ROLES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserRoles(userId: string, context: GraphQLContext): Promise<Role[]> {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/roles`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const roles = response.data as Role[];
|
||||||
|
return await Promise.all(
|
||||||
|
roles.map(async role => ({
|
||||||
|
...role,
|
||||||
|
permissions: await fetchRolePermissions(role.id, token),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la récupération des rôles de l'utilisateur", {
|
||||||
|
extensions: { code: "USER_ROLES_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignRoleToUser(userId: string, roleId: string, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/roles`,
|
||||||
|
{ roleIds: [roleId] }, // Correction ici
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
// Log détaillé côté serveur
|
||||||
|
console.error("Erreur assignRoleToUser:", err?.response?.data || err);
|
||||||
|
let message = "Erreur lors de l'assignation du rôle";
|
||||||
|
if (err?.response?.data?.message) {
|
||||||
|
message += `: ${err.response.data.message}`;
|
||||||
|
} else if (err?.message) {
|
||||||
|
message += `: ${err.message}`;
|
||||||
|
}
|
||||||
|
throw new GraphQLError(message, {
|
||||||
|
extensions: { code: "ASSIGN_ROLE_FAILED", logto: err?.response?.data },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeRoleFromUser(userId: string, roleId: string, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/roles/${roleId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression du rôle", {
|
||||||
|
extensions: { code: "REMOVE_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRole(name: string, description: string | null, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/roles`,
|
||||||
|
{ name, description, type: "User" },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
const role = response.data;
|
||||||
|
return {
|
||||||
|
...role,
|
||||||
|
permissions: await fetchRolePermissions(role.id, token),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (axios.isAxiosError(e) && e.response?.status === 409) {
|
||||||
|
throw new GraphQLError("Un rôle avec ce nom existe déjà", {
|
||||||
|
extensions: { code: "ROLE_ALREADY_EXISTS" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new GraphQLError("Erreur lors de la création du rôle", {
|
||||||
|
extensions: { code: "CREATE_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRole(id: string, context: GraphQLContext, name?: string, description?: string) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/roles/${id}`,
|
||||||
|
{ name, description },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
const role = response.data;
|
||||||
|
return {
|
||||||
|
...role,
|
||||||
|
permissions: await fetchRolePermissions(role.id, token),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la mise à jour du rôle", {
|
||||||
|
extensions: { code: "UPDATE_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRole(id: string, context: GraphQLContext) {
|
||||||
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.LOGTO_ENDPOINT}/api/roles/${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new GraphQLError("Erreur lors de la suppression du rôle", {
|
||||||
|
extensions: { code: "DELETE_ROLE_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/graphql/features/User/User.graphql
Normal file
63
src/graphql/features/User/User.graphql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
username: String
|
||||||
|
primaryEmail: String
|
||||||
|
primaryPhone: String
|
||||||
|
name: String
|
||||||
|
avatar: String
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input RegisterInput {
|
||||||
|
username: String
|
||||||
|
primaryEmail: String!
|
||||||
|
primaryPhone: String
|
||||||
|
name: String
|
||||||
|
password: String!
|
||||||
|
customInfo: String # Information personnalisée
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoginInput {
|
||||||
|
primaryEmail: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterResponse {
|
||||||
|
user: User
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
accessToken: String # Token d'accès personnel
|
||||||
|
tokenExpiry: String # Date d'expiration du token
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse {
|
||||||
|
user: User
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
accessToken: String # Token d'accès personnel
|
||||||
|
tokenExpiry: String # Date d'expiration du token
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogoutResponse {
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type PATCacheStats {
|
||||||
|
cacheSize: Int!
|
||||||
|
totalEntries: Int!
|
||||||
|
timestamp: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
me: User
|
||||||
|
users: [User!]!
|
||||||
|
patCacheStats: PATCacheStats # Statistiques du cache PAT (debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
signUp(input: RegisterInput!): RegisterResponse!
|
||||||
|
login(input: LoginInput!): LoginResponse!
|
||||||
|
logout: LogoutResponse!
|
||||||
|
}
|
||||||
7
src/graphql/features/User/index.ts
Normal file
7
src/graphql/features/User/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { resolvers } from "./resolver";
|
||||||
|
import typeDefs from "./User.graphql";
|
||||||
|
|
||||||
|
export const User = {
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
};
|
||||||
726
src/graphql/features/User/resolver.ts
Normal file
726
src/graphql/features/User/resolver.ts
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
|
import axios from "axios";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import {
|
||||||
|
LoginInput,
|
||||||
|
LoginResponse,
|
||||||
|
LogoutResponse,
|
||||||
|
LogtoUser,
|
||||||
|
RegisterInput,
|
||||||
|
RegisterResponse,
|
||||||
|
} from "../../../types/Logto";
|
||||||
|
import { GraphQLContext } from "../../context/types";
|
||||||
|
|
||||||
|
// Cache pour le token d'accès
|
||||||
|
let accessTokenCache: { token: string; expiry: number } | null = null;
|
||||||
|
|
||||||
|
async function getLogtoAccessToken(): Promise<string> {
|
||||||
|
if (accessTokenCache && Date.now() < accessTokenCache.expiry) {
|
||||||
|
return accessTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_id = process.env.LOGTO_APP_ID;
|
||||||
|
const client_secret = process.env.LOGTO_APP_SECRET;
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/oidc/token`,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: client_id || "",
|
||||||
|
client_secret: client_secret || "",
|
||||||
|
resource: process.env.LOGTO_RESOURCE || "",
|
||||||
|
scope: "all",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
validateStatus: status => status < 500,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error("Erreur HTTP lors de l'authentification:", {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
data: response.data,
|
||||||
|
});
|
||||||
|
throw new GraphQLError(`Erreur d'authentification HTTP ${response.status}`, {
|
||||||
|
extensions: { code: "LOGTO_HTTP_ERROR" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = response.data.access_token;
|
||||||
|
const expiresIn = response.data.expires_in;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error("Token manquant dans la réponse:", response.data);
|
||||||
|
throw new GraphQLError("Token d'accès manquant", {
|
||||||
|
extensions: { code: "LOGTO_TOKEN_MISSING" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
accessTokenCache = {
|
||||||
|
token,
|
||||||
|
expiry: Date.now() + (expiresIn || 3600) * 900,
|
||||||
|
};
|
||||||
|
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof GraphQLError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erreur lors de l'obtention du token Logto:", {
|
||||||
|
message: error instanceof Error ? error.message : "Erreur inconnue",
|
||||||
|
code: error instanceof Error && "code" in error ? error.code : "UNKNOWN",
|
||||||
|
endpoint: process.env.LOGTO_ENDPOINT,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new GraphQLError("Erreur d'authentification avec Logto", {
|
||||||
|
extensions: { code: "LOGTO_AUTH_ERROR" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createLogtoUser(userData: RegisterInput): Promise<LogtoUser> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
// Préparer les données utilisateur en filtrant les champs invalides
|
||||||
|
const userPayload: {
|
||||||
|
primaryEmail: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
} = {
|
||||||
|
primaryEmail: userData.primaryEmail,
|
||||||
|
password: userData.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter name s'il est fourni
|
||||||
|
if (userData.name && userData.name.trim()) {
|
||||||
|
userPayload.name = userData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter username seulement s'il est fourni
|
||||||
|
if (userData.username && userData.username.trim()) {
|
||||||
|
userPayload.username = userData.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les données personnalisées
|
||||||
|
if (userData.customInfo && userData.customInfo.trim()) {
|
||||||
|
userPayload.customData = {
|
||||||
|
customInfo: userData.customInfo,
|
||||||
|
registrationSource: "GraphQL API",
|
||||||
|
registrationDate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${process.env.LOGTO_ENDPOINT}/api/users`, userPayload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la création de l'utilisateur:", error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error("Détails de l'erreur Axios:", {
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
data: error.response?.data,
|
||||||
|
config: {
|
||||||
|
url: error.config?.url,
|
||||||
|
method: error.config?.method,
|
||||||
|
data: error.config?.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
const errorCode = error.response?.data?.code;
|
||||||
|
if (errorCode === "user.username_already_in_use") {
|
||||||
|
throw new GraphQLError("Ce nom d'utilisateur est déjà utilisé", {
|
||||||
|
extensions: { code: "USERNAME_ALREADY_EXISTS" },
|
||||||
|
});
|
||||||
|
} else if (errorCode === "user.email_already_in_use") {
|
||||||
|
throw new GraphQLError("Un utilisateur avec cet email existe déjà", {
|
||||||
|
extensions: { code: "USER_ALREADY_EXISTS" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new GraphQLError("Les données fournies ne sont pas valides", {
|
||||||
|
extensions: { code: "USER_ALREADY_EXISTS" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
const errorMessage = error.response?.data?.message || "Données invalides";
|
||||||
|
throw new GraphQLError(`Erreur de validation: ${errorMessage}`, {
|
||||||
|
extensions: { code: "VALIDATION_ERROR" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new GraphQLError("Échec de la création de l'utilisateur", {
|
||||||
|
extensions: { code: "USER_CREATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserByEmail(email: string): Promise<LogtoUser | null> {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users`, {
|
||||||
|
params: {
|
||||||
|
"search_params[primaryEmail]": email,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.length > 0 ? response.data[0] : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la recherche de l'utilisateur:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache pour les associations PAT -> userId pour éviter les recherches répétées
|
||||||
|
// Note: Maintenant géré via le contexte GraphQL (context.patCache)
|
||||||
|
|
||||||
|
async function getUserByPersonalAccessToken(pat: string, context: GraphQLContext): Promise<LogtoUser | null> {
|
||||||
|
// Récupérer l'userId directement depuis le cache du contexte
|
||||||
|
let userId = context.patCache.getUserId(pat);
|
||||||
|
|
||||||
|
// Si pas trouvé dans le cache, essayer de valider le PAT avec l'API Logto
|
||||||
|
if (!userId) {
|
||||||
|
userId = await validatePATWithLogtoAPI(pat, context);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new GraphQLError("Token d'accès personnel invalide ou non reconnu", {
|
||||||
|
extensions: { code: "UNKNOWN_PAT" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const m2mToken = await getLogtoAccessToken();
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users/${userId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de l'utilisateur par PAT:", error);
|
||||||
|
// Si l'utilisateur n'existe plus, nettoyer le cache
|
||||||
|
context.patCache.remove(pat);
|
||||||
|
throw new GraphQLError("Utilisateur associé au token non trouvé", {
|
||||||
|
extensions: { code: "PAT_USER_NOT_FOUND" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour valider un PAT avec l'API Logto et le remettre en cache
|
||||||
|
async function validatePATWithLogtoAPI(pat: string, context: GraphQLContext): Promise<string | null> {
|
||||||
|
const m2mToken = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Rechercher parmi tous les utilisateurs pour trouver celui qui possède ce PAT
|
||||||
|
const usersResponse = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: 1,
|
||||||
|
page_size: 100, // Augmenter la limite pour plus d'efficacité
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of usersResponse.data) {
|
||||||
|
try {
|
||||||
|
const tokensResponse = await axios.get(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${user.id}/personal-access-tokens`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chercher le PAT dans les tokens de cet utilisateur
|
||||||
|
const matchingToken = tokensResponse.data.find(
|
||||||
|
(tokenData: { value: string; name: string }) => tokenData.value === pat
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingToken) {
|
||||||
|
// Remettre en cache pour éviter de refaire cette recherche
|
||||||
|
context.patCache.register(pat, user.id, 24); // 24 heures
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs pour chaque utilisateur et continuer
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la validation du PAT avec l'API Logto:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createPersonalAccessToken(
|
||||||
|
userId: string,
|
||||||
|
context: GraphQLContext
|
||||||
|
): Promise<{ token: string; expiresIn: number; userId: string }> {
|
||||||
|
const m2mToken = await getLogtoAccessToken();
|
||||||
|
const expiresAtTimestamp = Date.now() + 365 * 24 * 60 * 60 * 1000; // 1 an en millisecondes
|
||||||
|
const requestBody = {
|
||||||
|
name: `graphql-pat-${userId}`,
|
||||||
|
expiresAt: expiresAtTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/personal-access-tokens`,
|
||||||
|
requestBody,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { value: token, expiresAt } = response.data;
|
||||||
|
const expiresIn = Math.floor((expiresAt - Date.now()) / 1000); // Convertir en secondes
|
||||||
|
|
||||||
|
// Enregistrer l'association PAT -> userId dans le cache du contexte
|
||||||
|
context.patCache.register(token, userId, 24); // 24 heures
|
||||||
|
|
||||||
|
return { token, expiresIn, userId };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la création du PAT:", error);
|
||||||
|
|
||||||
|
// Gérer le cas où le nom du PAT existe déjà
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 422) {
|
||||||
|
const errorData = error.response.data;
|
||||||
|
if (errorData?.code === "user.personal_access_token_name_exists") {
|
||||||
|
console.warn("PAT avec ce nom existe déjà, tentative de récupération...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Lister les PATs existants pour cet utilisateur
|
||||||
|
const listResponse = await axios.get(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/personal-access-tokens`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface PersonalAccessToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPats: PersonalAccessToken[] = listResponse.data;
|
||||||
|
const patName = `graphql-pat-${userId}`;
|
||||||
|
const existingPat = existingPats.find(pat => pat.name === patName);
|
||||||
|
|
||||||
|
if (existingPat) {
|
||||||
|
// Vérifier si le PAT existe et est encore valide
|
||||||
|
const now = Date.now();
|
||||||
|
const expiresAt = existingPat.expiresAt;
|
||||||
|
|
||||||
|
if (expiresAt > now) {
|
||||||
|
console.warn("PAT existant trouvé et encore valide, réutilisation...");
|
||||||
|
const expiresIn = Math.floor((expiresAt - now) / 1000);
|
||||||
|
|
||||||
|
// Enregistrer l'association PAT -> userId dans le cache du contexte
|
||||||
|
context.patCache.register(existingPat.value, userId, 24);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: existingPat.value,
|
||||||
|
expiresIn,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn("PAT existant expiré, suppression et recréation...");
|
||||||
|
// Supprimer le PAT expiré
|
||||||
|
await axios.delete(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/personal-access-tokens/${existingPat.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recréer le PAT
|
||||||
|
const recreateResponse = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${userId}/personal-access-tokens`,
|
||||||
|
requestBody,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { value: token, expiresAt: newExpiresAt } = recreateResponse.data;
|
||||||
|
const expiresIn = Math.floor((newExpiresAt - Date.now()) / 1000);
|
||||||
|
|
||||||
|
// Enregistrer l'association PAT -> userId dans le cache du contexte
|
||||||
|
context.patCache.register(token, userId, 24);
|
||||||
|
|
||||||
|
return { token, expiresIn, userId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (listError) {
|
||||||
|
console.error("Erreur lors de la récupération des PATs existants:", listError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher plus de détails sur l'erreur pour debug
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error("Détails de l'erreur Axios PAT:", {
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
data: error.response?.data,
|
||||||
|
url: error.config?.url,
|
||||||
|
requestData: error.config?.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Impossible de créer le token d'accès personnel", {
|
||||||
|
extensions: { code: "PAT_CREATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour authentifier un utilisateur avec email/password via l'API Logto
|
||||||
|
async function authenticateUser(email: string, password: string): Promise<LogtoUser | null> {
|
||||||
|
try {
|
||||||
|
const m2mToken = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
// D'abord, récupérer l'utilisateur par email pour obtenir son ID
|
||||||
|
const user = await getUserByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser l'API Logto pour vérifier le mot de passe
|
||||||
|
// Cette API vérifie si le mot de passe est correct pour l'utilisateur donné
|
||||||
|
const verificationPayload = {
|
||||||
|
identifier: {
|
||||||
|
type: "email",
|
||||||
|
value: email,
|
||||||
|
},
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const verificationResponse = await axios.post(
|
||||||
|
`${process.env.LOGTO_ENDPOINT}/api/users/${user.id}/password/verify`,
|
||||||
|
verificationPayload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${m2mToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
validateStatus: status => status < 500, // Accepter les erreurs 4xx pour une gestion propre
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si la vérification réussit (200 ou 204), les identifiants sont corrects
|
||||||
|
if (verificationResponse.status === 200 || verificationResponse.status === 204) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si 401/403, les identifiants sont incorrects
|
||||||
|
if (verificationResponse.status === 401 || verificationResponse.status === 403) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si 404, l'utilisateur n'existe pas ou n'a pas de mot de passe
|
||||||
|
if (verificationResponse.status === 404) {
|
||||||
|
return await authenticateUserFallback(email, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autres erreurs
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// Si l'endpoint n'existe pas, utiliser une méthode alternative
|
||||||
|
if (error.response?.status === 404 && error.config?.url?.includes("/password/verify")) {
|
||||||
|
return await authenticateUserFallback(email, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erreurs d'authentification explicites
|
||||||
|
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres erreurs, on considère que l'authentification a échoué
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode alternative si l'API de vérification directe n'est pas disponible
|
||||||
|
async function authenticateUserFallback(email: string, password: string): Promise<LogtoUser | null> {
|
||||||
|
try {
|
||||||
|
// Utiliser l'endpoint de connexion OIDC pour vérifier les identifiants
|
||||||
|
// Ceci simule une authentification en utilisant le flow Resource Owner Password Credentials
|
||||||
|
const authPayload = new URLSearchParams({
|
||||||
|
grant_type: "password",
|
||||||
|
username: email,
|
||||||
|
password: password,
|
||||||
|
client_id: process.env.LOGTO_APP_ID || "",
|
||||||
|
client_secret: process.env.LOGTO_APP_SECRET || "",
|
||||||
|
scope: "openid profile email",
|
||||||
|
});
|
||||||
|
|
||||||
|
const authResponse = await axios.post(`${process.env.LOGTO_ENDPOINT}/oidc/token`, authPayload, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
validateStatus: status => status < 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si l'authentification réussit, récupérer les détails de l'utilisateur
|
||||||
|
if (authResponse.status === 200 && authResponse.data.access_token) {
|
||||||
|
const user = await getUserByEmail(email);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si 401, les identifiants sont incorrects
|
||||||
|
if (authResponse.status === 401) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 400)) {
|
||||||
|
// Identifiants incorrects
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres erreurs, on considère que l'authentification a échoué
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolvers = {
|
||||||
|
Query: {
|
||||||
|
async me(_: unknown, __: unknown, context: GraphQLContext) {
|
||||||
|
// Priorité 1: Utilisation du Personal Access Token (PAT)
|
||||||
|
if (context.personalAccessToken) {
|
||||||
|
const user = await getUserByPersonalAccessToken(context.personalAccessToken, context);
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GraphQLError("Token d'accès personnel invalide ou expiré", {
|
||||||
|
extensions: { code: "INVALID_PAT" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 2: Utilisation de l'userId (JWT)
|
||||||
|
if (context.userId) {
|
||||||
|
try {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users/${context.userId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de l'utilisateur par userId:", error);
|
||||||
|
throw new GraphQLError("Utilisateur non trouvé", {
|
||||||
|
extensions: { code: "USER_NOT_FOUND" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aucune authentification valide
|
||||||
|
throw new GraphQLError("Non authentifié - token requis", {
|
||||||
|
extensions: { code: "UNAUTHENTICATED" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async users() {
|
||||||
|
const token = await getLogtoAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${process.env.LOGTO_ENDPOINT}/api/users`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des utilisateurs:", error);
|
||||||
|
throw new GraphQLError("Échec de la récupération des utilisateurs", {
|
||||||
|
extensions: { code: "USERS_FETCH_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Query de debug pour les statistiques du cache PAT
|
||||||
|
async patCacheStats(_: unknown, __: unknown, context: GraphQLContext) {
|
||||||
|
const stats = context.patCache.getStats();
|
||||||
|
return {
|
||||||
|
cacheSize: stats.size,
|
||||||
|
totalEntries: stats.entries,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
async signUp(_: unknown, { input }: { input: RegisterInput }, context: GraphQLContext): Promise<RegisterResponse> {
|
||||||
|
try {
|
||||||
|
if (!input.primaryEmail || !input.password) {
|
||||||
|
throw new GraphQLError("L'email et le mot de passe sont requis", {
|
||||||
|
extensions: { code: "INVALID_INPUT" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await getUserByEmail(input.primaryEmail);
|
||||||
|
if (existingUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Un utilisateur avec cet email existe déjà",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await createLogtoUser(input);
|
||||||
|
|
||||||
|
// Créer un token d'accès personnel pour l'utilisateur
|
||||||
|
let accessToken = undefined;
|
||||||
|
let tokenExpiry = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenData = await createPersonalAccessToken(user.id, context);
|
||||||
|
if (tokenData) {
|
||||||
|
accessToken = tokenData.token;
|
||||||
|
tokenExpiry = tokenData.expiresIn;
|
||||||
|
// Enregistrement global du PAT pour tous les contextes
|
||||||
|
const { patContextCache } = await import("@/graphql/context/features/patCache");
|
||||||
|
patContextCache.register(accessToken, user.id, 24);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// On continue même si la création du token échoue
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user,
|
||||||
|
message: "Utilisateur créé avec succès",
|
||||||
|
accessToken,
|
||||||
|
tokenExpiry,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof GraphQLError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erreur lors de l'inscription:", error);
|
||||||
|
throw new GraphQLError("Échec de l'inscription", {
|
||||||
|
extensions: { code: "REGISTRATION_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(_: unknown, { input }: { input: LoginInput }, context: GraphQLContext): Promise<LoginResponse> {
|
||||||
|
try {
|
||||||
|
if (!input.primaryEmail || !input.password) {
|
||||||
|
throw new GraphQLError("L'email et le mot de passe sont requis", {
|
||||||
|
extensions: { code: "INVALID_INPUT" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentifier l'utilisateur
|
||||||
|
const user = await authenticateUser(input.primaryEmail, input.password);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Email ou mot de passe incorrect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un nouveau token d'accès personnel pour cette session
|
||||||
|
let accessToken = undefined;
|
||||||
|
let tokenExpiry = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenData = await createPersonalAccessToken(user.id, context);
|
||||||
|
if (tokenData) {
|
||||||
|
accessToken = tokenData.token;
|
||||||
|
tokenExpiry = tokenData.expiresIn;
|
||||||
|
|
||||||
|
patContextCache.register(accessToken, user.id, 24);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// On continue même si la création du token échoue
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user,
|
||||||
|
message: "Connexion réussie",
|
||||||
|
accessToken,
|
||||||
|
tokenExpiry,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof GraphQLError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Erreur lors de la connexion:", error);
|
||||||
|
throw new GraphQLError("Échec de la connexion", {
|
||||||
|
extensions: { code: "LOGIN_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(_: unknown, __: unknown, context: GraphQLContext): Promise<LogoutResponse> {
|
||||||
|
try {
|
||||||
|
// Si un token est présent dans le contexte, le supprimer du cache
|
||||||
|
if (context.personalAccessToken) {
|
||||||
|
context.patCache.remove(context.personalAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Déconnexion réussie",
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erreur lors de la déconnexion:", error);
|
||||||
|
throw new GraphQLError("Échec de la déconnexion", {
|
||||||
|
extensions: { code: "LOGOUT_FAILED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getLogtoAccessToken };
|
||||||
52
src/graphql/features/index.ts
Normal file
52
src/graphql/features/index.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
|
||||||
|
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||||
|
import { Connector } from "./Connector";
|
||||||
|
import { Organization } from "./Organization";
|
||||||
|
import { Resource } from "./Resource";
|
||||||
|
import { Role } from "./Role";
|
||||||
|
import { User } from "./User";
|
||||||
|
|
||||||
|
const baseTypeDefs = `
|
||||||
|
scalar DateTime
|
||||||
|
scalar JSON
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
up: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
up: String
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const baseResolvers = {
|
||||||
|
Query: {
|
||||||
|
up: () => "ok",
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
up: () => "ok",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergedTypeDefs = mergeTypeDefs([
|
||||||
|
baseTypeDefs,
|
||||||
|
User.typeDefs,
|
||||||
|
Connector.typeDefs,
|
||||||
|
Organization.typeDefs,
|
||||||
|
Role.typeDefs,
|
||||||
|
Resource.typeDefs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mergedResolvers = mergeResolvers([
|
||||||
|
baseResolvers,
|
||||||
|
User.resolvers,
|
||||||
|
Connector.resolvers,
|
||||||
|
Organization.resolvers,
|
||||||
|
Role.resolvers,
|
||||||
|
Resource.resolvers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const schema = makeExecutableSchema({
|
||||||
|
typeDefs: mergedTypeDefs,
|
||||||
|
resolvers: mergedResolvers,
|
||||||
|
});
|
||||||
22
src/graphql/index.ts
Normal file
22
src/graphql/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { context } from "@/graphql/context";
|
||||||
|
import { GraphQLContext } from "@/graphql/context/types";
|
||||||
|
import { schema } from "@/graphql/features";
|
||||||
|
import { depthLimitPlugin, introspectionPlugin } from "@/graphql/plugin";
|
||||||
|
import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention";
|
||||||
|
import { createYoga } from "graphql-yoga";
|
||||||
|
|
||||||
|
export const yoga = createYoga<GraphQLContext>({
|
||||||
|
schema,
|
||||||
|
context,
|
||||||
|
graphqlEndpoint: "/api/graphql",
|
||||||
|
graphiql: process.env.NODE_ENV === "development",
|
||||||
|
maskedErrors: process.env.NODE_ENV !== "development",
|
||||||
|
plugins: [
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useCSRFPrevention({
|
||||||
|
requestHeaders: ["x-graphql-yoga-csrf"], // default
|
||||||
|
}),
|
||||||
|
depthLimitPlugin,
|
||||||
|
introspectionPlugin,
|
||||||
|
],
|
||||||
|
});
|
||||||
8
src/graphql/plugin/depthLimit.ts
Normal file
8
src/graphql/plugin/depthLimit.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import depthLimit from "graphql-depth-limit";
|
||||||
|
import type { Plugin } from "graphql-yoga";
|
||||||
|
|
||||||
|
export const depthLimitPlugin: Plugin = {
|
||||||
|
onValidate({ addValidationRule }) {
|
||||||
|
addValidationRule(depthLimit(5));
|
||||||
|
},
|
||||||
|
};
|
||||||
2
src/graphql/plugin/index.ts
Normal file
2
src/graphql/plugin/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "@/graphql/plugin/introspection";
|
||||||
|
export * from "@/graphql/plugin/depthLimit";
|
||||||
5
src/graphql/plugin/introspection.ts
Normal file
5
src/graphql/plugin/introspection.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
|
||||||
|
import { Plugin } from "graphql-yoga";
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
export const introspectionPlugin: Plugin = process.env.NODE_ENV === "development" ? {} : useDisableIntrospection();
|
||||||
159
src/types/Connector.ts
Normal file
159
src/types/Connector.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
export interface LogtoConnector {
|
||||||
|
id: string;
|
||||||
|
type: ConnectorType;
|
||||||
|
enabled?: boolean;
|
||||||
|
config?: ConnectorConfig;
|
||||||
|
|
||||||
|
// Propriétés disponibles au niveau racine selon l'API Logto
|
||||||
|
target?: string;
|
||||||
|
name?: Record<string, string>;
|
||||||
|
description?: Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
|
platform?: ConnectorPlatform | string | null;
|
||||||
|
|
||||||
|
// Metadata (peut être redondant avec les propriétés racine)
|
||||||
|
metadata: ConnectorMetadata;
|
||||||
|
createdAt?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConnectorType {
|
||||||
|
EMAIL = "Email",
|
||||||
|
SMS = "Sms",
|
||||||
|
SOCIAL = "Social",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConnectorPlatform {
|
||||||
|
// Social connectors
|
||||||
|
GOOGLE = "Google",
|
||||||
|
FACEBOOK = "Facebook",
|
||||||
|
GITHUB = "GitHub",
|
||||||
|
DISCORD = "Discord",
|
||||||
|
WECHAT = "WeChat",
|
||||||
|
ALIPAY = "Alipay",
|
||||||
|
KAKAO = "Kakao",
|
||||||
|
NAVER = "Naver",
|
||||||
|
AZUREAD = "AzureAd",
|
||||||
|
OIDC = "OIDC",
|
||||||
|
SAML = "SAML",
|
||||||
|
|
||||||
|
// Email connectors
|
||||||
|
SENDGRID = "SendGrid",
|
||||||
|
SES = "SES",
|
||||||
|
MAILGUN = "Mailgun",
|
||||||
|
SMTP = "SMTP",
|
||||||
|
|
||||||
|
// SMS connectors
|
||||||
|
TWILIO = "Twilio",
|
||||||
|
ALIYUN_SMS = "AliCloudSms",
|
||||||
|
TENCENT_SMS = "TencentSms",
|
||||||
|
|
||||||
|
// Other platforms
|
||||||
|
NATIVE = "Native",
|
||||||
|
WEB = "Web",
|
||||||
|
UNIVERSAL = "Universal",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectorConfig {
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
scope?: string;
|
||||||
|
customConfig?: Record<string, unknown>;
|
||||||
|
|
||||||
|
// Email specific
|
||||||
|
apiKey?: string;
|
||||||
|
domain?: string;
|
||||||
|
fromEmail?: string;
|
||||||
|
fromName?: string;
|
||||||
|
templates?: EmailTemplates;
|
||||||
|
|
||||||
|
// SMS specific
|
||||||
|
accountSid?: string;
|
||||||
|
authToken?: string;
|
||||||
|
fromNumber?: string;
|
||||||
|
|
||||||
|
// SMTP specific
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
secure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplates {
|
||||||
|
signIn?: EmailTemplate;
|
||||||
|
register?: EmailTemplate;
|
||||||
|
forgotPassword?: EmailTemplate;
|
||||||
|
passwordless?: EmailTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplate {
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
usageType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectorMetadata {
|
||||||
|
id?: string;
|
||||||
|
target?: string;
|
||||||
|
platform: ConnectorPlatform | null;
|
||||||
|
name?: Record<string, string>;
|
||||||
|
description?: Record<string, string>;
|
||||||
|
logo?: string;
|
||||||
|
logoDark: string | null;
|
||||||
|
readme?: string;
|
||||||
|
configTemplate: Record<string, unknown> | null;
|
||||||
|
formItems?: FormItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormItem {
|
||||||
|
key: string;
|
||||||
|
type: FormItemType;
|
||||||
|
label: Record<string, string>;
|
||||||
|
placeholder?: Record<string, string>;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: unknown;
|
||||||
|
description?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FormItemType {
|
||||||
|
TEXT = "Text",
|
||||||
|
MULTILINE_TEXT = "MultilineText",
|
||||||
|
PASSWORD = "Password",
|
||||||
|
SWITCH = "Switch",
|
||||||
|
SELECT = "Select",
|
||||||
|
JSON = "Json",
|
||||||
|
MULTI_SELECT = "MultiSelect",
|
||||||
|
NUMBER = "Number",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input types for GraphQL mutations
|
||||||
|
export interface CreateConnectorInput {
|
||||||
|
connectorId: string;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateConnectorInput {
|
||||||
|
id: string;
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectorResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connector?: LogtoConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectorsListResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
connectors?: LogtoConnector[];
|
||||||
|
availableConnectors?: ConnectorMetadata[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory connector configuration
|
||||||
|
export interface FactoryConnector extends ConnectorMetadata {
|
||||||
|
isAdded: boolean;
|
||||||
|
}
|
||||||
79
src/types/Logto.ts
Normal file
79
src/types/Logto.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
export interface LogtoUser {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
customData: Record<string, unknown>;
|
||||||
|
identities: Record<string, unknown>;
|
||||||
|
lastSignInAt?: number;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
profile: {
|
||||||
|
familyName?: string;
|
||||||
|
givenName?: string;
|
||||||
|
middleName?: string;
|
||||||
|
nickname?: string;
|
||||||
|
preferredUsername?: string;
|
||||||
|
profile?: string;
|
||||||
|
website?: string;
|
||||||
|
gender?: string;
|
||||||
|
birthdate?: string;
|
||||||
|
zoneinfo?: string;
|
||||||
|
locale?: string;
|
||||||
|
};
|
||||||
|
applicationId?: string;
|
||||||
|
isSuspended: boolean;
|
||||||
|
hasPassword?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterInput {
|
||||||
|
username?: string;
|
||||||
|
primaryEmail: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
password: string;
|
||||||
|
customInfo?: string; // Information personnalisée
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginInput {
|
||||||
|
primaryEmail: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogtoCreateUserRequest {
|
||||||
|
username?: string;
|
||||||
|
primaryEmail?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
password?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
user?: LogtoUser;
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
accessToken?: string; // Token d'accès personnel
|
||||||
|
tokenExpiry?: number; // Timestamp d'expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
user?: LogtoUser;
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
accessToken?: string; // Token d'accès personnel
|
||||||
|
tokenExpiry?: number; // Timestamp d'expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogoutResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogtoConfig {
|
||||||
|
endpoint: string;
|
||||||
|
appId: string;
|
||||||
|
appSecret: string;
|
||||||
|
}
|
||||||
137
src/types/Organization.ts
Normal file
137
src/types/Organization.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
export interface LogtoOrganization {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
isMfaRequired?: boolean;
|
||||||
|
createdAt: number; // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationRole {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: "User" | "Machine";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationPermission {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
resourceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationUser {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
primaryEmail?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
organizationRoles: OrganizationRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationInvitation {
|
||||||
|
id: string;
|
||||||
|
inviterId: string;
|
||||||
|
invitee: string; // email ou phone
|
||||||
|
organizationId: string;
|
||||||
|
status: "Pending" | "Accepted" | "Expired" | "Revoked";
|
||||||
|
organizationRoleIds: string[];
|
||||||
|
expiresAt: number;
|
||||||
|
createdAt: number;
|
||||||
|
acceptedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input types for mutations
|
||||||
|
export interface CreateOrganizationInput {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
isMfaRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateOrganizationInput {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
isMfaRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddUserToOrganizationInput {
|
||||||
|
organizationId: string;
|
||||||
|
userId: string;
|
||||||
|
organizationRoleIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveUserFromOrganizationInput {
|
||||||
|
organizationId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserOrganizationRolesInput {
|
||||||
|
organizationId: string;
|
||||||
|
userId: string;
|
||||||
|
organizationRoleIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOrganizationInvitationInput {
|
||||||
|
organizationId: string;
|
||||||
|
invitee: string; // email ou phone
|
||||||
|
organizationRoleIds?: string[];
|
||||||
|
messagePayload?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response types
|
||||||
|
export interface OrganizationResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
organization?: LogtoOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationsListResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
organizations?: LogtoOrganization[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationUsersResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
users?: OrganizationUser[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationRolesResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
roles?: OrganizationRole[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationPermissionsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
permissions?: OrganizationPermission[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationInvitationsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
invitations?: OrganizationInvitation[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationInvitationResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
invitation?: OrganizationInvitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationAPITestResponse {
|
||||||
|
available: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
7
src/types/Resource.ts
Normal file
7
src/types/Resource.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Resource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
indicator: string; // URI
|
||||||
|
scopes: string[]; // permissions (scope names)
|
||||||
|
}
|
||||||
7
src/types/ResourceScope.ts
Normal file
7
src/types/ResourceScope.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface ResourceScope {
|
||||||
|
id: string;
|
||||||
|
resourceId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
12
src/types/Role.ts
Normal file
12
src/types/Role.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Permission {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
permissions: Permission[];
|
||||||
|
}
|
||||||
34
src/types/User.ts
Normal file
34
src/types/User.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export interface UserAttributes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
profession?: string;
|
||||||
|
formula: string;
|
||||||
|
facebookPage?: string;
|
||||||
|
linkedinPage?: string;
|
||||||
|
website?: string;
|
||||||
|
address?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
city?: string;
|
||||||
|
country?: string;
|
||||||
|
region?: string;
|
||||||
|
avatar?: string;
|
||||||
|
companyId?: string;
|
||||||
|
disable: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
lastLogin?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define UserCreationAttributes interface (can be Partial except required fields)
|
||||||
|
export interface UserCreationAttributes extends Partial<UserAttributes> {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
formula: string;
|
||||||
|
disable: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
11
src/types/graphql.d.ts
vendored
Normal file
11
src/types/graphql.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
declare module "*.graphql" {
|
||||||
|
import { DocumentNode } from "graphql";
|
||||||
|
const Schema: DocumentNode;
|
||||||
|
export default Schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.gql" {
|
||||||
|
import { DocumentNode } from "graphql";
|
||||||
|
const Schema: DocumentNode;
|
||||||
|
export default Schema;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user