logto implementation

This commit is contained in:
Alexandre Bove 2025-07-28 15:37:21 +02:00
parent bbda602fac
commit b59fa5e267
77 changed files with 8852 additions and 75 deletions

21
.editorconfig Normal file
View 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
View 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
View 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
View 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
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View File

@ -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
View 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:

View File

@ -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
View File

@ -0,0 +1 @@
/// <reference types="./src/types/graphql.d.ts" />

View 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

View File

@ -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;

View File

@ -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
View File

View 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
View 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&apos;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&apos;inscription
</a>
</div>
</>
)}
</div>
</div>
</div>
);
}

621
src/app/connectors/page.tsx Normal file
View 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&apos;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>
);
}

View File

@ -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
View 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&apos;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&apos;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
View 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&apos;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>
);
}

View 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&apos;activation dans votre console Logto</li>
<li>La configuration d&apos;un template d&apos;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&apos;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&apos;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&apos;activation des organisations dans votre console Logto</li>
<li>La configuration d&apos;un template d&apos;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&apos;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&apos;êtes membre d&apos;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>
);
}

View File

@ -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
View 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 &quot;me&quot; - 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&apos;accès :</p>
<ul className="ml-4 list-inside list-disc">
<li>Allez sur la page d&apos;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&apos;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&apos;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&apos;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 &lt;your-token&gt;</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&apos;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
View 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
View 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>
);
}

View 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>
);
}

View 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
View 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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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 &quot;me&quot; (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>
);
}

View File

303
src/app/test-users/page.tsx Normal file
View 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 &quot;Récupérer les utilisateurs&quot; 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&apos;inscription
</a>
</div>
<div>
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
👤 Tester la query &quot;me&quot; (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
View File

View 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;
}

View File

@ -0,0 +1,9 @@
import { GuardFunction } from "@/graphql/context/types";
const initGuard: GuardFunction = auth => {
return {
isAuthenticated: !!auth?.accessToken,
};
};
export default initGuard;

View 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;

View 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

View File

@ -0,0 +1,2 @@
export * from "@/graphql/context/context";
export * from "@/graphql/context/types";

View 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
}

View 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!
}

View 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,
};
}
}

View File

@ -0,0 +1,8 @@
export * from "./connectorService";
import typeDefs from "./Connector.graphql";
import { connectorResolvers } from "./resolver";
export const Connector = {
typeDefs,
resolvers: connectorResolvers,
};

View 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",
};
}
},
},
};

View 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!
}

View File

@ -0,0 +1,7 @@
import typeDefs from "./Organization.graphql";
import { organizationResolvers } from "./resolver";
export const Organization = {
typeDefs,
resolvers: organizationResolvers,
};

View 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" },
});
}
}

View 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",
};
}
},
},
};

View 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!
}

View 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!
}

View 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,
},
},
],
};

View 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);
},
},
};

View 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" },
});
}
}

View 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);
},
},
};

View 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" },
});
}
}

View 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!
}

View File

@ -0,0 +1,7 @@
import typeDefs from "./Role.graphql";
import { resolvers } from "./resolver";
export const Role = {
typeDefs,
resolvers,
};

View 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" },
});
}
}

View 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);
},
},
};

View 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" },
});
}
}

View 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!
}

View File

@ -0,0 +1,7 @@
import { resolvers } from "./resolver";
import typeDefs from "./User.graphql";
export const User = {
typeDefs,
resolvers,
};

View 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 };

View 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
View 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,
],
});

View 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));
},
};

View File

@ -0,0 +1,2 @@
export * from "@/graphql/plugin/introspection";
export * from "@/graphql/plugin/depthLimit";

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
export interface Resource {
id: string;
name: string;
description?: string;
indicator: string; // URI
scopes: string[]; // permissions (scope names)
}

View 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
View 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
View 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
View 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;
}