From 61b6540f048f6a8b78a416bcfc0fc85449e975ed Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 23 Jul 2026 23:22:08 +0530 Subject: [PATCH 1/2] fix(graphql)!: standardize pagination on a single-level shape BREAKING CHANGE: 6 List*Request input types (ListClients, ListTrustedIssuers, ListSAMLServiceProviders, ListOrganizations, ListOrgDomains, ListOrgMembers) declared their pagination field as PaginatedRequest, a wrapper type whose only field is itself named `pagination: PaginationRequest` - forcing every caller to double-nest (`pagination: { pagination: { limit, page } }`). 3 more operations (_verification_requests, _webhooks, _email_templates) took PaginatedRequest as their entire top-level params type. This wrapper never existed on the canonical proto/gRPC surface (proto/authorizer/v1/pagination.proto and admin.proto - every List RPC takes PaginationRequest directly, no wrapper), and 4 other GraphQL-only request types (ListUsers, UserOrganizations, ListWebhookLog, ListAuditLog) already embedded PaginationRequest directly. The double-nesting was a GraphQL-only accident, not an intentional API design - and it was actively broken: OrgDomains.tsx (this repo's own dashboard) sent the single-nested shape and got GRAPHQL_VALIDATION_FAILED on every call until a prior fix matched the wrapper, and authorizer-js's TypeScript types for all 6 List*Request types were already (accidentally) typed as single-nested PaginationRequest, meaning no strictly-typed SDK consumer could construct the double-nested shape the schema required either. Removes the PaginatedRequest type entirely; the 9 affected fields/params now declare PaginationRequest directly, matching every other GraphQL endpoint and the proto surface. internal/utils/pagination.go's shared GetPagination helper now takes *model.PaginationRequest directly, eliminating the wrap-then-call boilerplate every "already single-nested" caller needed to reuse it. The gRPC handler layer's duplicate modelPaginatedRequest/modelPaginationRequest converters collapse into one. Every consumer in this repo (internal/service, internal/graphql, internal/grpcsrv/handlers, web/dashboard) is updated to match. --- internal/graph/generated/generated.go | 118 ++++++------------ internal/graph/model/models_gen.go | 24 ++-- internal/graph/schema.graphqls | 22 ++-- internal/graph/schema.resolvers.go | 6 +- internal/graphql/email_templates.go | 2 +- internal/graphql/provider.go | 6 +- internal/graphql/verification_requests.go | 2 +- internal/graphql/webhooks.go | 2 +- internal/grpcsrv/handlers/admin.go | 12 +- internal/grpcsrv/handlers/admin_project.go | 28 +---- .../integration_tests/email_templates_test.go | 6 +- .../verification_requests_test.go | 4 +- internal/integration_tests/webhooks_test.go | 6 +- internal/service/admin_audit.go | 4 +- internal/service/admin_clients.go | 2 +- internal/service/admin_email_templates.go | 2 +- internal/service/admin_organizations.go | 9 +- internal/service/admin_provider.go | 6 +- internal/service/admin_trusted_issuers.go | 2 +- internal/service/admin_users.go | 4 +- internal/service/admin_webhooks.go | 6 +- internal/utils/pagination.go | 14 +-- internal/utils/pagination_test.go | 35 ++++++ 23 files changed, 140 insertions(+), 182 deletions(-) create mode 100644 internal/utils/pagination_test.go diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index 9705b405f..1d00c1eed 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -476,7 +476,7 @@ type ComplexityRoot struct { CheckPermissions func(childComplexity int, params model.CheckPermissionsInput) int Client func(childComplexity int, params model.ClientRequest) int Clients func(childComplexity int, params *model.ListClientsRequest) int - EmailTemplates func(childComplexity int, params *model.PaginatedRequest) int + EmailTemplates func(childComplexity int, params *model.PaginationRequest) int Env func(childComplexity int) int FgaExpand func(childComplexity int, params model.FgaExpandInput) int FgaGetModel func(childComplexity int) int @@ -503,11 +503,11 @@ type ComplexityRoot struct { Users func(childComplexity int, params *model.ListUsersRequest) int ValidateJwtToken func(childComplexity int, params model.ValidateJWTTokenRequest) int ValidateSession func(childComplexity int, params *model.ValidateSessionRequest) int - VerificationRequests func(childComplexity int, params *model.PaginatedRequest) int + VerificationRequests func(childComplexity int, params *model.PaginationRequest) int WebauthnCredentials func(childComplexity int) int Webhook func(childComplexity int, params model.WebhookRequest) int WebhookLogs func(childComplexity int, params *model.ListWebhookLogRequest) int - Webhooks func(childComplexity int, params *model.PaginatedRequest) int + Webhooks func(childComplexity int, params *model.PaginationRequest) int } Response struct { @@ -793,12 +793,12 @@ type QueryResolver interface { Users(ctx context.Context, params *model.ListUsersRequest) (*model.Users, error) User(ctx context.Context, params model.GetUserRequest) (*model.User, error) UserOrganizations(ctx context.Context, params model.UserOrganizationsRequest) (*model.UserOrganizations, error) - VerificationRequests(ctx context.Context, params *model.PaginatedRequest) (*model.VerificationRequests, error) + VerificationRequests(ctx context.Context, params *model.PaginationRequest) (*model.VerificationRequests, error) AdminSession(ctx context.Context) (*model.Response, error) AdminMeta(ctx context.Context) (*model.AdminMeta, error) Env(ctx context.Context) (*model.Env, error) Webhook(ctx context.Context, params model.WebhookRequest) (*model.Webhook, error) - Webhooks(ctx context.Context, params *model.PaginatedRequest) (*model.Webhooks, error) + Webhooks(ctx context.Context, params *model.PaginationRequest) (*model.Webhooks, error) WebhookLogs(ctx context.Context, params *model.ListWebhookLogRequest) (*model.WebhookLogs, error) Client(ctx context.Context, params model.ClientRequest) (*model.Client, error) Clients(ctx context.Context, params *model.ListClientsRequest) (*model.Clients, error) @@ -814,7 +814,7 @@ type QueryResolver interface { OrgMembers(ctx context.Context, params model.ListOrgMembersRequest) (*model.OrgMembers, error) ScimEndpoint(ctx context.Context, params model.ScimEndpointRequest) (*model.ScimEndpoint, error) OrgDomains(ctx context.Context, params model.ListOrgDomainsRequest) (*model.OrgDomains, error) - EmailTemplates(ctx context.Context, params *model.PaginatedRequest) (*model.EmailTemplates, error) + EmailTemplates(ctx context.Context, params *model.PaginationRequest) (*model.EmailTemplates, error) AuditLogs(ctx context.Context, params *model.ListAuditLogRequest) (*model.AuditLogs, error) FgaGetModel(ctx context.Context) (*model.FgaModel, error) FgaReadTuples(ctx context.Context, params model.FgaReadTuplesInput) (*model.FgaTuples, error) @@ -3481,7 +3481,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.EmailTemplates(childComplexity, args["params"].(*model.PaginatedRequest)), true + return e.complexity.Query.EmailTemplates(childComplexity, args["params"].(*model.PaginationRequest)), true case "Query._env": if e.complexity.Query.Env == nil { @@ -3785,7 +3785,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.VerificationRequests(childComplexity, args["params"].(*model.PaginatedRequest)), true + return e.complexity.Query.VerificationRequests(childComplexity, args["params"].(*model.PaginationRequest)), true case "Query.webauthn_credentials": if e.complexity.Query.WebauthnCredentials == nil { @@ -3828,7 +3828,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.Webhooks(childComplexity, args["params"].(*model.PaginatedRequest)), true + return e.complexity.Query.Webhooks(childComplexity, args["params"].(*model.PaginationRequest)), true case "Response.message": if e.complexity.Response.Message == nil { @@ -4736,7 +4736,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputOrgSAMLConnectionRequest, ec.unmarshalInputOrganizationRequest, ec.unmarshalInputOtpMfaSetupRequest, - ec.unmarshalInputPaginatedRequest, ec.unmarshalInputPaginationRequest, ec.unmarshalInputPermissionCheckInput, ec.unmarshalInputRemoveOrgMemberRequest, @@ -5839,10 +5838,6 @@ input PaginationRequest { page: Int64 } -input PaginatedRequest { - pagination: PaginationRequest -} - # ListUsersRequest is the admin _users query input. query is an optional # case-insensitive substring filter matched against email, given_name, # family_name and nickname. Empty/absent means no filter (full list). @@ -5948,7 +5943,7 @@ input ClientRequest { } input ListClientsRequest { - pagination: PaginatedRequest + pagination: PaginationRequest } input AddTrustedIssuerRequest { @@ -6001,7 +5996,7 @@ input TrustedIssuerRequest { input ListTrustedIssuersRequest { service_account_id: String - pagination: PaginatedRequest + pagination: PaginationRequest } input CreateOrgOIDCConnectionRequest { @@ -6115,7 +6110,7 @@ input SAMLServiceProviderRequest { input ListSAMLServiceProvidersRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } # --- SAML IdP: signing key rotation & SP-metadata import --- @@ -6155,7 +6150,7 @@ input OrganizationRequest { } input ListOrganizationsRequest { - pagination: PaginatedRequest + pagination: PaginationRequest } # All SCIM endpoint admin ops are keyed by org_id — one endpoint per org. @@ -6187,7 +6182,7 @@ input AddVerifiedOrgDomainRequest { input ListOrgDomainsRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } input DeleteOrgDomainRequest { @@ -6208,7 +6203,7 @@ input RemoveOrgMemberRequest { input ListOrgMembersRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } input TestEndpointRequest { @@ -6540,7 +6535,7 @@ type Query { # the roles held in each. Super-admin only. Called lazily by the dashboard # user detail view — not exposed on the User type to keep user lists cheap. _user_organizations(params: UserOrganizationsRequest!): UserOrganizations! - _verification_requests(params: PaginatedRequest): VerificationRequests! + _verification_requests(params: PaginationRequest): VerificationRequests! _admin_session: Response! # Admin-only configuration metadata (e.g. configured roles). Non-deprecated # replacement for the bits of _env the dashboard needs. @@ -6548,7 +6543,7 @@ type Query { # Deprecated from v2.0.0 _env: Env! _webhook(params: WebhookRequest!): Webhook! - _webhooks(params: PaginatedRequest): Webhooks! + _webhooks(params: PaginationRequest): Webhooks! _webhook_logs(params: ListWebhookLogRequest): WebhookLogs! # Service accounts (machine/workload identity) _client(params: ClientRequest!): Client! @@ -6570,7 +6565,7 @@ type Query { _scim_endpoint(params: ScimEndpointRequest!): ScimEndpoint! # An org's verified domains (org-admin gated; never leaks another org's rows). _org_domains(params: ListOrgDomainsRequest!): OrgDomains! - _email_templates(params: PaginatedRequest): EmailTemplates! + _email_templates(params: PaginationRequest): EmailTemplates! _audit_logs(params: ListAuditLogRequest): AuditLogs! # FGA admin queries (super-admin only) _fga_get_model: FgaModel! @@ -8781,18 +8776,18 @@ func (ec *executionContext) field_Query__email_templates_args(ctx context.Contex func (ec *executionContext) field_Query__email_templates_argsParams( ctx context.Context, rawArgs map[string]any, -) (*model.PaginatedRequest, error) { +) (*model.PaginationRequest, error) { if _, ok := rawArgs["params"]; !ok { - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) if tmp, ok := rawArgs["params"]; ok { - return ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, tmp) + return ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, tmp) } - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } @@ -9313,18 +9308,18 @@ func (ec *executionContext) field_Query__verification_requests_args(ctx context. func (ec *executionContext) field_Query__verification_requests_argsParams( ctx context.Context, rawArgs map[string]any, -) (*model.PaginatedRequest, error) { +) (*model.PaginationRequest, error) { if _, ok := rawArgs["params"]; !ok { - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) if tmp, ok := rawArgs["params"]; ok { - return ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, tmp) + return ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, tmp) } - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } @@ -9397,18 +9392,18 @@ func (ec *executionContext) field_Query__webhooks_args(ctx context.Context, rawA func (ec *executionContext) field_Query__webhooks_argsParams( ctx context.Context, rawArgs map[string]any, -) (*model.PaginatedRequest, error) { +) (*model.PaginationRequest, error) { if _, ok := rawArgs["params"]; !ok { - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) if tmp, ok := rawArgs["params"]; ok { - return ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, tmp) + return ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, tmp) } - var zeroVal *model.PaginatedRequest + var zeroVal *model.PaginationRequest return zeroVal, nil } @@ -25934,7 +25929,7 @@ func (ec *executionContext) _Query__verification_requests(ctx context.Context, f }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().VerificationRequests(rctx, fc.Args["params"].(*model.PaginatedRequest)) + return ec.resolvers.Query().VerificationRequests(rctx, fc.Args["params"].(*model.PaginationRequest)) }) if err != nil { ec.Error(ctx, err) @@ -26358,7 +26353,7 @@ func (ec *executionContext) _Query__webhooks(ctx context.Context, field graphql. }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Webhooks(rctx, fc.Args["params"].(*model.PaginatedRequest)) + return ec.resolvers.Query().Webhooks(rctx, fc.Args["params"].(*model.PaginationRequest)) }) if err != nil { ec.Error(ctx, err) @@ -27454,7 +27449,7 @@ func (ec *executionContext) _Query__email_templates(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().EmailTemplates(rctx, fc.Args["params"].(*model.PaginatedRequest)) + return ec.resolvers.Query().EmailTemplates(rctx, fc.Args["params"].(*model.PaginationRequest)) }) if err != nil { ec.Error(ctx, err) @@ -36758,7 +36753,7 @@ func (ec *executionContext) unmarshalInputListClientsRequest(ctx context.Context switch k { case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -36792,7 +36787,7 @@ func (ec *executionContext) unmarshalInputListOrgDomainsRequest(ctx context.Cont it.OrgID = data case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -36826,7 +36821,7 @@ func (ec *executionContext) unmarshalInputListOrgMembersRequest(ctx context.Cont it.OrgID = data case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -36853,7 +36848,7 @@ func (ec *executionContext) unmarshalInputListOrganizationsRequest(ctx context.C switch k { case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -36955,7 +36950,7 @@ func (ec *executionContext) unmarshalInputListSAMLServiceProvidersRequest(ctx co it.OrgID = data case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -36989,7 +36984,7 @@ func (ec *executionContext) unmarshalInputListTrustedIssuersRequest(ctx context. it.ServiceAccountID = data case "pagination": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx, v) + data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) if err != nil { return it, err } @@ -37562,33 +37557,6 @@ func (ec *executionContext) unmarshalInputOtpMfaSetupRequest(ctx context.Context return it, nil } -func (ec *executionContext) unmarshalInputPaginatedRequest(ctx context.Context, obj any) (model.PaginatedRequest, error) { - var it model.PaginatedRequest - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"pagination"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "pagination": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - data, err := ec.unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx, v) - if err != nil { - return it, err - } - it.Pagination = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputPaginationRequest(ctx context.Context, obj any) (model.PaginationRequest, error) { var it model.PaginationRequest asMap := map[string]any{} @@ -47549,14 +47517,6 @@ func (ec *executionContext) unmarshalOOtpMfaSetupRequest2ᚖgithubᚗcomᚋautho return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalOPaginatedRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginatedRequest(ctx context.Context, v any) (*model.PaginatedRequest, error) { - if v == nil { - return nil, nil - } - res, err := ec.unmarshalInputPaginatedRequest(ctx, v) - return &res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalOPaginationRequest2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋinternalᚋgraphᚋmodelᚐPaginationRequest(ctx context.Context, v any) (*model.PaginationRequest, error) { if v == nil { return nil, nil diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 637d32ff2..4d6b7181c 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -404,21 +404,21 @@ type ListAuditLogRequest struct { } type ListClientsRequest struct { - Pagination *PaginatedRequest `json:"pagination,omitempty"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListOrgDomainsRequest struct { - OrgID string `json:"org_id"` - Pagination *PaginatedRequest `json:"pagination,omitempty"` + OrgID string `json:"org_id"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListOrgMembersRequest struct { - OrgID string `json:"org_id"` - Pagination *PaginatedRequest `json:"pagination,omitempty"` + OrgID string `json:"org_id"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListOrganizationsRequest struct { - Pagination *PaginatedRequest `json:"pagination,omitempty"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListPermissionsInput struct { @@ -438,13 +438,13 @@ type ListSAMLIDPKeysRequest struct { } type ListSAMLServiceProvidersRequest struct { - OrgID string `json:"org_id"` - Pagination *PaginatedRequest `json:"pagination,omitempty"` + OrgID string `json:"org_id"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListTrustedIssuersRequest struct { - ServiceAccountID *string `json:"service_account_id,omitempty"` - Pagination *PaginatedRequest `json:"pagination,omitempty"` + ServiceAccountID *string `json:"service_account_id,omitempty"` + Pagination *PaginationRequest `json:"pagination,omitempty"` } type ListUsersRequest struct { @@ -640,10 +640,6 @@ type OtpMfaSetupRequest struct { PhoneNumber *string `json:"phone_number,omitempty"` } -type PaginatedRequest struct { - Pagination *PaginationRequest `json:"pagination,omitempty"` -} - type Pagination struct { Limit int64 `json:"limit"` Page int64 `json:"page"` diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 7380df2ce..9094bd76d 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -966,10 +966,6 @@ input PaginationRequest { page: Int64 } -input PaginatedRequest { - pagination: PaginationRequest -} - # ListUsersRequest is the admin _users query input. query is an optional # case-insensitive substring filter matched against email, given_name, # family_name and nickname. Empty/absent means no filter (full list). @@ -1075,7 +1071,7 @@ input ClientRequest { } input ListClientsRequest { - pagination: PaginatedRequest + pagination: PaginationRequest } input AddTrustedIssuerRequest { @@ -1128,7 +1124,7 @@ input TrustedIssuerRequest { input ListTrustedIssuersRequest { service_account_id: String - pagination: PaginatedRequest + pagination: PaginationRequest } input CreateOrgOIDCConnectionRequest { @@ -1242,7 +1238,7 @@ input SAMLServiceProviderRequest { input ListSAMLServiceProvidersRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } # --- SAML IdP: signing key rotation & SP-metadata import --- @@ -1282,7 +1278,7 @@ input OrganizationRequest { } input ListOrganizationsRequest { - pagination: PaginatedRequest + pagination: PaginationRequest } # All SCIM endpoint admin ops are keyed by org_id — one endpoint per org. @@ -1314,7 +1310,7 @@ input AddVerifiedOrgDomainRequest { input ListOrgDomainsRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } input DeleteOrgDomainRequest { @@ -1335,7 +1331,7 @@ input RemoveOrgMemberRequest { input ListOrgMembersRequest { org_id: String! - pagination: PaginatedRequest + pagination: PaginationRequest } input TestEndpointRequest { @@ -1667,7 +1663,7 @@ type Query { # the roles held in each. Super-admin only. Called lazily by the dashboard # user detail view — not exposed on the User type to keep user lists cheap. _user_organizations(params: UserOrganizationsRequest!): UserOrganizations! - _verification_requests(params: PaginatedRequest): VerificationRequests! + _verification_requests(params: PaginationRequest): VerificationRequests! _admin_session: Response! # Admin-only configuration metadata (e.g. configured roles). Non-deprecated # replacement for the bits of _env the dashboard needs. @@ -1675,7 +1671,7 @@ type Query { # Deprecated from v2.0.0 _env: Env! _webhook(params: WebhookRequest!): Webhook! - _webhooks(params: PaginatedRequest): Webhooks! + _webhooks(params: PaginationRequest): Webhooks! _webhook_logs(params: ListWebhookLogRequest): WebhookLogs! # Service accounts (machine/workload identity) _client(params: ClientRequest!): Client! @@ -1697,7 +1693,7 @@ type Query { _scim_endpoint(params: ScimEndpointRequest!): ScimEndpoint! # An org's verified domains (org-admin gated; never leaks another org's rows). _org_domains(params: ListOrgDomainsRequest!): OrgDomains! - _email_templates(params: PaginatedRequest): EmailTemplates! + _email_templates(params: PaginationRequest): EmailTemplates! _audit_logs(params: ListAuditLogRequest): AuditLogs! # FGA admin queries (super-admin only) _fga_get_model: FgaModel! diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 464c27017..e5358c122 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -443,7 +443,7 @@ func (r *queryResolver) UserOrganizations(ctx context.Context, params model.User } // VerificationRequests is the resolver for the _verification_requests field. -func (r *queryResolver) VerificationRequests(ctx context.Context, params *model.PaginatedRequest) (*model.VerificationRequests, error) { +func (r *queryResolver) VerificationRequests(ctx context.Context, params *model.PaginationRequest) (*model.VerificationRequests, error) { return r.GraphQLProvider.VerificationRequests(ctx, params) } @@ -468,7 +468,7 @@ func (r *queryResolver) Webhook(ctx context.Context, params model.WebhookRequest } // Webhooks is the resolver for the _webhooks field. -func (r *queryResolver) Webhooks(ctx context.Context, params *model.PaginatedRequest) (*model.Webhooks, error) { +func (r *queryResolver) Webhooks(ctx context.Context, params *model.PaginationRequest) (*model.Webhooks, error) { return r.GraphQLProvider.Webhooks(ctx, params) } @@ -548,7 +548,7 @@ func (r *queryResolver) OrgDomains(ctx context.Context, params model.ListOrgDoma } // EmailTemplates is the resolver for the _email_templates field. -func (r *queryResolver) EmailTemplates(ctx context.Context, params *model.PaginatedRequest) (*model.EmailTemplates, error) { +func (r *queryResolver) EmailTemplates(ctx context.Context, params *model.PaginationRequest) (*model.EmailTemplates, error) { return r.GraphQLProvider.EmailTemplates(ctx, params) } diff --git a/internal/graphql/email_templates.go b/internal/graphql/email_templates.go index d394c584a..1ab780a8c 100644 --- a/internal/graphql/email_templates.go +++ b/internal/graphql/email_templates.go @@ -13,7 +13,7 @@ import ( // a thin transport adapter. // // Permissions: authorizer:admin -func (g *graphqlProvider) EmailTemplates(ctx context.Context, params *model.PaginatedRequest) (*model.EmailTemplates, error) { +func (g *graphqlProvider) EmailTemplates(ctx context.Context, params *model.PaginationRequest) (*model.EmailTemplates, error) { gc, err := utils.GinContextFromContext(ctx) if err != nil { g.Log.Debug().Err(err).Msg("failed to get gin context") diff --git a/internal/graphql/provider.go b/internal/graphql/provider.go index 54032cc20..da50da3b4 100644 --- a/internal/graphql/provider.go +++ b/internal/graphql/provider.go @@ -131,7 +131,7 @@ type Provider interface { DeleteWebhook(ctx context.Context, params *model.WebhookRequest) (*model.Response, error) // EmailTemplates is the method to list email templates. // Permissions: authorizer:admin - EmailTemplates(ctx context.Context, in *model.PaginatedRequest) (*model.EmailTemplates, error) + EmailTemplates(ctx context.Context, in *model.PaginationRequest) (*model.EmailTemplates, error) // EnableAccess is the method to enable access. // Permissions: authorizer:admin EnableAccess(ctx context.Context, params *model.UpdateAccessRequest) (*model.Response, error) @@ -218,7 +218,7 @@ type Provider interface { ValidateSession(ctx context.Context, params *model.ValidateSessionRequest) (*model.ValidateSessionResponse, error) // VerificationRequests is the method to list verification requests. // Permissions: authorizer:admin - VerificationRequests(ctx context.Context, in *model.PaginatedRequest) (*model.VerificationRequests, error) + VerificationRequests(ctx context.Context, in *model.PaginationRequest) (*model.VerificationRequests, error) // VerifyEmail is the method to verify email. // Permissions: none VerifyEmail(ctx context.Context, params *model.VerifyEmailRequest) (*model.AuthResponse, error) @@ -254,7 +254,7 @@ type Provider interface { Webhook(ctx context.Context, params *model.WebhookRequest) (*model.Webhook, error) // Webhooks is the method to list webhooks. // Permissions: authorizer:admin - Webhooks(ctx context.Context, in *model.PaginatedRequest) (*model.Webhooks, error) + Webhooks(ctx context.Context, in *model.PaginationRequest) (*model.Webhooks, error) // CreateClient creates a machine/workload service account. // Permissions: authorizer:admin CreateClient(ctx context.Context, params *model.CreateClientRequest) (*model.CreateClientResponse, error) diff --git a/internal/graphql/verification_requests.go b/internal/graphql/verification_requests.go index 04502dd40..1063fb25c 100644 --- a/internal/graphql/verification_requests.go +++ b/internal/graphql/verification_requests.go @@ -13,7 +13,7 @@ import ( // Resolver is a thin transport adapter. // // Permissions: authorizer:admin -func (g *graphqlProvider) VerificationRequests(ctx context.Context, params *model.PaginatedRequest) (*model.VerificationRequests, error) { +func (g *graphqlProvider) VerificationRequests(ctx context.Context, params *model.PaginationRequest) (*model.VerificationRequests, error) { gc, err := utils.GinContextFromContext(ctx) if err != nil { g.Log.Debug().Err(err).Msg("failed to get gin context") diff --git a/internal/graphql/webhooks.go b/internal/graphql/webhooks.go index 58757a198..92a67957b 100644 --- a/internal/graphql/webhooks.go +++ b/internal/graphql/webhooks.go @@ -13,7 +13,7 @@ import ( // transport adapter. // // Permission: authorizer:admin -func (g *graphqlProvider) Webhooks(ctx context.Context, params *model.PaginatedRequest) (*model.Webhooks, error) { +func (g *graphqlProvider) Webhooks(ctx context.Context, params *model.PaginationRequest) (*model.Webhooks, error) { gc, err := utils.GinContextFromContext(ctx) if err != nil { g.Log.Debug().Err(err).Msg("failed to get gin context") diff --git a/internal/grpcsrv/handlers/admin.go b/internal/grpcsrv/handlers/admin.go index b623ab9fd..aa42622a4 100644 --- a/internal/grpcsrv/handlers/admin.go +++ b/internal/grpcsrv/handlers/admin.go @@ -138,7 +138,7 @@ func (h *AdminHandler) DeleteUser(ctx context.Context, req *authorizerv1.DeleteU // VerificationRequests delegates to service.VerificationRequests and projects // the paginated result. Requires super-admin auth. func (h *AdminHandler) VerificationRequests(ctx context.Context, req *authorizerv1.VerificationRequestsRequest) (*authorizerv1.VerificationRequestsResponse, error) { - res, _, err := h.Service.VerificationRequests(ctx, transport.MetaFromGRPC(ctx), modelPaginatedRequest(req.GetPagination())) + res, _, err := h.Service.VerificationRequests(ctx, transport.MetaFromGRPC(ctx), modelPaginationRequest(req.GetPagination())) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func (h *AdminHandler) GetWebhook(ctx context.Context, req *authorizerv1.GetWebh // Webhooks delegates to service.Webhooks and projects the paginated result. // Requires super-admin auth. func (h *AdminHandler) Webhooks(ctx context.Context, req *authorizerv1.WebhooksRequest) (*authorizerv1.WebhooksResponse, error) { - res, _, err := h.Service.Webhooks(ctx, transport.MetaFromGRPC(ctx), modelPaginatedRequest(req.GetPagination())) + res, _, err := h.Service.Webhooks(ctx, transport.MetaFromGRPC(ctx), modelPaginationRequest(req.GetPagination())) if err != nil { return nil, err } @@ -320,7 +320,7 @@ func (h *AdminHandler) DeleteEmailTemplate(ctx context.Context, req *authorizerv // EmailTemplates delegates to service.EmailTemplates and projects the paginated // result. Requires super-admin auth. func (h *AdminHandler) EmailTemplates(ctx context.Context, req *authorizerv1.EmailTemplatesRequest) (*authorizerv1.EmailTemplatesResponse, error) { - res, _, err := h.Service.EmailTemplates(ctx, transport.MetaFromGRPC(ctx), modelPaginatedRequest(req.GetPagination())) + res, _, err := h.Service.EmailTemplates(ctx, transport.MetaFromGRPC(ctx), modelPaginationRequest(req.GetPagination())) if err != nil { return nil, err } @@ -524,7 +524,7 @@ func (h *AdminHandler) GetClient(ctx context.Context, req *authorizerv1.GetClien // paginated result. Client secrets are never surfaced. Requires super-admin auth. func (h *AdminHandler) Clients(ctx context.Context, req *authorizerv1.ClientsRequest) (*authorizerv1.ClientsResponse, error) { res, _, err := h.Service.Clients(ctx, transport.MetaFromGRPC(ctx), &model.ListClientsRequest{ - Pagination: modelPaginatedRequest(req.GetPagination()), + Pagination: modelPaginationRequest(req.GetPagination()), }) if err != nil { return nil, err @@ -604,7 +604,7 @@ func (h *AdminHandler) GetTrustedIssuer(ctx context.Context, req *authorizerv1.G func (h *AdminHandler) TrustedIssuers(ctx context.Context, req *authorizerv1.TrustedIssuersRequest) (*authorizerv1.TrustedIssuersResponse, error) { res, _, err := h.Service.TrustedIssuers(ctx, transport.MetaFromGRPC(ctx), &model.ListTrustedIssuersRequest{ ServiceAccountID: req.ServiceAccountId, - Pagination: modelPaginatedRequest(req.GetPagination()), + Pagination: modelPaginationRequest(req.GetPagination()), }) if err != nil { return nil, err @@ -682,7 +682,7 @@ func (h *AdminHandler) GetSamlServiceProvider(ctx context.Context, req *authoriz func (h *AdminHandler) ListSamlServiceProviders(ctx context.Context, req *authorizerv1.ListSamlServiceProvidersRequest) (*authorizerv1.ListSamlServiceProvidersResponse, error) { res, _, err := h.Service.ListSAMLServiceProviders(ctx, transport.MetaFromGRPC(ctx), &model.ListSAMLServiceProvidersRequest{ OrgID: req.GetOrgId(), - Pagination: modelPaginatedRequest(req.GetPagination()), + Pagination: modelPaginationRequest(req.GetPagination()), }) if err != nil { return nil, err diff --git a/internal/grpcsrv/handlers/admin_project.go b/internal/grpcsrv/handlers/admin_project.go index 3cbd67e7c..62095e084 100644 --- a/internal/grpcsrv/handlers/admin_project.go +++ b/internal/grpcsrv/handlers/admin_project.go @@ -26,30 +26,10 @@ func projectAdminMeta(m *model.AdminMeta) *authorizerv1.AdminMeta { } } -// modelPaginatedRequest converts the proto PaginationRequest carried by admin -// list RPCs into the GraphQL model.PaginatedRequest consumed by the service -// layer. A nil proto pagination yields a nil request so service.GetPagination -// applies its defaults (page 1, default limit). -func modelPaginatedRequest(in *authorizerv1.PaginationRequest) *model.PaginatedRequest { - if in == nil { - return nil - } - out := &model.PaginatedRequest{Pagination: &model.PaginationRequest{}} - if in.Page != 0 { - page := in.Page - out.Pagination.Page = &page - } - if in.Limit != 0 { - limit := in.Limit - out.Pagination.Limit = &limit - } - return out -} - -// modelPaginationRequest converts the proto PaginationRequest into the GraphQL -// model.PaginationRequest (the inner pagination shape carried by -// ListWebhookLogRequest, as opposed to the PaginatedRequest wrapper). A nil -// proto pagination yields nil so service.GetPagination applies its defaults. +// modelPaginationRequest converts the proto PaginationRequest carried by admin +// list RPCs into the GraphQL model.PaginationRequest consumed by the service +// layer. A nil proto pagination yields nil so service.GetPagination applies +// its defaults (page 1, default limit). func modelPaginationRequest(in *authorizerv1.PaginationRequest) *model.PaginationRequest { if in == nil { return nil diff --git a/internal/integration_tests/email_templates_test.go b/internal/integration_tests/email_templates_test.go index 35d3f6fd4..78f442913 100644 --- a/internal/integration_tests/email_templates_test.go +++ b/internal/integration_tests/email_templates_test.go @@ -19,7 +19,7 @@ func TestEmailTemplates(t *testing.T) { t.Run("should fail without admin auth", func(t *testing.T) { req.Header.Set("Cookie", "") - res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginationRequest{}) assert.Error(t, err) assert.Nil(t, res) }) @@ -29,7 +29,7 @@ func TestEmailTemplates(t *testing.T) { require.NoError(t, err) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) - res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginationRequest{}) require.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Pagination) @@ -40,7 +40,7 @@ func TestEmailTemplates(t *testing.T) { require.NoError(t, err) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) - res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.EmailTemplates(ctx, &model.PaginationRequest{}) require.NoError(t, err) assert.NotNil(t, res) for i, tmpl := range res.EmailTemplates { diff --git a/internal/integration_tests/verification_requests_test.go b/internal/integration_tests/verification_requests_test.go index 4d3912d12..1ced4c106 100644 --- a/internal/integration_tests/verification_requests_test.go +++ b/internal/integration_tests/verification_requests_test.go @@ -27,7 +27,7 @@ func TestVerificationRequests(t *testing.T) { t.Run("should fail without admin auth", func(t *testing.T) { req.Header.Set("Cookie", "") - res, err := ts.GraphQLProvider.VerificationRequests(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.VerificationRequests(ctx, &model.PaginationRequest{}) assert.Error(t, err) assert.Nil(t, res) }) @@ -37,7 +37,7 @@ func TestVerificationRequests(t *testing.T) { require.NoError(t, err) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) - res, err := ts.GraphQLProvider.VerificationRequests(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.VerificationRequests(ctx, &model.PaginationRequest{}) require.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Pagination) diff --git a/internal/integration_tests/webhooks_test.go b/internal/integration_tests/webhooks_test.go index 92d3a60f6..2120a4f44 100644 --- a/internal/integration_tests/webhooks_test.go +++ b/internal/integration_tests/webhooks_test.go @@ -21,7 +21,7 @@ func TestWebhooks(t *testing.T) { t.Run("should fail list webhooks without admin auth", func(t *testing.T) { req.Header.Set("Cookie", "") - res, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginationRequest{}) assert.Error(t, err) assert.Nil(t, res) }) @@ -31,7 +31,7 @@ func TestWebhooks(t *testing.T) { require.NoError(t, err) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) - res, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginatedRequest{}) + res, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginationRequest{}) require.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Pagination) @@ -53,7 +53,7 @@ func TestWebhooks(t *testing.T) { assert.NotNil(t, addRes) // List webhooks to get the ID - webhooks, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginatedRequest{}) + webhooks, err := ts.GraphQLProvider.Webhooks(ctx, &model.PaginationRequest{}) require.NoError(t, err) assert.GreaterOrEqual(t, len(webhooks.Webhooks), 1) diff --git a/internal/service/admin_audit.go b/internal/service/admin_audit.go index 3d451fd72..0016ee04f 100644 --- a/internal/service/admin_audit.go +++ b/internal/service/admin_audit.go @@ -31,9 +31,7 @@ func (p *provider) AuditLogs(ctx context.Context, meta RequestMetadata, params * filter := make(map[string]interface{}) if params != nil { - pagination = utils.GetPagination(&model.PaginatedRequest{ - Pagination: params.Pagination, - }) + pagination = utils.GetPagination(params.Pagination) if refs.StringValue(params.Action) != "" { filter[auditFilterAction] = refs.StringValue(params.Action) } diff --git a/internal/service/admin_clients.go b/internal/service/admin_clients.go index e05b42f61..5c5906fab 100644 --- a/internal/service/admin_clients.go +++ b/internal/service/admin_clients.go @@ -290,7 +290,7 @@ func (p *provider) Clients(ctx context.Context, meta RequestMetadata, params *mo return nil, nil, err } - var paginatedReq *model.PaginatedRequest + var paginatedReq *model.PaginationRequest if params != nil { paginatedReq = params.Pagination } diff --git a/internal/service/admin_email_templates.go b/internal/service/admin_email_templates.go index 0c2dd3107..b789852de 100644 --- a/internal/service/admin_email_templates.go +++ b/internal/service/admin_email_templates.go @@ -189,7 +189,7 @@ func (p *provider) DeleteEmailTemplate(ctx context.Context, meta RequestMetadata // EmailTemplates returns a paginated list of email templates. Requires // super-admin auth. Logic migrated from internal/graphql/email_templates.go. -func (p *provider) EmailTemplates(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.EmailTemplates, *ResponseSideEffects, error) { +func (p *provider) EmailTemplates(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.EmailTemplates, *ResponseSideEffects, error) { log := p.Log.With().Str("func", "EmailTemplates").Logger() if err := p.requireSuperAdmin(ctx, meta); err != nil { return nil, nil, err diff --git a/internal/service/admin_organizations.go b/internal/service/admin_organizations.go index a1c8c6ee2..a40df53d1 100644 --- a/internal/service/admin_organizations.go +++ b/internal/service/admin_organizations.go @@ -216,7 +216,7 @@ func (p *provider) Organizations(ctx context.Context, meta RequestMetadata, para return nil, nil, err } - var paginatedReq *model.PaginatedRequest + var paginatedReq *model.PaginationRequest if params != nil { paginatedReq = params.Pagination } @@ -415,12 +415,7 @@ func (p *provider) UserOrganizations(ctx context.Context, meta RequestMetadata, return nil, nil, InvalidArgument("user_id is required") } - var pagination *model.Pagination - if params.Pagination != nil { - pagination = utils.GetPagination(&model.PaginatedRequest{Pagination: params.Pagination}) - } else { - pagination = utils.GetPagination(nil) - } + pagination := utils.GetPagination(params.Pagination) memberships, pagination, err := p.StorageProvider.ListOrgMembershipsByUser(ctx, params.UserID, pagination) if err != nil { diff --git a/internal/service/admin_provider.go b/internal/service/admin_provider.go index aa76f71bc..496d1f832 100644 --- a/internal/service/admin_provider.go +++ b/internal/service/admin_provider.go @@ -35,7 +35,7 @@ type AdminProvider interface { UserOrganizations(ctx context.Context, meta RequestMetadata, params *model.UserOrganizationsRequest) (*model.UserOrganizations, *ResponseSideEffects, error) UpdateUser(ctx context.Context, meta RequestMetadata, params *model.UpdateUserRequest) (*model.User, *ResponseSideEffects, error) DeleteUser(ctx context.Context, meta RequestMetadata, params *model.DeleteUserRequest) (*model.Response, *ResponseSideEffects, error) - VerificationRequests(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.VerificationRequests, *ResponseSideEffects, error) + VerificationRequests(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.VerificationRequests, *ResponseSideEffects, error) // Access. RevokeAccess(ctx context.Context, meta RequestMetadata, params *model.UpdateAccessRequest) (*model.Response, *ResponseSideEffects, error) @@ -47,7 +47,7 @@ type AdminProvider interface { UpdateWebhook(ctx context.Context, meta RequestMetadata, params *model.UpdateWebhookRequest) (*model.Response, *ResponseSideEffects, error) DeleteWebhook(ctx context.Context, meta RequestMetadata, params *model.WebhookRequest) (*model.Response, *ResponseSideEffects, error) Webhook(ctx context.Context, meta RequestMetadata, params *model.WebhookRequest) (*model.Webhook, *ResponseSideEffects, error) - Webhooks(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.Webhooks, *ResponseSideEffects, error) + Webhooks(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.Webhooks, *ResponseSideEffects, error) WebhookLogs(ctx context.Context, meta RequestMetadata, params *model.ListWebhookLogRequest) (*model.WebhookLogs, *ResponseSideEffects, error) TestEndpoint(ctx context.Context, meta RequestMetadata, params *model.TestEndpointRequest) (*model.TestEndpointResponse, *ResponseSideEffects, error) @@ -115,7 +115,7 @@ type AdminProvider interface { AddEmailTemplate(ctx context.Context, meta RequestMetadata, params *model.AddEmailTemplateRequest) (*model.Response, *ResponseSideEffects, error) UpdateEmailTemplate(ctx context.Context, meta RequestMetadata, params *model.UpdateEmailTemplateRequest) (*model.Response, *ResponseSideEffects, error) DeleteEmailTemplate(ctx context.Context, meta RequestMetadata, params *model.DeleteEmailTemplateRequest) (*model.Response, *ResponseSideEffects, error) - EmailTemplates(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.EmailTemplates, *ResponseSideEffects, error) + EmailTemplates(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.EmailTemplates, *ResponseSideEffects, error) // Audit. AuditLogs(ctx context.Context, meta RequestMetadata, params *model.ListAuditLogRequest) (*model.AuditLogs, *ResponseSideEffects, error) diff --git a/internal/service/admin_trusted_issuers.go b/internal/service/admin_trusted_issuers.go index 76fbb38f5..2b2ed3a24 100644 --- a/internal/service/admin_trusted_issuers.go +++ b/internal/service/admin_trusted_issuers.go @@ -290,7 +290,7 @@ func (p *provider) TrustedIssuers(ctx context.Context, meta RequestMetadata, par return nil, nil, err } - var paginatedReq *model.PaginatedRequest + var paginatedReq *model.PaginationRequest var serviceAccountID string if params != nil { paginatedReq = params.Pagination diff --git a/internal/service/admin_users.go b/internal/service/admin_users.go index c024fe130..3bf8be376 100644 --- a/internal/service/admin_users.go +++ b/internal/service/admin_users.go @@ -48,7 +48,7 @@ func (p *provider) Users(ctx context.Context, meta RequestMetadata, params *mode var query string var pagination *model.Pagination if params != nil { - pagination = utils.GetPagination(&model.PaginatedRequest{Pagination: params.Pagination}) + pagination = utils.GetPagination(params.Pagination) query = refs.StringValue(params.Query) } else { pagination = utils.GetPagination(nil) @@ -448,7 +448,7 @@ func (p *provider) DeleteUser(ctx context.Context, meta RequestMetadata, params // VerificationRequests returns a paginated list of pending verification // requests. Requires super-admin auth. Logic migrated from // internal/graphql/verification_requests.go. -func (p *provider) VerificationRequests(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.VerificationRequests, *ResponseSideEffects, error) { +func (p *provider) VerificationRequests(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.VerificationRequests, *ResponseSideEffects, error) { log := p.Log.With().Str("func", "VerificationRequests").Logger() if err := p.requireSuperAdmin(ctx, meta); err != nil { return nil, nil, err diff --git a/internal/service/admin_webhooks.go b/internal/service/admin_webhooks.go index 0f93c6f41..881020a6c 100644 --- a/internal/service/admin_webhooks.go +++ b/internal/service/admin_webhooks.go @@ -237,7 +237,7 @@ func (p *provider) Webhook(ctx context.Context, meta RequestMetadata, params *mo // Webhooks returns a paginated list of webhooks. Requires super-admin auth. // Logic migrated from internal/graphql/webhooks.go. -func (p *provider) Webhooks(ctx context.Context, meta RequestMetadata, params *model.PaginatedRequest) (*model.Webhooks, *ResponseSideEffects, error) { +func (p *provider) Webhooks(ctx context.Context, meta RequestMetadata, params *model.PaginationRequest) (*model.Webhooks, *ResponseSideEffects, error) { log := p.Log.With().Str("func", "Webhooks").Logger() if err := p.requireSuperAdmin(ctx, meta); err != nil { return nil, nil, err @@ -271,9 +271,7 @@ func (p *provider) WebhookLogs(ctx context.Context, meta RequestMetadata, params var pagination *model.Pagination var webhookID string if params != nil { - pagination = utils.GetPagination(&model.PaginatedRequest{ - Pagination: params.Pagination, - }) + pagination = utils.GetPagination(params.Pagination) webhookID = refs.StringValue(params.WebhookID) } else { pagination = utils.GetPagination(nil) diff --git a/internal/utils/pagination.go b/internal/utils/pagination.go index 35aba6b0b..a91c21366 100644 --- a/internal/utils/pagination.go +++ b/internal/utils/pagination.go @@ -5,18 +5,18 @@ import ( "github.com/authorizerdev/authorizer/internal/graph/model" ) -// GetPagination helps getting pagination data from paginated input +// GetPagination helps getting pagination data from a pagination input // also returns default limit and offset if pagination data is not present -func GetPagination(paginatedInput *model.PaginatedRequest) *model.Pagination { +func GetPagination(pagination *model.PaginationRequest) *model.Pagination { limit := int64(constants.DefaultLimit) page := int64(1) - if paginatedInput != nil && paginatedInput.Pagination != nil { - if paginatedInput.Pagination.Limit != nil { - limit = *paginatedInput.Pagination.Limit + if pagination != nil { + if pagination.Limit != nil { + limit = *pagination.Limit } - if paginatedInput.Pagination.Page != nil { - page = *paginatedInput.Pagination.Page + if pagination.Page != nil { + page = *pagination.Page } } diff --git a/internal/utils/pagination_test.go b/internal/utils/pagination_test.go new file mode 100644 index 000000000..159384c2f --- /dev/null +++ b/internal/utils/pagination_test.go @@ -0,0 +1,35 @@ +package utils + +import ( + "testing" + + "github.com/authorizerdev/authorizer/internal/constants" + "github.com/authorizerdev/authorizer/internal/graph/model" +) + +func TestGetPagination(t *testing.T) { + limit10 := int64(10) + page2 := int64(2) + + cases := []struct { + name string + pagination *model.PaginationRequest + wantLimit int64 + wantPage int64 + wantOffset int64 + }{ + {"nil pagination uses defaults", nil, int64(constants.DefaultLimit), 1, 0}, + {"empty struct uses defaults", &model.PaginationRequest{}, int64(constants.DefaultLimit), 1, 0}, + {"explicit limit and page", &model.PaginationRequest{Limit: &limit10, Page: &page2}, 10, 2, 10}, + {"limit only, default page", &model.PaginationRequest{Limit: &limit10}, 10, 1, 0}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := GetPagination(c.pagination) + if got.Limit != c.wantLimit || got.Page != c.wantPage || got.Offset != c.wantOffset { + t.Errorf("GetPagination() = {Limit:%d Page:%d Offset:%d}, want {Limit:%d Page:%d Offset:%d}", + got.Limit, got.Page, got.Offset, c.wantLimit, c.wantPage, c.wantOffset) + } + }) + } +} From 97c010deb662b96b204804eb3c2b8c42a0920e92 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 23 Jul 2026 23:22:23 +0530 Subject: [PATCH 2/2] fix(dashboard): match the standardized single-level pagination shape Follows the schema fix (previous commit): Clients.tsx, TrustedIssuers.tsx, Organizations.tsx, and OrganizationDetail.tsx's org-members fetch drop their double-nested `pagination: { pagination: {...} } }` calls down to single-level. OrgDomains.tsx reverts the double-nesting patch from an earlier fix now that the schema itself no longer requires it, and keeps the res.error handling that fix added (matching every other mutation handler in this file - the query error was previously silently swallowed). Webhooks.tsx and EmailTemplates.tsx drop the `pagination:` wrapper key entirely, since _webhooks/_email_templates now take PaginationRequest directly as their whole params type instead of the removed PaginatedRequest wrapper. --- .../src/components/OrgDomains.test.tsx | 24 +++++++++++++++++++ web/dashboard/src/components/OrgDomains.tsx | 7 ++++++ web/dashboard/src/graphql/queries/index.ts | 4 ++-- web/dashboard/src/pages/Clients.tsx | 6 ++--- web/dashboard/src/pages/EmailTemplates.tsx | 6 ++--- .../src/pages/OrganizationDetail.tsx | 4 +--- web/dashboard/src/pages/Organizations.tsx | 6 ++--- web/dashboard/src/pages/TrustedIssuers.tsx | 6 ++--- web/dashboard/src/pages/Webhooks.tsx | 6 ++--- 9 files changed, 44 insertions(+), 25 deletions(-) diff --git a/web/dashboard/src/components/OrgDomains.test.tsx b/web/dashboard/src/components/OrgDomains.test.tsx index 95b1fbb2e..c9fd0ba8a 100644 --- a/web/dashboard/src/components/OrgDomains.test.tsx +++ b/web/dashboard/src/components/OrgDomains.test.tsx @@ -86,6 +86,30 @@ describe('OrgDomains', () => { expect(await screen.findByText('acme.com')).toBeTruthy(); }); + // REGRESSION: ListOrgDomainsRequest.pagination is PaginationRequest + // directly (internal/graph/schema.graphqls) - fetchDomains must send + // `pagination: { limit: 100 }` (single-nested), not wrapped in an extra + // `pagination:` layer. An earlier version of the schema had this field + // typed as a PaginatedRequest wrapper (requiring a double-nested call), + // which was itself a schema inconsistency later removed in favor of the + // single-level shape every other list endpoint already used. The mock + // client here doesn't validate variable shape against a real schema, so + // a wrong shape here would be invisible to every other test in this file + // - assert the exact shape directly instead. + it('requests domains with the pagination shape the schema requires', async () => { + render(); + await screen.findByText('No verified domains yet.'); + expect(mockClient.query).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + params: { + org_id: 'org1', + pagination: { limit: 100 }, + }, + }), + ); + }); + it('runs the DNS challenge flow: request → shows TXT → verify → verified', async () => { mockClient.mutation.mockImplementation((doc: unknown) => { if (doc === RequestOrgDomain) { diff --git a/web/dashboard/src/components/OrgDomains.tsx b/web/dashboard/src/components/OrgDomains.tsx index 16c64e228..2bc23dce2 100644 --- a/web/dashboard/src/components/OrgDomains.tsx +++ b/web/dashboard/src/components/OrgDomains.tsx @@ -113,6 +113,13 @@ const OrgDomains = ({ orgId, orgSlug }: OrgDomainsProps) => { params: { org_id: orgId, pagination: { limit: 100 } }, }) .toPromise(); + if (res.error) { + toast.error( + capitalizeFirstLetter( + getGraphQLErrorMessage(res.error, 'Failed to load verified domains'), + ), + ); + } setDomains(res.data?._org_domains?.org_domains || []); setDomainsLoading(false); }; diff --git a/web/dashboard/src/graphql/queries/index.ts b/web/dashboard/src/graphql/queries/index.ts index a2678f98e..4bd724d73 100644 --- a/web/dashboard/src/graphql/queries/index.ts +++ b/web/dashboard/src/graphql/queries/index.ts @@ -49,7 +49,7 @@ export const UserDetailsQuery = ` `; export const WebhooksDataQuery = ` - query getWebhooksData($params: PaginatedRequest!) { + query getWebhooksData($params: PaginationRequest!) { _webhooks(params: $params){ webhooks{ id @@ -70,7 +70,7 @@ export const WebhooksDataQuery = ` `; export const EmailTemplatesQuery = ` - query getEmailTemplates($params: PaginatedRequest!) { + query getEmailTemplates($params: PaginationRequest!) { _email_templates(params: $params) { email_templates { id diff --git a/web/dashboard/src/pages/Clients.tsx b/web/dashboard/src/pages/Clients.tsx index bc7faab36..a572ce293 100644 --- a/web/dashboard/src/pages/Clients.tsx +++ b/web/dashboard/src/pages/Clients.tsx @@ -73,10 +73,8 @@ const Clients = () => { .query(ClientsQuery, { params: { pagination: { - pagination: { - limit: paginationProps.limit, - page: paginationProps.page, - }, + limit: paginationProps.limit, + page: paginationProps.page, }, }, }) diff --git a/web/dashboard/src/pages/EmailTemplates.tsx b/web/dashboard/src/pages/EmailTemplates.tsx index dd5cf0f3d..4fa1f6ab6 100644 --- a/web/dashboard/src/pages/EmailTemplates.tsx +++ b/web/dashboard/src/pages/EmailTemplates.tsx @@ -73,10 +73,8 @@ const EmailTemplates = () => { const res = await client .query(EmailTemplatesQuery, { params: { - pagination: { - limit: paginationProps.limit, - page: paginationProps.page, - }, + limit: paginationProps.limit, + page: paginationProps.page, }, }) .toPromise(); diff --git a/web/dashboard/src/pages/OrganizationDetail.tsx b/web/dashboard/src/pages/OrganizationDetail.tsx index 344061ea7..ad231dab7 100644 --- a/web/dashboard/src/pages/OrganizationDetail.tsx +++ b/web/dashboard/src/pages/OrganizationDetail.tsx @@ -186,9 +186,7 @@ const OrganizationDetail = () => { }>(OrgMembersQuery, { params: { org_id: id, - pagination: { - pagination: { limit: MEMBERS_PAGE_LIMIT, page }, - }, + pagination: { limit: MEMBERS_PAGE_LIMIT, page }, }, }) .toPromise(); diff --git a/web/dashboard/src/pages/Organizations.tsx b/web/dashboard/src/pages/Organizations.tsx index dbd09779b..818207220 100644 --- a/web/dashboard/src/pages/Organizations.tsx +++ b/web/dashboard/src/pages/Organizations.tsx @@ -70,10 +70,8 @@ const Organizations = () => { .query(OrganizationsQuery, { params: { pagination: { - pagination: { - limit: paginationProps.limit, - page: paginationProps.page, - }, + limit: paginationProps.limit, + page: paginationProps.page, }, }, }) diff --git a/web/dashboard/src/pages/TrustedIssuers.tsx b/web/dashboard/src/pages/TrustedIssuers.tsx index 99a98e0f0..d63224d97 100644 --- a/web/dashboard/src/pages/TrustedIssuers.tsx +++ b/web/dashboard/src/pages/TrustedIssuers.tsx @@ -73,10 +73,8 @@ const TrustedIssuers = () => { .query(TrustedIssuersQuery, { params: { pagination: { - pagination: { - limit: paginationProps.limit, - page: paginationProps.page, - }, + limit: paginationProps.limit, + page: paginationProps.page, }, }, }) diff --git a/web/dashboard/src/pages/Webhooks.tsx b/web/dashboard/src/pages/Webhooks.tsx index cfd9645ce..52fb3b569 100644 --- a/web/dashboard/src/pages/Webhooks.tsx +++ b/web/dashboard/src/pages/Webhooks.tsx @@ -77,10 +77,8 @@ const Webhooks = () => { const res = await client .query(WebhooksDataQuery, { params: { - pagination: { - limit: paginationProps.limit, - page: paginationProps.page, - }, + limit: paginationProps.limit, + page: paginationProps.page, }, }) .toPromise();