From 7fb1a7b68e7b9709f7cf638f1fdb41f84da23cef Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:57:47 +0200 Subject: [PATCH 01/23] feat: rbac for the admin-api fix: linter happy fix: linter happy chore: prettier fix: tests fix: types fix: linter fix: env-vars for buck2 fix: env vars for workflow fix: change way to use service-account credentials fix: NODE_ENV test for integration tests fix: GCS_APPLICATION_CREDENTIALS_PATH within env.ts fix: prettier chore: tidy up, part 1 chore: tidy up part 2 fix: remove env from BUCK poc: Try to enrich the JWT token remove gcloud dependencies fix --- .../app/api/auth/[...nextauth]/options.ts | 37 ++- apps/admin-panel/app/env.ts | 11 + bats/core/api/admin.bats | 25 ++ core/api/src/graphql/admin/mutations.ts | 11 +- core/api/src/graphql/admin/queries.ts | 23 +- .../src/servers/graphql-admin-api-server.ts | 90 ++++--- core/api/src/servers/index.files.d.ts | 2 + core/api/src/services/auth/role-checker.ts | 47 ++++ dev/config/ory/oathkeeper_rules.yaml | 2 +- pnpm-lock.yaml | 229 +++++++++++------- 10 files changed, 349 insertions(+), 128 deletions(-) create mode 100644 core/api/src/services/auth/role-checker.ts diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index 87263ef308..cb29a87692 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -6,6 +6,15 @@ import { CallbacksOptions } from "next-auth" import { env } from "../../../env" +declare module "next-auth" { + interface Session { + sub: string | null + accessToken: string + role: string + scope: string[] + } +} + const providers: Provider[] = [ GoogleProvider({ clientId: env.GOOGLE_CLIENT_ID ?? "", @@ -30,7 +39,13 @@ if (env.NODE_ENV === "development") { }, authorize: async (credentials) => { if (credentials?.username === "admin" && credentials?.password === "admin") { - return { id: "1", name: "admin", email: "test@galoy.io" } + return { id: "1", name: "admin", email: "admintest@blinkbitcoin.test" } + } + if (credentials?.username === "alice" && credentials?.password === "alice") { + return { id: "2", name: "bob", email: "alicetest@blinkbitcoin.test" } + } + if (credentials?.username === "bob" && credentials?.password === "bob") { + return { id: "2", name: "bob", email: "bobtest@blinkbitcoin.test" } } return null }, @@ -40,6 +55,8 @@ if (env.NODE_ENV === "development") { const callbacks: Partial = { async signIn({ account, profile, user }) { + console.log("signIn", account, profile, user) + console.log("-------------------------") if (account?.provider === "credentials" && env.NODE_ENV === "development") { return !!user } @@ -57,8 +74,26 @@ const callbacks: Partial = { const verified = new Boolean("email_verified" in profile && profile.email_verified) return verified && env.AUTHORIZED_EMAILS.includes(email) }, + async jwt({ token, account, profile, user }) { + const role_mapping = env.ROLE_MAPPING + if (user) { + // get this from config depending if you prefere scope or role + token.scope = ["READ", "WRITE"] + token.role = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" + } else { + console.log("no user") + } + return token + }, + async session({ session, token }) { + session.scope = token.scope as string[] + session.role = token.role as string + return session + }, } + + export const authOptions = { providers, callbacks, diff --git a/apps/admin-panel/app/env.ts b/apps/admin-panel/app/env.ts index b4e8aabf7f..db6e2bf7c4 100644 --- a/apps/admin-panel/app/env.ts +++ b/apps/admin-panel/app/env.ts @@ -16,6 +16,16 @@ export const env = createEnv({ .string() .transform((x) => x.split(",").map((email) => email.trim())) .default("test@galoy.io"), + ROLE_MAPPING: z + .string() + .transform((str) => { + try { + return JSON.parse(str) + } catch { + return {} + } + }) + .default("{}"), }, /* * Environment variables available on the client (and server). @@ -40,5 +50,6 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, AUTHORIZED_EMAILS: process.env.AUTHORIZED_EMAILS, + ROLE_MAPPING: process.env.ROLE_MAPPING || "{}", }, }) diff --git a/bats/core/api/admin.bats b/bats/core/api/admin.bats index d4592d77e5..c8cf079868 100644 --- a/bats/core/api/admin.bats +++ b/bats/core/api/admin.bats @@ -318,6 +318,31 @@ getEmailCode() { [[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1 } +@test "admin: Some random functionality requires authentication" { + variables=$( + jq -n \ + '{ + input: { + localizedNotificationContents: [ + { + language: "en", + title: "Test title", + body: "test body" + } + ], + shouldSendPush: false, + shouldAddToHistory: true, + shouldAddToBulletin: true, + } + }' + ) + + # Try without any token (unauthenticated) + exec_admin_graphql "" 'marketing-notification-trigger' "$variables" + error_code="$(graphql_output '.error.code')" + [[ "$error_code" == "401" ]] || exit 1 +} + # TODO: add check by email # TODO: business update map info diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 24348556ef..5f840d904f 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -15,17 +15,24 @@ import { GT } from "@/graphql/index" export const mutationFields = { unauthed: {}, authed: { - userUpdatePhone: UserUpdatePhoneMutation, - userUpdateEmail: UserUpdateEmailMutation, accountUpdateLevel: AccountUpdateLevelMutation, accountUpdateStatus: AccountUpdateStatusMutation, accountForceDelete: AccountForceDeleteMutation, merchantMapValidate: MerchantMapValidateMutation, merchantMapDelete: MerchantMapDeleteMutation, marketingNotificationTrigger: TriggerMarketingNotificationMutation, + merchantMapDelete: MerchantMapDeleteMutation, + merchantMapValidate: MerchantMapValidateMutation, + userUpdateEmail: UserUpdateEmailMutation, + userUpdatePhone: UserUpdatePhoneMutation, }, } +// All mutations require modify permission by default +export const mutationPermissions = { + modify: Object.keys(mutationFields.authed) as (keyof typeof mutationFields.authed)[], +} as const + export const MutationType = GT.Object({ name: "Mutation", fields: () => ({ ...mutationFields.authed }), diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index b9e9622a87..da191e96dc 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -19,24 +19,29 @@ import { GT } from "@/graphql/index" export const queryFields = { unauthed: {}, authed: { - allLevels: AllLevelsQuery, - accountDetailsByUserPhone: AccountDetailsByUserPhoneQuery, - accountDetailsByUsername: AccountDetailsByUsernameQuery, - accountDetailsByEmail: AccountDetailsByUserEmailQuery, accountDetailsByAccountId: AccountDetailsByAccountId, + accountDetailsByEmail: AccountDetailsByUserEmailQuery, + accountDetailsByUserPhone: AccountDetailsByUserPhoneQuery, accountDetailsByUserId: AccountDetailsByUserId, + accountDetailsByUsername: AccountDetailsByUsernameQuery, + allLevels: AllLevelsQuery, + filteredUserCount: FilteredUserCountQuery, + inactiveMerchants: InactiveMerchantsQuery, + lightningInvoice: LightningInvoiceQuery, + lightningPayment: LightningPaymentQuery, + merchantsPendingApproval: MerchantsPendingApprovalQuery, transactionById: TransactionByIdQuery, transactionsByHash: TransactionsByHashQuery, transactionsByPaymentRequest: TransactionsByPaymentRequestQuery, - lightningInvoice: LightningInvoiceQuery, - lightningPayment: LightningPaymentQuery, wallet: WalletQuery, - merchantsPendingApproval: MerchantsPendingApprovalQuery, - inactiveMerchants: InactiveMerchantsQuery, - filteredUserCount: FilteredUserCountQuery, }, } +// All authed queries require view permission by default +export const queryPermissions = { + view: Object.keys(queryFields.authed) as (keyof typeof queryFields.authed)[], +} as const + export const QueryType = GT.Object({ name: "Query", fields: () => ({ ...queryFields.unauthed, ...queryFields.authed }), diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 1bfc5df263..862d778d95 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -10,7 +10,11 @@ import { startApolloServer } from "./graphql-server" import { baseLogger } from "@/services/logger" -import { adminMutationFields, adminQueryFields, gqlAdminSchema } from "@/graphql/admin" +import { gqlAdminSchema } from "@/graphql/admin" + +import { queryPermissions } from "@/graphql/admin/queries" + +import { mutationPermissions } from "@/graphql/admin/mutations" import { GALOY_ADMIN_PORT } from "@/config" @@ -24,6 +28,31 @@ import { Transactions } from "@/app" import { AuthorizationError } from "@/graphql/error" +import { AdminAccessRight, AdminRoleString, hasAccessRight } from "@/services/auth/role-checker" + +// Helper function to validate role +const isValidAdminRole = (role: string): role is AdminRoleString => { + return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN" +} + +// TODO: loaders probably not needed for the admin panel +const loaders = { + txnMetadata: new DataLoader(async (keys) => { + const txnMetadata = await Transactions.getTransactionsMetadataByIds( + keys as LedgerTransactionId[], + ) + if (txnMetadata instanceof Error) { + recordExceptionInCurrentSpan({ + error: txnMetadata, + }) + + return keys.map(() => undefined) + } + + return txnMetadata + }), +} + const setGqlAdminContext = async ( req: Request, res: Response, @@ -32,27 +61,19 @@ const setGqlAdminContext = async ( const logger = baseLogger const tokenPayload = req.token - // TODO: loaders probably not needed for the admin panel - const loaders = { - txnMetadata: new DataLoader(async (keys) => { - const txnMetadata = await Transactions.getTransactionsMetadataByIds( - keys as LedgerTransactionId[], - ) - if (txnMetadata instanceof Error) { - recordExceptionInCurrentSpan({ - error: txnMetadata, - }) - - return keys.map(() => undefined) - } - - return txnMetadata - }), - } + console.log("JWT Token payload:", tokenPayload) + const userEmail = tokenPayload.sub as string // This should be the email from OAuth + const role = tokenPayload.role as string const privilegedClientId = tokenPayload.sub as PrivilegedClientId - req.gqlContext = { loaders, privilegedClientId, logger } + req.gqlContext = { + loaders, + privilegedClientId, + userEmail, // Add email to context + role, + logger, + } addAttributesToCurrentSpanAndPropagate( { @@ -64,32 +85,39 @@ const setGqlAdminContext = async ( ) } -const isAuthenticated = rule({ cache: "contextual" })(async ( +const requiresViewAccess = rule({ cache: "contextual" })(async ( parent, args, ctx: GraphQLAdminContext, ) => { - return ( - ctx.privilegedClientId !== null && - ctx.privilegedClientId !== undefined && - ctx.privilegedClientId !== "" - ) + if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false + console.log("ctx",ctx) + return hasAccessRight(ctx.role, AdminAccessRight.VIEW_ACCOUNTS) +}) + +const requiresModifyAccess = rule({ cache: "contextual" })(async ( + parent, + args, + ctx: GraphQLAdminContext, +) => { + if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false + return hasAccessRight(ctx.role, AdminAccessRight.MODIFY_ACCOUNTS) }) export async function startApolloServerForAdminSchema() { - const authedQueryFields: { [key: string]: Rule } = {} - for (const key of Object.keys(adminQueryFields.authed)) { - authedQueryFields[key] = isAuthenticated + const viewerQueryFields: { [key: string]: Rule } = {} + for (const key of queryPermissions.view) { + viewerQueryFields[key] = requiresViewAccess } const authedMutationFields: { [key: string]: Rule } = {} - for (const key of Object.keys(adminMutationFields.authed)) { - authedMutationFields[key] = isAuthenticated + for (const key of mutationPermissions.modify) { + authedMutationFields[key] = requiresModifyAccess } const permissions = shield( { - Query: authedQueryFields, + Query: viewerQueryFields, Mutation: authedMutationFields, }, { diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index add09bc539..6f99d1904d 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -24,6 +24,8 @@ type GraphQLAdminContext = { logger: Logger loaders: Loaders privilegedClientId: PrivilegedClientId + userEmail: string + role: string } type GraphQLContext = diff --git a/core/api/src/services/auth/role-checker.ts b/core/api/src/services/auth/role-checker.ts new file mode 100644 index 0000000000..14dcb2ed72 --- /dev/null +++ b/core/api/src/services/auth/role-checker.ts @@ -0,0 +1,47 @@ + + + + +export enum AdminRole { + VIEWER = "roles/adminPanelViewer", + SUPPORT = "roles/adminPanelSupport", + ADMIN = "roles/adminPanelAdmin", +} + +export enum AdminAccessRight { + VIEW_ACCOUNTS = "VIEW_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + DELETE_ACCOUNTS = "DELETE_ACCOUNTS", + VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", + SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", + SYSTEM_CONFIG = "SYSTEM_CONFIG", +} + +// String-based role values from options.ts +export type AdminRoleString = "VIEWER" | "SUPPORT" | "ADMIN" + +// String-based role access rights mapping +const STRING_ROLE_ACCESS_RIGHTS = { + VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], + SUPPORT: [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + ], + ADMIN: Object.values(AdminAccessRight), +} + + +export const hasAccessRight = async ( + role: AdminRoleString, + accessRight: AdminAccessRight, +): Promise => { + return STRING_ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false +} + +// Backward compatibility - deprecated, use AdminAccessRight instead +export const AdminFeature = AdminAccessRight + +// Backward compatibility - deprecated, use hasAccessRight instead +export const hasFeature = hasAccessRight diff --git a/dev/config/ory/oathkeeper_rules.yaml b/dev/config/ory/oathkeeper_rules.yaml index 7cd4cff2d4..afe1267397 100644 --- a/dev/config/ory/oathkeeper_rules.yaml +++ b/dev/config/ory/oathkeeper_rules.yaml @@ -106,4 +106,4 @@ mutators: - handler: id_token config: #! TODO: add aud: {"aud": ["https://api/admin/graphql"] } - claims: '{"sub": "{{ print .Subject }}", "scope": "{{ print .Extra.scope }}" }' + claims: '{"sub": "{{ print .Subject }}", "scope": "{{ print .Extra.scope }}", "role": "{{ print .Extra.role }}" }' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 730cbb6a8b..7e7483cb68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1208,6 +1208,9 @@ importers: firebase-admin: specifier: ^12.6.0 version: 12.7.0 + google-auth-library: + specifier: ^10.2.0 + version: 10.2.0 google-protobuf: specifier: ^3.21.4 version: 3.21.4 @@ -2849,7 +2852,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2872,7 +2875,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2987,7 +2990,7 @@ packages: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -4366,7 +4369,7 @@ packages: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5179,7 +5182,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5209,7 +5212,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5225,7 +5228,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -6908,7 +6911,7 @@ packages: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.8 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dotenv: 16.6.1 graphql: 16.11.0 graphql-request: 6.1.0(graphql@16.11.0) @@ -7207,7 +7210,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8075,7 +8078,7 @@ packages: dependencies: '@types/node': 22.16.0 async-exit-hook: 2.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -12062,7 +12065,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -13685,7 +13688,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13713,7 +13716,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13829,7 +13832,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13849,7 +13852,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13870,7 +13873,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13891,7 +13894,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.3.3 transitivePeerDependencies: @@ -13912,7 +13915,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13933,7 +13936,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13954,7 +13957,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13975,7 +13978,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13996,7 +13999,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14014,7 +14017,7 @@ packages: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14029,7 +14032,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -14043,7 +14046,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -14057,7 +14060,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14133,7 +14136,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 tsutils: 3.21.0(typescript@5.6.3) typescript: 5.6.3 @@ -14153,7 +14156,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.2.2) typescript: 5.2.2 @@ -14173,7 +14176,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.8.3) typescript: 5.8.3 @@ -14190,7 +14193,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14233,7 +14236,7 @@ packages: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14254,7 +14257,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14275,7 +14278,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14296,7 +14299,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14317,7 +14320,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14339,7 +14342,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14361,7 +14364,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14383,7 +14386,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14405,7 +14408,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14427,7 +14430,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14449,7 +14452,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14470,7 +14473,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14491,7 +14494,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14512,7 +14515,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15277,7 +15280,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -16844,7 +16847,7 @@ packages: '@testim/chrome-version': 1.1.4 axios: 1.11.0 compare-versions: 6.1.1 - extract-zip: 2.0.1(supports-color@8.1.1) + extract-zip: 2.0.1 proxy-agent: 6.5.0 proxy-from-env: 1.1.0 tcp-port-used: 1.0.2 @@ -17746,7 +17749,6 @@ packages: /data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - dev: true /data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} @@ -17871,7 +17873,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@4.4.1(supports-color@8.1.1): resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -18080,7 +18081,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) filing-cabinet: 3.3.1 precinct: 9.2.1 typescript: 4.9.5 @@ -18150,7 +18151,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -18211,7 +18212,7 @@ packages: resolution: {integrity: sha512-Rps1xDkEEBSq3kLdsdnHZL1x2S4NGDcbrjmd4q+PykK5aJwDdP5MBgrJw1Xo+kyUHuv3JEzPqxr+Dj9ryeDRTA==} engines: {node: '>= 6.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gonzales-pe: 4.3.0 node-source-walk: 4.3.0 transitivePeerDependencies: @@ -18222,7 +18223,7 @@ packages: resolution: {integrity: sha512-Fwc/g9VcrowODIAeKRWZfVA/EufxYL7XfuqJQFroBKGikKX83d2G7NFw6kDlSYGG3LNQIyVa+eWv1mqre+v4+A==} engines: {node: ^10 || ^12 || >=14} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) is-url: 1.2.4 postcss: 8.5.6 postcss-values-parser: 2.0.1 @@ -18797,7 +18798,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -19105,7 +19106,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.40.0) get-tsconfig: 4.10.1 @@ -19131,7 +19132,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) get-tsconfig: 4.10.1 @@ -19842,7 +19843,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19893,7 +19894,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19950,7 +19951,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -20212,6 +20213,20 @@ packages: - supports-color dev: true + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.4.1(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + /extract-zip@2.0.1(supports-color@8.1.1): resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -20386,7 +20401,6 @@ packages: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - dev: true /fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} @@ -20445,7 +20459,7 @@ packages: dependencies: app-module-path: 2.2.0 commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) enhanced-resolve: 5.18.2 is-relative-path: 1.0.2 module-definition: 3.4.0 @@ -20756,7 +20770,6 @@ packages: engines: {node: '>=12.20.0'} dependencies: fetch-blob: 3.2.0 - dev: true /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -20887,6 +20900,17 @@ packages: - supports-color dev: false + /gaxios@7.1.1: + resolution: {integrity: sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==} + engines: {node: '>=18'} + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + dev: false + /gaze@1.1.3: resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==} engines: {node: '>= 4.0.0'} @@ -20906,6 +20930,17 @@ packages: - supports-color dev: false + /gcp-metadata@7.0.1: + resolution: {integrity: sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==} + engines: {node: '>=18'} + dependencies: + gaxios: 7.1.1 + google-logging-utils: 1.1.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -21014,7 +21049,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -21238,6 +21273,21 @@ packages: minimist: 1.2.8 dev: true + /google-auth-library@10.2.0: + resolution: {integrity: sha512-gy/0hRx8+Ye0HlUm3GrfpR4lbmJQ6bJ7F44DmN7GtMxxzWSojLzx0Bhv/hj7Wlj7a2On0FcT8jrz8Y1c1nxCyg==} + engines: {node: '>=18'} + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.1 + gcp-metadata: 7.0.1 + google-logging-utils: 1.1.1 + gtoken: 8.0.0 + jws: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} @@ -21281,6 +21331,11 @@ packages: engines: {node: '>=14'} dev: false + /google-logging-utils@1.1.1: + resolution: {integrity: sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==} + engines: {node: '>=14'} + dev: false + /google-protobuf@3.15.8: resolution: {integrity: sha512-2jtfdqTaSxk0cuBJBtTTWsot4WtR9RVr2rXg7x7OoqiuOKopPrwXpM1G4dXIkLcUNRh3RKzz76C8IOkksZSeOw==} dev: true @@ -21769,6 +21824,16 @@ packages: - supports-color dev: false + /gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + dependencies: + gaxios: 7.1.1 + jws: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -21836,7 +21901,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -22065,7 +22129,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22074,7 +22138,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -22131,7 +22195,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -22141,7 +22205,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22150,7 +22214,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22411,7 +22475,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -22901,7 +22965,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -23858,7 +23922,7 @@ packages: dependencies: '@types/express': 4.17.23 '@types/jsonwebtoken': 9.0.10 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24431,7 +24495,7 @@ packages: chalk: 4.1.2 commander: 7.2.0 commondir: 1.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dependency-tree: 9.0.0 detective-amd: 4.2.0 detective-cjs: 4.1.0 @@ -24850,7 +24914,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) glob: 7.2.3 requirejs: 2.3.7 requirejs-config-file: 4.0.0 @@ -25007,7 +25071,7 @@ packages: resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} engines: {node: '>=14.0.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -25319,7 +25383,6 @@ packages: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - dev: true /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} @@ -25831,7 +25894,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -26610,7 +26673,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) detective-amd: 3.1.2 detective-cjs: 3.1.3 detective-es6: 2.2.2 @@ -26839,7 +26902,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -26919,7 +26982,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -27868,7 +27931,7 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) module-details-from-path: 1.0.4 resolve: 1.22.10 transitivePeerDependencies: @@ -28543,7 +28606,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -29113,7 +29176,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -29161,7 +29224,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -30942,7 +31004,6 @@ packages: /web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - dev: true /web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} From 0e58f4d4d3e2d5647dccfa2138661ca4ee769188 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:03:34 +0200 Subject: [PATCH 02/23] feat: access rights mapping in admin-panel --- apps/admin-panel/app/access-rights.ts | 70 +++++++++++++++++++ .../app/api/auth/[...nextauth]/options.ts | 33 +++++++-- .../src/servers/graphql-admin-api-server.ts | 19 +++-- core/api/src/servers/index.files.d.ts | 1 + core/api/src/services/auth/role-checker.ts | 13 ++++ dev/config/ory/oathkeeper_rules.yaml | 2 +- 6 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 apps/admin-panel/app/access-rights.ts diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts new file mode 100644 index 0000000000..011acb053e --- /dev/null +++ b/apps/admin-panel/app/access-rights.ts @@ -0,0 +1,70 @@ +// Admin access rights definitions +export enum AdminAccessRight { + VIEW_ACCOUNTS = "VIEW_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + DELETE_ACCOUNTS = "DELETE_ACCOUNTS", + VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", + SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", + SYSTEM_CONFIG = "SYSTEM_CONFIG", +} + +// Role types +export type AdminRole = "VIEWER" | "SUPPORT" | "ADMIN" + +// Role to access rights mapping +const ROLE_ACCESS_RIGHTS: Record = { + VIEWER: [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + ], + SUPPORT: [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + ], + ADMIN: [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.DELETE_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + AdminAccessRight.SYSTEM_CONFIG, + ], +} + +/** + * Get access rights for a given role + * @param role - The admin role + * @returns Array of access rights for the role + */ +export function getAccessRightsForRole(role: AdminRole): AdminAccessRight[] { + return ROLE_ACCESS_RIGHTS[role] || [] +} + +/** + * Check if a role has a specific access right + * @param role - The admin role + * @param accessRight - The access right to check + * @returns True if the role has the access right + */ +export function hasAccessRight(role: AdminRole, accessRight: AdminAccessRight): boolean { + return ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false +} + +/** + * Get all available access rights + * @returns Array of all access rights + */ +export function getAllAccessRights(): AdminAccessRight[] { + return Object.values(AdminAccessRight) +} + +/** + * Validate if a string is a valid admin role + * @param role - The role string to validate + * @returns True if the role is valid + */ +export function isValidAdminRole(role: string): role is AdminRole { + return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN" +} diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index cb29a87692..b2194711dd 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -5,13 +5,14 @@ import type { Provider } from "next-auth/providers" import { CallbacksOptions } from "next-auth" import { env } from "../../../env" +import { getAccessRightsForRole, isValidAdminRole, type AdminRole } from "../../../access-rights" declare module "next-auth" { interface Session { sub: string | null accessToken: string role: string - scope: string[] + scope: string } } @@ -75,18 +76,38 @@ const callbacks: Partial = { return verified && env.AUTHORIZED_EMAILS.includes(email) }, async jwt({ token, account, profile, user }) { - const role_mapping = env.ROLE_MAPPING + var role_mapping: { [key: string]: string } + if (env.NODE_ENV === "development") { + + role_mapping = { + "admintest@blinkbitcoin.test": "ADMIN", + "alicetest@blinkbitcoin.test": "VIEWER", + "bobtest@blinkbitcoin.test": "SUPPORT", + } + } else { + role_mapping = env.ROLE_MAPPING + } if (user) { - // get this from config depending if you prefere scope or role - token.scope = ["READ", "WRITE"] - token.role = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" + const userRole = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" + + // Validate and get access rights for the role + if (isValidAdminRole(userRole)) { + const accessRights = getAccessRightsForRole(userRole as AdminRole) + token.scope = JSON.stringify(accessRights) + token.role = userRole + } else { + // Fallback to VIEWER if invalid role + const accessRights = getAccessRightsForRole("VIEWER") + token.scope = JSON.stringify(accessRights) + token.role = "VIEWER" + } } else { console.log("no user") } return token }, async session({ session, token }) { - session.scope = token.scope as string[] + session.scope = token.scope as string session.role = token.role as string return session }, diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 862d778d95..9f7f8b6df9 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -28,12 +28,7 @@ import { Transactions } from "@/app" import { AuthorizationError } from "@/graphql/error" -import { AdminAccessRight, AdminRoleString, hasAccessRight } from "@/services/auth/role-checker" - -// Helper function to validate role -const isValidAdminRole = (role: string): role is AdminRoleString => { - return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN" -} +import { AdminAccessRight, hasAccessRightInScope } from "@/services/auth/role-checker" // TODO: loaders probably not needed for the admin panel const loaders = { @@ -65,6 +60,8 @@ const setGqlAdminContext = async ( const userEmail = tokenPayload.sub as string // This should be the email from OAuth const role = tokenPayload.role as string + const scopeString = tokenPayload.scope as string || "[]" + const scope = JSON.parse(scopeString) as string[] const privilegedClientId = tokenPayload.sub as PrivilegedClientId req.gqlContext = { @@ -72,6 +69,7 @@ const setGqlAdminContext = async ( privilegedClientId, userEmail, // Add email to context role, + scope, logger, } @@ -90,9 +88,8 @@ const requiresViewAccess = rule({ cache: "contextual" })(async ( args, ctx: GraphQLAdminContext, ) => { - if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false - console.log("ctx",ctx) - return hasAccessRight(ctx.role, AdminAccessRight.VIEW_ACCOUNTS) + if (!ctx.userEmail || !ctx.scope) return false + return hasAccessRightInScope(ctx.scope, AdminAccessRight.VIEW_ACCOUNTS) }) const requiresModifyAccess = rule({ cache: "contextual" })(async ( @@ -100,8 +97,8 @@ const requiresModifyAccess = rule({ cache: "contextual" })(async ( args, ctx: GraphQLAdminContext, ) => { - if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false - return hasAccessRight(ctx.role, AdminAccessRight.MODIFY_ACCOUNTS) + if (!ctx.userEmail || !ctx.scope) return false + return hasAccessRightInScope(ctx.scope, AdminAccessRight.MODIFY_ACCOUNTS) }) export async function startApolloServerForAdminSchema() { diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index 6f99d1904d..18d25131ac 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -26,6 +26,7 @@ type GraphQLAdminContext = { privilegedClientId: PrivilegedClientId userEmail: string role: string + scope: string[] } type GraphQLContext = diff --git a/core/api/src/services/auth/role-checker.ts b/core/api/src/services/auth/role-checker.ts index 14dcb2ed72..0923209764 100644 --- a/core/api/src/services/auth/role-checker.ts +++ b/core/api/src/services/auth/role-checker.ts @@ -40,6 +40,19 @@ export const hasAccessRight = async ( return STRING_ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false } +/** + * Check if a scope array contains a specific access right + * @param scope - Array of access rights from JWT token scope + * @param accessRight - The access right to check for + * @returns True if the scope contains the access right + */ +export const hasAccessRightInScope = async ( + scope: string[], + accessRight: AdminAccessRight, +): Promise => { + return scope.includes(accessRight) +} + // Backward compatibility - deprecated, use AdminAccessRight instead export const AdminFeature = AdminAccessRight diff --git a/dev/config/ory/oathkeeper_rules.yaml b/dev/config/ory/oathkeeper_rules.yaml index afe1267397..11c7576656 100644 --- a/dev/config/ory/oathkeeper_rules.yaml +++ b/dev/config/ory/oathkeeper_rules.yaml @@ -106,4 +106,4 @@ mutators: - handler: id_token config: #! TODO: add aud: {"aud": ["https://api/admin/graphql"] } - claims: '{"sub": "{{ print .Subject }}", "scope": "{{ print .Extra.scope }}", "role": "{{ print .Extra.role }}" }' + claims: '{"sub": "{{ print .Subject }}", "scope": {{ printf "%+q" .Extra.scope }}, "role": "{{ print .Extra.role }}"}' From 6d40d5986bc30d09e75181dd4799f3b690f17185 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:52:47 +0200 Subject: [PATCH 03/23] more granular access on admin-api, linting tests etc --- apps/admin-panel/app/access-rights.ts | 5 +- .../app/api/auth/[...nextauth]/options.ts | 15 +- apps/admin-panel/app/env.ts | 4 +- bats/admin-gql/all-levels.gql | 3 + bats/core/api/admin.bats | 96 +++++--- bats/helpers/admin.bash | 52 +++- core/api/src/graphql/admin/mutations.ts | 22 +- core/api/src/graphql/admin/queries.ts | 27 +- .../src/servers/graphql-admin-api-server.ts | 77 ++++-- core/api/src/services/auth/role-checker.ts | 5 - pnpm-lock.yaml | 230 +++++++----------- 11 files changed, 295 insertions(+), 241 deletions(-) create mode 100644 bats/admin-gql/all-levels.gql diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index 011acb053e..e223199bf5 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -13,10 +13,7 @@ export type AdminRole = "VIEWER" | "SUPPORT" | "ADMIN" // Role to access rights mapping const ROLE_ACCESS_RIGHTS: Record = { - VIEWER: [ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - ], + VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], SUPPORT: [ AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.MODIFY_ACCOUNTS, diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index b2194711dd..1f31caf606 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -5,7 +5,11 @@ import type { Provider } from "next-auth/providers" import { CallbacksOptions } from "next-auth" import { env } from "../../../env" -import { getAccessRightsForRole, isValidAdminRole, type AdminRole } from "../../../access-rights" +import { + getAccessRightsForRole, + isValidAdminRole, + type AdminRole, +} from "../../../access-rights" declare module "next-auth" { interface Session { @@ -75,17 +79,16 @@ const callbacks: Partial = { const verified = new Boolean("email_verified" in profile && profile.email_verified) return verified && env.AUTHORIZED_EMAILS.includes(email) }, - async jwt({ token, account, profile, user }) { - var role_mapping: { [key: string]: string } + async jwt({ token, user }) { + let role_mapping: { [key: string]: string } if (env.NODE_ENV === "development") { - role_mapping = { "admintest@blinkbitcoin.test": "ADMIN", "alicetest@blinkbitcoin.test": "VIEWER", "bobtest@blinkbitcoin.test": "SUPPORT", } } else { - role_mapping = env.ROLE_MAPPING + role_mapping = env.USER_ROLE_MAP } if (user) { const userRole = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" @@ -113,8 +116,6 @@ const callbacks: Partial = { }, } - - export const authOptions = { providers, callbacks, diff --git a/apps/admin-panel/app/env.ts b/apps/admin-panel/app/env.ts index db6e2bf7c4..f08f1468e4 100644 --- a/apps/admin-panel/app/env.ts +++ b/apps/admin-panel/app/env.ts @@ -16,7 +16,7 @@ export const env = createEnv({ .string() .transform((x) => x.split(",").map((email) => email.trim())) .default("test@galoy.io"), - ROLE_MAPPING: z + USER_ROLE_MAP: z .string() .transform((str) => { try { @@ -50,6 +50,6 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, AUTHORIZED_EMAILS: process.env.AUTHORIZED_EMAILS, - ROLE_MAPPING: process.env.ROLE_MAPPING || "{}", + USER_ROLE_MAP: process.env.ROLE_MAPPING || "{}", }, }) diff --git a/bats/admin-gql/all-levels.gql b/bats/admin-gql/all-levels.gql new file mode 100644 index 0000000000..154fdfa3e2 --- /dev/null +++ b/bats/admin-gql/all-levels.gql @@ -0,0 +1,3 @@ +query AllLevels { + allLevels +} diff --git a/bats/core/api/admin.bats b/bats/core/api/admin.bats index c8cf079868..ce86ed5e19 100644 --- a/bats/core/api/admin.bats +++ b/bats/core/api/admin.bats @@ -15,6 +15,8 @@ setup_file() { create_user 'tester2' login_admin + login_support_user + login_view_user } getEmailCode() { @@ -35,21 +37,21 @@ getEmailCode() { echo "$code" } -@test "admin: can query account details by phone" { - admin_token="$(read_value 'admin.token')" +@test "view_user: can query account details by phone" { + token="$(read_value 'view_user.token')" variables=$( jq -n \ --arg phone "$(read_value 'tester.phone')" \ '{phone: $phone}' ) - exec_admin_graphql $admin_token 'account-details-by-user-phone' "$variables" + exec_admin_graphql $token 'account-details-by-user-phone' "$variables" id="$(graphql_output '.data.accountDetailsByUserPhone.id')" [[ "$id" != "null" && "$id" != "" ]] || exit 1 cache_value 'tester.id' "$id" } -@test "admin: can update user phone number" { - admin_token="$(read_value 'admin.token')" +@test "support_user: can update user phone number" { + token="$(read_value 'support_user.token')" id="$(read_value 'tester.id')" new_phone="$(random_phone)" variables=$( @@ -59,7 +61,7 @@ getEmailCode() { '{input: {phone: $phone, accountId:$accountId}}' ) - exec_admin_graphql $admin_token 'user-update-phone' "$variables" + exec_admin_graphql $token 'user-update-phone' "$variables" num_errors="$(graphql_output '.data.userUpdatePhone.errors | length')" [[ "$num_errors" == "0" ]] || exit 1 @@ -68,12 +70,30 @@ getEmailCode() { --arg phone "$new_phone" \ '{phone: $phone}' ) - exec_admin_graphql "$admin_token" 'account-details-by-user-phone' "$variables" + exec_admin_graphql "$token" 'account-details-by-user-phone' "$variables" refetched_id="$(graphql_output '.data.accountDetailsByUserPhone.id')" [[ "$refetched_id" == "$id" ]] || exit 1 } -@test "admin: can update user email" { +@test "view_user: cannot update user phone number" { + token="$(read_value 'view_user.token')" + id="$(read_value 'tester.id')" + new_phone="$(random_phone)" + variables=$( + jq -n \ + --arg phone "$new_phone" \ + --arg accountId "$id" \ + '{input: {phone: $phone, accountId:$accountId}}' + ) + + # Attempt to update phone number - should fail with authorization error + exec_admin_graphql $token 'user-update-phone' "$variables" + error_message="$(graphql_output '.errors[0].message')" + echo "error_message: $error_message" >&2 + [[ "$error_message" == "Not authorized" ]] || exit 1 +} + +@test "support_user: can update user email" { email="$(read_value tester.username)@blink.sv" cache_value "tester.email" "$email" @@ -89,7 +109,7 @@ getEmailCode() { [[ "$(graphql_output '.data.userEmailRegistrationValidate.me.email.address')" == "$email" ]] || exit 1 [[ "$(graphql_output '.data.userEmailRegistrationValidate.me.email.verified')" == "true" ]] || exit 1 - admin_token="$(read_value 'admin.token')" + token="$(read_value 'admin.token')" id="$(read_value 'tester.id')" new_email="$(read_value 'tester.username')_updated@blink.sv" variables=$( @@ -99,7 +119,7 @@ getEmailCode() { '{input: {email: $email, accountId:$accountId}}' ) - exec_admin_graphql $admin_token 'user-update-email' "$variables" + exec_admin_graphql $token 'user-update-email' "$variables" num_errors="$(graphql_output '.data.userUpdateEmail.errors | length')" [[ "$num_errors" == "0" ]] || exit 1 @@ -108,7 +128,7 @@ getEmailCode() { --arg accountId "$id" \ '{accountId: $accountId}' ) - exec_admin_graphql "$admin_token" 'account-details-by-account-id' "$variables" + exec_admin_graphql "$token" 'account-details-by-account-id' "$variables" refetched_id="$(graphql_output '.data.accountDetailsByAccountId.id')" [[ "$refetched_id" == "$id" ]] || exit 1 @@ -274,8 +294,8 @@ getEmailCode() { [[ "$count" -eq 1 ]] || exit 1 } -@test "admin: can trigger marketing notification" { - admin_token="$(read_value 'admin.token')" +@test "support_user: can trigger marketing notification" { + token="$(read_value 'support_user.token')" variables=$( jq -n \ @@ -294,7 +314,7 @@ getEmailCode() { } }' ) - exec_admin_graphql "$admin_token" 'marketing-notification-trigger' "$variables" + exec_admin_graphql "$token" 'marketing-notification-trigger' "$variables" num_errors="$(graphql_output '.data.marketingNotificationTrigger.errors | length')" success="$(graphql_output '.data.marketingNotificationTrigger.success')" [[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1 @@ -318,29 +338,37 @@ getEmailCode() { [[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1 } -@test "admin: Some random functionality requires authentication" { +@test "admin: can access system configuration (SYSTEM_CONFIG scope)" { + admin_token="$(read_value 'admin.token')" + + # Use allLevels query as a proxy for system configuration access + # This represents a system configuration endpoint that should require SYSTEM_CONFIG scope variables=$( jq -n \ - '{ - input: { - localizedNotificationContents: [ - { - language: "en", - title: "Test title", - body: "test body" - } - ], - shouldSendPush: false, - shouldAddToHistory: true, - shouldAddToBulletin: true, - } - }' + '{}' + ) + exec_admin_graphql "$admin_token" 'all-levels' "$variables" + levels="$(graphql_output '.data.allLevels')" + [[ "$levels" != "null" && "$levels" != "" ]] || exit 1 + + # Verify we get expected account levels + level_count="$(graphql_output '.data.allLevels | length')" + [[ "$level_count" -gt 0 ]] || exit 1 +} + +@test "support_user: cannot access system configuration (SYSTEM_CONFIG scope)" { + support_token="$(read_value 'support_user.token')" + + # Use allLevels query as a proxy for system configuration access + # Support user should not have SYSTEM_CONFIG scope, so this should fail + variables=$( + jq -n \ + '{}' ) - - # Try without any token (unauthenticated) - exec_admin_graphql "" 'marketing-notification-trigger' "$variables" - error_code="$(graphql_output '.error.code')" - [[ "$error_code" == "401" ]] || exit 1 + exec_admin_graphql "$support_token" 'all-levels' "$variables" + error_message="$(graphql_output '.errors[0].message')" + echo "error_message: $error_message" >&2 + [[ "$error_message" == "Not authorized" ]] || exit 1 } # TODO: add check by email diff --git a/bats/helpers/admin.bash b/bats/helpers/admin.bash index 27088b7b63..7860964a9d 100644 --- a/bats/helpers/admin.bash +++ b/bats/helpers/admin.bash @@ -2,22 +2,52 @@ HYDRA_PUBLIC_API="http://localhost:4444" HYDRA_ADMIN_API="http://localhost:4445" -login_admin() { +# Helper function to create a client and get token with specified scopes +_create_admin_client_and_token() { + local scopes="$1" + local token_cache_key="$2" + + # Create the JSON payload properly + local json_payload=$(jq -n \ + --arg scopes "$scopes" \ + '{ + "grant_types": ["client_credentials"], + "scope": $scopes + }') + client=$(curl -L -s -X POST $HYDRA_ADMIN_API/admin/clients \ -H 'Content-Type: application/json' \ - -d '{ - "grant_types": ["client_credentials"] - }') + -d "$json_payload") - client_id=$(echo $client | jq -r '.client_id') - client_secret=$(echo $client | jq -r '.client_secret') + client_id=$(echo "$client" | jq -r '.client_id') + client_secret=$(echo "$client" | jq -r '.client_secret') - admin_token=$(curl -s -X POST $HYDRA_PUBLIC_API/oauth2/token \ + token=$(curl -s -X POST $HYDRA_PUBLIC_API/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -u "$client_id:$client_secret" \ - -d "grant_type=client_credentials" | jq -r '.access_token' + -d "grant_type=client_credentials&scope=$scopes" | jq -r '.access_token' ) - echo "admin_token: $admin_token" - [[ -n "$admin_token" ]] || exit 1 - cache_value 'admin.token' "$admin_token" + echo "${token_cache_key}: $token" + [[ -n "$token" && "$token" != "null" ]] || exit 1 + cache_value "$token_cache_key" "$token" +} + +# Below user specification is mimicking the users as they are defined in apps/admin-panel/app/api/auth/[...nextauth]/options.ts + +# Full admin access with all permissions +login_admin() { + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","MODIFY_ACCOUNTS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS","SYSTEM_CONFIG"]' + _create_admin_client_and_token "$scopes" "admin.token" +} + +# Modify user access (can view and modify accounts/transactions, but no notifications or system config) +login_support_user() { + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","MODIFY_ACCOUNTS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS"]' + _create_admin_client_and_token "$scopes" "support_user.token" +} + +# View-only access (can only view accounts and transactions) +login_view_user() { + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS"]' + _create_admin_client_and_token "$scopes" "view_user.token" } diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 5f840d904f..9d80198750 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -21,16 +21,30 @@ export const mutationFields = { merchantMapValidate: MerchantMapValidateMutation, merchantMapDelete: MerchantMapDeleteMutation, marketingNotificationTrigger: TriggerMarketingNotificationMutation, - merchantMapDelete: MerchantMapDeleteMutation, - merchantMapValidate: MerchantMapValidateMutation, userUpdateEmail: UserUpdateEmailMutation, userUpdatePhone: UserUpdatePhoneMutation, }, } -// All mutations require modify permission by default +// Detailed mutation permissions mapping by access right export const mutationPermissions = { - modify: Object.keys(mutationFields.authed) as (keyof typeof mutationFields.authed)[], + // Account modification operations - require MODIFY_ACCOUNTS + modifyAccounts: [ + "accountUpdateLevel", + "accountUpdateStatus", + "userUpdateEmail", + "userUpdatePhone", + "merchantMapValidate", + "merchantMapDelete", + ] as (keyof typeof mutationFields.authed)[], + + // Account deletion operations - require DELETE_ACCOUNTS + deleteAccounts: ["accountForceDelete"] as (keyof typeof mutationFields.authed)[], + + // Notification operations - require SEND_NOTIFICATIONS + sendNotifications: [ + "marketingNotificationTrigger", + ] as (keyof typeof mutationFields.authed)[], } as const export const MutationType = GT.Object({ diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index da191e96dc..ab1052a796 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -37,9 +37,32 @@ export const queryFields = { }, } -// All authed queries require view permission by default +// Detailed query permissions mapping by access right export const queryPermissions = { - view: Object.keys(queryFields.authed) as (keyof typeof queryFields.authed)[], + // Account viewing operations - require VIEW_ACCOUNTS + viewAccounts: [ + "accountDetailsByAccountId", + "accountDetailsByEmail", + "accountDetailsByUserPhone", + "accountDetailsByUserId", + "accountDetailsByUsername", + "filteredUserCount", + "wallet", + "inactiveMerchants", + "merchantsPendingApproval", + ] as (keyof typeof queryFields.authed)[], + + // Transaction viewing operations - require VIEW_TRANSACTIONS + viewTransactions: [ + "lightningInvoice", + "lightningPayment", + "transactionById", + "transactionsByHash", + "transactionsByPaymentRequest", + ] as (keyof typeof queryFields.authed)[], + + // System configuration operations - require SYSTEM_CONFIG + systemConfig: ["allLevels"] as (keyof typeof queryFields.authed)[], } as const export const QueryType = GT.Object({ diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 9f7f8b6df9..cadff5c9ce 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -60,7 +60,7 @@ const setGqlAdminContext = async ( const userEmail = tokenPayload.sub as string // This should be the email from OAuth const role = tokenPayload.role as string - const scopeString = tokenPayload.scope as string || "[]" + const scopeString = (tokenPayload.scope as string) || "[]" const scope = JSON.parse(scopeString) as string[] const privilegedClientId = tokenPayload.sub as PrivilegedClientId @@ -83,39 +83,64 @@ const setGqlAdminContext = async ( ) } -const requiresViewAccess = rule({ cache: "contextual" })(async ( - parent, - args, - ctx: GraphQLAdminContext, -) => { - if (!ctx.userEmail || !ctx.scope) return false - return hasAccessRightInScope(ctx.scope, AdminAccessRight.VIEW_ACCOUNTS) -}) - -const requiresModifyAccess = rule({ cache: "contextual" })(async ( - parent, - args, - ctx: GraphQLAdminContext, -) => { - if (!ctx.userEmail || !ctx.scope) return false - return hasAccessRightInScope(ctx.scope, AdminAccessRight.MODIFY_ACCOUNTS) -}) +// Helper function to create access right rules +const createAccessRightRule = (accessRight: AdminAccessRight) => + rule({ cache: "contextual" })(async (parent, args, ctx: GraphQLAdminContext) => { + if (!ctx.userEmail || !ctx.scope) return false + return hasAccessRightInScope(ctx.scope, accessRight) + }) + +// Create access right rules object +const accessRules = { + viewAccounts: createAccessRightRule(AdminAccessRight.VIEW_ACCOUNTS), + modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), + deleteAccounts: createAccessRightRule(AdminAccessRight.DELETE_ACCOUNTS), + viewTransactions: createAccessRightRule(AdminAccessRight.VIEW_TRANSACTIONS), + sendNotifications: createAccessRightRule(AdminAccessRight.SEND_NOTIFICATIONS), + systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), +} export async function startApolloServerForAdminSchema() { - const viewerQueryFields: { [key: string]: Rule } = {} - for (const key of queryPermissions.view) { - viewerQueryFields[key] = requiresViewAccess + // Build query permissions from queries.ts definitions + const queryFields: { [key: string]: Rule } = {} + + // Apply VIEW_ACCOUNTS permission to specified queries + for (const queryName of queryPermissions.viewAccounts) { + queryFields[queryName] = accessRules.viewAccounts + } + + // Apply VIEW_TRANSACTIONS permission to specified queries + for (const queryName of queryPermissions.viewTransactions) { + queryFields[queryName] = accessRules.viewTransactions + } + + // Apply SYSTEM_CONFIG permission to specified queries + for (const queryName of queryPermissions.systemConfig) { + queryFields[queryName] = accessRules.systemConfig + } + + // Build mutation permissions from mutations.ts definitions + const mutationFields: { [key: string]: Rule } = {} + + // Apply MODIFY_ACCOUNTS permission to specified mutations + for (const mutationName of mutationPermissions.modifyAccounts) { + mutationFields[mutationName] = accessRules.modifyAccounts + } + + // Apply DELETE_ACCOUNTS permission to specified mutations + for (const mutationName of mutationPermissions.deleteAccounts) { + mutationFields[mutationName] = accessRules.deleteAccounts } - const authedMutationFields: { [key: string]: Rule } = {} - for (const key of mutationPermissions.modify) { - authedMutationFields[key] = requiresModifyAccess + // Apply SEND_NOTIFICATIONS permission to specified mutations + for (const mutationName of mutationPermissions.sendNotifications) { + mutationFields[mutationName] = accessRules.sendNotifications } const permissions = shield( { - Query: viewerQueryFields, - Mutation: authedMutationFields, + Query: queryFields, + Mutation: mutationFields, }, { allowExternalErrors: true, diff --git a/core/api/src/services/auth/role-checker.ts b/core/api/src/services/auth/role-checker.ts index 0923209764..499d784b09 100644 --- a/core/api/src/services/auth/role-checker.ts +++ b/core/api/src/services/auth/role-checker.ts @@ -1,7 +1,3 @@ - - - - export enum AdminRole { VIEWER = "roles/adminPanelViewer", SUPPORT = "roles/adminPanelSupport", @@ -32,7 +28,6 @@ const STRING_ROLE_ACCESS_RIGHTS = { ADMIN: Object.values(AdminAccessRight), } - export const hasAccessRight = async ( role: AdminRoleString, accessRight: AdminAccessRight, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e7483cb68..bf8a98363f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1208,9 +1208,6 @@ importers: firebase-admin: specifier: ^12.6.0 version: 12.7.0 - google-auth-library: - specifier: ^10.2.0 - version: 10.2.0 google-protobuf: specifier: ^3.21.4 version: 3.21.4 @@ -2852,7 +2849,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2875,7 +2872,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2990,7 +2987,7 @@ packages: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -4369,7 +4366,7 @@ packages: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5182,7 +5179,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5212,7 +5209,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5228,7 +5225,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -6911,7 +6908,7 @@ packages: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.8 chalk: 4.1.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) dotenv: 16.6.1 graphql: 16.11.0 graphql-request: 6.1.0(graphql@16.11.0) @@ -7210,7 +7207,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8078,7 +8075,7 @@ packages: dependencies: '@types/node': 22.16.0 async-exit-hook: 2.0.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -12065,7 +12062,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -13688,7 +13685,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13716,7 +13713,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13832,7 +13829,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.40.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13852,7 +13849,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13873,7 +13870,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13894,7 +13891,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.3.3 transitivePeerDependencies: @@ -13915,7 +13912,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13936,7 +13933,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13957,7 +13954,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13978,7 +13975,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13999,7 +13996,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14017,7 +14014,7 @@ packages: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14032,7 +14029,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -14046,7 +14043,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -14060,7 +14057,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14136,7 +14133,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 tsutils: 3.21.0(typescript@5.6.3) typescript: 5.6.3 @@ -14156,7 +14153,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.2.2) typescript: 5.2.2 @@ -14176,7 +14173,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.8.3) typescript: 5.8.3 @@ -14193,7 +14190,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14236,7 +14233,7 @@ packages: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14257,7 +14254,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14278,7 +14275,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14299,7 +14296,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14320,7 +14317,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14342,7 +14339,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14364,7 +14361,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14386,7 +14383,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14408,7 +14405,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14430,7 +14427,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14452,7 +14449,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14473,7 +14470,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14494,7 +14491,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14515,7 +14512,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15280,7 +15277,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16847,7 +16844,7 @@ packages: '@testim/chrome-version': 1.1.4 axios: 1.11.0 compare-versions: 6.1.1 - extract-zip: 2.0.1 + extract-zip: 2.0.1(supports-color@8.1.1) proxy-agent: 6.5.0 proxy-from-env: 1.1.0 tcp-port-used: 1.0.2 @@ -17749,6 +17746,7 @@ packages: /data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dev: true /data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} @@ -17873,6 +17871,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 + dev: true /debug@4.4.1(supports-color@8.1.1): resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -18081,7 +18080,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) filing-cabinet: 3.3.1 precinct: 9.2.1 typescript: 4.9.5 @@ -18151,7 +18150,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -18212,7 +18211,7 @@ packages: resolution: {integrity: sha512-Rps1xDkEEBSq3kLdsdnHZL1x2S4NGDcbrjmd4q+PykK5aJwDdP5MBgrJw1Xo+kyUHuv3JEzPqxr+Dj9ryeDRTA==} engines: {node: '>= 6.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gonzales-pe: 4.3.0 node-source-walk: 4.3.0 transitivePeerDependencies: @@ -18223,7 +18222,7 @@ packages: resolution: {integrity: sha512-Fwc/g9VcrowODIAeKRWZfVA/EufxYL7XfuqJQFroBKGikKX83d2G7NFw6kDlSYGG3LNQIyVa+eWv1mqre+v4+A==} engines: {node: ^10 || ^12 || >=14} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) is-url: 1.2.4 postcss: 8.5.6 postcss-values-parser: 2.0.1 @@ -18798,7 +18797,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -19106,7 +19105,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.40.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.40.0) get-tsconfig: 4.10.1 @@ -19132,7 +19131,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) get-tsconfig: 4.10.1 @@ -19843,7 +19842,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19894,7 +19893,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19951,7 +19950,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -20213,20 +20212,6 @@ packages: - supports-color dev: true - /extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - dependencies: - debug: 4.4.1(supports-color@5.5.0) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - dev: true - /extract-zip@2.0.1(supports-color@8.1.1): resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -20401,6 +20386,7 @@ packages: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + dev: true /fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} @@ -20459,7 +20445,7 @@ packages: dependencies: app-module-path: 2.2.0 commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) enhanced-resolve: 5.18.2 is-relative-path: 1.0.2 module-definition: 3.4.0 @@ -20770,6 +20756,7 @@ packages: engines: {node: '>=12.20.0'} dependencies: fetch-blob: 3.2.0 + dev: true /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -20900,17 +20887,6 @@ packages: - supports-color dev: false - /gaxios@7.1.1: - resolution: {integrity: sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==} - engines: {node: '>=18'} - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false - /gaze@1.1.3: resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==} engines: {node: '>= 4.0.0'} @@ -20930,17 +20906,6 @@ packages: - supports-color dev: false - /gcp-metadata@7.0.1: - resolution: {integrity: sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==} - engines: {node: '>=18'} - dependencies: - gaxios: 7.1.1 - google-logging-utils: 1.1.1 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -21049,7 +21014,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -21273,21 +21238,6 @@ packages: minimist: 1.2.8 dev: true - /google-auth-library@10.2.0: - resolution: {integrity: sha512-gy/0hRx8+Ye0HlUm3GrfpR4lbmJQ6bJ7F44DmN7GtMxxzWSojLzx0Bhv/hj7Wlj7a2On0FcT8jrz8Y1c1nxCyg==} - engines: {node: '>=18'} - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.1 - gcp-metadata: 7.0.1 - google-logging-utils: 1.1.1 - gtoken: 8.0.0 - jws: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} @@ -21331,11 +21281,6 @@ packages: engines: {node: '>=14'} dev: false - /google-logging-utils@1.1.1: - resolution: {integrity: sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==} - engines: {node: '>=14'} - dev: false - /google-protobuf@3.15.8: resolution: {integrity: sha512-2jtfdqTaSxk0cuBJBtTTWsot4WtR9RVr2rXg7x7OoqiuOKopPrwXpM1G4dXIkLcUNRh3RKzz76C8IOkksZSeOw==} dev: true @@ -21824,16 +21769,6 @@ packages: - supports-color dev: false - /gtoken@8.0.0: - resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} - engines: {node: '>=18'} - dependencies: - gaxios: 7.1.1 - jws: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -21901,6 +21836,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -22129,7 +22065,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22138,7 +22074,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -22195,7 +22131,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -22205,7 +22141,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22214,7 +22150,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22475,7 +22411,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -22965,7 +22901,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -23922,7 +23858,7 @@ packages: dependencies: '@types/express': 4.17.23 '@types/jsonwebtoken': 9.0.10 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24495,7 +24431,7 @@ packages: chalk: 4.1.2 commander: 7.2.0 commondir: 1.0.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) dependency-tree: 9.0.0 detective-amd: 4.2.0 detective-cjs: 4.1.0 @@ -24914,7 +24850,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) glob: 7.2.3 requirejs: 2.3.7 requirejs-config-file: 4.0.0 @@ -25071,7 +25007,7 @@ packages: resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} engines: {node: '>=14.0.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: false @@ -25359,7 +25295,6 @@ packages: /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead /node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -25383,6 +25318,7 @@ packages: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + dev: true /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} @@ -25894,7 +25830,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -26673,7 +26609,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) detective-amd: 3.1.2 detective-cjs: 3.1.3 detective-es6: 2.2.2 @@ -26902,7 +26838,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -26982,7 +26918,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -27931,7 +27867,7 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) module-details-from-path: 1.0.4 resolve: 1.22.10 transitivePeerDependencies: @@ -28606,7 +28542,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -29176,7 +29112,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -29224,6 +29160,7 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 + dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -31004,6 +30941,7 @@ packages: /web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + dev: true /web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} From 7e8c7eba97f05df756f34192bca9a6ab66698b6d Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:54:28 +0200 Subject: [PATCH 04/23] fix: address comments --- apps/admin-panel/app/access-rights.ts | 13 ++++ .../app/api/auth/[...nextauth]/options.ts | 78 ++++++++++++------- apps/admin-panel/app/env.ts | 2 +- .../src/servers/graphql-admin-api-server.ts | 18 ++++- core/api/src/services/auth/role-checker.ts | 55 ------------- 5 files changed, 81 insertions(+), 85 deletions(-) delete mode 100644 core/api/src/services/auth/role-checker.ts diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index e223199bf5..c49eba076d 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -57,6 +57,19 @@ export function getAllAccessRights(): AdminAccessRight[] { return Object.values(AdminAccessRight) } +/** + * Check if a scope array contains a specific access right + * @param scope - Array of access rights from JWT token scope + * @param accessRight - The access right to check for + * @returns True if the scope contains the access right + */ +export function hasAccessRightInScope( + scope: string[], + accessRight: AdminAccessRight, +): boolean { + return scope.includes(accessRight) +} + /** * Validate if a string is a valid admin role * @param role - The role string to validate diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index 1f31caf606..47b7af0f24 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -3,6 +3,7 @@ import CredentialsProvider from "next-auth/providers/credentials" import type { Provider } from "next-auth/providers" import { CallbacksOptions } from "next-auth" +import { trace } from "@opentelemetry/api" import { env } from "../../../env" import { @@ -47,7 +48,7 @@ if (env.NODE_ENV === "development") { return { id: "1", name: "admin", email: "admintest@blinkbitcoin.test" } } if (credentials?.username === "alice" && credentials?.password === "alice") { - return { id: "2", name: "bob", email: "alicetest@blinkbitcoin.test" } + return { id: "2", name: "alice", email: "alicetest@blinkbitcoin.test" } } if (credentials?.username === "bob" && credentials?.password === "bob") { return { id: "2", name: "bob", email: "bobtest@blinkbitcoin.test" } @@ -60,8 +61,6 @@ if (env.NODE_ENV === "development") { const callbacks: Partial = { async signIn({ account, profile, user }) { - console.log("signIn", account, profile, user) - console.log("-------------------------") if (account?.provider === "credentials" && env.NODE_ENV === "development") { return !!user } @@ -79,37 +78,60 @@ const callbacks: Partial = { const verified = new Boolean("email_verified" in profile && profile.email_verified) return verified && env.AUTHORIZED_EMAILS.includes(email) }, + // https://next-auth.js.org/configuration/callbacks#jwt-callback async jwt({ token, user }) { - let role_mapping: { [key: string]: string } - if (env.NODE_ENV === "development") { - role_mapping = { - "admintest@blinkbitcoin.test": "ADMIN", - "alicetest@blinkbitcoin.test": "VIEWER", - "bobtest@blinkbitcoin.test": "SUPPORT", + const tracer = trace.getTracer("admin-panel-auth") + + return tracer.startActiveSpan("jwt-callback", (span) => { + span.setAttributes({ + "auth.user_email": user?.email || token.email || "notSet", + "auth.has_user": !!user, + }) + + let role_mapping: { [key: string]: string } + if (env.NODE_ENV === "development") { + role_mapping = { + "admintest@blinkbitcoin.test": "ADMIN", + "alicetest@blinkbitcoin.test": "VIEWER", + "bobtest@blinkbitcoin.test": "SUPPORT", + } + } else { + role_mapping = env.USER_ROLE_MAP } - } else { - role_mapping = env.USER_ROLE_MAP - } - if (user) { - const userRole = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" - - // Validate and get access rights for the role - if (isValidAdminRole(userRole)) { - const accessRights = getAccessRightsForRole(userRole as AdminRole) - token.scope = JSON.stringify(accessRights) - token.role = userRole + + if (user) { + const userRole = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" + + if (isValidAdminRole(userRole)) { + const accessRights = getAccessRightsForRole(userRole as AdminRole) + token.scope = JSON.stringify(accessRights) + token.role = userRole + } else { + token.scope = JSON.stringify([]) + token.role = "" + } } else { - // Fallback to VIEWER if invalid role - const accessRights = getAccessRightsForRole("VIEWER") - token.scope = JSON.stringify(accessRights) - token.role = "VIEWER" + if (!(token.email && token.scope && token.role)) { + token.scope = JSON.stringify([]) + token.role = "" + } } - } else { - console.log("no user") - } - return token + + span.setAttributes({ + "auth.final_role": (token.role as string) || "notSet", + "auth.has_scope": !!token.scope, + }) + + span.end() + return token + }) }, + // https://next-auth.js.org/configuration/callbacks#session-callback async session({ session, token }) { + // If token has an error (like NoUser), deny the session + if (token.error) { + throw new Error("Access denied: Invalid authentication") + } session.scope = token.scope as string session.role = token.role as string return session diff --git a/apps/admin-panel/app/env.ts b/apps/admin-panel/app/env.ts index f08f1468e4..6eb455cace 100644 --- a/apps/admin-panel/app/env.ts +++ b/apps/admin-panel/app/env.ts @@ -50,6 +50,6 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, AUTHORIZED_EMAILS: process.env.AUTHORIZED_EMAILS, - USER_ROLE_MAP: process.env.ROLE_MAPPING || "{}", + USER_ROLE_MAP: process.env.USER_ROLE_MAP || "{}", }, }) diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index cadff5c9ce..2280f5635e 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -28,7 +28,23 @@ import { Transactions } from "@/app" import { AuthorizationError } from "@/graphql/error" -import { AdminAccessRight, hasAccessRightInScope } from "@/services/auth/role-checker" +// Admin access rights - duplicated from admin-panel to avoid dependency +enum AdminAccessRight { + VIEW_ACCOUNTS = "VIEW_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + DELETE_ACCOUNTS = "DELETE_ACCOUNTS", + VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", + SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", + SYSTEM_CONFIG = "SYSTEM_CONFIG", +} + +// Simple inline function to check if scope contains access right +const hasAccessRightInScope = ( + scope: string[], + accessRight: AdminAccessRight, +): boolean => { + return scope.includes(accessRight) +} // TODO: loaders probably not needed for the admin panel const loaders = { diff --git a/core/api/src/services/auth/role-checker.ts b/core/api/src/services/auth/role-checker.ts deleted file mode 100644 index 499d784b09..0000000000 --- a/core/api/src/services/auth/role-checker.ts +++ /dev/null @@ -1,55 +0,0 @@ -export enum AdminRole { - VIEWER = "roles/adminPanelViewer", - SUPPORT = "roles/adminPanelSupport", - ADMIN = "roles/adminPanelAdmin", -} - -export enum AdminAccessRight { - VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", - DELETE_ACCOUNTS = "DELETE_ACCOUNTS", - VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", - SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", - SYSTEM_CONFIG = "SYSTEM_CONFIG", -} - -// String-based role values from options.ts -export type AdminRoleString = "VIEWER" | "SUPPORT" | "ADMIN" - -// String-based role access rights mapping -const STRING_ROLE_ACCESS_RIGHTS = { - VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], - SUPPORT: [ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.SEND_NOTIFICATIONS, - ], - ADMIN: Object.values(AdminAccessRight), -} - -export const hasAccessRight = async ( - role: AdminRoleString, - accessRight: AdminAccessRight, -): Promise => { - return STRING_ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false -} - -/** - * Check if a scope array contains a specific access right - * @param scope - Array of access rights from JWT token scope - * @param accessRight - The access right to check for - * @returns True if the scope contains the access right - */ -export const hasAccessRightInScope = async ( - scope: string[], - accessRight: AdminAccessRight, -): Promise => { - return scope.includes(accessRight) -} - -// Backward compatibility - deprecated, use AdminAccessRight instead -export const AdminFeature = AdminAccessRight - -// Backward compatibility - deprecated, use hasAccessRight instead -export const hasFeature = hasAccessRight From c14fda3ee2ba8dbe3344f7dd6ed3be4fa2c344b3 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:34:03 +0200 Subject: [PATCH 05/23] chore: more tests --- .../app/api/auth/[...nextauth]/options.ts | 4 -- bats/core/api/admin.bats | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index 47b7af0f24..b57fd4cf12 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -128,10 +128,6 @@ const callbacks: Partial = { }, // https://next-auth.js.org/configuration/callbacks#session-callback async session({ session, token }) { - // If token has an error (like NoUser), deny the session - if (token.error) { - throw new Error("Access denied: Invalid authentication") - } session.scope = token.scope as string session.role = token.role as string return session diff --git a/bats/core/api/admin.bats b/bats/core/api/admin.bats index ce86ed5e19..e670c0d639 100644 --- a/bats/core/api/admin.bats +++ b/bats/core/api/admin.bats @@ -37,6 +37,68 @@ getEmailCode() { echo "$code" } + +@test "no_token: access denied without JWT token" { + # Test multiple endpoints to ensure comprehensive access control + exec_admin_graphql "" 'all-levels' '{}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-details-by-user-phone' '{"phone": "+1234567890"}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-details-by-username' '{"username": "test"}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-details-by-account-id' '{"accountId": "test-id"}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-details-by-user-id' '{"userId": "test-id"}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'user-update-phone' '{"input": {"phone": "+1234567890", "accountId": "test-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'user-update-email' '{"input": {"email": "test@example.com", "accountId": "test-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-update-level' '{"input": {"level": "TWO", "accountId": "test-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-update-status' '{"input": {"status": "LOCKED", "accountId": "test-id", "comment": "test"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'filtered-user-count' '{}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'marketing-notification-trigger' '{"input": {"localizedNotificationContents": [{"language": "en", "title": "Test", "body": "Test"}], "shouldSendPush": false, "shouldAddToHistory": true, "shouldAddToBulletin": true}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 + + exec_admin_graphql "" 'account-force-delete' '{"input": {"accountId": "test-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" || "$(graphql_output '.error.message')" == "Access credentials are invalid" ]] || exit 1 +} + +@test "empty_scope: access denied with empty scope token" { + _create_admin_client_and_token '[]' "empty_scope.token" + empty_token="$(read_value 'empty_scope.token')" + + # Test different endpoints than the no_token test for broader coverage (using only existing .gql files) + echo "Testing inactive-merchants..." >&2 + exec_admin_graphql "$empty_token" 'inactive-merchants' '{}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" ]] || exit 1 + + echo "Testing merchants-pending-approval..." >&2 + exec_admin_graphql "$empty_token" 'merchants-pending-approval' '{}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" ]] || exit 1 + + echo "Testing merchant-map-validate..." >&2 + exec_admin_graphql "$empty_token" 'merchant-map-validate' '{"input": {"id": "test-merchant-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" ]] || exit 1 + + echo "Testing merchant-map-delete..." >&2 + exec_admin_graphql "$empty_token" 'merchant-map-delete' '{"input": {"id": "test-merchant-id"}}' + [[ "$(graphql_output '.errors[0].message')" == "Not authorized" ]] || exit 1 +} + @test "view_user: can query account details by phone" { token="$(read_value 'view_user.token')" variables=$( From f9a001c2203dfc864f566de9b973b930d81fd126 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:27:07 +0200 Subject: [PATCH 06/23] chore: proper validation and failure at startup and docs --- apps/admin-panel/README.md | 19 ++++++++++++++----- apps/admin-panel/app/env.ts | 8 ++++++-- apps/admin-panel/instrumentation.node.ts | 7 +++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/admin-panel/README.md b/apps/admin-panel/README.md index 0cb58e9dfa..429f0cea8a 100644 --- a/apps/admin-panel/README.md +++ b/apps/admin-panel/README.md @@ -13,18 +13,16 @@ With a default installation, Admin Panel can be accessed with `admin.domain.com` copy `.env` to `.env.local`. and edit environment variable accordingly. ``` -yarn install -export PORT=3004 -yarn dev +pnpm install +pnpm dev ``` Runs the app in the development mode. Open [http://localhost:3004](http://localhost:3004) to view it in the browser. The page will reload if you make edits. -You will also see any lint errors in the console. -### `yarn build` +### `pnpm build` Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance. @@ -34,5 +32,16 @@ Your app is ready to be deployed! ### Development mode credentials +As we have three different roles in the admin panel, we have three different credentials for development mode: + +#### Admin - username: `admin` - password: `admin` + +#### Support +- username: `bob` +- password: `bob` + +#### Viewer +- username: `alice` +- password: `alice` diff --git a/apps/admin-panel/app/env.ts b/apps/admin-panel/app/env.ts index 6eb455cace..2ae97b051a 100644 --- a/apps/admin-panel/app/env.ts +++ b/apps/admin-panel/app/env.ts @@ -21,8 +21,12 @@ export const env = createEnv({ .transform((str) => { try { return JSON.parse(str) - } catch { - return {} + } catch (error) { + throw new Error( + `Invalid JSON in USER_ROLE_MAP environment variable: ${ + error instanceof Error ? error.message : "Unknown parsing error" + }`, + ) } }) .default("{}"), diff --git a/apps/admin-panel/instrumentation.node.ts b/apps/admin-panel/instrumentation.node.ts index 2be38d5b05..dd9c2dd9ce 100644 --- a/apps/admin-panel/instrumentation.node.ts +++ b/apps/admin-panel/instrumentation.node.ts @@ -8,6 +8,13 @@ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http" import { GraphQLInstrumentation } from "@opentelemetry/instrumentation-graphql" import { W3CTraceContextPropagator } from "@opentelemetry/core" +import { env } from "./app/env" + +console.log("Validating environment variables...") +// Simply accessing the env object triggers validation of all variables +JSON.stringify(env) +console.log("✅ Environment variables validated successfully") + const sdk = new NodeSDK({ textMapPropagator: new W3CTraceContextPropagator(), resource: new Resource({ From 92928fcd49e4028153c6b9e804a2437d9f9e804e Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:00:03 +0200 Subject: [PATCH 07/23] chore: simplify access-rules --- core/api/src/graphql/admin/access-rules.ts | 60 +++++++++ core/api/src/graphql/admin/mutations.ts | 73 ++++++----- core/api/src/graphql/admin/queries.ts | 114 +++++++++++------- .../src/servers/graphql-admin-api-server.ts | 76 +----------- 4 files changed, 181 insertions(+), 142 deletions(-) create mode 100644 core/api/src/graphql/admin/access-rules.ts diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts new file mode 100644 index 0000000000..dcb51c9afd --- /dev/null +++ b/core/api/src/graphql/admin/access-rules.ts @@ -0,0 +1,60 @@ +import { rule } from "graphql-shield" + +/** + * Admin Access Rules for GraphQL Shield + * + * This module contains the actual GraphQL Shield rules that can be used + * directly in field definitions. Each rule checks if the user has the + * required permission in their JWT scope. + */ + +// Admin access rights enum +export enum AdminAccessRight { + VIEW_ACCOUNTS = "VIEW_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + DELETE_ACCOUNTS = "DELETE_ACCOUNTS", + VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", + SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", + SYSTEM_CONFIG = "SYSTEM_CONFIG", +} + +// Helper function to create access right rules +const createAccessRightRule = (accessRight: AdminAccessRight) => + rule({ cache: "contextual" })(async (parent, args, ctx: GraphQLAdminContext) => { + if (!ctx.userEmail || !ctx.scope) return false + return ctx.scope.includes(accessRight) + }) + +// Export the actual GraphQL Shield rules that can be used directly +export const accessRules = { + viewAccounts: createAccessRightRule(AdminAccessRight.VIEW_ACCOUNTS), + modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), + deleteAccounts: createAccessRightRule(AdminAccessRight.DELETE_ACCOUNTS), + viewTransactions: createAccessRightRule(AdminAccessRight.VIEW_TRANSACTIONS), + sendNotifications: createAccessRightRule(AdminAccessRight.SEND_NOTIFICATIONS), + systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), +} + +// Helper function to extract just the GraphQL fields from our structure +export function extractFields>( + fieldsWithRules: T +): Record { + const result: Record = {} + for (const [key, value] of Object.entries(fieldsWithRules)) { + result[key] = value.field + } + return result as Record +} + +// Helper function to build permission mappings from our flat structure +export function buildPermissionMappings>( + fieldsWithRules: T +): Record { + const permissionMap: Record = {} + + for (const [fieldName, { rule }] of Object.entries(fieldsWithRules)) { + permissionMap[fieldName] = rule + } + + return permissionMap +} diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 9d80198750..ccfe94df33 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -11,43 +11,58 @@ import AccountForceDeleteMutation from "./root/mutation/account-force-delete" import TriggerMarketingNotificationMutation from "./root/mutation/marketing-notification-trigger" import { GT } from "@/graphql/index" +import { accessRules, extractFields, buildPermissionMappings } from "./access-rules" +// Mutation fields with embedded access rules export const mutationFields = { unauthed: {}, authed: { - accountUpdateLevel: AccountUpdateLevelMutation, - accountUpdateStatus: AccountUpdateStatusMutation, - accountForceDelete: AccountForceDeleteMutation, - merchantMapValidate: MerchantMapValidateMutation, - merchantMapDelete: MerchantMapDeleteMutation, - marketingNotificationTrigger: TriggerMarketingNotificationMutation, - userUpdateEmail: UserUpdateEmailMutation, - userUpdatePhone: UserUpdatePhoneMutation, + // Account modification operations - require MODIFY_ACCOUNTS + accountUpdateLevel: { + field: AccountUpdateLevelMutation, + rule: accessRules.modifyAccounts + }, + accountUpdateStatus: { + field: AccountUpdateStatusMutation, + rule: accessRules.modifyAccounts + }, + userUpdateEmail: { + field: UserUpdateEmailMutation, + rule: accessRules.modifyAccounts + }, + userUpdatePhone: { + field: UserUpdatePhoneMutation, + rule: accessRules.modifyAccounts + }, + merchantMapValidate: { + field: MerchantMapValidateMutation, + rule: accessRules.modifyAccounts + }, + merchantMapDelete: { + field: MerchantMapDeleteMutation, + rule: accessRules.modifyAccounts + }, + + // Account deletion operations - require DELETE_ACCOUNTS + accountForceDelete: { + field: AccountForceDeleteMutation, + rule: accessRules.deleteAccounts + }, + + // Notification operations - require SEND_NOTIFICATIONS + marketingNotificationTrigger: { + field: TriggerMarketingNotificationMutation, + rule: accessRules.sendNotifications + }, }, } +// Extract the actual GraphQL fields for the schema +const extractedMutationFields = extractFields(mutationFields.authed) -// Detailed mutation permissions mapping by access right -export const mutationPermissions = { - // Account modification operations - require MODIFY_ACCOUNTS - modifyAccounts: [ - "accountUpdateLevel", - "accountUpdateStatus", - "userUpdateEmail", - "userUpdatePhone", - "merchantMapValidate", - "merchantMapDelete", - ] as (keyof typeof mutationFields.authed)[], - - // Account deletion operations - require DELETE_ACCOUNTS - deleteAccounts: ["accountForceDelete"] as (keyof typeof mutationFields.authed)[], - - // Notification operations - require SEND_NOTIFICATIONS - sendNotifications: [ - "marketingNotificationTrigger", - ] as (keyof typeof mutationFields.authed)[], -} as const +// Build permission mappings automatically from the field definitions (now field -> rule mapping) +export const mutationPermissions = buildPermissionMappings(mutationFields.authed) export const MutationType = GT.Object({ name: "Mutation", - fields: () => ({ ...mutationFields.authed }), + fields: () => ({ ...extractedMutationFields }), }) diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index ab1052a796..4f76c887af 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -15,57 +15,89 @@ import InactiveMerchantsQuery from "./root/query/inactive-merchants-listing" import FilteredUserCountQuery from "./root/query/filtered-user-count" import { GT } from "@/graphql/index" +import { accessRules, extractFields, buildPermissionMappings } from "./access-rules" +// Query fields with embedded access rules export const queryFields = { unauthed: {}, authed: { - accountDetailsByAccountId: AccountDetailsByAccountId, - accountDetailsByEmail: AccountDetailsByUserEmailQuery, - accountDetailsByUserPhone: AccountDetailsByUserPhoneQuery, - accountDetailsByUserId: AccountDetailsByUserId, - accountDetailsByUsername: AccountDetailsByUsernameQuery, - allLevels: AllLevelsQuery, - filteredUserCount: FilteredUserCountQuery, - inactiveMerchants: InactiveMerchantsQuery, - lightningInvoice: LightningInvoiceQuery, - lightningPayment: LightningPaymentQuery, - merchantsPendingApproval: MerchantsPendingApprovalQuery, - transactionById: TransactionByIdQuery, - transactionsByHash: TransactionsByHashQuery, - transactionsByPaymentRequest: TransactionsByPaymentRequestQuery, - wallet: WalletQuery, + // Account viewing operations - require VIEW_ACCOUNTS + accountDetailsByAccountId: { + field: AccountDetailsByAccountId, + rule: accessRules.viewAccounts + }, + accountDetailsByEmail: { + field: AccountDetailsByUserEmailQuery, + rule: accessRules.viewAccounts + }, + accountDetailsByUserPhone: { + field: AccountDetailsByUserPhoneQuery, + rule: accessRules.viewAccounts + }, + accountDetailsByUserId: { + field: AccountDetailsByUserId, + rule: accessRules.viewAccounts + }, + accountDetailsByUsername: { + field: AccountDetailsByUsernameQuery, + rule: accessRules.viewAccounts + }, + filteredUserCount: { + field: FilteredUserCountQuery, + rule: accessRules.viewAccounts + }, + wallet: { + field: WalletQuery, + rule: accessRules.viewAccounts + }, + inactiveMerchants: { + field: InactiveMerchantsQuery, + rule: accessRules.viewAccounts + }, + merchantsPendingApproval: { + field: MerchantsPendingApprovalQuery, + rule: accessRules.viewAccounts + }, + + // Transaction viewing operations - require VIEW_TRANSACTIONS + lightningInvoice: { + field: LightningInvoiceQuery, + rule: accessRules.viewTransactions + }, + lightningPayment: { + field: LightningPaymentQuery, + rule: accessRules.viewTransactions + }, + transactionById: { + field: TransactionByIdQuery, + rule: accessRules.viewTransactions + }, + transactionsByHash: { + field: TransactionsByHashQuery, + rule: accessRules.viewTransactions + }, + transactionsByPaymentRequest: { + field: TransactionsByPaymentRequestQuery, + rule: accessRules.viewTransactions + }, + + // System configuration operations - require SYSTEM_CONFIG + allLevels: { + field: AllLevelsQuery, + rule: accessRules.systemConfig + }, }, } -// Detailed query permissions mapping by access right -export const queryPermissions = { - // Account viewing operations - require VIEW_ACCOUNTS - viewAccounts: [ - "accountDetailsByAccountId", - "accountDetailsByEmail", - "accountDetailsByUserPhone", - "accountDetailsByUserId", - "accountDetailsByUsername", - "filteredUserCount", - "wallet", - "inactiveMerchants", - "merchantsPendingApproval", - ] as (keyof typeof queryFields.authed)[], +// Helper functions are now imported from access-rules.ts to avoid duplication - // Transaction viewing operations - require VIEW_TRANSACTIONS - viewTransactions: [ - "lightningInvoice", - "lightningPayment", - "transactionById", - "transactionsByHash", - "transactionsByPaymentRequest", - ] as (keyof typeof queryFields.authed)[], +// Extract the actual GraphQL fields for the schema +const extractedQueryFields = extractFields(queryFields.authed) - // System configuration operations - require SYSTEM_CONFIG - systemConfig: ["allLevels"] as (keyof typeof queryFields.authed)[], -} as const +// Build permission mappings automatically from the field definitions (now field -> rule mapping) +export const queryPermissions = buildPermissionMappings(queryFields.authed) export const QueryType = GT.Object({ name: "Query", - fields: () => ({ ...queryFields.unauthed, ...queryFields.authed }), + fields: () => ({ ...queryFields.unauthed, ...extractedQueryFields }), }) diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 2280f5635e..665e61969f 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -28,24 +28,6 @@ import { Transactions } from "@/app" import { AuthorizationError } from "@/graphql/error" -// Admin access rights - duplicated from admin-panel to avoid dependency -enum AdminAccessRight { - VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", - DELETE_ACCOUNTS = "DELETE_ACCOUNTS", - VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", - SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", - SYSTEM_CONFIG = "SYSTEM_CONFIG", -} - -// Simple inline function to check if scope contains access right -const hasAccessRightInScope = ( - scope: string[], - accessRight: AdminAccessRight, -): boolean => { - return scope.includes(accessRight) -} - // TODO: loaders probably not needed for the admin panel const loaders = { txnMetadata: new DataLoader(async (keys) => { @@ -99,64 +81,14 @@ const setGqlAdminContext = async ( ) } -// Helper function to create access right rules -const createAccessRightRule = (accessRight: AdminAccessRight) => - rule({ cache: "contextual" })(async (parent, args, ctx: GraphQLAdminContext) => { - if (!ctx.userEmail || !ctx.scope) return false - return hasAccessRightInScope(ctx.scope, accessRight) - }) - -// Create access right rules object -const accessRules = { - viewAccounts: createAccessRightRule(AdminAccessRight.VIEW_ACCOUNTS), - modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), - deleteAccounts: createAccessRightRule(AdminAccessRight.DELETE_ACCOUNTS), - viewTransactions: createAccessRightRule(AdminAccessRight.VIEW_TRANSACTIONS), - sendNotifications: createAccessRightRule(AdminAccessRight.SEND_NOTIFICATIONS), - systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), -} +// No need for complex permission mapping - the queries and mutations now export direct field -> rule mappings export async function startApolloServerForAdminSchema() { - // Build query permissions from queries.ts definitions - const queryFields: { [key: string]: Rule } = {} - - // Apply VIEW_ACCOUNTS permission to specified queries - for (const queryName of queryPermissions.viewAccounts) { - queryFields[queryName] = accessRules.viewAccounts - } - - // Apply VIEW_TRANSACTIONS permission to specified queries - for (const queryName of queryPermissions.viewTransactions) { - queryFields[queryName] = accessRules.viewTransactions - } - - // Apply SYSTEM_CONFIG permission to specified queries - for (const queryName of queryPermissions.systemConfig) { - queryFields[queryName] = accessRules.systemConfig - } - - // Build mutation permissions from mutations.ts definitions - const mutationFields: { [key: string]: Rule } = {} - - // Apply MODIFY_ACCOUNTS permission to specified mutations - for (const mutationName of mutationPermissions.modifyAccounts) { - mutationFields[mutationName] = accessRules.modifyAccounts - } - - // Apply DELETE_ACCOUNTS permission to specified mutations - for (const mutationName of mutationPermissions.deleteAccounts) { - mutationFields[mutationName] = accessRules.deleteAccounts - } - - // Apply SEND_NOTIFICATIONS permission to specified mutations - for (const mutationName of mutationPermissions.sendNotifications) { - mutationFields[mutationName] = accessRules.sendNotifications - } - + // The permission mappings are now direct field -> rule mappings, so we can use them directly const permissions = shield( { - Query: queryFields, - Mutation: mutationFields, + Query: queryPermissions, + Mutation: mutationPermissions, }, { allowExternalErrors: true, From 9e6ce27f1ef5a2824845a4dcac4c774f31616685 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:07:16 +0200 Subject: [PATCH 08/23] docs: documenting functions --- core/api/src/graphql/admin/access-rules.ts | 46 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index dcb51c9afd..3e6bba6e6d 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -9,7 +9,7 @@ import { rule } from "graphql-shield" */ // Admin access rights enum -export enum AdminAccessRight { +enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", DELETE_ACCOUNTS = "DELETE_ACCOUNTS", @@ -35,7 +35,27 @@ export const accessRules = { systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), } -// Helper function to extract just the GraphQL fields from our structure +/** + * Extracts GraphQL field configurations from a structure that contains both fields and rules. + * + * This function takes an object where each property contains both a GraphQL field configuration + * and an access rule, and returns a new object with just the field configurations. This is + * used to prepare the fields for GraphQL schema construction. + * + * @template T - The type of the input object containing field/rule pairs + * @param fieldsWithRules - Object where each property has { field, rule } structure + * @returns Object containing only the GraphQL field configurations + * + * @example + * ```typescript + * const input = { + * userQuery: { field: UserQueryField, rule: viewAccountsRule }, + * adminQuery: { field: AdminQueryField, rule: adminRule } + * } + * const fields = extractFields(input) + * // Result: { userQuery: UserQueryField, adminQuery: AdminQueryField } + * ``` + */ export function extractFields>( fieldsWithRules: T ): Record { @@ -46,7 +66,27 @@ export function extractFields } -// Helper function to build permission mappings from our flat structure +/** + * Builds a permission mapping from field names to their corresponding access rules. + * + * This function takes an object where each property contains both a GraphQL field configuration + * and an access rule, and returns a mapping from field names to their access rules. This + * mapping is used by GraphQL Shield to apply authorization rules to specific fields. + * + * @template T - The type of the input object containing field/rule pairs + * @param fieldsWithRules - Object where each property has { field, rule } structure + * @returns Object mapping field names to their access rules + * + * @example + * ```typescript + * const input = { + * userQuery: { field: UserQueryField, rule: viewAccountsRule }, + * adminQuery: { field: AdminQueryField, rule: adminRule } + * } + * const permissions = buildPermissionMappings(input) + * // Result: { userQuery: viewAccountsRule, adminQuery: adminRule } + * ``` + */ export function buildPermissionMappings>( fieldsWithRules: T ): Record { From b444d2ac280b250d856ec08726eb01c871cdfb6d Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:34:40 +0200 Subject: [PATCH 09/23] chore: linting, typing and spelling --- core/api/src/graphql/admin/access-rules.ts | 49 ++--- core/api/src/graphql/admin/mutations.ts | 19 +- core/api/src/graphql/admin/queries.ts | 33 ++-- core/api/src/graphql/admin/types/index.ts | 19 ++ .../src/servers/graphql-admin-api-server.ts | 3 +- package.json | 3 +- pnpm-lock.yaml | 169 ++++++++++-------- 7 files changed, 168 insertions(+), 127 deletions(-) diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index 3e6bba6e6d..e43dd970a1 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -1,8 +1,12 @@ import { rule } from "graphql-shield" +import { Rule } from "graphql-shield/typings/rules" +import { GraphQLFieldConfig } from "graphql" + +import { AdminFieldDefinitions } from "./types" /** * Admin Access Rules for GraphQL Shield - * + * * This module contains the actual GraphQL Shield rules that can be used * directly in field definitions. Each rule checks if the user has the * required permission in their JWT scope. @@ -12,7 +16,7 @@ import { rule } from "graphql-shield" enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", - DELETE_ACCOUNTS = "DELETE_ACCOUNTS", + DELETE_ACCOUNTS = "DELETE_ACCOUNTS", VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", SYSTEM_CONFIG = "SYSTEM_CONFIG", @@ -20,7 +24,7 @@ enum AdminAccessRight { // Helper function to create access right rules const createAccessRightRule = (accessRight: AdminAccessRight) => - rule({ cache: "contextual" })(async (parent, args, ctx: GraphQLAdminContext) => { + rule({ cache: "contextual" })(async (_parent, _args, ctx: GraphQLAdminContext) => { if (!ctx.userEmail || !ctx.scope) return false return ctx.scope.includes(accessRight) }) @@ -48,22 +52,26 @@ export const accessRules = { * * @example * ```typescript - * const input = { +* const input = { * userQuery: { field: UserQueryField, rule: viewAccountsRule }, * adminQuery: { field: AdminQueryField, rule: adminRule } * } * const fields = extractFields(input) * // Result: { userQuery: UserQueryField, adminQuery: AdminQueryField } - * ``` - */ -export function extractFields>( - fieldsWithRules: T -): Record { - const result: Record = {} + * +```javascript +*/ +export function extractFields( + fieldsWithRules: T, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Record> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: Record> = {} for (const [key, value] of Object.entries(fieldsWithRules)) { result[key] = value.field } - return result as Record + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return result as Record> } /** @@ -78,23 +86,24 @@ export function extractFields>( - fieldsWithRules: T -): Record { - const permissionMap: Record = {} + * +``` + */ +export function buildPermissionMappings( + fieldsWithRules: T, +): Record { + const permissionMap: Record = {} for (const [fieldName, { rule }] of Object.entries(fieldsWithRules)) { permissionMap[fieldName] = rule } - return permissionMap } diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index ccfe94df33..6fbbdd8980 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -10,9 +10,10 @@ import AccountForceDeleteMutation from "./root/mutation/account-force-delete" import TriggerMarketingNotificationMutation from "./root/mutation/marketing-notification-trigger" -import { GT } from "@/graphql/index" import { accessRules, extractFields, buildPermissionMappings } from "./access-rules" +import { GT } from "@/graphql/index" + // Mutation fields with embedded access rules export const mutationFields = { unauthed: {}, @@ -20,39 +21,39 @@ export const mutationFields = { // Account modification operations - require MODIFY_ACCOUNTS accountUpdateLevel: { field: AccountUpdateLevelMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, accountUpdateStatus: { field: AccountUpdateStatusMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, userUpdateEmail: { field: UserUpdateEmailMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, userUpdatePhone: { field: UserUpdatePhoneMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, merchantMapValidate: { field: MerchantMapValidateMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, merchantMapDelete: { field: MerchantMapDeleteMutation, - rule: accessRules.modifyAccounts + rule: accessRules.modifyAccounts, }, // Account deletion operations - require DELETE_ACCOUNTS accountForceDelete: { field: AccountForceDeleteMutation, - rule: accessRules.deleteAccounts + rule: accessRules.deleteAccounts, }, // Notification operations - require SEND_NOTIFICATIONS marketingNotificationTrigger: { field: TriggerMarketingNotificationMutation, - rule: accessRules.sendNotifications + rule: accessRules.sendNotifications, }, }, } diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index 4f76c887af..8a97003b44 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -14,9 +14,10 @@ import MerchantsPendingApprovalQuery from "./root/query/merchants-pending-approv import InactiveMerchantsQuery from "./root/query/inactive-merchants-listing" import FilteredUserCountQuery from "./root/query/filtered-user-count" -import { GT } from "@/graphql/index" import { accessRules, extractFields, buildPermissionMappings } from "./access-rules" +import { GT } from "@/graphql/index" + // Query fields with embedded access rules export const queryFields = { unauthed: {}, @@ -24,67 +25,67 @@ export const queryFields = { // Account viewing operations - require VIEW_ACCOUNTS accountDetailsByAccountId: { field: AccountDetailsByAccountId, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, accountDetailsByEmail: { field: AccountDetailsByUserEmailQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, accountDetailsByUserPhone: { field: AccountDetailsByUserPhoneQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, accountDetailsByUserId: { field: AccountDetailsByUserId, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, accountDetailsByUsername: { field: AccountDetailsByUsernameQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, filteredUserCount: { field: FilteredUserCountQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, wallet: { field: WalletQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, inactiveMerchants: { field: InactiveMerchantsQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, merchantsPendingApproval: { field: MerchantsPendingApprovalQuery, - rule: accessRules.viewAccounts + rule: accessRules.viewAccounts, }, // Transaction viewing operations - require VIEW_TRANSACTIONS lightningInvoice: { field: LightningInvoiceQuery, - rule: accessRules.viewTransactions + rule: accessRules.viewTransactions, }, lightningPayment: { field: LightningPaymentQuery, - rule: accessRules.viewTransactions + rule: accessRules.viewTransactions, }, transactionById: { field: TransactionByIdQuery, - rule: accessRules.viewTransactions + rule: accessRules.viewTransactions, }, transactionsByHash: { field: TransactionsByHashQuery, - rule: accessRules.viewTransactions + rule: accessRules.viewTransactions, }, transactionsByPaymentRequest: { field: TransactionsByPaymentRequestQuery, - rule: accessRules.viewTransactions + rule: accessRules.viewTransactions, }, // System configuration operations - require SYSTEM_CONFIG allLevels: { field: AllLevelsQuery, - rule: accessRules.systemConfig + rule: accessRules.systemConfig, }, }, } diff --git a/core/api/src/graphql/admin/types/index.ts b/core/api/src/graphql/admin/types/index.ts index be595fab5b..48c7878554 100644 --- a/core/api/src/graphql/admin/types/index.ts +++ b/core/api/src/graphql/admin/types/index.ts @@ -1,5 +1,24 @@ +import { Rule } from "graphql-shield/typings/rules" +import { GraphQLFieldConfig } from "graphql" + import BtcWallet from "@/graphql/shared/types/object/btc-wallet" import GraphQLApplicationError from "@/graphql/shared/types/object/graphql-application-error" import UsdWallet from "@/graphql/shared/types/object/usd-wallet" export const ALL_INTERFACE_TYPES = [GraphQLApplicationError, BtcWallet, UsdWallet] + +/** + * Represents a GraphQL field configuration paired with its access rule. + * Used to define both the field behavior and authorization requirements in one place. + */ +export type AdminFieldDefinition = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: GraphQLFieldConfig + rule: Rule +} + +/** + * Collection of GraphQL fields with their associated access rules. + * Provides type safety for admin GraphQL field definitions. + */ +export type AdminFieldDefinitions = Record diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 665e61969f..a71a3ac73c 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -1,6 +1,5 @@ import { applyMiddleware } from "graphql-middleware" -import { rule, shield } from "graphql-shield" -import { Rule } from "graphql-shield/typings/rules" +import { shield } from "graphql-shield" import { NextFunction, Request, Response } from "express" diff --git a/package.json b/package.json index 3227c00398..79bede9b58 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "preinstall": "npx only-allow pnpm", "nodev": "node -v", - "whichnode": "which node" + "whichnode": "which node", + "prepare": "husky || true # prepare sometimes called without dev-deps" }, "engines": { "node": "20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf8a98363f..972ea79201 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2849,7 +2849,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2872,7 +2872,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2987,7 +2987,7 @@ packages: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -4366,7 +4366,7 @@ packages: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5179,7 +5179,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5209,7 +5209,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5225,7 +5225,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -6908,7 +6908,7 @@ packages: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.8 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dotenv: 16.6.1 graphql: 16.11.0 graphql-request: 6.1.0(graphql@16.11.0) @@ -7207,7 +7207,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8075,7 +8075,7 @@ packages: dependencies: '@types/node': 22.16.0 async-exit-hook: 2.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -12062,7 +12062,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -13685,7 +13685,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13713,7 +13713,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13829,7 +13829,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13849,7 +13849,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13870,7 +13870,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13891,7 +13891,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.3.3 transitivePeerDependencies: @@ -13912,7 +13912,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13933,7 +13933,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13954,7 +13954,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13975,7 +13975,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13996,7 +13996,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14014,7 +14014,7 @@ packages: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14029,7 +14029,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -14043,7 +14043,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -14057,7 +14057,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14133,7 +14133,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 tsutils: 3.21.0(typescript@5.6.3) typescript: 5.6.3 @@ -14153,7 +14153,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.2.2) typescript: 5.2.2 @@ -14173,7 +14173,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.8.3) typescript: 5.8.3 @@ -14190,7 +14190,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14233,7 +14233,7 @@ packages: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14254,7 +14254,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14275,7 +14275,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14296,7 +14296,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14317,7 +14317,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14339,7 +14339,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14361,7 +14361,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14383,7 +14383,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14405,7 +14405,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14427,7 +14427,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14449,7 +14449,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14470,7 +14470,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14491,7 +14491,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14512,7 +14512,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15277,7 +15277,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -16844,7 +16844,7 @@ packages: '@testim/chrome-version': 1.1.4 axios: 1.11.0 compare-versions: 6.1.1 - extract-zip: 2.0.1(supports-color@8.1.1) + extract-zip: 2.0.1 proxy-agent: 6.5.0 proxy-from-env: 1.1.0 tcp-port-used: 1.0.2 @@ -17871,7 +17871,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@4.4.1(supports-color@8.1.1): resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -18080,7 +18079,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) filing-cabinet: 3.3.1 precinct: 9.2.1 typescript: 4.9.5 @@ -18150,7 +18149,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -18211,7 +18210,7 @@ packages: resolution: {integrity: sha512-Rps1xDkEEBSq3kLdsdnHZL1x2S4NGDcbrjmd4q+PykK5aJwDdP5MBgrJw1Xo+kyUHuv3JEzPqxr+Dj9ryeDRTA==} engines: {node: '>= 6.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gonzales-pe: 4.3.0 node-source-walk: 4.3.0 transitivePeerDependencies: @@ -18222,7 +18221,7 @@ packages: resolution: {integrity: sha512-Fwc/g9VcrowODIAeKRWZfVA/EufxYL7XfuqJQFroBKGikKX83d2G7NFw6kDlSYGG3LNQIyVa+eWv1mqre+v4+A==} engines: {node: ^10 || ^12 || >=14} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) is-url: 1.2.4 postcss: 8.5.6 postcss-values-parser: 2.0.1 @@ -18797,7 +18796,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -19105,7 +19104,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.40.0) get-tsconfig: 4.10.1 @@ -19131,7 +19130,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) get-tsconfig: 4.10.1 @@ -19842,7 +19841,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19893,7 +19892,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19950,7 +19949,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -20212,6 +20211,20 @@ packages: - supports-color dev: true + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.4.1(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + /extract-zip@2.0.1(supports-color@8.1.1): resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -20445,7 +20458,7 @@ packages: dependencies: app-module-path: 2.2.0 commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) enhanced-resolve: 5.18.2 is-relative-path: 1.0.2 module-definition: 3.4.0 @@ -21014,7 +21027,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -21836,7 +21849,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -22065,7 +22077,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22074,7 +22086,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -22131,7 +22143,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -22141,7 +22153,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22150,7 +22162,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22411,7 +22423,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -22901,7 +22913,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -23858,7 +23870,7 @@ packages: dependencies: '@types/express': 4.17.23 '@types/jsonwebtoken': 9.0.10 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24431,7 +24443,7 @@ packages: chalk: 4.1.2 commander: 7.2.0 commondir: 1.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dependency-tree: 9.0.0 detective-amd: 4.2.0 detective-cjs: 4.1.0 @@ -24850,7 +24862,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) glob: 7.2.3 requirejs: 2.3.7 requirejs-config-file: 4.0.0 @@ -25007,7 +25019,7 @@ packages: resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} engines: {node: '>=14.0.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -25830,7 +25842,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -26609,7 +26621,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) detective-amd: 3.1.2 detective-cjs: 3.1.3 detective-es6: 2.2.2 @@ -26838,7 +26850,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -26918,7 +26930,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -27867,7 +27879,7 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) module-details-from-path: 1.0.4 resolve: 1.22.10 transitivePeerDependencies: @@ -28542,7 +28554,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -29112,7 +29124,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -29160,7 +29172,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} From eeff19546340c74d216ca7077da4b0205d270bf7 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:29:21 +0200 Subject: [PATCH 10/23] feat: support multiple roles per user --- apps/admin-panel/BUCK | 7 + .../app/__tests__/access-rights.test.ts | 133 ++++++++++++++++++ apps/admin-panel/app/access-rights.ts | 38 +++++ .../app/api/auth/[...nextauth]/options.ts | 28 ++-- apps/admin-panel/app/env.ts | 18 ++- apps/admin-panel/package.json | 3 + 6 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 apps/admin-panel/app/__tests__/access-rights.test.ts diff --git a/apps/admin-panel/BUCK b/apps/admin-panel/BUCK index e1a76acfb4..f2b51064ab 100644 --- a/apps/admin-panel/BUCK +++ b/apps/admin-panel/BUCK @@ -107,10 +107,17 @@ eslint( dev_deps_srcs=dev_deps_srcs, ) +dev_pnpm_task_test( + name="unit-test", + command="test", + deps = ["//:node_modules"], +) + test_suite( name="test", tests=[ ":audit", ":lint", + ":unit-test", ], ) diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts new file mode 100644 index 0000000000..73d5c519bf --- /dev/null +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -0,0 +1,133 @@ +import { + getAccessRightsForRole, + getAccessRightsForRoles, + hasAccessRight, + hasAccessRightInRoles, + areValidAdminRoles, + AdminAccessRight, + type AdminRole, +} from "../access-rights" + +describe("Access Rights - Multiple Roles Support", () => { + describe("Single Role Functions (existing functionality)", () => { + test("getAccessRightsForRole returns correct rights for VIEWER", () => { + const rights = getAccessRightsForRole("VIEWER") + expect(rights).toEqual([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + ]) + }) + + test("getAccessRightsForRole returns correct rights for SUPPORT", () => { + const rights = getAccessRightsForRole("SUPPORT") + expect(rights).toEqual([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + ]) + }) + + test("getAccessRightsForRole returns correct rights for ADMIN", () => { + const rights = getAccessRightsForRole("ADMIN") + expect(rights).toEqual([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.DELETE_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + AdminAccessRight.SYSTEM_CONFIG, + ]) + }) + + test("hasAccessRight works correctly", () => { + expect(hasAccessRight("VIEWER", AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) + expect(hasAccessRight("VIEWER", AdminAccessRight.DELETE_ACCOUNTS)).toBe(false) + expect(hasAccessRight("ADMIN", AdminAccessRight.DELETE_ACCOUNTS)).toBe(true) + }) + }) + + describe("Multiple Roles Functions (new functionality)", () => { + test("getAccessRightsForRoles combines rights from multiple roles", () => { + const rights = getAccessRightsForRoles(["VIEWER", "SUPPORT"]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + ]), + ) + expect(rights).toHaveLength(4) // No duplicates + }) + + test("getAccessRightsForRoles handles single role in array", () => { + const rights = getAccessRightsForRoles(["VIEWER"]) + expect(rights).toEqual([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + ]) + }) + + test("getAccessRightsForRoles handles all roles", () => { + const rights = getAccessRightsForRoles(["VIEWER", "SUPPORT", "ADMIN"]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.DELETE_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + AdminAccessRight.SYSTEM_CONFIG, + ]), + ) + expect(rights).toHaveLength(6) // All unique rights + }) + + test("getAccessRightsForRoles handles empty array", () => { + const rights = getAccessRightsForRoles([]) + expect(rights).toEqual([]) + }) + + test("hasAccessRightInRoles works correctly", () => { + expect(hasAccessRightInRoles(["VIEWER"], AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) + expect(hasAccessRightInRoles(["VIEWER"], AdminAccessRight.DELETE_ACCOUNTS)).toBe( + false, + ) + expect( + hasAccessRightInRoles(["VIEWER", "ADMIN"], AdminAccessRight.DELETE_ACCOUNTS), + ).toBe(true) + expect( + hasAccessRightInRoles(["VIEWER", "SUPPORT"], AdminAccessRight.DELETE_ACCOUNTS), + ).toBe(false) + }) + + test("areValidAdminRoles validates role arrays correctly", () => { + expect(areValidAdminRoles(["VIEWER", "SUPPORT"])).toBe(true) + expect(areValidAdminRoles(["ADMIN"])).toBe(true) + expect(areValidAdminRoles(["VIEWER", "INVALID"])).toBe(false) + expect(areValidAdminRoles(["INVALID"])).toBe(false) + expect(areValidAdminRoles([])).toBe(true) // Empty array is valid + }) + }) + + describe("Edge Cases", () => { + test("duplicate roles in array are handled correctly", () => { + const rights = getAccessRightsForRoles(["VIEWER", "VIEWER", "SUPPORT"]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + ]), + ) + expect(rights).toHaveLength(4) // No duplicates despite duplicate roles + }) + + test("invalid role in getAccessRightsForRole returns empty array", () => { + const rights = getAccessRightsForRole("INVALID" as AdminRole) + expect(rights).toEqual([]) + }) + }) +}) diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index c49eba076d..5c7d09c3d9 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -39,6 +39,22 @@ export function getAccessRightsForRole(role: AdminRole): AdminAccessRight[] { return ROLE_ACCESS_RIGHTS[role] || [] } +/** + * Get aggregated access rights for multiple roles + * @param roles - Array of admin roles + * @returns Array of unique access rights from all roles + */ +export function getAccessRightsForRoles(roles: AdminRole[]): AdminAccessRight[] { + const allRights = new Set() + + for (const role of roles) { + const roleRights = getAccessRightsForRole(role) + roleRights.forEach((right) => allRights.add(right)) + } + + return Array.from(allRights) +} + /** * Check if a role has a specific access right * @param role - The admin role @@ -49,6 +65,19 @@ export function hasAccessRight(role: AdminRole, accessRight: AdminAccessRight): return ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false } +/** + * Check if any of the roles has a specific access right + * @param roles - Array of admin roles + * @param accessRight - The access right to check + * @returns True if any role has the access right + */ +export function hasAccessRightInRoles( + roles: AdminRole[], + accessRight: AdminAccessRight, +): boolean { + return roles.some((role) => hasAccessRight(role, accessRight)) +} + /** * Get all available access rights * @returns Array of all access rights @@ -78,3 +107,12 @@ export function hasAccessRightInScope( export function isValidAdminRole(role: string): role is AdminRole { return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN" } + +/** + * Validate if all strings in an array are valid admin roles + * @param roles - Array of role strings to validate + * @returns True if all roles are valid + */ +export function areValidAdminRoles(roles: string[]): roles is AdminRole[] { + return roles.every((role) => isValidAdminRole(role)) +} diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index b57fd4cf12..a9078644cb 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -7,8 +7,8 @@ import { trace } from "@opentelemetry/api" import { env } from "../../../env" import { - getAccessRightsForRole, - isValidAdminRole, + getAccessRightsForRoles, + areValidAdminRoles, type AdminRole, } from "../../../access-rights" @@ -16,7 +16,6 @@ declare module "next-auth" { interface Session { sub: string | null accessToken: string - role: string scope: string } } @@ -88,37 +87,35 @@ const callbacks: Partial = { "auth.has_user": !!user, }) - let role_mapping: { [key: string]: string } + let role_mapping: { [key: string]: string[] } if (env.NODE_ENV === "development") { role_mapping = { - "admintest@blinkbitcoin.test": "ADMIN", - "alicetest@blinkbitcoin.test": "VIEWER", - "bobtest@blinkbitcoin.test": "SUPPORT", + "admintest@blinkbitcoin.test": ["ADMIN"], + "alicetest@blinkbitcoin.test": ["VIEWER"], + "bobtest@blinkbitcoin.test": ["SUPPORT"], } } else { role_mapping = env.USER_ROLE_MAP } if (user) { - const userRole = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER" + const userRoles = role_mapping[user.email as keyof typeof role_mapping] || [ + "VIEWER", + ] - if (isValidAdminRole(userRole)) { - const accessRights = getAccessRightsForRole(userRole as AdminRole) + if (areValidAdminRoles(userRoles)) { + const accessRights = getAccessRightsForRoles(userRoles as AdminRole[]) token.scope = JSON.stringify(accessRights) - token.role = userRole } else { token.scope = JSON.stringify([]) - token.role = "" } } else { - if (!(token.email && token.scope && token.role)) { + if (!(token.email && token.scope)) { token.scope = JSON.stringify([]) - token.role = "" } } span.setAttributes({ - "auth.final_role": (token.role as string) || "notSet", "auth.has_scope": !!token.scope, }) @@ -129,7 +126,6 @@ const callbacks: Partial = { // https://next-auth.js.org/configuration/callbacks#session-callback async session({ session, token }) { session.scope = token.scope as string - session.role = token.role as string return session }, } diff --git a/apps/admin-panel/app/env.ts b/apps/admin-panel/app/env.ts index 2ae97b051a..e0fa66734d 100644 --- a/apps/admin-panel/app/env.ts +++ b/apps/admin-panel/app/env.ts @@ -20,7 +20,23 @@ export const env = createEnv({ .string() .transform((str) => { try { - return JSON.parse(str) + const parsed = JSON.parse(str) + // Normalize the parsed object to support both single roles and arrays + const normalized: { [key: string]: string[] } = {} + for (const [email, roles] of Object.entries(parsed)) { + if (typeof roles === "string") { + // Convert single role string to array + normalized[email] = [roles] + } else if (Array.isArray(roles)) { + // Keep arrays as is + normalized[email] = roles + } else { + throw new Error( + `Invalid role format for ${email}: must be string or array of strings`, + ) + } + } + return normalized } catch (error) { throw new Error( `Invalid JSON in USER_ROLE_MAP environment variable: ${ diff --git a/apps/admin-panel/package.json b/apps/admin-panel/package.json index aa1eeb798c..7a3f70838b 100644 --- a/apps/admin-panel/package.json +++ b/apps/admin-panel/package.json @@ -9,7 +9,10 @@ "dev": "next dev -p 3004", "start": "next start", "build": "next build", + "typecheck": "npx tsc --noEmit", + "lint": "eslint --ext .ts,.tsx .", "lint:fix": "eslint --fix --ext .ts,.tsx .", + "eslint-fix": "eslint --fix --ext .ts,.tsx .", "codegen": "graphql-codegen --config codegen.yml", "test": "jest", "cypress:run": "echo 'Need to cypress test'", From a5d74badd23089ea5e3ecc9f748918969286a661 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:19:27 +0200 Subject: [PATCH 11/23] feat: new roles and access-rights --- .../app/__tests__/access-rights.test.ts | 69 +++++++++++++++++-- apps/admin-panel/app/access-rights.ts | 52 ++++++++++++-- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts index 73d5c519bf..6c3e177cb4 100644 --- a/apps/admin-panel/app/__tests__/access-rights.test.ts +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -28,22 +28,70 @@ describe("Access Rights - Multiple Roles Support", () => { ]) }) - test("getAccessRightsForRole returns correct rights for ADMIN", () => { - const rights = getAccessRightsForRole("ADMIN") + test("getAccessRightsForRole returns correct rights for MARKETING", () => { + const rights = getAccessRightsForRole("MARKETING") + expect(rights).toEqual([AdminAccessRight.SEND_NOTIFICATIONS]) + }) + + test("getAccessRightsForRole returns correct rights for SUPPORTLV1", () => { + const rights = getAccessRightsForRole("SUPPORTLV1") expect(rights).toEqual([ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, - AdminAccessRight.DELETE_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.SEND_NOTIFICATIONS, - AdminAccessRight.SYSTEM_CONFIG, ]) }) + test("getAccessRightsForRole returns correct rights for SUPPORTLV2", () => { + const rights = getAccessRightsForRole("SUPPORTLV2") + expect(rights).toEqual([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + ]) + }) + + test("getAccessRightsForRole returns correct rights for ADMIN", () => { + const rights = getAccessRightsForRole("ADMIN") + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, // Legacy + AdminAccessRight.DELETE_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.SEND_NOTIFICATIONS, + AdminAccessRight.SYSTEM_CONFIG, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.VIEW_MERCHANTS, + ]), + ) + expect(rights).toHaveLength(11) // All rights + }) + test("hasAccessRight works correctly", () => { expect(hasAccessRight("VIEWER", AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) expect(hasAccessRight("VIEWER", AdminAccessRight.DELETE_ACCOUNTS)).toBe(false) expect(hasAccessRight("ADMIN", AdminAccessRight.DELETE_ACCOUNTS)).toBe(true) + + // Test new granular rights + expect(hasAccessRight("MARKETING", AdminAccessRight.SEND_NOTIFICATIONS)).toBe(true) + expect(hasAccessRight("MARKETING", AdminAccessRight.APPROVE_MERCHANT)).toBe(false) + expect(hasAccessRight("SUPPORTLV1", AdminAccessRight.APPROVE_MERCHANT)).toBe(true) + expect(hasAccessRight("SUPPORTLV1", AdminAccessRight.CHANGELEVEL_ACCOUNT)).toBe( + false, + ) + expect(hasAccessRight("SUPPORTLV2", AdminAccessRight.CHANGELEVEL_ACCOUNT)).toBe( + true, + ) }) }) @@ -79,9 +127,14 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, AdminAccessRight.SYSTEM_CONFIG, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.VIEW_MERCHANTS, ]), ) - expect(rights).toHaveLength(6) // All unique rights + expect(rights).toHaveLength(11) // All unique rights }) test("getAccessRightsForRoles handles empty array", () => { @@ -104,6 +157,8 @@ describe("Access Rights - Multiple Roles Support", () => { test("areValidAdminRoles validates role arrays correctly", () => { expect(areValidAdminRoles(["VIEWER", "SUPPORT"])).toBe(true) + expect(areValidAdminRoles(["MARKETING", "SUPPORTLV1"])).toBe(true) + expect(areValidAdminRoles(["SUPPORTLV2", "ADMIN"])).toBe(true) expect(areValidAdminRoles(["ADMIN"])).toBe(true) expect(areValidAdminRoles(["VIEWER", "INVALID"])).toBe(false) expect(areValidAdminRoles(["INVALID"])).toBe(false) diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index 5c7d09c3d9..be027fac49 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -1,32 +1,69 @@ // Admin access rights definitions export enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", // Legacy - will be removed DELETE_ACCOUNTS = "DELETE_ACCOUNTS", VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", SYSTEM_CONFIG = "SYSTEM_CONFIG", + + // New granular access rights + APPROVE_MERCHANT = "APPROVE_MERCHANT", + CHANGECONTACTS_ACCOUNT = "CHANGECONTACTS_ACCOUNT", + CHANGELEVEL_ACCOUNT = "CHANGELEVEL_ACCOUNT", + LOCK_ACCOUNT = "LOCK_ACCOUNT", + VIEW_MERCHANTS = "VIEW_MERCHANTS", } // Role types -export type AdminRole = "VIEWER" | "SUPPORT" | "ADMIN" +export type AdminRole = + | "VIEWER" + | "SUPPORT" + | "MARKETING" + | "SUPPORTLV1" + | "SUPPORTLV2" + | "ADMIN" // Role to access rights mapping const ROLE_ACCESS_RIGHTS: Record = { VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], SUPPORT: [ + // Legacy role - will be removed AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.MODIFY_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, ], + MARKETING: [AdminAccessRight.SEND_NOTIFICATIONS], + SUPPORTLV1: [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.VIEW_TRANSACTIONS, + ], + SUPPORTLV2: [ + // Inherits all SUPPORTLV1 rights plus additional ones + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + ], ADMIN: [ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, + AdminAccessRight.MODIFY_ACCOUNTS, // Legacy - will be removed AdminAccessRight.DELETE_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, AdminAccessRight.SYSTEM_CONFIG, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.VIEW_MERCHANTS, ], } @@ -105,7 +142,14 @@ export function hasAccessRightInScope( * @returns True if the role is valid */ export function isValidAdminRole(role: string): role is AdminRole { - return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN" + return ( + role === "VIEWER" || + role === "SUPPORT" || + role === "MARKETING" || + role === "SUPPORTLV1" || + role === "SUPPORTLV2" || + role === "ADMIN" + ) } /** From e21a0496e7cb2e474638c27fdbc343c7f8363d76 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:17:59 +0200 Subject: [PATCH 12/23] feat: new roles and access-rights for adminAPI --- .../app/api/auth/[...nextauth]/options.ts | 3 ++- core/api/src/graphql/admin/access-rules.ts | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index a9078644cb..d1a14ff296 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -92,7 +92,8 @@ const callbacks: Partial = { role_mapping = { "admintest@blinkbitcoin.test": ["ADMIN"], "alicetest@blinkbitcoin.test": ["VIEWER"], - "bobtest@blinkbitcoin.test": ["SUPPORT"], + "bobtest@blinkbitcoin.test": ["SUPPORTLV1"], + "caroltest@blinkbitcoin.test": ["SUPPORTLV2", "MARKETING"], } } else { role_mapping = env.USER_ROLE_MAP diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index e43dd970a1..22466330a8 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -15,11 +15,18 @@ import { AdminFieldDefinitions } from "./types" // Admin access rights enum enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", + MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", // Legacy - will be removed DELETE_ACCOUNTS = "DELETE_ACCOUNTS", VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", SYSTEM_CONFIG = "SYSTEM_CONFIG", + + // New granular access rights + APPROVE_MERCHANT = "APPROVE_MERCHANT", + CHANGECONTACTS_ACCOUNT = "CHANGECONTACTS_ACCOUNT", + CHANGELEVEL_ACCOUNT = "CHANGELEVEL_ACCOUNT", + LOCK_ACCOUNT = "LOCK_ACCOUNT", + VIEW_MERCHANTS = "VIEW_MERCHANTS", } // Helper function to create access right rules @@ -32,11 +39,18 @@ const createAccessRightRule = (accessRight: AdminAccessRight) => // Export the actual GraphQL Shield rules that can be used directly export const accessRules = { viewAccounts: createAccessRightRule(AdminAccessRight.VIEW_ACCOUNTS), - modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), + modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), // Legacy - will be removed deleteAccounts: createAccessRightRule(AdminAccessRight.DELETE_ACCOUNTS), viewTransactions: createAccessRightRule(AdminAccessRight.VIEW_TRANSACTIONS), sendNotifications: createAccessRightRule(AdminAccessRight.SEND_NOTIFICATIONS), systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), + + // New granular access rules + approveMerchant: createAccessRightRule(AdminAccessRight.APPROVE_MERCHANT), + changeContactsAccount: createAccessRightRule(AdminAccessRight.CHANGECONTACTS_ACCOUNT), + changeLevelAccount: createAccessRightRule(AdminAccessRight.CHANGELEVEL_ACCOUNT), + lockAccount: createAccessRightRule(AdminAccessRight.LOCK_ACCOUNT), + viewMerchants: createAccessRightRule(AdminAccessRight.VIEW_MERCHANTS), } /** From 4f401adc80236bd92dccb7a23f5da2699546703c Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:57:33 +0200 Subject: [PATCH 13/23] feat: new roles and access-rights for adminAPI, part 3 --- .../app/__tests__/access-rights.test.ts | 38 ++--- apps/admin-panel/app/access-rights.ts | 20 +-- bats/core/api/admin.bats | 151 +++++++++++------- bats/helpers/admin.bash | 32 ++-- core/api/src/graphql/admin/access-rules.ts | 6 - core/api/src/graphql/admin/mutations.ts | 17 +- core/api/src/graphql/admin/queries.ts | 4 +- 7 files changed, 147 insertions(+), 121 deletions(-) diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts index 6c3e177cb4..05dd310086 100644 --- a/apps/admin-panel/app/__tests__/access-rights.test.ts +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -18,16 +18,6 @@ describe("Access Rights - Multiple Roles Support", () => { ]) }) - test("getAccessRightsForRole returns correct rights for SUPPORT", () => { - const rights = getAccessRightsForRole("SUPPORT") - expect(rights).toEqual([ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.SEND_NOTIFICATIONS, - ]) - }) - test("getAccessRightsForRole returns correct rights for MARKETING", () => { const rights = getAccessRightsForRole("MARKETING") expect(rights).toEqual([AdminAccessRight.SEND_NOTIFICATIONS]) @@ -62,7 +52,6 @@ describe("Access Rights - Multiple Roles Support", () => { expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, // Legacy AdminAccessRight.DELETE_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, @@ -74,7 +63,7 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.VIEW_MERCHANTS, ]), ) - expect(rights).toHaveLength(11) // All rights + expect(rights).toHaveLength(10) // All rights }) test("hasAccessRight works correctly", () => { @@ -97,16 +86,15 @@ describe("Access Rights - Multiple Roles Support", () => { describe("Multiple Roles Functions (new functionality)", () => { test("getAccessRightsForRoles combines rights from multiple roles", () => { - const rights = getAccessRightsForRoles(["VIEWER", "SUPPORT"]) + const rights = getAccessRightsForRoles(["VIEWER", "MARKETING"]) expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, ]), ) - expect(rights).toHaveLength(4) // No duplicates + expect(rights).toHaveLength(3) // No duplicates }) test("getAccessRightsForRoles handles single role in array", () => { @@ -118,11 +106,16 @@ describe("Access Rights - Multiple Roles Support", () => { }) test("getAccessRightsForRoles handles all roles", () => { - const rights = getAccessRightsForRoles(["VIEWER", "SUPPORT", "ADMIN"]) + const rights = getAccessRightsForRoles([ + "VIEWER", + "MARKETING", + "SUPPORTLV1", + "SUPPORTLV2", + "ADMIN", + ]) expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, AdminAccessRight.DELETE_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, @@ -134,7 +127,7 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.VIEW_MERCHANTS, ]), ) - expect(rights).toHaveLength(11) // All unique rights + expect(rights).toHaveLength(10) // All unique rights }) test("getAccessRightsForRoles handles empty array", () => { @@ -151,12 +144,12 @@ describe("Access Rights - Multiple Roles Support", () => { hasAccessRightInRoles(["VIEWER", "ADMIN"], AdminAccessRight.DELETE_ACCOUNTS), ).toBe(true) expect( - hasAccessRightInRoles(["VIEWER", "SUPPORT"], AdminAccessRight.DELETE_ACCOUNTS), + hasAccessRightInRoles(["VIEWER", "MARKETING"], AdminAccessRight.DELETE_ACCOUNTS), ).toBe(false) }) test("areValidAdminRoles validates role arrays correctly", () => { - expect(areValidAdminRoles(["VIEWER", "SUPPORT"])).toBe(true) + expect(areValidAdminRoles(["VIEWER", "MARKETING"])).toBe(true) expect(areValidAdminRoles(["MARKETING", "SUPPORTLV1"])).toBe(true) expect(areValidAdminRoles(["SUPPORTLV2", "ADMIN"])).toBe(true) expect(areValidAdminRoles(["ADMIN"])).toBe(true) @@ -168,16 +161,15 @@ describe("Access Rights - Multiple Roles Support", () => { describe("Edge Cases", () => { test("duplicate roles in array are handled correctly", () => { - const rights = getAccessRightsForRoles(["VIEWER", "VIEWER", "SUPPORT"]) + const rights = getAccessRightsForRoles(["VIEWER", "VIEWER", "MARKETING"]) expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, ]), ) - expect(rights).toHaveLength(4) // No duplicates despite duplicate roles + expect(rights).toHaveLength(3) // No duplicates despite duplicate roles }) test("invalid role in getAccessRightsForRole returns empty array", () => { diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index be027fac49..aab7ee8080 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -1,13 +1,12 @@ // Admin access rights definitions export enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", // Legacy - will be removed DELETE_ACCOUNTS = "DELETE_ACCOUNTS", VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", SYSTEM_CONFIG = "SYSTEM_CONFIG", - // New granular access rights + // Granular access rights APPROVE_MERCHANT = "APPROVE_MERCHANT", CHANGECONTACTS_ACCOUNT = "CHANGECONTACTS_ACCOUNT", CHANGELEVEL_ACCOUNT = "CHANGELEVEL_ACCOUNT", @@ -16,24 +15,11 @@ export enum AdminAccessRight { } // Role types -export type AdminRole = - | "VIEWER" - | "SUPPORT" - | "MARKETING" - | "SUPPORTLV1" - | "SUPPORTLV2" - | "ADMIN" +export type AdminRole = "VIEWER" | "MARKETING" | "SUPPORTLV1" | "SUPPORTLV2" | "ADMIN" // Role to access rights mapping const ROLE_ACCESS_RIGHTS: Record = { VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], - SUPPORT: [ - // Legacy role - will be removed - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.SEND_NOTIFICATIONS, - ], MARKETING: [AdminAccessRight.SEND_NOTIFICATIONS], SUPPORTLV1: [ AdminAccessRight.VIEW_ACCOUNTS, @@ -54,7 +40,6 @@ const ROLE_ACCESS_RIGHTS: Record = { ], ADMIN: [ AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.MODIFY_ACCOUNTS, // Legacy - will be removed AdminAccessRight.DELETE_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.SEND_NOTIFICATIONS, @@ -144,7 +129,6 @@ export function hasAccessRightInScope( export function isValidAdminRole(role: string): role is AdminRole { return ( role === "VIEWER" || - role === "SUPPORT" || role === "MARKETING" || role === "SUPPORTLV1" || role === "SUPPORTLV2" || diff --git a/bats/core/api/admin.bats b/bats/core/api/admin.bats index e670c0d639..5b0bd8dfcd 100644 --- a/bats/core/api/admin.bats +++ b/bats/core/api/admin.bats @@ -14,9 +14,12 @@ setup_file() { create_user 'tester2' + # Login all role types for comprehensive testing login_admin - login_support_user - login_view_user + login_supportlv2_user + login_supportlv1_user + login_marketing_user + login_viewer_user } getEmailCode() { @@ -99,8 +102,8 @@ getEmailCode() { [[ "$(graphql_output '.errors[0].message')" == "Not authorized" ]] || exit 1 } -@test "view_user: can query account details by phone" { - token="$(read_value 'view_user.token')" +@test "viewer_user: can query account details by phone" { + token="$(read_value 'viewer_user.token')" variables=$( jq -n \ --arg phone "$(read_value 'tester.phone')" \ @@ -112,8 +115,21 @@ getEmailCode() { cache_value 'tester.id' "$id" } -@test "support_user: can update user phone number" { - token="$(read_value 'support_user.token')" +@test "admin: can query account details for tester2 by phone" { + token="$(read_value 'admin.token')" + variables=$( + jq -n \ + --arg phone "$(read_value 'tester2.phone')" \ + '{phone: $phone}' + ) + exec_admin_graphql $token 'account-details-by-user-phone' "$variables" + id="$(graphql_output '.data.accountDetailsByUserPhone.id')" + [[ "$id" != "null" && "$id" != "" ]] || exit 1 + cache_value 'tester2.id' "$id" +} + +@test "supportlv2_user: can update user phone number" { + token="$(read_value 'supportlv2_user.token')" id="$(read_value 'tester.id')" new_phone="$(random_phone)" variables=$( @@ -137,25 +153,8 @@ getEmailCode() { [[ "$refetched_id" == "$id" ]] || exit 1 } -@test "view_user: cannot update user phone number" { - token="$(read_value 'view_user.token')" - id="$(read_value 'tester.id')" - new_phone="$(random_phone)" - variables=$( - jq -n \ - --arg phone "$new_phone" \ - --arg accountId "$id" \ - '{input: {phone: $phone, accountId:$accountId}}' - ) - - # Attempt to update phone number - should fail with authorization error - exec_admin_graphql $token 'user-update-phone' "$variables" - error_message="$(graphql_output '.errors[0].message')" - echo "error_message: $error_message" >&2 - [[ "$error_message" == "Not authorized" ]] || exit 1 -} -@test "support_user: can update user email" { +@test "admin: can update user email" { email="$(read_value tester.username)@blink.sv" cache_value "tester.email" "$email" @@ -356,31 +355,7 @@ getEmailCode() { [[ "$count" -eq 1 ]] || exit 1 } -@test "support_user: can trigger marketing notification" { - token="$(read_value 'support_user.token')" - variables=$( - jq -n \ - '{ - input: { - localizedNotificationContents: [ - { - language: "en", - title: "Test title", - body: "test body" - } - ], - shouldSendPush: false, - shouldAddToHistory: true, - shouldAddToBulletin: true, - } - }' - ) - exec_admin_graphql "$token" 'marketing-notification-trigger' "$variables" - num_errors="$(graphql_output '.data.marketingNotificationTrigger.errors | length')" - success="$(graphql_output '.data.marketingNotificationTrigger.success')" - [[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1 -} @test "admin: can force delete account" { admin_token="$(read_value 'admin.token')" @@ -418,21 +393,85 @@ getEmailCode() { [[ "$level_count" -gt 0 ]] || exit 1 } -@test "support_user: cannot access system configuration (SYSTEM_CONFIG scope)" { - support_token="$(read_value 'support_user.token')" - # Use allLevels query as a proxy for system configuration access - # Support user should not have SYSTEM_CONFIG scope, so this should fail + +# ============================================================================ +# ACCESS DENIED TESTS - ONE PER ROLE +# ============================================================================ + +# VIEWER role - cannot lock accounts (should only have VIEW_ACCOUNTS, VIEW_TRANSACTIONS) +@test "viewer_user: cannot lock accounts (LOCK_ACCOUNT scope)" { + viewer_token="$(read_value 'viewer_user.token')" + id="$(read_value 'tester.id')" + variables=$( jq -n \ - '{}' + --arg account_status "LOCKED" \ + --arg accountId "$id" \ + --arg comment "Test lock attempt by viewer" \ + '{input: {status: $account_status, accountId: $accountId, comment: $comment}}' ) - exec_admin_graphql "$support_token" 'all-levels' "$variables" + exec_admin_graphql "$viewer_token" 'account-update-status' "$variables" error_message="$(graphql_output '.errors[0].message')" - echo "error_message: $error_message" >&2 [[ "$error_message" == "Not authorized" ]] || exit 1 } -# TODO: add check by email +# MARKETING role - cannot view accounts (should only have SEND_NOTIFICATIONS) +@test "marketing_user: cannot view accounts (VIEW_ACCOUNTS scope)" { + marketing_token="$(read_value 'marketing_user.token')" + id="$(read_value 'tester.id')" + + variables=$( + jq -n \ + --arg accountId "$id" \ + '{accountId: $accountId}' + ) + exec_admin_graphql "$marketing_token" 'account-details-by-account-id' "$variables" + error_message="$(graphql_output '.errors[0].message')" + [[ "$error_message" == "Not authorized" ]] || exit 1 +} + +# SUPPORTLV1 role - cannot change account level (missing CHANGELEVEL_ACCOUNT) +@test "supportlv1_user: cannot change account level (CHANGELEVEL_ACCOUNT scope)" { + supportlv1_token="$(read_value 'supportlv1_user.token')" + id="$(read_value 'tester.id')" + + variables=$( + jq -n \ + --arg level "TWO" \ + --arg accountId "$id" \ + '{input: {level: $level, accountId: $accountId}}' + ) + exec_admin_graphql "$supportlv1_token" 'account-update-level' "$variables" + error_message="$(graphql_output '.errors[0].message')" + [[ "$error_message" == "Not authorized" ]] || exit 1 +} +# SUPPORTLV2 role - cannot send notifications (missing SEND_NOTIFICATIONS) +@test "supportlv2_user: cannot send notifications (SEND_NOTIFICATIONS scope)" { + supportlv2_token="$(read_value 'supportlv2_user.token')" + + variables=$( + jq -n \ + '{ + input: { + localizedNotificationContents: [ + { + language: "en", + title: "Test title", + body: "test body" + } + ], + shouldSendPush: false, + shouldAddToHistory: true, + shouldAddToBulletin: true, + } + }' + ) + exec_admin_graphql "$supportlv2_token" 'marketing-notification-trigger' "$variables" + error_message="$(graphql_output '.errors[0].message')" + [[ "$error_message" == "Not authorized" ]] || exit 1 +} + +# TODO: add check by email # TODO: business update map info diff --git a/bats/helpers/admin.bash b/bats/helpers/admin.bash index 7860964a9d..59d59f695d 100644 --- a/bats/helpers/admin.bash +++ b/bats/helpers/admin.bash @@ -34,20 +34,34 @@ _create_admin_client_and_token() { # Below user specification is mimicking the users as they are defined in apps/admin-panel/app/api/auth/[...nextauth]/options.ts -# Full admin access with all permissions +# ADMIN role - Full system access login_admin() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","MODIFY_ACCOUNTS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS","SYSTEM_CONFIG"]' + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS","SYSTEM_CONFIG","APPROVE_MERCHANT","CHANGECONTACTS_ACCOUNT","CHANGELEVEL_ACCOUNT","LOCK_ACCOUNT","VIEW_MERCHANTS"]' _create_admin_client_and_token "$scopes" "admin.token" } -# Modify user access (can view and modify accounts/transactions, but no notifications or system config) -login_support_user() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","MODIFY_ACCOUNTS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS"]' - _create_admin_client_and_token "$scopes" "support_user.token" +# SUPPORTLV2 role - Advanced support operations (all SUPPORTLV1 + change contacts and levels) +login_supportlv2_user() { + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS","LOCK_ACCOUNT","APPROVE_MERCHANT","CHANGECONTACTS_ACCOUNT","CHANGELEVEL_ACCOUNT"]' + _create_admin_client_and_token "$scopes" "supportlv2_user.token" } -# View-only access (can only view accounts and transactions) -login_view_user() { +# SUPPORTLV1 role - Basic support operations (view accounts/merchants, lock accounts, approve merchants) +login_supportlv1_user() { + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS","LOCK_ACCOUNT","APPROVE_MERCHANT"]' + _create_admin_client_and_token "$scopes" "supportlv1_user.token" +} + +# MARKETING role - Can send notifications only +login_marketing_user() { + local scopes='["SEND_NOTIFICATIONS"]' + _create_admin_client_and_token "$scopes" "marketing_user.token" +} + +# VIEWER role - Read-only access (can only view accounts and transactions) +login_viewer_user() { local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS"]' - _create_admin_client_and_token "$scopes" "view_user.token" + _create_admin_client_and_token "$scopes" "viewer_user.token" } + + diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index 22466330a8..3ca2749a9d 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -15,13 +15,10 @@ import { AdminFieldDefinitions } from "./types" // Admin access rights enum enum AdminAccessRight { VIEW_ACCOUNTS = "VIEW_ACCOUNTS", - MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS", // Legacy - will be removed DELETE_ACCOUNTS = "DELETE_ACCOUNTS", VIEW_TRANSACTIONS = "VIEW_TRANSACTIONS", SEND_NOTIFICATIONS = "SEND_NOTIFICATIONS", SYSTEM_CONFIG = "SYSTEM_CONFIG", - - // New granular access rights APPROVE_MERCHANT = "APPROVE_MERCHANT", CHANGECONTACTS_ACCOUNT = "CHANGECONTACTS_ACCOUNT", CHANGELEVEL_ACCOUNT = "CHANGELEVEL_ACCOUNT", @@ -39,13 +36,10 @@ const createAccessRightRule = (accessRight: AdminAccessRight) => // Export the actual GraphQL Shield rules that can be used directly export const accessRules = { viewAccounts: createAccessRightRule(AdminAccessRight.VIEW_ACCOUNTS), - modifyAccounts: createAccessRightRule(AdminAccessRight.MODIFY_ACCOUNTS), // Legacy - will be removed deleteAccounts: createAccessRightRule(AdminAccessRight.DELETE_ACCOUNTS), viewTransactions: createAccessRightRule(AdminAccessRight.VIEW_TRANSACTIONS), sendNotifications: createAccessRightRule(AdminAccessRight.SEND_NOTIFICATIONS), systemConfig: createAccessRightRule(AdminAccessRight.SYSTEM_CONFIG), - - // New granular access rules approveMerchant: createAccessRightRule(AdminAccessRight.APPROVE_MERCHANT), changeContactsAccount: createAccessRightRule(AdminAccessRight.CHANGECONTACTS_ACCOUNT), changeLevelAccount: createAccessRightRule(AdminAccessRight.CHANGELEVEL_ACCOUNT), diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 6fbbdd8980..527124d065 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -18,30 +18,33 @@ import { GT } from "@/graphql/index" export const mutationFields = { unauthed: {}, authed: { - // Account modification operations - require MODIFY_ACCOUNTS + // Account level operations - require CHANGELEVEL_ACCOUNT accountUpdateLevel: { field: AccountUpdateLevelMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.changeLevelAccount, }, + // Account status operations - require LOCK_ACCOUNT accountUpdateStatus: { field: AccountUpdateStatusMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.lockAccount, }, + // Contact update operations - require CHANGECONTACTS_ACCOUNT userUpdateEmail: { field: UserUpdateEmailMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.changeContactsAccount, }, userUpdatePhone: { field: UserUpdatePhoneMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.changeContactsAccount, }, + // Merchant operations - require APPROVE_MERCHANT merchantMapValidate: { field: MerchantMapValidateMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.approveMerchant, }, merchantMapDelete: { field: MerchantMapDeleteMutation, - rule: accessRules.modifyAccounts, + rule: accessRules.approveMerchant, }, // Account deletion operations - require DELETE_ACCOUNTS diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index 8a97003b44..d30ab8feef 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -53,11 +53,11 @@ export const queryFields = { }, inactiveMerchants: { field: InactiveMerchantsQuery, - rule: accessRules.viewAccounts, + rule: accessRules.viewMerchants, }, merchantsPendingApproval: { field: MerchantsPendingApprovalQuery, - rule: accessRules.viewAccounts, + rule: accessRules.viewMerchants, }, // Transaction viewing operations - require VIEW_TRANSACTIONS From ee0032086855f07e4b700df95f362453c8824819 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:45:26 +0200 Subject: [PATCH 14/23] fix: add VIEW_MERCHANTS to VIEWER --- apps/admin-panel/app/__tests__/access-rights.test.ts | 6 ++++-- apps/admin-panel/app/access-rights.ts | 2 +- bats/core/api/admin.bats | 2 +- bats/helpers/admin.bash | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts index 05dd310086..a23664e74f 100644 --- a/apps/admin-panel/app/__tests__/access-rights.test.ts +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -15,6 +15,7 @@ describe("Access Rights - Multiple Roles Support", () => { expect(rights).toEqual([ AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.VIEW_MERCHANTS, ]) }) @@ -94,7 +95,7 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.SEND_NOTIFICATIONS, ]), ) - expect(rights).toHaveLength(3) // No duplicates + expect(rights).toHaveLength(4) // No duplicates }) test("getAccessRightsForRoles handles single role in array", () => { @@ -102,6 +103,7 @@ describe("Access Rights - Multiple Roles Support", () => { expect(rights).toEqual([ AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.VIEW_MERCHANTS, ]) }) @@ -169,7 +171,7 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.SEND_NOTIFICATIONS, ]), ) - expect(rights).toHaveLength(3) // No duplicates despite duplicate roles + expect(rights).toHaveLength(4) // No duplicates despite duplicate roles }) test("invalid role in getAccessRightsForRole returns empty array", () => { diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index aab7ee8080..0c6a3cc5f1 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -19,7 +19,7 @@ export type AdminRole = "VIEWER" | "MARKETING" | "SUPPORTLV1" | "SUPPORTLV2" | " // Role to access rights mapping const ROLE_ACCESS_RIGHTS: Record = { - VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS], + VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.VIEW_MERCHANTS], MARKETING: [AdminAccessRight.SEND_NOTIFICATIONS], SUPPORTLV1: [ AdminAccessRight.VIEW_ACCOUNTS, diff --git a/bats/core/api/admin.bats b/bats/core/api/admin.bats index 5b0bd8dfcd..c128f61eff 100644 --- a/bats/core/api/admin.bats +++ b/bats/core/api/admin.bats @@ -399,7 +399,7 @@ getEmailCode() { # ACCESS DENIED TESTS - ONE PER ROLE # ============================================================================ -# VIEWER role - cannot lock accounts (should only have VIEW_ACCOUNTS, VIEW_TRANSACTIONS) +# VIEWER role - cannot lock accounts (should only have VIEW_ACCOUNTS, VIEW_TRANSACTIONS, VIEW_MERCHANTS) @test "viewer_user: cannot lock accounts (LOCK_ACCOUNT scope)" { viewer_token="$(read_value 'viewer_user.token')" id="$(read_value 'tester.id')" diff --git a/bats/helpers/admin.bash b/bats/helpers/admin.bash index 59d59f695d..dbc455f86b 100644 --- a/bats/helpers/admin.bash +++ b/bats/helpers/admin.bash @@ -58,9 +58,9 @@ login_marketing_user() { _create_admin_client_and_token "$scopes" "marketing_user.token" } -# VIEWER role - Read-only access (can only view accounts and transactions) +# VIEWER role - Read-only access (can view accounts, transactions, and merchants) login_viewer_user() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS"]' + local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS"]' _create_admin_client_and_token "$scopes" "viewer_user.token" } From 327d5112e099f32e307fcdca032a4b10d06fe938 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:01:46 +0200 Subject: [PATCH 15/23] feat: deriving roles from one another, space sep scopes, role reqorks, --- apps/admin-panel/README.md | 12 +- .../app/__tests__/access-rights.test.ts | 122 ++++++++++++------ apps/admin-panel/app/access-rights.ts | 83 ++++++------ .../app/api/auth/[...nextauth]/options.ts | 8 +- apps/admin-panel/instrumentation.node.ts | 12 +- bats/helpers/admin.bash | 10 +- core/api/src/graphql/admin/mutations.ts | 11 +- core/api/src/graphql/admin/queries.ts | 7 - .../src/servers/graphql-admin-api-server.ts | 9 +- core/api/src/servers/index.files.d.ts | 1 - dev/config/ory/oathkeeper_rules.yaml | 2 +- 11 files changed, 155 insertions(+), 122 deletions(-) diff --git a/apps/admin-panel/README.md b/apps/admin-panel/README.md index 429f0cea8a..7dd181fd30 100644 --- a/apps/admin-panel/README.md +++ b/apps/admin-panel/README.md @@ -38,10 +38,14 @@ As we have three different roles in the admin panel, we have three different cre - username: `admin` - password: `admin` -#### Support +#### VIEWER +- username: `alice` +- password: `alice` + +#### SUPPORTLV1 - username: `bob` - password: `bob` -#### Viewer -- username: `alice` -- password: `alice` +#### SUPPORTLV2 + MARKETING_GLOBAL +- username: `carol` +- password: `carol` diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts index a23664e74f..ad42fe29c6 100644 --- a/apps/admin-panel/app/__tests__/access-rights.test.ts +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -3,6 +3,7 @@ import { getAccessRightsForRoles, hasAccessRight, hasAccessRightInRoles, + hasAccessRightInScope, areValidAdminRoles, AdminAccessRight, type AdminRole, @@ -12,40 +13,50 @@ describe("Access Rights - Multiple Roles Support", () => { describe("Single Role Functions (existing functionality)", () => { test("getAccessRightsForRole returns correct rights for VIEWER", () => { const rights = getAccessRightsForRole("VIEWER") - expect(rights).toEqual([ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.VIEW_MERCHANTS, - ]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.VIEW_MERCHANTS, + ]), + ) + expect(rights).toHaveLength(3) }) - test("getAccessRightsForRole returns correct rights for MARKETING", () => { - const rights = getAccessRightsForRole("MARKETING") + test("getAccessRightsForRole returns correct rights for MARKETING_GLOBAL", () => { + const rights = getAccessRightsForRole("MARKETING_GLOBAL") expect(rights).toEqual([AdminAccessRight.SEND_NOTIFICATIONS]) }) test("getAccessRightsForRole returns correct rights for SUPPORTLV1", () => { const rights = getAccessRightsForRole("SUPPORTLV1") - expect(rights).toEqual([ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_MERCHANTS, - AdminAccessRight.LOCK_ACCOUNT, - AdminAccessRight.APPROVE_MERCHANT, - AdminAccessRight.VIEW_TRANSACTIONS, - ]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.CHANGELEVEL_ACCOUNT + ]), + ) + expect(rights).toHaveLength(6) }) test("getAccessRightsForRole returns correct rights for SUPPORTLV2", () => { const rights = getAccessRightsForRole("SUPPORTLV2") - expect(rights).toEqual([ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_MERCHANTS, - AdminAccessRight.LOCK_ACCOUNT, - AdminAccessRight.APPROVE_MERCHANT, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.CHANGECONTACTS_ACCOUNT, - AdminAccessRight.CHANGELEVEL_ACCOUNT, - ]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_MERCHANTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, + ]), + ) + expect(rights).toHaveLength(7) }) test("getAccessRightsForRole returns correct rights for ADMIN", () => { @@ -73,11 +84,15 @@ describe("Access Rights - Multiple Roles Support", () => { expect(hasAccessRight("ADMIN", AdminAccessRight.DELETE_ACCOUNTS)).toBe(true) // Test new granular rights - expect(hasAccessRight("MARKETING", AdminAccessRight.SEND_NOTIFICATIONS)).toBe(true) - expect(hasAccessRight("MARKETING", AdminAccessRight.APPROVE_MERCHANT)).toBe(false) + expect( + hasAccessRight("MARKETING_GLOBAL", AdminAccessRight.SEND_NOTIFICATIONS), + ).toBe(true) + expect(hasAccessRight("MARKETING_GLOBAL", AdminAccessRight.APPROVE_MERCHANT)).toBe( + false, + ) expect(hasAccessRight("SUPPORTLV1", AdminAccessRight.APPROVE_MERCHANT)).toBe(true) expect(hasAccessRight("SUPPORTLV1", AdminAccessRight.CHANGELEVEL_ACCOUNT)).toBe( - false, + true, ) expect(hasAccessRight("SUPPORTLV2", AdminAccessRight.CHANGELEVEL_ACCOUNT)).toBe( true, @@ -87,7 +102,7 @@ describe("Access Rights - Multiple Roles Support", () => { describe("Multiple Roles Functions (new functionality)", () => { test("getAccessRightsForRoles combines rights from multiple roles", () => { - const rights = getAccessRightsForRoles(["VIEWER", "MARKETING"]) + const rights = getAccessRightsForRoles(["VIEWER", "MARKETING_GLOBAL"]) expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, @@ -100,17 +115,20 @@ describe("Access Rights - Multiple Roles Support", () => { test("getAccessRightsForRoles handles single role in array", () => { const rights = getAccessRightsForRoles(["VIEWER"]) - expect(rights).toEqual([ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.VIEW_MERCHANTS, - ]) + expect(rights).toEqual( + expect.arrayContaining([ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.VIEW_MERCHANTS, + ]), + ) + expect(rights).toHaveLength(3) }) test("getAccessRightsForRoles handles all roles", () => { const rights = getAccessRightsForRoles([ "VIEWER", - "MARKETING", + "MARKETING_GLOBAL", "SUPPORTLV1", "SUPPORTLV2", "ADMIN", @@ -146,13 +164,16 @@ describe("Access Rights - Multiple Roles Support", () => { hasAccessRightInRoles(["VIEWER", "ADMIN"], AdminAccessRight.DELETE_ACCOUNTS), ).toBe(true) expect( - hasAccessRightInRoles(["VIEWER", "MARKETING"], AdminAccessRight.DELETE_ACCOUNTS), + hasAccessRightInRoles( + ["VIEWER", "MARKETING_GLOBAL"], + AdminAccessRight.DELETE_ACCOUNTS, + ), ).toBe(false) }) test("areValidAdminRoles validates role arrays correctly", () => { - expect(areValidAdminRoles(["VIEWER", "MARKETING"])).toBe(true) - expect(areValidAdminRoles(["MARKETING", "SUPPORTLV1"])).toBe(true) + expect(areValidAdminRoles(["VIEWER", "MARKETING_GLOBAL"])).toBe(true) + expect(areValidAdminRoles(["MARKETING_GLOBAL", "SUPPORTLV1"])).toBe(true) expect(areValidAdminRoles(["SUPPORTLV2", "ADMIN"])).toBe(true) expect(areValidAdminRoles(["ADMIN"])).toBe(true) expect(areValidAdminRoles(["VIEWER", "INVALID"])).toBe(false) @@ -163,7 +184,7 @@ describe("Access Rights - Multiple Roles Support", () => { describe("Edge Cases", () => { test("duplicate roles in array are handled correctly", () => { - const rights = getAccessRightsForRoles(["VIEWER", "VIEWER", "MARKETING"]) + const rights = getAccessRightsForRoles(["VIEWER", "VIEWER", "MARKETING_GLOBAL"]) expect(rights).toEqual( expect.arrayContaining([ AdminAccessRight.VIEW_ACCOUNTS, @@ -179,4 +200,31 @@ describe("Access Rights - Multiple Roles Support", () => { expect(rights).toEqual([]) }) }) + + describe("Scope Functions (space-separated format)", () => { + test("hasAccessRightInScope works with space-separated string", () => { + const scope = "VIEW_ACCOUNTS VIEW_TRANSACTIONS SEND_NOTIFICATIONS" + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_TRANSACTIONS)).toBe(true) + expect(hasAccessRightInScope(scope, AdminAccessRight.SEND_NOTIFICATIONS)).toBe(true) + expect(hasAccessRightInScope(scope, AdminAccessRight.DELETE_ACCOUNTS)).toBe(false) + }) + + test("hasAccessRightInScope works with single permission", () => { + const scope = "VIEW_ACCOUNTS" + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) + expect(hasAccessRightInScope(scope, AdminAccessRight.DELETE_ACCOUNTS)).toBe(false) + }) + + test("hasAccessRightInScope works with empty string", () => { + const scope = "" + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_ACCOUNTS)).toBe(false) + }) + + test("hasAccessRightInScope handles extra spaces", () => { + const scope = " VIEW_ACCOUNTS VIEW_TRANSACTIONS " + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_ACCOUNTS)).toBe(true) + expect(hasAccessRightInScope(scope, AdminAccessRight.VIEW_TRANSACTIONS)).toBe(true) + }) + }) }) diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index 0c6a3cc5f1..c02ca90744 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -15,41 +15,41 @@ export enum AdminAccessRight { } // Role types -export type AdminRole = "VIEWER" | "MARKETING" | "SUPPORTLV1" | "SUPPORTLV2" | "ADMIN" +export type AdminRole = + | "VIEWER" + | "MARKETING_GLOBAL" + | "SUPPORTLV1" + | "SUPPORTLV2" + | "ADMIN" + +// Define roles individually so they can be used as bases +const VIEWER_RIGHTS = [ + AdminAccessRight.VIEW_ACCOUNTS, + AdminAccessRight.VIEW_TRANSACTIONS, + AdminAccessRight.VIEW_MERCHANTS, +] +const MARKETING_GLOBAL_RIGHTS = [AdminAccessRight.SEND_NOTIFICATIONS] +const SUPPORTLV1_RIGHTS = [ + ...VIEWER_RIGHTS, + AdminAccessRight.LOCK_ACCOUNT, + AdminAccessRight.APPROVE_MERCHANT, + AdminAccessRight.CHANGELEVEL_ACCOUNT, +] +const SUPPORTLV2_RIGHTS = [ + ...SUPPORTLV1_RIGHTS, + AdminAccessRight.CHANGECONTACTS_ACCOUNT, +] + +// ADMIN has all rights +const ADMIN_RIGHTS = Object.values(AdminAccessRight) // Role to access rights mapping const ROLE_ACCESS_RIGHTS: Record = { - VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS, AdminAccessRight.VIEW_MERCHANTS], - MARKETING: [AdminAccessRight.SEND_NOTIFICATIONS], - SUPPORTLV1: [ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_MERCHANTS, - AdminAccessRight.LOCK_ACCOUNT, - AdminAccessRight.APPROVE_MERCHANT, - AdminAccessRight.VIEW_TRANSACTIONS, - ], - SUPPORTLV2: [ - // Inherits all SUPPORTLV1 rights plus additional ones - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.VIEW_MERCHANTS, - AdminAccessRight.LOCK_ACCOUNT, - AdminAccessRight.APPROVE_MERCHANT, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.CHANGECONTACTS_ACCOUNT, - AdminAccessRight.CHANGELEVEL_ACCOUNT, - ], - ADMIN: [ - AdminAccessRight.VIEW_ACCOUNTS, - AdminAccessRight.DELETE_ACCOUNTS, - AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.SEND_NOTIFICATIONS, - AdminAccessRight.SYSTEM_CONFIG, - AdminAccessRight.APPROVE_MERCHANT, - AdminAccessRight.CHANGECONTACTS_ACCOUNT, - AdminAccessRight.CHANGELEVEL_ACCOUNT, - AdminAccessRight.LOCK_ACCOUNT, - AdminAccessRight.VIEW_MERCHANTS, - ], + VIEWER: VIEWER_RIGHTS, + MARKETING_GLOBAL: MARKETING_GLOBAL_RIGHTS, + SUPPORTLV1: SUPPORTLV1_RIGHTS, + SUPPORTLV2: SUPPORTLV2_RIGHTS, + ADMIN: ADMIN_RIGHTS, } /** @@ -109,16 +109,19 @@ export function getAllAccessRights(): AdminAccessRight[] { } /** - * Check if a scope array contains a specific access right - * @param scope - Array of access rights from JWT token scope + * Check if a scope string contains a specific access right + * @param scope - Space-separated string of access rights from JWT token scope * @param accessRight - The access right to check for * @returns True if the scope contains the access right */ export function hasAccessRightInScope( - scope: string[], + scope: string, accessRight: AdminAccessRight, ): boolean { - return scope.includes(accessRight) + return scope + .split(" ") + .filter((s) => s.trim() !== "") + .includes(accessRight) } /** @@ -127,13 +130,7 @@ export function hasAccessRightInScope( * @returns True if the role is valid */ export function isValidAdminRole(role: string): role is AdminRole { - return ( - role === "VIEWER" || - role === "MARKETING" || - role === "SUPPORTLV1" || - role === "SUPPORTLV2" || - role === "ADMIN" - ) + return Object.keys(ROLE_ACCESS_RIGHTS).includes(role) } /** diff --git a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts index d1a14ff296..c8901243d3 100644 --- a/apps/admin-panel/app/api/auth/[...nextauth]/options.ts +++ b/apps/admin-panel/app/api/auth/[...nextauth]/options.ts @@ -93,7 +93,7 @@ const callbacks: Partial = { "admintest@blinkbitcoin.test": ["ADMIN"], "alicetest@blinkbitcoin.test": ["VIEWER"], "bobtest@blinkbitcoin.test": ["SUPPORTLV1"], - "caroltest@blinkbitcoin.test": ["SUPPORTLV2", "MARKETING"], + "caroltest@blinkbitcoin.test": ["SUPPORTLV2", "MARKETING_GLOBAL"], } } else { role_mapping = env.USER_ROLE_MAP @@ -106,13 +106,13 @@ const callbacks: Partial = { if (areValidAdminRoles(userRoles)) { const accessRights = getAccessRightsForRoles(userRoles as AdminRole[]) - token.scope = JSON.stringify(accessRights) + token.scope = accessRights.join(" ") } else { - token.scope = JSON.stringify([]) + token.scope = "" } } else { if (!(token.email && token.scope)) { - token.scope = JSON.stringify([]) + token.scope = "" } } diff --git a/apps/admin-panel/instrumentation.node.ts b/apps/admin-panel/instrumentation.node.ts index dd9c2dd9ce..75ada5c1a1 100644 --- a/apps/admin-panel/instrumentation.node.ts +++ b/apps/admin-panel/instrumentation.node.ts @@ -10,10 +10,16 @@ import { W3CTraceContextPropagator } from "@opentelemetry/core" import { env } from "./app/env" -console.log("Validating environment variables...") // Simply accessing the env object triggers validation of all variables -JSON.stringify(env) -console.log("✅ Environment variables validated successfully") +try { + JSON.stringify(env) +} catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error( + `Failed to validate environment variables: ${errorMessage}. ` + + `Please check your environment configuration.` + ) +} const sdk = new NodeSDK({ textMapPropagator: new W3CTraceContextPropagator(), diff --git a/bats/helpers/admin.bash b/bats/helpers/admin.bash index dbc455f86b..d2d184c3e1 100644 --- a/bats/helpers/admin.bash +++ b/bats/helpers/admin.bash @@ -36,31 +36,31 @@ _create_admin_client_and_token() { # ADMIN role - Full system access login_admin() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","DELETE_ACCOUNTS","SEND_NOTIFICATIONS","SYSTEM_CONFIG","APPROVE_MERCHANT","CHANGECONTACTS_ACCOUNT","CHANGELEVEL_ACCOUNT","LOCK_ACCOUNT","VIEW_MERCHANTS"]' + local scopes='VIEW_ACCOUNTS VIEW_TRANSACTIONS DELETE_ACCOUNTS SEND_NOTIFICATIONS SYSTEM_CONFIG APPROVE_MERCHANT CHANGECONTACTS_ACCOUNT CHANGELEVEL_ACCOUNT LOCK_ACCOUNT VIEW_MERCHANTS' _create_admin_client_and_token "$scopes" "admin.token" } # SUPPORTLV2 role - Advanced support operations (all SUPPORTLV1 + change contacts and levels) login_supportlv2_user() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS","LOCK_ACCOUNT","APPROVE_MERCHANT","CHANGECONTACTS_ACCOUNT","CHANGELEVEL_ACCOUNT"]' + local scopes='VIEW_ACCOUNTS VIEW_TRANSACTIONS VIEW_MERCHANTS LOCK_ACCOUNT APPROVE_MERCHANT CHANGECONTACTS_ACCOUNT CHANGELEVEL_ACCOUNT' _create_admin_client_and_token "$scopes" "supportlv2_user.token" } # SUPPORTLV1 role - Basic support operations (view accounts/merchants, lock accounts, approve merchants) login_supportlv1_user() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS","LOCK_ACCOUNT","APPROVE_MERCHANT"]' + local scopes='VIEW_ACCOUNTS VIEW_TRANSACTIONS VIEW_MERCHANTS LOCK_ACCOUNT APPROVE_MERCHANT' _create_admin_client_and_token "$scopes" "supportlv1_user.token" } # MARKETING role - Can send notifications only login_marketing_user() { - local scopes='["SEND_NOTIFICATIONS"]' + local scopes='SEND_NOTIFICATIONS' _create_admin_client_and_token "$scopes" "marketing_user.token" } # VIEWER role - Read-only access (can view accounts, transactions, and merchants) login_viewer_user() { - local scopes='["VIEW_ACCOUNTS","VIEW_TRANSACTIONS","VIEW_MERCHANTS"]' + local scopes='VIEW_ACCOUNTS VIEW_TRANSACTIONS VIEW_MERCHANTS' _create_admin_client_and_token "$scopes" "viewer_user.token" } diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 527124d065..f5622f7e71 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -18,17 +18,14 @@ import { GT } from "@/graphql/index" export const mutationFields = { unauthed: {}, authed: { - // Account level operations - require CHANGELEVEL_ACCOUNT accountUpdateLevel: { field: AccountUpdateLevelMutation, rule: accessRules.changeLevelAccount, }, - // Account status operations - require LOCK_ACCOUNT accountUpdateStatus: { field: AccountUpdateStatusMutation, rule: accessRules.lockAccount, }, - // Contact update operations - require CHANGECONTACTS_ACCOUNT userUpdateEmail: { field: UserUpdateEmailMutation, rule: accessRules.changeContactsAccount, @@ -37,7 +34,6 @@ export const mutationFields = { field: UserUpdatePhoneMutation, rule: accessRules.changeContactsAccount, }, - // Merchant operations - require APPROVE_MERCHANT merchantMapValidate: { field: MerchantMapValidateMutation, rule: accessRules.approveMerchant, @@ -46,24 +42,19 @@ export const mutationFields = { field: MerchantMapDeleteMutation, rule: accessRules.approveMerchant, }, - - // Account deletion operations - require DELETE_ACCOUNTS accountForceDelete: { field: AccountForceDeleteMutation, rule: accessRules.deleteAccounts, }, - - // Notification operations - require SEND_NOTIFICATIONS marketingNotificationTrigger: { field: TriggerMarketingNotificationMutation, rule: accessRules.sendNotifications, }, }, } -// Extract the actual GraphQL fields for the schema + const extractedMutationFields = extractFields(mutationFields.authed) -// Build permission mappings automatically from the field definitions (now field -> rule mapping) export const mutationPermissions = buildPermissionMappings(mutationFields.authed) export const MutationType = GT.Object({ diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index d30ab8feef..7d83629ce0 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -22,7 +22,6 @@ import { GT } from "@/graphql/index" export const queryFields = { unauthed: {}, authed: { - // Account viewing operations - require VIEW_ACCOUNTS accountDetailsByAccountId: { field: AccountDetailsByAccountId, rule: accessRules.viewAccounts, @@ -60,7 +59,6 @@ export const queryFields = { rule: accessRules.viewMerchants, }, - // Transaction viewing operations - require VIEW_TRANSACTIONS lightningInvoice: { field: LightningInvoiceQuery, rule: accessRules.viewTransactions, @@ -82,7 +80,6 @@ export const queryFields = { rule: accessRules.viewTransactions, }, - // System configuration operations - require SYSTEM_CONFIG allLevels: { field: AllLevelsQuery, rule: accessRules.systemConfig, @@ -90,12 +87,8 @@ export const queryFields = { }, } -// Helper functions are now imported from access-rules.ts to avoid duplication - -// Extract the actual GraphQL fields for the schema const extractedQueryFields = extractFields(queryFields.authed) -// Build permission mappings automatically from the field definitions (now field -> rule mapping) export const queryPermissions = buildPermissionMappings(queryFields.authed) export const QueryType = GT.Object({ diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index a71a3ac73c..6f6a618dad 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -52,20 +52,15 @@ const setGqlAdminContext = async ( ): Promise => { const logger = baseLogger const tokenPayload = req.token - - console.log("JWT Token payload:", tokenPayload) - const userEmail = tokenPayload.sub as string // This should be the email from OAuth - const role = tokenPayload.role as string - const scopeString = (tokenPayload.scope as string) || "[]" - const scope = JSON.parse(scopeString) as string[] + const scopeString = (tokenPayload.scope as string) || "" + const scope = scopeString.split(" ").filter((s) => s.trim() !== "") const privilegedClientId = tokenPayload.sub as PrivilegedClientId req.gqlContext = { loaders, privilegedClientId, userEmail, // Add email to context - role, scope, logger, } diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index 18d25131ac..b49dfb2e84 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -25,7 +25,6 @@ type GraphQLAdminContext = { loaders: Loaders privilegedClientId: PrivilegedClientId userEmail: string - role: string scope: string[] } diff --git a/dev/config/ory/oathkeeper_rules.yaml b/dev/config/ory/oathkeeper_rules.yaml index 11c7576656..7cd4cff2d4 100644 --- a/dev/config/ory/oathkeeper_rules.yaml +++ b/dev/config/ory/oathkeeper_rules.yaml @@ -106,4 +106,4 @@ mutators: - handler: id_token config: #! TODO: add aud: {"aud": ["https://api/admin/graphql"] } - claims: '{"sub": "{{ print .Subject }}", "scope": {{ printf "%+q" .Extra.scope }}, "role": "{{ print .Extra.role }}"}' + claims: '{"sub": "{{ print .Subject }}", "scope": "{{ print .Extra.scope }}" }' From 4d41c1758761e38736feac9845522dd9a68d0a31 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:07:01 +0200 Subject: [PATCH 16/23] fix: tidy up --- pnpm-lock.yaml | 169 +++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 90 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 972ea79201..bf8a98363f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2849,7 +2849,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2872,7 +2872,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2987,7 +2987,7 @@ packages: '@babel/core': 7.27.7 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -4366,7 +4366,7 @@ packages: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5179,7 +5179,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5209,7 +5209,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5225,7 +5225,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -6908,7 +6908,7 @@ packages: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.8 chalk: 4.1.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) dotenv: 16.6.1 graphql: 16.11.0 graphql-request: 6.1.0(graphql@16.11.0) @@ -7207,7 +7207,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8075,7 +8075,7 @@ packages: dependencies: '@types/node': 22.16.0 async-exit-hook: 2.0.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -12062,7 +12062,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -13685,7 +13685,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13713,7 +13713,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13829,7 +13829,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.40.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13849,7 +13849,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13870,7 +13870,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13891,7 +13891,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.3.3 transitivePeerDependencies: @@ -13912,7 +13912,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13933,7 +13933,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -13954,7 +13954,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -13975,7 +13975,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -13996,7 +13996,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14014,7 +14014,7 @@ packages: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14029,7 +14029,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -14043,7 +14043,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -14057,7 +14057,7 @@ packages: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14133,7 +14133,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 tsutils: 3.21.0(typescript@5.6.3) typescript: 5.6.3 @@ -14153,7 +14153,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.2.2) typescript: 5.2.2 @@ -14173,7 +14173,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.8.3) typescript: 5.8.3 @@ -14190,7 +14190,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/utils': 8.35.1(eslint@9.30.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14233,7 +14233,7 @@ packages: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14254,7 +14254,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14275,7 +14275,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14296,7 +14296,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -14317,7 +14317,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14339,7 +14339,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14361,7 +14361,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14383,7 +14383,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14405,7 +14405,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14427,7 +14427,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14449,7 +14449,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14470,7 +14470,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.3.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14491,7 +14491,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.6.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14512,7 +14512,7 @@ packages: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15277,7 +15277,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16844,7 +16844,7 @@ packages: '@testim/chrome-version': 1.1.4 axios: 1.11.0 compare-versions: 6.1.1 - extract-zip: 2.0.1 + extract-zip: 2.0.1(supports-color@8.1.1) proxy-agent: 6.5.0 proxy-from-env: 1.1.0 tcp-port-used: 1.0.2 @@ -17871,6 +17871,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 + dev: true /debug@4.4.1(supports-color@8.1.1): resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -18079,7 +18080,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) filing-cabinet: 3.3.1 precinct: 9.2.1 typescript: 4.9.5 @@ -18149,7 +18150,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -18210,7 +18211,7 @@ packages: resolution: {integrity: sha512-Rps1xDkEEBSq3kLdsdnHZL1x2S4NGDcbrjmd4q+PykK5aJwDdP5MBgrJw1Xo+kyUHuv3JEzPqxr+Dj9ryeDRTA==} engines: {node: '>= 6.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gonzales-pe: 4.3.0 node-source-walk: 4.3.0 transitivePeerDependencies: @@ -18221,7 +18222,7 @@ packages: resolution: {integrity: sha512-Fwc/g9VcrowODIAeKRWZfVA/EufxYL7XfuqJQFroBKGikKX83d2G7NFw6kDlSYGG3LNQIyVa+eWv1mqre+v4+A==} engines: {node: ^10 || ^12 || >=14} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) is-url: 1.2.4 postcss: 8.5.6 postcss-values-parser: 2.0.1 @@ -18796,7 +18797,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -19104,7 +19105,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.40.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.40.0) get-tsconfig: 4.10.1 @@ -19130,7 +19131,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) get-tsconfig: 4.10.1 @@ -19841,7 +19842,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19892,7 +19893,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -19949,7 +19950,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -20211,20 +20212,6 @@ packages: - supports-color dev: true - /extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - dependencies: - debug: 4.4.1(supports-color@5.5.0) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - dev: true - /extract-zip@2.0.1(supports-color@8.1.1): resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -20458,7 +20445,7 @@ packages: dependencies: app-module-path: 2.2.0 commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) enhanced-resolve: 5.18.2 is-relative-path: 1.0.2 module-definition: 3.4.0 @@ -21027,7 +21014,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -21849,6 +21836,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -22077,7 +22065,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22086,7 +22074,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -22143,7 +22131,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -22153,7 +22141,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22162,7 +22150,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22423,7 +22411,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -22913,7 +22901,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -23870,7 +23858,7 @@ packages: dependencies: '@types/express': 4.17.23 '@types/jsonwebtoken': 9.0.10 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24443,7 +24431,7 @@ packages: chalk: 4.1.2 commander: 7.2.0 commondir: 1.0.1 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) dependency-tree: 9.0.0 detective-amd: 4.2.0 detective-cjs: 4.1.0 @@ -24862,7 +24850,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) glob: 7.2.3 requirejs: 2.3.7 requirejs-config-file: 4.0.0 @@ -25019,7 +25007,7 @@ packages: resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} engines: {node: '>=14.0.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: false @@ -25842,7 +25830,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -26621,7 +26609,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) detective-amd: 3.1.2 detective-cjs: 3.1.3 detective-es6: 2.2.2 @@ -26850,7 +26838,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -26930,7 +26918,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -27879,7 +27867,7 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) module-details-from-path: 1.0.4 resolve: 1.22.10 transitivePeerDependencies: @@ -28554,7 +28542,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -29124,7 +29112,7 @@ packages: hasBin: true dependencies: commander: 2.20.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -29172,6 +29160,7 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 + dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} From a7a0995905aa583d4b4684c6dfce292b647966c4 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:08:43 +0200 Subject: [PATCH 17/23] fix: tidy up --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 79bede9b58..3227c00398 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "scripts": { "preinstall": "npx only-allow pnpm", "nodev": "node -v", - "whichnode": "which node", - "prepare": "husky || true # prepare sometimes called without dev-deps" + "whichnode": "which node" }, "engines": { "node": "20", From 7e163c94e7f622ea486b7e783428ffaeb1d021c4 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:24:04 +0200 Subject: [PATCH 18/23] prettier --- apps/admin-panel/instrumentation.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin-panel/instrumentation.node.ts b/apps/admin-panel/instrumentation.node.ts index 75ada5c1a1..8de0f0557c 100644 --- a/apps/admin-panel/instrumentation.node.ts +++ b/apps/admin-panel/instrumentation.node.ts @@ -17,7 +17,7 @@ try { const errorMessage = error instanceof Error ? error.message : String(error) throw new Error( `Failed to validate environment variables: ${errorMessage}. ` + - `Please check your environment configuration.` + `Please check your environment configuration.`, ) } From 2db22d4102f63e050ab1ffa90ec8bd7f78858d90 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:17:40 +0200 Subject: [PATCH 19/23] prettier! --- apps/admin-panel/app/__tests__/access-rights.test.ts | 2 +- apps/admin-panel/app/access-rights.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/admin-panel/app/__tests__/access-rights.test.ts b/apps/admin-panel/app/__tests__/access-rights.test.ts index ad42fe29c6..1376161388 100644 --- a/apps/admin-panel/app/__tests__/access-rights.test.ts +++ b/apps/admin-panel/app/__tests__/access-rights.test.ts @@ -37,7 +37,7 @@ describe("Access Rights - Multiple Roles Support", () => { AdminAccessRight.LOCK_ACCOUNT, AdminAccessRight.APPROVE_MERCHANT, AdminAccessRight.VIEW_TRANSACTIONS, - AdminAccessRight.CHANGELEVEL_ACCOUNT + AdminAccessRight.CHANGELEVEL_ACCOUNT, ]), ) expect(rights).toHaveLength(6) diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index c02ca90744..d587af19ff 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -35,10 +35,7 @@ const SUPPORTLV1_RIGHTS = [ AdminAccessRight.APPROVE_MERCHANT, AdminAccessRight.CHANGELEVEL_ACCOUNT, ] -const SUPPORTLV2_RIGHTS = [ - ...SUPPORTLV1_RIGHTS, - AdminAccessRight.CHANGECONTACTS_ACCOUNT, -] +const SUPPORTLV2_RIGHTS = [...SUPPORTLV1_RIGHTS, AdminAccessRight.CHANGECONTACTS_ACCOUNT] // ADMIN has all rights const ADMIN_RIGHTS = Object.values(AdminAccessRight) From 9c3dd47dbfe8dd8ba9553e295e7e43ec274ad5b1 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:10:09 -0300 Subject: [PATCH 20/23] chore: revert pnpm-lock.yaml --- pnpm-lock.yaml | 231 +++++++++++++++++++++++++------------------------ 1 file changed, 118 insertions(+), 113 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf8a98363f..7a89f05016 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 16.11.0 next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) next-auth: specifier: ^4.24.11 version: 4.24.11(next@14.2.26)(react-dom@18.3.1)(react@18.3.1) @@ -149,7 +149,7 @@ importers: version: 3.4.17 ts-jest: specifier: ^29.2.5 - version: 29.4.0(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.2.2) + version: 29.4.0(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.2.2) apps/consent: dependencies: @@ -212,7 +212,7 @@ importers: version: 1.12.9 next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 version: 0.2.1(next@14.2.26)(react-dom@18.2.0)(react@18.2.0) @@ -360,7 +360,7 @@ importers: version: 16.11.0 next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.24.11 version: 4.24.11(next@14.2.26)(react-dom@18.2.0)(react@18.2.0) @@ -514,7 +514,7 @@ importers: version: 16.11.0 next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -674,7 +674,7 @@ importers: version: 4.0.8 next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) next-auth: specifier: ^4.24.11 version: 4.24.11(next@14.2.26)(react-dom@18.3.1)(react@18.3.1) @@ -945,7 +945,7 @@ importers: version: 3.1.0(pg@8.16.3) next: specifier: 14.2.26 - version: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.24.11 version: 4.24.11(next@14.2.26)(react-dom@18.2.0)(react@18.2.0) @@ -1036,7 +1036,7 @@ importers: version: 2.6.0 ts-jest: specifier: ^29.1.1 - version: 29.4.0(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.8.3) + version: 29.4.0(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.8.3) ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@20.11.24)(typescript@5.8.3) @@ -1104,8 +1104,8 @@ importers: specifier: ^0.7.15 version: 0.7.15 '@ip1sms/disposable-phone-numbers': - specifier: ^2.1.1322 - version: 2.1.1322 + specifier: ^2.1.1335 + version: 2.1.1335 '@merqva/telegram-passport': specifier: ^1.0.2 version: 1.0.2 @@ -1923,7 +1923,7 @@ packages: dependencies: '@apollo/client': 3.13.8(@types/react@18.3.23)(graphql-ws@5.16.2)(graphql@16.11.0)(react-dom@18.3.1)(react@18.3.1) '@apollo/client-react-streaming': 0.11.11(@apollo/client@3.13.8)(graphql@16.11.0)(react-dom@18.3.1)(react@18.3.1) - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 transitivePeerDependencies: - graphql @@ -1939,7 +1939,7 @@ packages: react: ^18 dependencies: '@apollo/client': 3.13.8(@types/react@18.2.62)(graphql@16.11.0)(react-dom@18.2.0)(react@18.2.0) - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 server-only: 0.0.1 superjson: 1.13.3 @@ -1954,7 +1954,7 @@ packages: react: ^18 dependencies: '@apollo/client': 3.13.8(@types/react@18.2.62)(graphql@16.11.0)(react-dom@18.3.1)(react@18.3.1) - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 server-only: 0.0.1 superjson: 2.2.2 @@ -1969,7 +1969,7 @@ packages: react: ^18 dependencies: '@apollo/client': 3.13.8(@types/react@18.3.23)(graphql-ws@5.16.2)(graphql@16.11.0)(react-dom@18.2.0)(react@18.2.0) - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 server-only: 0.0.1 superjson: 2.2.2 @@ -2213,7 +2213,7 @@ packages: next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 dependencies: '@apollo/server': 4.12.2(graphql@16.11.0) - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) dev: false /@aw-web-design/x-default-browser@1.4.126: @@ -2879,19 +2879,19 @@ packages: transitivePeerDependencies: - supports-color - /@babel/core@7.28.4: - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + /@babel/core@7.28.5: + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -2925,12 +2925,12 @@ packages: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 - /@babel/generator@7.28.3: - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + /@babel/generator@7.28.5: + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -3042,16 +3042,16 @@ packages: transitivePeerDependencies: - supports-color - /@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4): + /@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5): resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -3108,6 +3108,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.28.5: + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + /@babel/helper-validator-option@7.27.1: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -3134,7 +3138,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 /@babel/parser@7.27.7: resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} @@ -3143,12 +3147,12 @@ packages: dependencies: '@babel/types': 7.27.7 - /@babel/parser@7.28.4: - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + /@babel/parser@7.28.5: + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.7): resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} @@ -3382,13 +3386,13 @@ packages: '@babel/helper-plugin-utils': 7.27.1 dev: true - /@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.4): + /@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5): resolution: {integrity: sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 dev: true @@ -3411,13 +3415,13 @@ packages: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - /@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4): + /@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5): resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 dev: true @@ -3457,13 +3461,13 @@ packages: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - /@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4): + /@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5): resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 dev: true @@ -4026,17 +4030,17 @@ packages: transitivePeerDependencies: - supports-color - /@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4): + /@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5): resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) '@babel/types': 7.27.7 transitivePeerDependencies: - supports-color @@ -4371,16 +4375,16 @@ packages: transitivePeerDependencies: - supports-color - /@babel/traverse@7.28.4: - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + /@babel/traverse@7.28.5: + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4392,12 +4396,12 @@ packages: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - /@babel/types@7.28.4: - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + /@babel/types@7.28.5: + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} @@ -4466,16 +4470,16 @@ packages: engines: {node: '>=10.0.0'} dev: true - /@emnapi/core@1.5.0: - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + /@emnapi/core@1.6.0: + resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} requiresBuild: true dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - /@emnapi/runtime@1.5.0: - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + /@emnapi/runtime@1.6.0: + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -6059,7 +6063,7 @@ packages: '@graphql-inspector/graphql-loader': 4.0.2(graphql@16.11.0) '@graphql-inspector/introspect-command': 4.0.3(@graphql-inspector/config@4.0.2)(@graphql-inspector/loaders@4.0.3)(graphql@16.11.0)(yargs@17.7.2) '@graphql-inspector/json-loader': 4.0.2(graphql@16.11.0) - '@graphql-inspector/loaders': 4.0.3(@babel/core@7.28.4)(@graphql-inspector/config@4.0.2)(graphql@16.11.0) + '@graphql-inspector/loaders': 4.0.3(@babel/core@7.28.5)(@graphql-inspector/config@4.0.2)(graphql@16.11.0) '@graphql-inspector/serve-command': 4.0.3(@graphql-inspector/config@4.0.2)(@graphql-inspector/loaders@4.0.3)(graphql@16.11.0)(yargs@17.7.2) '@graphql-inspector/similar-command': 4.0.3(@graphql-inspector/config@4.0.2)(@graphql-inspector/loaders@4.0.3)(graphql@16.11.0)(yargs@17.7.2) '@graphql-inspector/url-loader': 4.0.2(@types/node@20.11.24)(graphql@16.11.0) @@ -6099,7 +6103,7 @@ packages: yargs: 17.7.2 dependencies: '@graphql-inspector/config': 4.0.2(graphql@16.11.0) - '@graphql-inspector/loaders': 4.0.3(@babel/core@7.28.4)(@graphql-inspector/config@4.0.2)(graphql@16.11.0) + '@graphql-inspector/loaders': 4.0.3(@babel/core@7.28.5)(@graphql-inspector/config@4.0.2)(graphql@16.11.0) graphql: 16.11.0 tslib: 2.6.2 yargs: 17.7.2 @@ -6246,7 +6250,7 @@ packages: tslib: 2.6.2 dev: true - /@graphql-inspector/loaders@4.0.3(@babel/core@7.28.4)(@graphql-inspector/config@4.0.2)(graphql@16.11.0): + /@graphql-inspector/loaders@4.0.3(@babel/core@7.28.5)(@graphql-inspector/config@4.0.2)(graphql@16.11.0): resolution: {integrity: sha512-V9T+IUJgmneWx1kEaSbFHLcjMs929TKLs6m3p544/CMPdFIsRYmYhd2jfUBhzXC8P6avnsLxxzVEOR45wJgOYA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -6254,7 +6258,7 @@ packages: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: '@graphql-inspector/config': 4.0.2(graphql@16.11.0) - '@graphql-tools/code-file-loader': 8.0.1(@babel/core@7.28.4)(graphql@16.11.0) + '@graphql-tools/code-file-loader': 8.0.1(@babel/core@7.28.5)(graphql@16.11.0) '@graphql-tools/load': 8.0.0(graphql@16.11.0) '@graphql-tools/utils': 10.0.3(graphql@16.11.0) graphql: 16.11.0 @@ -6399,13 +6403,13 @@ packages: - supports-color dev: true - /@graphql-tools/code-file-loader@8.0.1(@babel/core@7.28.4)(graphql@16.11.0): + /@graphql-tools/code-file-loader@8.0.1(@babel/core@7.28.5)(graphql@16.11.0): resolution: {integrity: sha512-pmg81lsIXGW3uW+nFSCIG0lFQIxWVbgDjeBkSWlnP8CZsrHTQEkB53DT7t4BHLryoxDS4G4cPxM52yNINDSL8w==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.1(@babel/core@7.28.4)(graphql@16.11.0) + '@graphql-tools/graphql-tag-pluck': 8.0.1(@babel/core@7.28.5)(graphql@16.11.0) '@graphql-tools/utils': 10.0.3(graphql@16.11.0) globby: 11.1.0 graphql: 16.11.0 @@ -6710,14 +6714,14 @@ packages: - supports-color dev: true - /@graphql-tools/graphql-tag-pluck@8.0.1(@babel/core@7.28.4)(graphql@16.11.0): + /@graphql-tools/graphql-tag-pluck@8.0.1(@babel/core@7.28.5)(graphql@16.11.0): resolution: {integrity: sha512-4sfBJSoXxVB4rRCCp2GTFhAYsUJgAPSKxSV+E3Voc600mK52JO+KsHCCTnPgCeyJFMNR9l94J6+tqxVKmlqKvw==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@babel/parser': 7.27.7 - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.5) '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 '@graphql-tools/utils': 10.0.3(graphql@16.11.0) @@ -7234,8 +7238,8 @@ packages: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} dev: false - /@ip1sms/disposable-phone-numbers@2.1.1322: - resolution: {integrity: sha512-8HLC303wTVmBWu9UGYyauf3KW7/Gj8JHG87fiB1k2WF9pl7tunX05RWfGH15PKQYKepUoU4Wff1dRhnWeYEWUw==} + /@ip1sms/disposable-phone-numbers@2.1.1335: + resolution: {integrity: sha512-rU1pwgJQ9on+eNXVaSvCtTCZStZr61oouJnAdNwLOSfG6PFDQfXUWjwaMu5g43LPwRW5bgjpC9UxD5ovGcap6g==} dev: false /@isaacs/cliui@8.0.2: @@ -7588,8 +7592,8 @@ packages: dependencies: sparse-bitfield: 3.0.3 - /@mongodb-js/saslprep@1.3.1: - resolution: {integrity: sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==} + /@mongodb-js/saslprep@1.3.2: + resolution: {integrity: sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==} dependencies: sparse-bitfield: 3.0.3 dev: true @@ -8057,8 +8061,8 @@ packages: resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} requiresBuild: true dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/core': 1.6.0 + '@emnapi/runtime': 1.6.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -16034,8 +16038,8 @@ packages: dev: false optional: true - /bare-events@2.8.0: - resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==} + /bare-events@2.8.1: + resolution: {integrity: sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==} requiresBuild: true peerDependencies: bare-abort-controller: '*' @@ -16045,8 +16049,8 @@ packages: dev: false optional: true - /bare-fs@4.4.10: - resolution: {integrity: sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w==} + /bare-fs@4.5.0: + resolution: {integrity: sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==} engines: {bare: '>=1.16.0'} requiresBuild: true peerDependencies: @@ -16134,8 +16138,8 @@ packages: engines: {node: '>=6.0.0'} dev: true - /baseline-browser-mapping@2.8.16: - resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} + /baseline-browser-mapping@2.8.20: + resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} hasBin: true dev: true @@ -16464,16 +16468,16 @@ packages: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) - /browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + /browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - baseline-browser-mapping: 2.8.16 - caniuse-lite: 1.0.30001750 - electron-to-chromium: 1.5.235 - node-releases: 2.0.23 - update-browserslist-db: 1.1.3(browserslist@4.26.3) + baseline-browser-mapping: 2.8.20 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.240 + node-releases: 2.0.26 + update-browserslist-db: 1.1.4(browserslist@4.27.0) dev: true /bs-logger@0.2.6: @@ -16630,8 +16634,8 @@ packages: /caniuse-lite@1.0.30001726: resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} - /caniuse-lite@1.0.30001750: - resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} + /caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} dev: true /capital-case@1.0.4: @@ -18499,7 +18503,7 @@ packages: peerDependencies: next: ^13.0.0 || ^14.0.0 dependencies: - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) dev: false /editorconfig@1.0.4: @@ -18527,8 +18531,8 @@ packages: /electron-to-chromium@1.5.178: resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==} - /electron-to-chromium@1.5.235: - resolution: {integrity: sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==} + /electron-to-chromium@1.5.240: + resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==} dev: true /elliptic@6.6.1: @@ -19307,8 +19311,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) eslint: 8.57.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -24957,7 +24961,7 @@ packages: socks: optional: true dependencies: - '@mongodb-js/saslprep': 1.3.1 + '@mongodb-js/saslprep': 1.3.2 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 snappy: 7.2.2 @@ -25112,7 +25116,7 @@ packages: '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.26.9 @@ -25140,7 +25144,7 @@ packages: '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.26.9 @@ -25157,12 +25161,12 @@ packages: react: '*' react-dom: '*' dependencies: - next: 14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0): + /next@14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw==} engines: {node: '>=18.17.0'} hasBin: true @@ -25189,7 +25193,7 @@ packages: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.28.4)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.28.5)(react@18.2.0) optionalDependencies: '@next/swc-darwin-arm64': 14.2.26 '@next/swc-darwin-x64': 14.2.26 @@ -25205,7 +25209,7 @@ packages: - babel-plugin-macros dev: false - /next@14.2.26(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1): + /next@14.2.26(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw==} engines: {node: '>=18.17.0'} hasBin: true @@ -25232,7 +25236,7 @@ packages: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.28.4)(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.28.5)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.26 '@next/swc-darwin-x64': 14.2.26 @@ -25295,6 +25299,7 @@ packages: /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead /node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -25367,8 +25372,8 @@ packages: /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - /node-releases@2.0.23: - resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + /node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} dev: true /node-source-walk@4.3.0: @@ -28839,7 +28844,7 @@ packages: fast-fifo: 1.3.2 text-decoder: 1.2.3 optionalDependencies: - bare-events: 2.8.0 + bare-events: 2.8.1 transitivePeerDependencies: - bare-abort-controller dev: false @@ -29067,7 +29072,7 @@ packages: webpack: 5.99.9(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true - /styled-jsx@5.1.1(@babel/core@7.28.4)(react@18.2.0): + /styled-jsx@5.1.1(@babel/core@7.28.5)(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -29080,12 +29085,12 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 client-only: 0.0.1 react: 18.2.0 dev: false - /styled-jsx@5.1.1(@babel/core@7.28.4)(react@18.3.1): + /styled-jsx@5.1.1(@babel/core@7.28.5)(react@18.3.1): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -29098,7 +29103,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 client-only: 0.0.1 react: 18.3.1 dev: false @@ -29408,7 +29413,7 @@ packages: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.4.10 + bare-fs: 4.5.0 bare-path: 3.0.0 transitivePeerDependencies: - bare-abort-controller @@ -29947,7 +29952,7 @@ packages: tslib: 2.8.1 dev: false - /ts-jest@29.4.0(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.2.2): + /ts-jest@29.4.0(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.2.2): resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -29974,7 +29979,7 @@ packages: jest-util: optional: true dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 @@ -29988,7 +29993,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.4.0(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.8.3): + /ts-jest@29.4.0(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.8.3): resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -30015,7 +30020,7 @@ packages: jest-util: optional: true dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 @@ -30569,13 +30574,13 @@ packages: escalade: 3.2.0 picocolors: 1.1.1 - /update-browserslist-db@1.1.3(browserslist@4.26.3): - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + /update-browserslist-db@1.1.4(browserslist@4.27.0): + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.26.3 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1 dev: true @@ -31049,7 +31054,7 @@ packages: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.26.3 + browserslist: 4.27.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 From a190e6da4c5a72c08045b74fe9e9990252f9a5af Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:19:23 -0300 Subject: [PATCH 21/23] chore: remove userEmail in favor of privilegedClientId --- core/api/src/graphql/admin/access-rules.ts | 2 +- core/api/src/servers/graphql-admin-api-server.ts | 4 +--- core/api/src/servers/index.files.d.ts | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index 3ca2749a9d..bd2174c2bf 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -29,7 +29,7 @@ enum AdminAccessRight { // Helper function to create access right rules const createAccessRightRule = (accessRight: AdminAccessRight) => rule({ cache: "contextual" })(async (_parent, _args, ctx: GraphQLAdminContext) => { - if (!ctx.userEmail || !ctx.scope) return false + if (!ctx.privilegedClientId || !ctx.scope) return false return ctx.scope.includes(accessRight) }) diff --git a/core/api/src/servers/graphql-admin-api-server.ts b/core/api/src/servers/graphql-admin-api-server.ts index 6f6a618dad..d941c764b5 100644 --- a/core/api/src/servers/graphql-admin-api-server.ts +++ b/core/api/src/servers/graphql-admin-api-server.ts @@ -52,15 +52,13 @@ const setGqlAdminContext = async ( ): Promise => { const logger = baseLogger const tokenPayload = req.token - const userEmail = tokenPayload.sub as string // This should be the email from OAuth const scopeString = (tokenPayload.scope as string) || "" const scope = scopeString.split(" ").filter((s) => s.trim() !== "") - const privilegedClientId = tokenPayload.sub as PrivilegedClientId + const privilegedClientId = tokenPayload.sub as PrivilegedClientId // defacto email from OAuth req.gqlContext = { loaders, privilegedClientId, - userEmail, // Add email to context scope, logger, } diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index b49dfb2e84..6a7bd0aa66 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -24,7 +24,6 @@ type GraphQLAdminContext = { logger: Logger loaders: Loaders privilegedClientId: PrivilegedClientId - userEmail: string scope: string[] } From 1f9f4133b6873e763018cc7e6a5b6a08a5e4d9e2 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:23:44 -0300 Subject: [PATCH 22/23] chore: made case insensitive --- apps/admin-panel/app/access-rights.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin-panel/app/access-rights.ts b/apps/admin-panel/app/access-rights.ts index d587af19ff..49fa9d969d 100644 --- a/apps/admin-panel/app/access-rights.ts +++ b/apps/admin-panel/app/access-rights.ts @@ -118,7 +118,7 @@ export function hasAccessRightInScope( return scope .split(" ") .filter((s) => s.trim() !== "") - .includes(accessRight) + .some((s) => s.toUpperCase() === accessRight.toUpperCase()) } /** From edb7d2d0238bcb9d22d4992d83c841eed4433381 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:32:49 -0300 Subject: [PATCH 23/23] chore: typing - fix any by using unknown --- core/api/src/graphql/admin/access-rules.ts | 9 +++------ core/api/src/graphql/admin/types/index.ts | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/api/src/graphql/admin/access-rules.ts b/core/api/src/graphql/admin/access-rules.ts index bd2174c2bf..38fe470762 100644 --- a/core/api/src/graphql/admin/access-rules.ts +++ b/core/api/src/graphql/admin/access-rules.ts @@ -71,15 +71,12 @@ export const accessRules = { */ export function extractFields( fieldsWithRules: T, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Record> { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: Record> = {} +): Record> { + const result: Record> = {} for (const [key, value] of Object.entries(fieldsWithRules)) { result[key] = value.field } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return result as Record> + return result as Record> } /** diff --git a/core/api/src/graphql/admin/types/index.ts b/core/api/src/graphql/admin/types/index.ts index 48c7878554..dd2f4789cb 100644 --- a/core/api/src/graphql/admin/types/index.ts +++ b/core/api/src/graphql/admin/types/index.ts @@ -12,8 +12,7 @@ export const ALL_INTERFACE_TYPES = [GraphQLApplicationError, BtcWallet, UsdWalle * Used to define both the field behavior and authorization requirements in one place. */ export type AdminFieldDefinition = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - field: GraphQLFieldConfig + field: GraphQLFieldConfig rule: Rule }