From edea237e1427a5b86c44f57532eb180a9538558e Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 22:38:14 +0200 Subject: [PATCH 01/10] Generate API server from OpenAPI spec using ogen --- api/openapi.yaml | 11 + oas/oas_cfg_gen.go | 125 + oas/oas_client_gen.go | 180 +- oas/oas_handlers_gen.go | 2787 +++++++++++++++++++++++ oas/oas_middleware_gen.go | 10 + oas/oas_parameters_gen.go | 886 +++++++ oas/oas_request_decoders_gen.go | 426 ++++ oas/oas_response_encoders_gen.go | 272 +++ oas/oas_router_gen.go | 942 ++++++++ oas/oas_security_gen.go | 61 + oas/oas_server_gen.go | 184 ++ oas/oas_unimplemented_gen.go | 202 ++ ogen-config.yml | 1 - server/handlers/errors.go | 15 + server/handlers/handlers.go | 61 + server/handlers/repository.go | 313 ++- server/handlers/security.go | 101 + server/handlers/team.go | 476 ++-- server/handlers/token.go | 236 +- server/handlers/user.go | 292 +-- server/handlers/util.go | 17 - server/middleware/auth.go | 161 -- server/routes.go | 80 +- store/postgres/personal_access_token.go | 4 +- store/postgres/repository.go | 8 +- store/postgres/team.go | 8 +- store/postgres/user.go | 4 +- 27 files changed, 6777 insertions(+), 1086 deletions(-) create mode 100644 oas/oas_handlers_gen.go create mode 100644 oas/oas_middleware_gen.go create mode 100644 oas/oas_request_decoders_gen.go create mode 100644 oas/oas_response_encoders_gen.go create mode 100644 oas/oas_router_gen.go create mode 100644 oas/oas_server_gen.go create mode 100644 oas/oas_unimplemented_gen.go create mode 100644 server/handlers/errors.go create mode 100644 server/handlers/security.go delete mode 100644 server/handlers/util.go delete mode 100644 server/middleware/auth.go diff --git a/api/openapi.yaml b/api/openapi.yaml index 508238d..a8d836b 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -17,6 +17,7 @@ tags: - name: Users paths: /v1/repositories: + x-ogen-operation-group: Repository get: operationId: listRepositories summary: List repositories @@ -60,6 +61,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/repositories/{namespace}/{name}: + x-ogen-operation-group: Repository get: operationId: getRepository summary: Get repository @@ -113,6 +115,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/tokens: + x-ogen-operation-group: Token get: operationId: listPersonalAccessTokens summary: List personal access tokens @@ -159,6 +162,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/tokens/{id}: + x-ogen-operation-group: Token get: operationId: getPersonalAccessToken summary: Get personal access token @@ -204,6 +208,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/teams: + x-ogen-operation-group: Team get: operationId: listTeams summary: List teams @@ -247,6 +252,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/teams/{name}: + x-ogen-operation-group: Team get: operationId: getTeam summary: Get team @@ -290,6 +296,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/teams/{name}/members: + x-ogen-operation-group: Team get: operationId: listTeamMembers summary: List team members @@ -345,6 +352,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/teams/{name}/members/{username}: + x-ogen-operation-group: Team delete: operationId: removeTeamMember summary: Remove team member @@ -370,6 +378,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/users: + x-ogen-operation-group: User get: operationId: listUsers summary: List users @@ -413,6 +422,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/users/{username}: + x-ogen-operation-group: User get: operationId: getUser summary: Get user @@ -456,6 +466,7 @@ paths: schema: $ref: "#/components/schemas/Error" /v1/users/{username}/password: + x-ogen-operation-group: User post: operationId: changeUserPassword summary: Change password for user diff --git a/oas/oas_cfg_gen.go b/oas/oas_cfg_gen.go index 796ed64..d167279 100644 --- a/oas/oas_cfg_gen.go +++ b/oas/oas_cfg_gen.go @@ -6,12 +6,78 @@ import ( "net/http" ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" ) type ( optionFunc[C any] func(*C) ) +// ErrorHandler is error handler. +type ErrorHandler = ogenerrors.ErrorHandler + +type serverConfig struct { + NotFound http.HandlerFunc + MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string) + ErrorHandler ErrorHandler + Prefix string + Middleware Middleware + MaxMultipartMemory int64 +} + +// ServerOption is server config option. +type ServerOption interface { + applyServer(*serverConfig) +} + +var _ ServerOption = (optionFunc[serverConfig])(nil) + +func (o optionFunc[C]) applyServer(c *C) { + o(c) +} + +func newServerConfig(opts ...ServerOption) serverConfig { + cfg := serverConfig{ + NotFound: http.NotFound, + MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) { + status := http.StatusMethodNotAllowed + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", allowed) + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + status = http.StatusNoContent + } else { + w.Header().Set("Allow", allowed) + } + w.WriteHeader(status) + }, + ErrorHandler: ogenerrors.DefaultErrorHandler, + Middleware: nil, + MaxMultipartMemory: 32 << 20, // 32 MB + } + for _, opt := range opts { + opt.applyServer(&cfg) + } + return cfg +} + +type baseServer struct { + cfg serverConfig +} + +func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) { + s.cfg.NotFound(w, r) +} + +func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) { + s.cfg.MethodNotAllowed(w, r, allowed) +} + +func (cfg serverConfig) baseServer() (s baseServer, err error) { + s = baseServer{cfg: cfg} + return s, nil +} + type clientConfig struct { Client ht.Client } @@ -48,6 +114,7 @@ func (cfg clientConfig) baseClient() (c baseClient, err error) { // Option is config option. type Option interface { + ServerOption ClientOption } @@ -59,3 +126,61 @@ func WithClient(client ht.Client) ClientOption { } }) } + +// WithNotFound specifies Not Found handler to use. +func WithNotFound(notFound http.HandlerFunc) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if notFound != nil { + cfg.NotFound = notFound + } + }) +} + +// WithMethodNotAllowed specifies Method Not Allowed handler to use. +func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if methodNotAllowed != nil { + cfg.MethodNotAllowed = methodNotAllowed + } + }) +} + +// WithErrorHandler specifies error handler to use. +func WithErrorHandler(h ErrorHandler) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if h != nil { + cfg.ErrorHandler = h + } + }) +} + +// WithPathPrefix specifies server path prefix. +func WithPathPrefix(prefix string) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + cfg.Prefix = prefix + }) +} + +// WithMiddleware specifies middlewares to use. +func WithMiddleware(m ...Middleware) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + switch len(m) { + case 0: + cfg.Middleware = nil + case 1: + cfg.Middleware = m[0] + default: + cfg.Middleware = middleware.ChainMiddlewares(m...) + } + }) +} + +// WithMaxMultipartMemory specifies limit of memory for storing file parts. +// File parts which can't be stored in memory will be stored on disk in temporary files. +func WithMaxMultipartMemory(max int64) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if max > 0 { + cfg.MaxMultipartMemory = max + } + }) +} diff --git a/oas/oas_client_gen.go b/oas/oas_client_gen.go index bec9a36..4ad0993 100644 --- a/oas/oas_client_gen.go +++ b/oas/oas_client_gen.go @@ -22,126 +22,154 @@ func trimTrailingSlashes(u *url.URL) { // Invoker invokes operations described by OpenAPI v3 specification. type Invoker interface { - // AddTeamMember invokes addTeamMember operation. - // - // Add team member. - // - // POST /v1/teams/{name}/members - AddTeamMember(ctx context.Context, request *TeamMemberRequest, params AddTeamMemberParams) (*TeamMemberResponse, error) - // ChangeUserPassword invokes changeUserPassword operation. - // - // Change password for user. - // - // POST /v1/users/{username}/password - ChangeUserPassword(ctx context.Context, request *UserPasswordChangeRequest, params ChangeUserPasswordParams) error - // CreatePersonalAccessToken invokes createPersonalAccessToken operation. - // - // Create personal access token. - // - // POST /v1/tokens - CreatePersonalAccessToken(ctx context.Context, request *PersonalAccessTokenRequest) (*PersonalAccessTokenCreationResponse, error) + RepositoryInvoker + TeamInvoker + TokenInvoker + UserInvoker +} + +// RepositoryInvoker invokes operations described by OpenAPI v3 specification. +// +// x-gen-operation-group: Repository +type RepositoryInvoker interface { // CreateRepository invokes createRepository operation. // // Create repository. // // POST /v1/repositories CreateRepository(ctx context.Context, request *RepositoryRequest) (*RepositoryResponse, error) - // CreateTeam invokes createTeam operation. + // DeleteRepository invokes deleteRepository operation. // - // Create team. + // Delete repository. // - // POST /v1/teams - CreateTeam(ctx context.Context, request *TeamRequest) (*TeamResponse, error) - // CreateUser invokes createUser operation. + // DELETE /v1/repositories/{namespace}/{name} + DeleteRepository(ctx context.Context, params DeleteRepositoryParams) error + // GetRepository invokes getRepository operation. // - // Create user. + // Get repository. // - // POST /v1/users - CreateUser(ctx context.Context, request *UserRequest) (*UserResponse, error) - // DeletePersonalAccessToken invokes deletePersonalAccessToken operation. + // GET /v1/repositories/{namespace}/{name} + GetRepository(ctx context.Context, params GetRepositoryParams) (*RepositoryResponse, error) + // ListRepositories invokes listRepositories operation. // - // Delete personal access token. + // List repositories. // - // DELETE /v1/tokens/{id} - DeletePersonalAccessToken(ctx context.Context, params DeletePersonalAccessTokenParams) error - // DeleteRepository invokes deleteRepository operation. + // GET /v1/repositories + ListRepositories(ctx context.Context) ([]RepositoryResponse, error) +} + +// TeamInvoker invokes operations described by OpenAPI v3 specification. +// +// x-gen-operation-group: Team +type TeamInvoker interface { + // AddTeamMember invokes addTeamMember operation. // - // Delete repository. + // Add team member. // - // DELETE /v1/repositories/{namespace}/{name} - DeleteRepository(ctx context.Context, params DeleteRepositoryParams) error + // POST /v1/teams/{name}/members + AddTeamMember(ctx context.Context, request *TeamMemberRequest, params AddTeamMemberParams) (*TeamMemberResponse, error) + // CreateTeam invokes createTeam operation. + // + // Create team. + // + // POST /v1/teams + CreateTeam(ctx context.Context, request *TeamRequest) (*TeamResponse, error) // DeleteTeam invokes deleteTeam operation. // // Delete team. // // DELETE /v1/teams/{name} DeleteTeam(ctx context.Context, params DeleteTeamParams) error - // DeleteUser invokes deleteUser operation. + // GetTeam invokes getTeam operation. // - // Delete user. + // Get team. // - // DELETE /v1/users/{username} - DeleteUser(ctx context.Context, params DeleteUserParams) error - // GetPersonalAccessToken invokes getPersonalAccessToken operation. + // GET /v1/teams/{name} + GetTeam(ctx context.Context, params GetTeamParams) (*TeamResponse, error) + // ListTeamMembers invokes listTeamMembers operation. // - // Get personal access token. + // List team members. // - // GET /v1/tokens/{id} - GetPersonalAccessToken(ctx context.Context, params GetPersonalAccessTokenParams) (*PersonalAccessTokenResponse, error) - // GetRepository invokes getRepository operation. + // GET /v1/teams/{name}/members + ListTeamMembers(ctx context.Context, params ListTeamMembersParams) ([]TeamMemberResponse, error) + // ListTeams invokes listTeams operation. // - // Get repository. + // List teams. // - // GET /v1/repositories/{namespace}/{name} - GetRepository(ctx context.Context, params GetRepositoryParams) (*RepositoryResponse, error) - // GetTeam invokes getTeam operation. + // GET /v1/teams + ListTeams(ctx context.Context) ([]TeamResponse, error) + // RemoveTeamMember invokes removeTeamMember operation. // - // Get team. + // Remove team member. // - // GET /v1/teams/{name} - GetTeam(ctx context.Context, params GetTeamParams) (*TeamResponse, error) - // GetUser invokes getUser operation. + // DELETE /v1/teams/{name}/members/{username} + RemoveTeamMember(ctx context.Context, params RemoveTeamMemberParams) error +} + +// TokenInvoker invokes operations described by OpenAPI v3 specification. +// +// x-gen-operation-group: Token +type TokenInvoker interface { + // CreatePersonalAccessToken invokes createPersonalAccessToken operation. // - // Get user. + // Create personal access token. // - // GET /v1/users/{username} - GetUser(ctx context.Context, params GetUserParams) (*UserResponse, error) + // POST /v1/tokens + CreatePersonalAccessToken(ctx context.Context, request *PersonalAccessTokenRequest) (*PersonalAccessTokenCreationResponse, error) + // DeletePersonalAccessToken invokes deletePersonalAccessToken operation. + // + // Delete personal access token. + // + // DELETE /v1/tokens/{id} + DeletePersonalAccessToken(ctx context.Context, params DeletePersonalAccessTokenParams) error + // GetPersonalAccessToken invokes getPersonalAccessToken operation. + // + // Get personal access token. + // + // GET /v1/tokens/{id} + GetPersonalAccessToken(ctx context.Context, params GetPersonalAccessTokenParams) (*PersonalAccessTokenResponse, error) // ListPersonalAccessTokens invokes listPersonalAccessTokens operation. // // List personal access tokens. // // GET /v1/tokens ListPersonalAccessTokens(ctx context.Context) ([]PersonalAccessTokenResponse, error) - // ListRepositories invokes listRepositories operation. +} + +// UserInvoker invokes operations described by OpenAPI v3 specification. +// +// x-gen-operation-group: User +type UserInvoker interface { + // ChangeUserPassword invokes changeUserPassword operation. // - // List repositories. + // Change password for user. // - // GET /v1/repositories - ListRepositories(ctx context.Context) ([]RepositoryResponse, error) - // ListTeamMembers invokes listTeamMembers operation. + // POST /v1/users/{username}/password + ChangeUserPassword(ctx context.Context, request *UserPasswordChangeRequest, params ChangeUserPasswordParams) error + // CreateUser invokes createUser operation. // - // List team members. + // Create user. // - // GET /v1/teams/{name}/members - ListTeamMembers(ctx context.Context, params ListTeamMembersParams) ([]TeamMemberResponse, error) - // ListTeams invokes listTeams operation. + // POST /v1/users + CreateUser(ctx context.Context, request *UserRequest) (*UserResponse, error) + // DeleteUser invokes deleteUser operation. // - // List teams. + // Delete user. // - // GET /v1/teams - ListTeams(ctx context.Context) ([]TeamResponse, error) + // DELETE /v1/users/{username} + DeleteUser(ctx context.Context, params DeleteUserParams) error + // GetUser invokes getUser operation. + // + // Get user. + // + // GET /v1/users/{username} + GetUser(ctx context.Context, params GetUserParams) (*UserResponse, error) // ListUsers invokes listUsers operation. // // List users. // // GET /v1/users ListUsers(ctx context.Context) ([]UserResponse, error) - // RemoveTeamMember invokes removeTeamMember operation. - // - // Remove team member. - // - // DELETE /v1/teams/{name}/members/{username} - RemoveTeamMember(ctx context.Context, params RemoveTeamMemberParams) error } // Client implements OAS client. @@ -150,6 +178,14 @@ type Client struct { sec SecuritySource baseClient } +type errorHandler interface { + NewError(ctx context.Context, err error) *ErrorStatusCode +} + +var _ Handler = struct { + errorHandler + *Client +}{} // NewClient initializes new Client defined by OAS. func NewClient(serverURL string, sec SecuritySource, opts ...ClientOption) (*Client, error) { diff --git a/oas/oas_handlers_gen.go b/oas/oas_handlers_gen.go new file mode 100644 index 0000000..ef006f0 --- /dev/null +++ b/oas/oas_handlers_gen.go @@ -0,0 +1,2787 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "context" + "net/http" + + "github.com/go-faster/errors" + + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" +) + +type codeRecorder struct { + http.ResponseWriter + status int +} + +func (c *codeRecorder) WriteHeader(status int) { + c.status = status + c.ResponseWriter.WriteHeader(status) +} + +func recordError(string, error) {} + +// handleAddTeamMemberRequest handles addTeamMember operation. +// +// Add team member. +// +// POST /v1/teams/{name}/members +func (s *Server) handleAddTeamMemberRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: AddTeamMemberOperation, + ID: "addTeamMember", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, AddTeamMemberOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeAddTeamMemberParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + request, close, err := s.decodeAddTeamMemberRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *TeamMemberResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: AddTeamMemberOperation, + OperationSummary: "Add team member", + OperationID: "addTeamMember", + Body: request, + Params: middleware.Parameters{ + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = *TeamMemberRequest + Params = AddTeamMemberParams + Response = *TeamMemberResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackAddTeamMemberParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.AddTeamMember(ctx, request, params) + return response, err + }, + ) + } else { + response, err = s.h.AddTeamMember(ctx, request, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeAddTeamMemberResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleChangeUserPasswordRequest handles changeUserPassword operation. +// +// Change password for user. +// +// POST /v1/users/{username}/password +func (s *Server) handleChangeUserPasswordRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ChangeUserPasswordOperation, + ID: "changeUserPassword", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ChangeUserPasswordOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeChangeUserPasswordParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + request, close, err := s.decodeChangeUserPasswordRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *ChangeUserPasswordNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ChangeUserPasswordOperation, + OperationSummary: "Change password for user", + OperationID: "changeUserPassword", + Body: request, + Params: middleware.Parameters{ + { + Name: "username", + In: "path", + }: params.Username, + }, + Raw: r, + } + + type ( + Request = *UserPasswordChangeRequest + Params = ChangeUserPasswordParams + Response = *ChangeUserPasswordNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackChangeUserPasswordParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ChangeUserPassword(ctx, request, params) + return response, err + }, + ) + } else { + err = s.h.ChangeUserPassword(ctx, request, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeChangeUserPasswordResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleCreatePersonalAccessTokenRequest handles createPersonalAccessToken operation. +// +// Create personal access token. +// +// POST /v1/tokens +func (s *Server) handleCreatePersonalAccessTokenRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: CreatePersonalAccessTokenOperation, + ID: "createPersonalAccessToken", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, CreatePersonalAccessTokenOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + { + sctx, ok, err := s.securityUsernamePassword(ctx, CreatePersonalAccessTokenOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "UsernamePassword", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:UsernamePassword", err) + } + return + } + if ok { + satisfied[0] |= 1 << 1 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + {0b00000010}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + request, close, err := s.decodeCreatePersonalAccessTokenRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *PersonalAccessTokenCreationResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: CreatePersonalAccessTokenOperation, + OperationSummary: "Create personal access token", + OperationID: "createPersonalAccessToken", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *PersonalAccessTokenRequest + Params = struct{} + Response = *PersonalAccessTokenCreationResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.CreatePersonalAccessToken(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.CreatePersonalAccessToken(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeCreatePersonalAccessTokenResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleCreateRepositoryRequest handles createRepository operation. +// +// Create repository. +// +// POST /v1/repositories +func (s *Server) handleCreateRepositoryRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: CreateRepositoryOperation, + ID: "createRepository", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, CreateRepositoryOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + request, close, err := s.decodeCreateRepositoryRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *RepositoryResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: CreateRepositoryOperation, + OperationSummary: "Create repository", + OperationID: "createRepository", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *RepositoryRequest + Params = struct{} + Response = *RepositoryResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.CreateRepository(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.CreateRepository(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeCreateRepositoryResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleCreateTeamRequest handles createTeam operation. +// +// Create team. +// +// POST /v1/teams +func (s *Server) handleCreateTeamRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: CreateTeamOperation, + ID: "createTeam", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, CreateTeamOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + request, close, err := s.decodeCreateTeamRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *TeamResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: CreateTeamOperation, + OperationSummary: "Create team", + OperationID: "createTeam", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *TeamRequest + Params = struct{} + Response = *TeamResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.CreateTeam(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.CreateTeam(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeCreateTeamResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleCreateUserRequest handles createUser operation. +// +// Create user. +// +// POST /v1/users +func (s *Server) handleCreateUserRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: CreateUserOperation, + ID: "createUser", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, CreateUserOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + request, close, err := s.decodeCreateUserRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *UserResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: CreateUserOperation, + OperationSummary: "Create user", + OperationID: "createUser", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *UserRequest + Params = struct{} + Response = *UserResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.CreateUser(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.CreateUser(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeCreateUserResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleDeletePersonalAccessTokenRequest handles deletePersonalAccessToken operation. +// +// Delete personal access token. +// +// DELETE /v1/tokens/{id} +func (s *Server) handleDeletePersonalAccessTokenRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: DeletePersonalAccessTokenOperation, + ID: "deletePersonalAccessToken", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, DeletePersonalAccessTokenOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeDeletePersonalAccessTokenParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *DeletePersonalAccessTokenNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: DeletePersonalAccessTokenOperation, + OperationSummary: "Delete personal access token", + OperationID: "deletePersonalAccessToken", + Body: nil, + Params: middleware.Parameters{ + { + Name: "id", + In: "path", + }: params.ID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = DeletePersonalAccessTokenParams + Response = *DeletePersonalAccessTokenNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackDeletePersonalAccessTokenParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.DeletePersonalAccessToken(ctx, params) + return response, err + }, + ) + } else { + err = s.h.DeletePersonalAccessToken(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeDeletePersonalAccessTokenResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleDeleteRepositoryRequest handles deleteRepository operation. +// +// Delete repository. +// +// DELETE /v1/repositories/{namespace}/{name} +func (s *Server) handleDeleteRepositoryRequest(args [2]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: DeleteRepositoryOperation, + ID: "deleteRepository", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, DeleteRepositoryOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeDeleteRepositoryParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *DeleteRepositoryNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: DeleteRepositoryOperation, + OperationSummary: "Delete repository", + OperationID: "deleteRepository", + Body: nil, + Params: middleware.Parameters{ + { + Name: "namespace", + In: "path", + }: params.Namespace, + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = DeleteRepositoryParams + Response = *DeleteRepositoryNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackDeleteRepositoryParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.DeleteRepository(ctx, params) + return response, err + }, + ) + } else { + err = s.h.DeleteRepository(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeDeleteRepositoryResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleDeleteTeamRequest handles deleteTeam operation. +// +// Delete team. +// +// DELETE /v1/teams/{name} +func (s *Server) handleDeleteTeamRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: DeleteTeamOperation, + ID: "deleteTeam", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, DeleteTeamOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeDeleteTeamParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *DeleteTeamNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: DeleteTeamOperation, + OperationSummary: "Delete team", + OperationID: "deleteTeam", + Body: nil, + Params: middleware.Parameters{ + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = DeleteTeamParams + Response = *DeleteTeamNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackDeleteTeamParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.DeleteTeam(ctx, params) + return response, err + }, + ) + } else { + err = s.h.DeleteTeam(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeDeleteTeamResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleDeleteUserRequest handles deleteUser operation. +// +// Delete user. +// +// DELETE /v1/users/{username} +func (s *Server) handleDeleteUserRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: DeleteUserOperation, + ID: "deleteUser", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, DeleteUserOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeDeleteUserParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *DeleteUserNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: DeleteUserOperation, + OperationSummary: "Delete user", + OperationID: "deleteUser", + Body: nil, + Params: middleware.Parameters{ + { + Name: "username", + In: "path", + }: params.Username, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = DeleteUserParams + Response = *DeleteUserNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackDeleteUserParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.DeleteUser(ctx, params) + return response, err + }, + ) + } else { + err = s.h.DeleteUser(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeDeleteUserResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetPersonalAccessTokenRequest handles getPersonalAccessToken operation. +// +// Get personal access token. +// +// GET /v1/tokens/{id} +func (s *Server) handleGetPersonalAccessTokenRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetPersonalAccessTokenOperation, + ID: "getPersonalAccessToken", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, GetPersonalAccessTokenOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeGetPersonalAccessTokenParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *PersonalAccessTokenResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetPersonalAccessTokenOperation, + OperationSummary: "Get personal access token", + OperationID: "getPersonalAccessToken", + Body: nil, + Params: middleware.Parameters{ + { + Name: "id", + In: "path", + }: params.ID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetPersonalAccessTokenParams + Response = *PersonalAccessTokenResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetPersonalAccessTokenParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetPersonalAccessToken(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetPersonalAccessToken(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetPersonalAccessTokenResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetRepositoryRequest handles getRepository operation. +// +// Get repository. +// +// GET /v1/repositories/{namespace}/{name} +func (s *Server) handleGetRepositoryRequest(args [2]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetRepositoryOperation, + ID: "getRepository", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, GetRepositoryOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeGetRepositoryParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *RepositoryResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetRepositoryOperation, + OperationSummary: "Get repository", + OperationID: "getRepository", + Body: nil, + Params: middleware.Parameters{ + { + Name: "namespace", + In: "path", + }: params.Namespace, + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetRepositoryParams + Response = *RepositoryResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetRepositoryParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetRepository(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetRepository(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetRepositoryResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetTeamRequest handles getTeam operation. +// +// Get team. +// +// GET /v1/teams/{name} +func (s *Server) handleGetTeamRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetTeamOperation, + ID: "getTeam", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, GetTeamOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeGetTeamParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *TeamResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetTeamOperation, + OperationSummary: "Get team", + OperationID: "getTeam", + Body: nil, + Params: middleware.Parameters{ + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetTeamParams + Response = *TeamResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetTeamParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetTeam(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetTeam(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetTeamResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetUserRequest handles getUser operation. +// +// Get user. +// +// GET /v1/users/{username} +func (s *Server) handleGetUserRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetUserOperation, + ID: "getUser", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, GetUserOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeGetUserParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *UserResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetUserOperation, + OperationSummary: "Get user", + OperationID: "getUser", + Body: nil, + Params: middleware.Parameters{ + { + Name: "username", + In: "path", + }: params.Username, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetUserParams + Response = *UserResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetUserParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetUser(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetUser(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetUserResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListPersonalAccessTokensRequest handles listPersonalAccessTokens operation. +// +// List personal access tokens. +// +// GET /v1/tokens +func (s *Server) handleListPersonalAccessTokensRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListPersonalAccessTokensOperation, + ID: "listPersonalAccessTokens", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ListPersonalAccessTokensOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + + var response []PersonalAccessTokenResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListPersonalAccessTokensOperation, + OperationSummary: "List personal access tokens", + OperationID: "listPersonalAccessTokens", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = []PersonalAccessTokenResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListPersonalAccessTokens(ctx) + return response, err + }, + ) + } else { + response, err = s.h.ListPersonalAccessTokens(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListPersonalAccessTokensResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListRepositoriesRequest handles listRepositories operation. +// +// List repositories. +// +// GET /v1/repositories +func (s *Server) handleListRepositoriesRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListRepositoriesOperation, + ID: "listRepositories", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ListRepositoriesOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + + var response []RepositoryResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListRepositoriesOperation, + OperationSummary: "List repositories", + OperationID: "listRepositories", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = []RepositoryResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListRepositories(ctx) + return response, err + }, + ) + } else { + response, err = s.h.ListRepositories(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListRepositoriesResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListTeamMembersRequest handles listTeamMembers operation. +// +// List team members. +// +// GET /v1/teams/{name}/members +func (s *Server) handleListTeamMembersRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListTeamMembersOperation, + ID: "listTeamMembers", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ListTeamMembersOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeListTeamMembersParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response []TeamMemberResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListTeamMembersOperation, + OperationSummary: "List team members", + OperationID: "listTeamMembers", + Body: nil, + Params: middleware.Parameters{ + { + Name: "name", + In: "path", + }: params.Name, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ListTeamMembersParams + Response = []TeamMemberResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackListTeamMembersParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListTeamMembers(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.ListTeamMembers(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListTeamMembersResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListTeamsRequest handles listTeams operation. +// +// List teams. +// +// GET /v1/teams +func (s *Server) handleListTeamsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListTeamsOperation, + ID: "listTeams", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ListTeamsOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + + var response []TeamResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListTeamsOperation, + OperationSummary: "List teams", + OperationID: "listTeams", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = []TeamResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListTeams(ctx) + return response, err + }, + ) + } else { + response, err = s.h.ListTeams(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListTeamsResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListUsersRequest handles listUsers operation. +// +// List users. +// +// GET /v1/users +func (s *Server) handleListUsersRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListUsersOperation, + ID: "listUsers", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, ListUsersOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + + var response []UserResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListUsersOperation, + OperationSummary: "List users", + OperationID: "listUsers", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = []UserResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListUsers(ctx) + return response, err + }, + ) + } else { + response, err = s.h.ListUsers(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListUsersResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleRemoveTeamMemberRequest handles removeTeamMember operation. +// +// Remove team member. +// +// DELETE /v1/teams/{name}/members/{username} +func (s *Server) handleRemoveTeamMemberRequest(args [2]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + ctx := r.Context() + + var ( + err error + opErrContext = ogenerrors.OperationContext{ + Name: RemoveTeamMemberOperation, + ID: "removeTeamMember", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityPersonalAccessToken(ctx, RemoveTeamMemberOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "PersonalAccessToken", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security:PersonalAccessToken", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeRemoveTeamMemberParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *RemoveTeamMemberNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: RemoveTeamMemberOperation, + OperationSummary: "Remove team member", + OperationID: "removeTeamMember", + Body: nil, + Params: middleware.Parameters{ + { + Name: "name", + In: "path", + }: params.Name, + { + Name: "username", + In: "path", + }: params.Username, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = RemoveTeamMemberParams + Response = *RemoveTeamMemberNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackRemoveTeamMemberParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.RemoveTeamMember(ctx, params) + return response, err + }, + ) + } else { + err = s.h.RemoveTeamMember(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeRemoveTeamMemberResponse(response, w); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} diff --git a/oas/oas_middleware_gen.go b/oas/oas_middleware_gen.go new file mode 100644 index 0000000..7ba299f --- /dev/null +++ b/oas/oas_middleware_gen.go @@ -0,0 +1,10 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "github.com/ogen-go/ogen/middleware" +) + +// Middleware is middleware type. +type Middleware = middleware.Middleware diff --git a/oas/oas_parameters_gen.go b/oas/oas_parameters_gen.go index 2f1eb39..bd98ec2 100644 --- a/oas/oas_parameters_gen.go +++ b/oas/oas_parameters_gen.go @@ -3,7 +3,17 @@ package oas import ( + "net/http" + "net/url" + + "github.com/go-faster/errors" "github.com/google/uuid" + + "github.com/ogen-go/ogen/conv" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/uri" + "github.com/ogen-go/ogen/validate" ) // AddTeamMemberParams is parameters of addTeamMember operation. @@ -11,60 +21,936 @@ type AddTeamMemberParams struct { Name string } +func unpackAddTeamMemberParams(packed middleware.Parameters) (params AddTeamMemberParams) { + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeAddTeamMemberParams(args [1]string, argsEscaped bool, r *http.Request) (params AddTeamMemberParams, _ error) { + // Decode path: name. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // ChangeUserPasswordParams is parameters of changeUserPassword operation. type ChangeUserPasswordParams struct { Username string } +func unpackChangeUserPasswordParams(packed middleware.Parameters) (params ChangeUserPasswordParams) { + { + key := middleware.ParameterKey{ + Name: "username", + In: "path", + } + params.Username = packed[key].(string) + } + return params +} + +func decodeChangeUserPasswordParams(args [1]string, argsEscaped bool, r *http.Request) (params ChangeUserPasswordParams, _ error) { + // Decode path: username. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "username", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Username = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "username", + In: "path", + Err: err, + } + } + return params, nil +} + // DeletePersonalAccessTokenParams is parameters of deletePersonalAccessToken operation. type DeletePersonalAccessTokenParams struct { ID uuid.UUID } +func unpackDeletePersonalAccessTokenParams(packed middleware.Parameters) (params DeletePersonalAccessTokenParams) { + { + key := middleware.ParameterKey{ + Name: "id", + In: "path", + } + params.ID = packed[key].(uuid.UUID) + } + return params +} + +func decodeDeletePersonalAccessTokenParams(args [1]string, argsEscaped bool, r *http.Request) (params DeletePersonalAccessTokenParams, _ error) { + // Decode path: id. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "id", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUUID(val) + if err != nil { + return err + } + + params.ID = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "id", + In: "path", + Err: err, + } + } + return params, nil +} + // DeleteRepositoryParams is parameters of deleteRepository operation. type DeleteRepositoryParams struct { Namespace string Name string } +func unpackDeleteRepositoryParams(packed middleware.Parameters) (params DeleteRepositoryParams) { + { + key := middleware.ParameterKey{ + Name: "namespace", + In: "path", + } + params.Namespace = packed[key].(string) + } + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeDeleteRepositoryParams(args [2]string, argsEscaped bool, r *http.Request) (params DeleteRepositoryParams, _ error) { + // Decode path: namespace. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "namespace", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Namespace = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "namespace", + In: "path", + Err: err, + } + } + // Decode path: name. + if err := func() error { + param := args[1] + if argsEscaped { + unescaped, err := url.PathUnescape(args[1]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // DeleteTeamParams is parameters of deleteTeam operation. type DeleteTeamParams struct { Name string } +func unpackDeleteTeamParams(packed middleware.Parameters) (params DeleteTeamParams) { + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeDeleteTeamParams(args [1]string, argsEscaped bool, r *http.Request) (params DeleteTeamParams, _ error) { + // Decode path: name. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // DeleteUserParams is parameters of deleteUser operation. type DeleteUserParams struct { Username string } +func unpackDeleteUserParams(packed middleware.Parameters) (params DeleteUserParams) { + { + key := middleware.ParameterKey{ + Name: "username", + In: "path", + } + params.Username = packed[key].(string) + } + return params +} + +func decodeDeleteUserParams(args [1]string, argsEscaped bool, r *http.Request) (params DeleteUserParams, _ error) { + // Decode path: username. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "username", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Username = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "username", + In: "path", + Err: err, + } + } + return params, nil +} + // GetPersonalAccessTokenParams is parameters of getPersonalAccessToken operation. type GetPersonalAccessTokenParams struct { ID uuid.UUID } +func unpackGetPersonalAccessTokenParams(packed middleware.Parameters) (params GetPersonalAccessTokenParams) { + { + key := middleware.ParameterKey{ + Name: "id", + In: "path", + } + params.ID = packed[key].(uuid.UUID) + } + return params +} + +func decodeGetPersonalAccessTokenParams(args [1]string, argsEscaped bool, r *http.Request) (params GetPersonalAccessTokenParams, _ error) { + // Decode path: id. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "id", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUUID(val) + if err != nil { + return err + } + + params.ID = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "id", + In: "path", + Err: err, + } + } + return params, nil +} + // GetRepositoryParams is parameters of getRepository operation. type GetRepositoryParams struct { Namespace string Name string } +func unpackGetRepositoryParams(packed middleware.Parameters) (params GetRepositoryParams) { + { + key := middleware.ParameterKey{ + Name: "namespace", + In: "path", + } + params.Namespace = packed[key].(string) + } + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeGetRepositoryParams(args [2]string, argsEscaped bool, r *http.Request) (params GetRepositoryParams, _ error) { + // Decode path: namespace. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "namespace", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Namespace = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "namespace", + In: "path", + Err: err, + } + } + // Decode path: name. + if err := func() error { + param := args[1] + if argsEscaped { + unescaped, err := url.PathUnescape(args[1]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // GetTeamParams is parameters of getTeam operation. type GetTeamParams struct { Name string } +func unpackGetTeamParams(packed middleware.Parameters) (params GetTeamParams) { + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeGetTeamParams(args [1]string, argsEscaped bool, r *http.Request) (params GetTeamParams, _ error) { + // Decode path: name. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // GetUserParams is parameters of getUser operation. type GetUserParams struct { Username string } +func unpackGetUserParams(packed middleware.Parameters) (params GetUserParams) { + { + key := middleware.ParameterKey{ + Name: "username", + In: "path", + } + params.Username = packed[key].(string) + } + return params +} + +func decodeGetUserParams(args [1]string, argsEscaped bool, r *http.Request) (params GetUserParams, _ error) { + // Decode path: username. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "username", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Username = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "username", + In: "path", + Err: err, + } + } + return params, nil +} + // ListTeamMembersParams is parameters of listTeamMembers operation. type ListTeamMembersParams struct { Name string } +func unpackListTeamMembersParams(packed middleware.Parameters) (params ListTeamMembersParams) { + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + return params +} + +func decodeListTeamMembersParams(args [1]string, argsEscaped bool, r *http.Request) (params ListTeamMembersParams, _ error) { + // Decode path: name. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + return params, nil +} + // RemoveTeamMemberParams is parameters of removeTeamMember operation. type RemoveTeamMemberParams struct { Name string Username string } + +func unpackRemoveTeamMemberParams(packed middleware.Parameters) (params RemoveTeamMemberParams) { + { + key := middleware.ParameterKey{ + Name: "name", + In: "path", + } + params.Name = packed[key].(string) + } + { + key := middleware.ParameterKey{ + Name: "username", + In: "path", + } + params.Username = packed[key].(string) + } + return params +} + +func decodeRemoveTeamMemberParams(args [2]string, argsEscaped bool, r *http.Request) (params RemoveTeamMemberParams, _ error) { + // Decode path: name. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "name", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Name = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "name", + In: "path", + Err: err, + } + } + // Decode path: username. + if err := func() error { + param := args[1] + if argsEscaped { + unescaped, err := url.PathUnescape(args[1]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "username", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.Username = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "username", + In: "path", + Err: err, + } + } + return params, nil +} diff --git a/oas/oas_request_decoders_gen.go b/oas/oas_request_decoders_gen.go new file mode 100644 index 0000000..f8aa9fd --- /dev/null +++ b/oas/oas_request_decoders_gen.go @@ -0,0 +1,426 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "io" + "mime" + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "go.uber.org/multierr" + + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/validate" +) + +func (s *Server) decodeAddTeamMemberRequest(r *http.Request) ( + req *TeamMemberRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request TeamMemberRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeChangeUserPasswordRequest(r *http.Request) ( + req *UserPasswordChangeRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request UserPasswordChangeRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeCreatePersonalAccessTokenRequest(r *http.Request) ( + req *PersonalAccessTokenRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request PersonalAccessTokenRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeCreateRepositoryRequest(r *http.Request) ( + req *RepositoryRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request RepositoryRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeCreateTeamRequest(r *http.Request) ( + req *TeamRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request TeamRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeCreateUserRequest(r *http.Request) ( + req *UserRequest, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + + var request UserRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } + return &request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} diff --git a/oas/oas_response_encoders_gen.go b/oas/oas_response_encoders_gen.go new file mode 100644 index 0000000..1a25669 --- /dev/null +++ b/oas/oas_response_encoders_gen.go @@ -0,0 +1,272 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + + ht "github.com/ogen-go/ogen/http" +) + +func encodeAddTeamMemberResponse(response *TeamMemberResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeChangeUserPasswordResponse(response *ChangeUserPasswordNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeCreatePersonalAccessTokenResponse(response *PersonalAccessTokenCreationResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeCreateRepositoryResponse(response *RepositoryResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeCreateTeamResponse(response *TeamResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeCreateUserResponse(response *UserResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeDeletePersonalAccessTokenResponse(response *DeletePersonalAccessTokenNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeDeleteRepositoryResponse(response *DeleteRepositoryNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeDeleteTeamResponse(response *DeleteTeamNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeDeleteUserResponse(response *DeleteUserNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeGetPersonalAccessTokenResponse(response *PersonalAccessTokenResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeGetRepositoryResponse(response *RepositoryResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeGetTeamResponse(response *TeamResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeGetUserResponse(response *UserResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeListPersonalAccessTokensResponse(response []PersonalAccessTokenResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + e.ArrStart() + for _, elem := range response { + elem.Encode(e) + } + e.ArrEnd() + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeListRepositoriesResponse(response []RepositoryResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + e.ArrStart() + for _, elem := range response { + elem.Encode(e) + } + e.ArrEnd() + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeListTeamMembersResponse(response []TeamMemberResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + e.ArrStart() + for _, elem := range response { + elem.Encode(e) + } + e.ArrEnd() + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeListTeamsResponse(response []TeamResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + e.ArrStart() + for _, elem := range response { + elem.Encode(e) + } + e.ArrEnd() + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeListUsersResponse(response []UserResponse, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + + e := new(jx.Encoder) + e.ArrStart() + for _, elem := range response { + elem.Encode(e) + } + e.ArrEnd() + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeRemoveTeamMemberResponse(response *RemoveTeamMemberNoContent, w http.ResponseWriter) error { + w.WriteHeader(204) + + return nil +} + +func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + code := response.StatusCode + if code == 0 { + // Set default status code. + code = http.StatusOK + } + w.WriteHeader(code) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + if code >= http.StatusInternalServerError { + return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code)) + } + return nil + +} diff --git a/oas/oas_router_gen.go b/oas/oas_router_gen.go new file mode 100644 index 0000000..2876c62 --- /dev/null +++ b/oas/oas_router_gen.go @@ -0,0 +1,942 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "net/http" + "net/url" + "strings" + + "github.com/ogen-go/ogen/uri" +) + +func (s *Server) cutPrefix(path string) (string, bool) { + prefix := s.cfg.Prefix + if prefix == "" { + return path, true + } + if !strings.HasPrefix(path, prefix) { + // Prefix doesn't match. + return "", false + } + // Cut prefix from the path. + return strings.TrimPrefix(path, prefix), true +} + +// ServeHTTP serves http request as defined by OpenAPI v3 specification, +// calling handler that matches the path or returning not found error. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + elem := r.URL.Path + elemIsEscaped := false + if rawPath := r.URL.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + elemIsEscaped = strings.ContainsRune(elem, '%') + } + } + + elem, ok := s.cutPrefix(elem) + if !ok || len(elem) == 0 { + s.notFound(w, r) + return + } + args := [2]string{} + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/v1/" + + if l := len("/v1/"); len(elem) >= l && elem[0:l] == "/v1/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'r': // Prefix: "repositories" + + if l := len("repositories"); len(elem) >= l && elem[0:l] == "repositories" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleListRepositoriesRequest([0]string{}, elemIsEscaped, w, r) + case "POST": + s.handleCreateRepositoryRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "namespace" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "name" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[1] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "DELETE": + s.handleDeleteRepositoryRequest([2]string{ + args[0], + args[1], + }, elemIsEscaped, w, r) + case "GET": + s.handleGetRepositoryRequest([2]string{ + args[0], + args[1], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "DELETE,GET") + } + + return + } + + } + + } + + case 't': // Prefix: "t" + + if l := len("t"); len(elem) >= l && elem[0:l] == "t" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "eams" + + if l := len("eams"); len(elem) >= l && elem[0:l] == "eams" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleListTeamsRequest([0]string{}, elemIsEscaped, w, r) + case "POST": + s.handleCreateTeamRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "name" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch r.Method { + case "DELETE": + s.handleDeleteTeamRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + case "GET": + s.handleGetTeamRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "DELETE,GET") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/members" + + if l := len("/members"); len(elem) >= l && elem[0:l] == "/members" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleListTeamMembersRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + case "POST": + s.handleAddTeamMemberRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "username" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[1] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "DELETE": + s.handleRemoveTeamMemberRequest([2]string{ + args[0], + args[1], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "DELETE") + } + + return + } + + } + + } + + } + + case 'o': // Prefix: "okens" + + if l := len("okens"); len(elem) >= l && elem[0:l] == "okens" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleListPersonalAccessTokensRequest([0]string{}, elemIsEscaped, w, r) + case "POST": + s.handleCreatePersonalAccessTokenRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "DELETE": + s.handleDeletePersonalAccessTokenRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + case "GET": + s.handleGetPersonalAccessTokenRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "DELETE,GET") + } + + return + } + + } + + } + + case 'u': // Prefix: "users" + + if l := len("users"); len(elem) >= l && elem[0:l] == "users" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleListUsersRequest([0]string{}, elemIsEscaped, w, r) + case "POST": + s.handleCreateUserRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET,POST") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "username" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch r.Method { + case "DELETE": + s.handleDeleteUserRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + case "GET": + s.handleGetUserRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "DELETE,GET") + } + + return + } + switch elem[0] { + case '/': // Prefix: "/password" + + if l := len("/password"); len(elem) >= l && elem[0:l] == "/password" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleChangeUserPasswordRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + + } + + } + + } + + } + } + s.notFound(w, r) +} + +// Route is route object. +type Route struct { + name string + summary string + operationID string + pathPattern string + count int + args [2]string +} + +// Name returns ogen operation name. +// +// It is guaranteed to be unique and not empty. +func (r Route) Name() string { + return r.name +} + +// Summary returns OpenAPI summary. +func (r Route) Summary() string { + return r.summary +} + +// OperationID returns OpenAPI operationId. +func (r Route) OperationID() string { + return r.operationID +} + +// PathPattern returns OpenAPI path. +func (r Route) PathPattern() string { + return r.pathPattern +} + +// Args returns parsed arguments. +func (r Route) Args() []string { + return r.args[:r.count] +} + +// FindRoute finds Route for given method and path. +// +// Note: this method does not unescape path or handle reserved characters in path properly. Use FindPath instead. +func (s *Server) FindRoute(method, path string) (Route, bool) { + return s.FindPath(method, &url.URL{Path: path}) +} + +// FindPath finds Route for given method and URL. +func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { + var ( + elem = u.Path + args = r.args + ) + if rawPath := u.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + } + defer func() { + for i, arg := range r.args[:r.count] { + if unescaped, err := url.PathUnescape(arg); err == nil { + r.args[i] = unescaped + } + } + }() + } + + elem, ok := s.cutPrefix(elem) + if !ok { + return r, false + } + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/v1/" + + if l := len("/v1/"); len(elem) >= l && elem[0:l] == "/v1/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'r': // Prefix: "repositories" + + if l := len("repositories"); len(elem) >= l && elem[0:l] == "repositories" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = ListRepositoriesOperation + r.summary = "List repositories" + r.operationID = "listRepositories" + r.pathPattern = "/v1/repositories" + r.args = args + r.count = 0 + return r, true + case "POST": + r.name = CreateRepositoryOperation + r.summary = "Create repository" + r.operationID = "createRepository" + r.pathPattern = "/v1/repositories" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "namespace" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "name" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[1] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch method { + case "DELETE": + r.name = DeleteRepositoryOperation + r.summary = "Delete repository" + r.operationID = "deleteRepository" + r.pathPattern = "/v1/repositories/{namespace}/{name}" + r.args = args + r.count = 2 + return r, true + case "GET": + r.name = GetRepositoryOperation + r.summary = "Get repository" + r.operationID = "getRepository" + r.pathPattern = "/v1/repositories/{namespace}/{name}" + r.args = args + r.count = 2 + return r, true + default: + return + } + } + + } + + } + + case 't': // Prefix: "t" + + if l := len("t"); len(elem) >= l && elem[0:l] == "t" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "eams" + + if l := len("eams"); len(elem) >= l && elem[0:l] == "eams" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = ListTeamsOperation + r.summary = "List teams" + r.operationID = "listTeams" + r.pathPattern = "/v1/teams" + r.args = args + r.count = 0 + return r, true + case "POST": + r.name = CreateTeamOperation + r.summary = "Create team" + r.operationID = "createTeam" + r.pathPattern = "/v1/teams" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "name" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch method { + case "DELETE": + r.name = DeleteTeamOperation + r.summary = "Delete team" + r.operationID = "deleteTeam" + r.pathPattern = "/v1/teams/{name}" + r.args = args + r.count = 1 + return r, true + case "GET": + r.name = GetTeamOperation + r.summary = "Get team" + r.operationID = "getTeam" + r.pathPattern = "/v1/teams/{name}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/members" + + if l := len("/members"); len(elem) >= l && elem[0:l] == "/members" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = ListTeamMembersOperation + r.summary = "List team members" + r.operationID = "listTeamMembers" + r.pathPattern = "/v1/teams/{name}/members" + r.args = args + r.count = 1 + return r, true + case "POST": + r.name = AddTeamMemberOperation + r.summary = "Add team member" + r.operationID = "addTeamMember" + r.pathPattern = "/v1/teams/{name}/members" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "username" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[1] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch method { + case "DELETE": + r.name = RemoveTeamMemberOperation + r.summary = "Remove team member" + r.operationID = "removeTeamMember" + r.pathPattern = "/v1/teams/{name}/members/{username}" + r.args = args + r.count = 2 + return r, true + default: + return + } + } + + } + + } + + } + + case 'o': // Prefix: "okens" + + if l := len("okens"); len(elem) >= l && elem[0:l] == "okens" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = ListPersonalAccessTokensOperation + r.summary = "List personal access tokens" + r.operationID = "listPersonalAccessTokens" + r.pathPattern = "/v1/tokens" + r.args = args + r.count = 0 + return r, true + case "POST": + r.name = CreatePersonalAccessTokenOperation + r.summary = "Create personal access token" + r.operationID = "createPersonalAccessToken" + r.pathPattern = "/v1/tokens" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch method { + case "DELETE": + r.name = DeletePersonalAccessTokenOperation + r.summary = "Delete personal access token" + r.operationID = "deletePersonalAccessToken" + r.pathPattern = "/v1/tokens/{id}" + r.args = args + r.count = 1 + return r, true + case "GET": + r.name = GetPersonalAccessTokenOperation + r.summary = "Get personal access token" + r.operationID = "getPersonalAccessToken" + r.pathPattern = "/v1/tokens/{id}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + } + + } + + case 'u': // Prefix: "users" + + if l := len("users"); len(elem) >= l && elem[0:l] == "users" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + r.name = ListUsersOperation + r.summary = "List users" + r.operationID = "listUsers" + r.pathPattern = "/v1/users" + r.args = args + r.count = 0 + return r, true + case "POST": + r.name = CreateUserOperation + r.summary = "Create user" + r.operationID = "createUser" + r.pathPattern = "/v1/users" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "username" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch method { + case "DELETE": + r.name = DeleteUserOperation + r.summary = "Delete user" + r.operationID = "deleteUser" + r.pathPattern = "/v1/users/{username}" + r.args = args + r.count = 1 + return r, true + case "GET": + r.name = GetUserOperation + r.summary = "Get user" + r.operationID = "getUser" + r.pathPattern = "/v1/users/{username}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/password" + + if l := len("/password"); len(elem) >= l && elem[0:l] == "/password" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ChangeUserPasswordOperation + r.summary = "Change password for user" + r.operationID = "changeUserPassword" + r.pathPattern = "/v1/users/{username}/password" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + } + + } + + } + + } + } + return r, false +} diff --git a/oas/oas_security_gen.go b/oas/oas_security_gen.go index 20e2fd2..85c52de 100644 --- a/oas/oas_security_gen.go +++ b/oas/oas_security_gen.go @@ -5,10 +5,71 @@ package oas import ( "context" "net/http" + "strings" "github.com/go-faster/errors" + + "github.com/ogen-go/ogen/ogenerrors" ) +// SecurityHandler is handler for security parameters. +type SecurityHandler interface { + // HandlePersonalAccessToken handles personalAccessToken security. + HandlePersonalAccessToken(ctx context.Context, operationName OperationName, t PersonalAccessToken) (context.Context, error) + // HandleUsernamePassword handles usernamePassword security. + HandleUsernamePassword(ctx context.Context, operationName OperationName, t UsernamePassword) (context.Context, error) +} + +func findAuthorization(h http.Header, prefix string) (string, bool) { + v, ok := h["Authorization"] + if !ok { + return "", false + } + for _, vv := range v { + scheme, value, ok := strings.Cut(vv, " ") + if !ok || !strings.EqualFold(scheme, prefix) { + continue + } + return value, true + } + return "", false +} + +func (s *Server) securityPersonalAccessToken(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) { + var t PersonalAccessToken + token, ok := findAuthorization(req.Header, "Bearer") + if !ok { + return ctx, false, nil + } + t.Token = token + rctx, err := s.sec.HandlePersonalAccessToken(ctx, operationName, t) + if errors.Is(err, ogenerrors.ErrSkipServerSecurity) { + return nil, false, nil + } else if err != nil { + return nil, false, err + } + return rctx, true, err +} +func (s *Server) securityUsernamePassword(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) { + var t UsernamePassword + if _, ok := findAuthorization(req.Header, "Basic"); !ok { + return ctx, false, nil + } + username, password, ok := req.BasicAuth() + if !ok { + return nil, false, errors.New("invalid basic auth") + } + t.Username = username + t.Password = password + rctx, err := s.sec.HandleUsernamePassword(ctx, operationName, t) + if errors.Is(err, ogenerrors.ErrSkipServerSecurity) { + return nil, false, nil + } else if err != nil { + return nil, false, err + } + return rctx, true, err +} + // SecuritySource is provider of security values (tokens, passwords, etc.). type SecuritySource interface { // PersonalAccessToken provides personalAccessToken security value. diff --git a/oas/oas_server_gen.go b/oas/oas_server_gen.go new file mode 100644 index 0000000..06fc637 --- /dev/null +++ b/oas/oas_server_gen.go @@ -0,0 +1,184 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "context" +) + +// Handler handles operations described by OpenAPI v3 specification. +type Handler interface { + RepositoryHandler + TeamHandler + TokenHandler + UserHandler + // NewError creates *ErrorStatusCode from error returned by handler. + // + // Used for common default response. + NewError(ctx context.Context, err error) *ErrorStatusCode +} + +// RepositoryHandler handles operations described by OpenAPI v3 specification. +// +// x-ogen-operation-group: Repository +type RepositoryHandler interface { + // CreateRepository implements createRepository operation. + // + // Create repository. + // + // POST /v1/repositories + CreateRepository(ctx context.Context, req *RepositoryRequest) (*RepositoryResponse, error) + // DeleteRepository implements deleteRepository operation. + // + // Delete repository. + // + // DELETE /v1/repositories/{namespace}/{name} + DeleteRepository(ctx context.Context, params DeleteRepositoryParams) error + // GetRepository implements getRepository operation. + // + // Get repository. + // + // GET /v1/repositories/{namespace}/{name} + GetRepository(ctx context.Context, params GetRepositoryParams) (*RepositoryResponse, error) + // ListRepositories implements listRepositories operation. + // + // List repositories. + // + // GET /v1/repositories + ListRepositories(ctx context.Context) ([]RepositoryResponse, error) +} + +// TeamHandler handles operations described by OpenAPI v3 specification. +// +// x-ogen-operation-group: Team +type TeamHandler interface { + // AddTeamMember implements addTeamMember operation. + // + // Add team member. + // + // POST /v1/teams/{name}/members + AddTeamMember(ctx context.Context, req *TeamMemberRequest, params AddTeamMemberParams) (*TeamMemberResponse, error) + // CreateTeam implements createTeam operation. + // + // Create team. + // + // POST /v1/teams + CreateTeam(ctx context.Context, req *TeamRequest) (*TeamResponse, error) + // DeleteTeam implements deleteTeam operation. + // + // Delete team. + // + // DELETE /v1/teams/{name} + DeleteTeam(ctx context.Context, params DeleteTeamParams) error + // GetTeam implements getTeam operation. + // + // Get team. + // + // GET /v1/teams/{name} + GetTeam(ctx context.Context, params GetTeamParams) (*TeamResponse, error) + // ListTeamMembers implements listTeamMembers operation. + // + // List team members. + // + // GET /v1/teams/{name}/members + ListTeamMembers(ctx context.Context, params ListTeamMembersParams) ([]TeamMemberResponse, error) + // ListTeams implements listTeams operation. + // + // List teams. + // + // GET /v1/teams + ListTeams(ctx context.Context) ([]TeamResponse, error) + // RemoveTeamMember implements removeTeamMember operation. + // + // Remove team member. + // + // DELETE /v1/teams/{name}/members/{username} + RemoveTeamMember(ctx context.Context, params RemoveTeamMemberParams) error +} + +// TokenHandler handles operations described by OpenAPI v3 specification. +// +// x-ogen-operation-group: Token +type TokenHandler interface { + // CreatePersonalAccessToken implements createPersonalAccessToken operation. + // + // Create personal access token. + // + // POST /v1/tokens + CreatePersonalAccessToken(ctx context.Context, req *PersonalAccessTokenRequest) (*PersonalAccessTokenCreationResponse, error) + // DeletePersonalAccessToken implements deletePersonalAccessToken operation. + // + // Delete personal access token. + // + // DELETE /v1/tokens/{id} + DeletePersonalAccessToken(ctx context.Context, params DeletePersonalAccessTokenParams) error + // GetPersonalAccessToken implements getPersonalAccessToken operation. + // + // Get personal access token. + // + // GET /v1/tokens/{id} + GetPersonalAccessToken(ctx context.Context, params GetPersonalAccessTokenParams) (*PersonalAccessTokenResponse, error) + // ListPersonalAccessTokens implements listPersonalAccessTokens operation. + // + // List personal access tokens. + // + // GET /v1/tokens + ListPersonalAccessTokens(ctx context.Context) ([]PersonalAccessTokenResponse, error) +} + +// UserHandler handles operations described by OpenAPI v3 specification. +// +// x-ogen-operation-group: User +type UserHandler interface { + // ChangeUserPassword implements changeUserPassword operation. + // + // Change password for user. + // + // POST /v1/users/{username}/password + ChangeUserPassword(ctx context.Context, req *UserPasswordChangeRequest, params ChangeUserPasswordParams) error + // CreateUser implements createUser operation. + // + // Create user. + // + // POST /v1/users + CreateUser(ctx context.Context, req *UserRequest) (*UserResponse, error) + // DeleteUser implements deleteUser operation. + // + // Delete user. + // + // DELETE /v1/users/{username} + DeleteUser(ctx context.Context, params DeleteUserParams) error + // GetUser implements getUser operation. + // + // Get user. + // + // GET /v1/users/{username} + GetUser(ctx context.Context, params GetUserParams) (*UserResponse, error) + // ListUsers implements listUsers operation. + // + // List users. + // + // GET /v1/users + ListUsers(ctx context.Context) ([]UserResponse, error) +} + +// Server implements http server based on OpenAPI v3 specification and +// calls Handler to handle requests. +type Server struct { + h Handler + sec SecurityHandler + baseServer +} + +// NewServer creates new Server. +func NewServer(h Handler, sec SecurityHandler, opts ...ServerOption) (*Server, error) { + s, err := newServerConfig(opts...).baseServer() + if err != nil { + return nil, err + } + return &Server{ + h: h, + sec: sec, + baseServer: s, + }, nil +} diff --git a/oas/oas_unimplemented_gen.go b/oas/oas_unimplemented_gen.go new file mode 100644 index 0000000..ed55cbd --- /dev/null +++ b/oas/oas_unimplemented_gen.go @@ -0,0 +1,202 @@ +// Code generated by ogen, DO NOT EDIT. + +package oas + +import ( + "context" + + ht "github.com/ogen-go/ogen/http" +) + +// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented. +type UnimplementedHandler struct{} + +var _ Handler = UnimplementedHandler{} + +// AddTeamMember implements addTeamMember operation. +// +// Add team member. +// +// POST /v1/teams/{name}/members +func (UnimplementedHandler) AddTeamMember(ctx context.Context, req *TeamMemberRequest, params AddTeamMemberParams) (r *TeamMemberResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ChangeUserPassword implements changeUserPassword operation. +// +// Change password for user. +// +// POST /v1/users/{username}/password +func (UnimplementedHandler) ChangeUserPassword(ctx context.Context, req *UserPasswordChangeRequest, params ChangeUserPasswordParams) error { + return ht.ErrNotImplemented +} + +// CreatePersonalAccessToken implements createPersonalAccessToken operation. +// +// Create personal access token. +// +// POST /v1/tokens +func (UnimplementedHandler) CreatePersonalAccessToken(ctx context.Context, req *PersonalAccessTokenRequest) (r *PersonalAccessTokenCreationResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// CreateRepository implements createRepository operation. +// +// Create repository. +// +// POST /v1/repositories +func (UnimplementedHandler) CreateRepository(ctx context.Context, req *RepositoryRequest) (r *RepositoryResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// CreateTeam implements createTeam operation. +// +// Create team. +// +// POST /v1/teams +func (UnimplementedHandler) CreateTeam(ctx context.Context, req *TeamRequest) (r *TeamResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// CreateUser implements createUser operation. +// +// Create user. +// +// POST /v1/users +func (UnimplementedHandler) CreateUser(ctx context.Context, req *UserRequest) (r *UserResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// DeletePersonalAccessToken implements deletePersonalAccessToken operation. +// +// Delete personal access token. +// +// DELETE /v1/tokens/{id} +func (UnimplementedHandler) DeletePersonalAccessToken(ctx context.Context, params DeletePersonalAccessTokenParams) error { + return ht.ErrNotImplemented +} + +// DeleteRepository implements deleteRepository operation. +// +// Delete repository. +// +// DELETE /v1/repositories/{namespace}/{name} +func (UnimplementedHandler) DeleteRepository(ctx context.Context, params DeleteRepositoryParams) error { + return ht.ErrNotImplemented +} + +// DeleteTeam implements deleteTeam operation. +// +// Delete team. +// +// DELETE /v1/teams/{name} +func (UnimplementedHandler) DeleteTeam(ctx context.Context, params DeleteTeamParams) error { + return ht.ErrNotImplemented +} + +// DeleteUser implements deleteUser operation. +// +// Delete user. +// +// DELETE /v1/users/{username} +func (UnimplementedHandler) DeleteUser(ctx context.Context, params DeleteUserParams) error { + return ht.ErrNotImplemented +} + +// GetPersonalAccessToken implements getPersonalAccessToken operation. +// +// Get personal access token. +// +// GET /v1/tokens/{id} +func (UnimplementedHandler) GetPersonalAccessToken(ctx context.Context, params GetPersonalAccessTokenParams) (r *PersonalAccessTokenResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// GetRepository implements getRepository operation. +// +// Get repository. +// +// GET /v1/repositories/{namespace}/{name} +func (UnimplementedHandler) GetRepository(ctx context.Context, params GetRepositoryParams) (r *RepositoryResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// GetTeam implements getTeam operation. +// +// Get team. +// +// GET /v1/teams/{name} +func (UnimplementedHandler) GetTeam(ctx context.Context, params GetTeamParams) (r *TeamResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// GetUser implements getUser operation. +// +// Get user. +// +// GET /v1/users/{username} +func (UnimplementedHandler) GetUser(ctx context.Context, params GetUserParams) (r *UserResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ListPersonalAccessTokens implements listPersonalAccessTokens operation. +// +// List personal access tokens. +// +// GET /v1/tokens +func (UnimplementedHandler) ListPersonalAccessTokens(ctx context.Context) (r []PersonalAccessTokenResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ListRepositories implements listRepositories operation. +// +// List repositories. +// +// GET /v1/repositories +func (UnimplementedHandler) ListRepositories(ctx context.Context) (r []RepositoryResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ListTeamMembers implements listTeamMembers operation. +// +// List team members. +// +// GET /v1/teams/{name}/members +func (UnimplementedHandler) ListTeamMembers(ctx context.Context, params ListTeamMembersParams) (r []TeamMemberResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ListTeams implements listTeams operation. +// +// List teams. +// +// GET /v1/teams +func (UnimplementedHandler) ListTeams(ctx context.Context) (r []TeamResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// ListUsers implements listUsers operation. +// +// List users. +// +// GET /v1/users +func (UnimplementedHandler) ListUsers(ctx context.Context) (r []UserResponse, _ error) { + return r, ht.ErrNotImplemented +} + +// RemoveTeamMember implements removeTeamMember operation. +// +// Remove team member. +// +// DELETE /v1/teams/{name}/members/{username} +func (UnimplementedHandler) RemoveTeamMember(ctx context.Context, params RemoveTeamMemberParams) error { + return ht.ErrNotImplemented +} + +// NewError creates *ErrorStatusCode from error returned by handler. +// +// Used for common default response. +func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) { + r = new(ErrorStatusCode) + return r +} diff --git a/ogen-config.yml b/ogen-config.yml index c75b26c..8b62d93 100644 --- a/ogen-config.yml +++ b/ogen-config.yml @@ -1,5 +1,4 @@ generator: features: disable: - - 'paths/server' - 'ogen/otel' diff --git a/server/handlers/errors.go b/server/handlers/errors.go new file mode 100644 index 0000000..ce35ddb --- /dev/null +++ b/server/handlers/errors.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "github.com/evanebb/regauth/oas" + "net/http" +) + +// newErrorResponse is a small wrapper function to create an oas.ErrorStatusCode with less boilerplate. +func newErrorResponse(statusCode int, message string) *oas.ErrorStatusCode { + return &oas.ErrorStatusCode{StatusCode: statusCode, Response: oas.Error{Message: message}} +} + +func newInternalServerErrorResponse() *oas.ErrorStatusCode { + return newErrorResponse(http.StatusInternalServerError, "internal server error") +} diff --git a/server/handlers/handlers.go b/server/handlers/handlers.go index 2d70f21..50a189d 100644 --- a/server/handlers/handlers.go +++ b/server/handlers/handlers.go @@ -1,10 +1,71 @@ package handlers import ( + "context" + "errors" + "github.com/evanebb/regauth/auth/local" + "github.com/evanebb/regauth/oas" + "github.com/evanebb/regauth/repository" "github.com/evanebb/regauth/server/response" + "github.com/evanebb/regauth/token" + "github.com/evanebb/regauth/user" + "github.com/ogen-go/ogen/ogenerrors" + "log/slog" "net/http" ) func NotFound(w http.ResponseWriter, r *http.Request) { response.WriteJSONError(w, http.StatusNotFound, "requested endpoint does not exist, please refer to the API documentation") } + +type Handler struct { + logger *slog.Logger + RepositoryHandler + TeamHandler + TokenHandler + UserHandler +} + +func NewHandler( + logger *slog.Logger, + repoStore repository.Store, + userStore user.Store, + teamStore user.TeamStore, + tokenStore token.Store, + credentialsStore local.UserCredentialsStore, +) Handler { + return Handler{ + logger: logger, + RepositoryHandler: RepositoryHandler{ + logger: logger, + repoStore: repoStore, + teamStore: teamStore, + }, + TeamHandler: TeamHandler{ + logger: logger, + teamStore: teamStore, + userStore: userStore, + }, + TokenHandler: TokenHandler{ + logger: logger, + tokenStore: tokenStore, + }, + UserHandler: UserHandler{ + logger: logger, + userStore: userStore, + credentialsStore: credentialsStore, + }, + } +} + +func (h Handler) NewError(ctx context.Context, err error) *oas.ErrorStatusCode { + switch { + case errors.Is(err, ogenerrors.ErrSecurityRequirementIsNotSatisfied): + // no credentials given + return newErrorResponse(http.StatusUnauthorized, "authentication failed") + } + + // log the error and return a generic internal server error by default, to avoid potentially leaking sensitive info + h.logger.ErrorContext(ctx, "unhandled error occurred: "+err.Error(), slog.Any("error", err)) + return newErrorResponse(http.StatusInternalServerError, "internal server error") +} diff --git a/server/handlers/repository.go b/server/handlers/repository.go index 1c2e6e1..ec788b0 100644 --- a/server/handlers/repository.go +++ b/server/handlers/repository.go @@ -2,227 +2,176 @@ package handlers import ( "context" - "encoding/json" "errors" + "fmt" + "github.com/evanebb/regauth/oas" "github.com/evanebb/regauth/repository" - "github.com/evanebb/regauth/server/middleware" - "github.com/evanebb/regauth/server/response" "github.com/evanebb/regauth/user" - "github.com/evanebb/regauth/util" - "github.com/go-chi/chi/v5" "github.com/google/uuid" "log/slog" "net/http" + "slices" + "time" ) -type repositoryCtxKey struct{} - -func RepositoryParser(l *slog.Logger, repoStore repository.Store, teamStore user.TeamStore) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - namespace, name := chi.URLParam(r, "namespace"), chi.URLParam(r, "name") - if namespace == "" || name == "" { - response.WriteJSONError(w, http.StatusBadRequest, "no repository namespace or name given") - return - } - - repo, err := repoStore.GetByNamespaceAndName(r.Context(), namespace, name) - if err != nil { - if errors.Is(err, repository.ErrNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "repository not found") - return - } - - l.ErrorContext(r.Context(), "could not get repository", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - teams, err := teamStore.GetAllByUser(r.Context(), u.ID) - if err != nil { - l.ErrorContext(r.Context(), "failed to get teams for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - authorized := false - for _, team := range teams { - if repo.Namespace == string(team.Name) { - authorized = true - } - } - - if repo.Namespace == string(u.Username) { - authorized = true - } - - if !authorized { - response.WriteJSONError(w, http.StatusForbidden, "not authorized for given namespace") - return - } - - ctx := withRepository(r.Context(), repo) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) - } +type RepositoryHandler struct { + logger *slog.Logger + repoStore repository.Store + teamStore user.TeamStore + oas.UnimplementedHandler } -// withRepository sets the given repository.Repository in the context. -// Use repositoryFromContext to retrieve the repository. -func withRepository(ctx context.Context, r repository.Repository) context.Context { - return context.WithValue(ctx, repositoryCtxKey{}, r) -} +func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.RepositoryRequest) (*oas.RepositoryResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -// repositoryFromContext parses the current repository.Repository from the given request context. -// This requires the repository to have been previously set in the context, for example by the RepositoryParser middleware. -func repositoryFromContext(ctx context.Context) (repository.Repository, bool) { - val, ok := ctx.Value(repositoryCtxKey{}).(repository.Repository) - return val, ok -} + authorizedNamespaces, err := h.getUserNamespaces(ctx, u) + if err != nil { + h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -func CreateRepository(l *slog.Logger, repoStore repository.Store, teamStore user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authenticated failed") - return - } + if !slices.Contains(authorizedNamespaces, req.Namespace) { + return nil, newErrorResponse(http.StatusForbidden, "not authorized for given namespace") + } - var repo repository.Repository - if err := json.NewDecoder(r.Body).Decode(&repo); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return - } + id, err := uuid.NewV7() + if err != nil { + h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - teams, err := teamStore.GetAllByUser(r.Context(), u.ID) - if err != nil { - l.ErrorContext(r.Context(), "failed to get teams for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + repo := repository.Repository{ + ID: id, + Namespace: req.Namespace, + Name: repository.Name(req.Name), + Visibility: repository.Visibility(req.Visibility), + CreatedAt: time.Now(), + } - authorized := false - for _, team := range teams { - if repo.Namespace == string(team.Name) { - authorized = true - } - } + if err := repo.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) + } - if repo.Namespace == string(u.Username) { - authorized = true - } + if err := h.repoStore.Create(ctx, repo); err != nil { + h.logger.ErrorContext(ctx, "could not create repository", "error", err) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - if !authorized { - response.WriteJSONError(w, http.StatusForbidden, "not authorized to create repository in given namespace") - return - } + resp := mapRepositoryResponse(repo) + return &resp, nil +} - _, err = repoStore.GetByNamespaceAndName(r.Context(), repo.Namespace, string(repo.Name)) - if err == nil { - response.WriteJSONError(w, http.StatusBadRequest, "repository already exists") - return - } +func (h RepositoryHandler) ListRepositories(ctx context.Context) ([]oas.RepositoryResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - if !errors.Is(err, repository.ErrNotFound) { - l.Error("could not get repository", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + namespaces, err := h.getUserNamespaces(ctx, u) + if err != nil { + h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - repo.ID, err = uuid.NewV7() - if err != nil { - l.Error("could not generate UUID", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + repos, err := h.repoStore.GetAllByNamespace(ctx, namespaces...) + if err != nil { + h.logger.ErrorContext(ctx, "failed to get repositories for user", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - if err := repo.IsValid(); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) - return - } + resp := make([]oas.RepositoryResponse, len(repos)) + for i := 0; i < len(repos); i++ { + resp[i] = mapRepositoryResponse(repos[i]) + } - if err := repoStore.Create(r.Context(), repo); err != nil { - l.Error("could not create repository", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + return resp, nil +} - response.WriteJSONResponse(w, http.StatusOK, repo) +func (h RepositoryHandler) GetRepository(ctx context.Context, params oas.GetRepositoryParams) (*oas.RepositoryResponse, error) { + repo, err := h.getRepositoryFromRequest(ctx, params.Namespace, params.Name) + if err != nil { + return nil, err } + + resp := mapRepositoryResponse(repo) + return &resp, nil } -func ListRepositories(l *slog.Logger, repoStore repository.Store, teamStore user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } +func (h RepositoryHandler) DeleteRepository(ctx context.Context, params oas.DeleteRepositoryParams) error { + repo, err := h.getRepositoryFromRequest(ctx, params.Namespace, params.Name) + if err != nil { + return err + } - // gather all the authorized namespaces for the user, and retrieve repositories in those namespaces - teams, err := teamStore.GetAllByUser(r.Context(), u.ID) - if err != nil { - l.ErrorContext(r.Context(), "failed to get teams for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if err := h.repoStore.DeleteByID(ctx, repo.ID); err != nil { + h.logger.ErrorContext(ctx, "could not delete repository", slog.Any("error", err)) + return newErrorResponse(http.StatusInternalServerError, "internal server error") + } - namespaces := make([]string, 0, len(teams)+1) + return nil +} - namespaces = append(namespaces, string(u.Username)) - for _, team := range teams { - namespaces = append(namespaces, string(team.Name)) - } +func (h RepositoryHandler) getUserNamespaces(ctx context.Context, u user.User) ([]string, error) { + teams, err := h.teamStore.GetAllByUser(ctx, u.ID) + if err != nil { + return nil, fmt.Errorf("failed to get teams for user: %w", err) + } - repos, err := repoStore.GetAllByNamespace(r.Context(), namespaces...) - if err != nil { - l.ErrorContext(r.Context(), "could not get repositories for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + nsCount := len(teams) + 1 + namespaces := make([]string, nsCount) - response.WriteJSONResponse(w, http.StatusOK, util.NilSliceToEmpty(repos)) + namespaces[0] = string(u.Username) + for i := 1; i < nsCount; i++ { + namespaces[i] = string(teams[i-1].Name) } + + return namespaces, nil } -func GetRepository(l *slog.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - repo, ok := repositoryFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse repository from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h RepositoryHandler) getRepositoryFromRequest(ctx context.Context, namespace, name string) (repository.Repository, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - response.WriteJSONResponse(w, http.StatusOK, repo) + authorizedNamespaces, err := h.getUserNamespaces(ctx, u) + if err != nil { + h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) + return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") } -} -func DeleteRepository(l *slog.Logger, s repository.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - repo, ok := repositoryFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse repository from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if !slices.Contains(authorizedNamespaces, namespace) { + return repository.Repository{}, newErrorResponse(http.StatusForbidden, "not authorized for given namespace") + } - if err := s.DeleteByID(r.Context(), repo.ID); err != nil { - l.ErrorContext(r.Context(), "could not delete repository", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + repo, err := h.repoStore.GetByNamespaceAndName(ctx, namespace, name) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return repository.Repository{}, newErrorResponse(http.StatusNotFound, "repository not found") } - w.WriteHeader(http.StatusNoContent) + h.logger.ErrorContext(ctx, "failed to get repository", + slog.Any("error", err), + slog.String("namespace", namespace), + slog.String("name", name)) + return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + return repo, nil +} + +func mapRepositoryResponse(r repository.Repository) oas.RepositoryResponse { + return oas.RepositoryResponse{ + ID: r.ID, + Namespace: r.Namespace, + Name: string(r.Name), + Visibility: oas.RepositoryResponseVisibility(r.Visibility), + CreatedAt: r.CreatedAt, } } diff --git a/server/handlers/security.go b/server/handlers/security.go new file mode 100644 index 0000000..2d7cb3c --- /dev/null +++ b/server/handlers/security.go @@ -0,0 +1,101 @@ +package handlers + +import ( + "context" + "errors" + "github.com/evanebb/regauth/auth/local" + "github.com/evanebb/regauth/oas" + "github.com/evanebb/regauth/token" + "github.com/evanebb/regauth/user" + "log/slog" + "net/http" +) + +type SecurityHandler struct { + logger *slog.Logger + tokenStore token.Store + userStore user.Store + credentialsStore local.UserCredentialsStore +} + +func NewSecurityHandler( + logger *slog.Logger, + tokenStore token.Store, + userStore user.Store, + credentialsStore local.UserCredentialsStore, +) SecurityHandler { + return SecurityHandler{ + logger: logger, + tokenStore: tokenStore, + userStore: userStore, + credentialsStore: credentialsStore, + } +} + +func (s SecurityHandler) HandlePersonalAccessToken(ctx context.Context, operationName oas.OperationName, t oas.PersonalAccessToken) (context.Context, error) { + tok, err := s.tokenStore.GetByPlainTextToken(ctx, t.GetToken()) + if err != nil { + if errors.Is(err, token.ErrNotFound) { + s.logger.DebugContext(ctx, "token does not exist") + return ctx, newErrorResponse(http.StatusUnauthorized, "authentication failed") + } + + s.logger.ErrorContext(ctx, "could not get personal access token", slog.Any("error", err)) + return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + u, err := s.userStore.GetByID(ctx, tok.UserID) + if err != nil { + return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + s.logger.DebugContext(ctx, "token authentication successful") + return WithAuthenticatedUser(ctx, u), nil +} + +func (s SecurityHandler) HandleUsernamePassword(ctx context.Context, operationName oas.OperationName, t oas.UsernamePassword) (context.Context, error) { + u, err := s.userStore.GetByUsername(ctx, t.GetUsername()) + if err != nil { + if errors.Is(err, user.ErrNotFound) { + s.logger.DebugContext(ctx, "user not found", slog.String("username", t.GetUsername())) + return ctx, newErrorResponse(http.StatusUnauthorized, "authentication failed") + } + + s.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) + return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + credentials, err := s.credentialsStore.GetByUserID(ctx, u.ID) + if err != nil { + if errors.Is(err, user.ErrNotFound) { + s.logger.DebugContext(ctx, "no credentials set for user", slog.String("username", t.GetUsername())) + return ctx, newErrorResponse(http.StatusUnauthorized, "authentication failed") + } + + s.logger.ErrorContext(ctx, "could not get credentials", slog.Any("error", err)) + return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + if err := credentials.CheckPassword(t.GetPassword()); err != nil { + s.logger.DebugContext(ctx, "password does not match", slog.String("username", t.GetUsername())) + return ctx, newErrorResponse(http.StatusUnauthorized, "authentication failed") + } + + s.logger.DebugContext(ctx, "token authentication successful") + return WithAuthenticatedUser(ctx, u), nil +} + +type authenticatedUserCtxKey struct{} + +// WithAuthenticatedUser sets the authenticated user.User in the context. +// Use AuthenticatedUserFromContext to retrieve the user. +func WithAuthenticatedUser(ctx context.Context, u user.User) context.Context { + return context.WithValue(ctx, authenticatedUserCtxKey{}, u) +} + +// AuthenticatedUserFromContext parses the authenticated user.User from the given request context. +// This requires the user to have been previously set in the context, for example by the TokenAuthentication middleware. +func AuthenticatedUserFromContext(ctx context.Context) (user.User, bool) { + val, ok := ctx.Value(authenticatedUserCtxKey{}).(user.User) + return val, ok +} diff --git a/server/handlers/team.go b/server/handlers/team.go index cf6d9d8..5bd1a4c 100644 --- a/server/handlers/team.go +++ b/server/handlers/team.go @@ -2,340 +2,278 @@ package handlers import ( "context" - "encoding/json" "errors" - "github.com/evanebb/regauth/server/middleware" - "github.com/evanebb/regauth/server/response" + "github.com/evanebb/regauth/oas" "github.com/evanebb/regauth/user" - "github.com/evanebb/regauth/util" - "github.com/go-chi/chi/v5" "github.com/google/uuid" "log/slog" "net/http" + "time" ) -type teamCtxKey struct{} -type teamMemberCtxKey struct{} - -func TeamParser(l *slog.Logger, s user.TeamStore) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - name := chi.URLParam(r, "name") - if name == "" { - response.WriteJSONError(w, http.StatusBadRequest, "no team name given") - return - } - - team, err := s.GetByName(r.Context(), name) - if err != nil { - if errors.Is(err, user.ErrTeamNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "team not found") - return - } - - l.ErrorContext(r.Context(), "could not get team", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - teamMember, err := s.GetTeamMember(r.Context(), team.ID, u.ID) - if err != nil { - if errors.Is(err, user.ErrTeamMemberNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "team not found") - return - } - - l.ErrorContext(r.Context(), "could not get team member", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - ctx := withTeam(r.Context(), team) - ctx = withTeamMember(ctx, teamMember) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) - } +type TeamHandler struct { + logger *slog.Logger + teamStore user.TeamStore + userStore user.Store } -// withTeam sets the given team.Team in the context. -// Use teamFromContext to retrieve the team. -func withTeam(ctx context.Context, t user.Team) context.Context { - return context.WithValue(ctx, teamCtxKey{}, t) -} +func (h TeamHandler) CreateTeam(ctx context.Context, req *oas.TeamRequest) (*oas.TeamResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -// teamFromContext parses the current team.Team from the given request context. -// This requires the team to have been previously set in the context, for example by the TeamParser middleware. -func teamFromContext(ctx context.Context) (user.Team, bool) { - val, ok := ctx.Value(teamCtxKey{}).(user.Team) - return val, ok -} + _, err := h.teamStore.GetByName(ctx, req.Name) + if err == nil { + return nil, newErrorResponse(http.StatusBadRequest, "team already exists") + } -// withTeamMember sets team membership information for the current user for the current team in the context. -// Use teamMemberFromContext to retrieve it. -func withTeamMember(ctx context.Context, tm user.TeamMember) context.Context { - return context.WithValue(ctx, teamMemberCtxKey{}, tm) -} + if !errors.Is(err, user.ErrTeamNotFound) { + h.logger.ErrorContext(ctx, "could not get team", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -// teamMemberFromContext parses team membership information for the current user from the given request context. -// This requires this to have been previously set in the context. -func teamMemberFromContext(ctx context.Context) (user.TeamMember, bool) { - val, ok := ctx.Value(teamMemberCtxKey{}).(user.TeamMember) - return val, ok -} + id, err := uuid.NewV7() + if err != nil { + h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -func CreateTeam(l *slog.Logger, s user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authenticated failed") - return - } + team := user.Team{ + ID: id, + Name: user.TeamName(req.Name), + CreatedAt: time.Now(), + } - var team user.Team - if err := json.NewDecoder(r.Body).Decode(&team); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return - } + if err := team.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) + } - _, err := s.GetByName(r.Context(), string(team.Name)) - if err == nil { - response.WriteJSONError(w, http.StatusBadRequest, "team already exists") - return - } + member := user.TeamMember{ + UserID: u.ID, + TeamID: team.ID, + Username: u.Username, + Role: user.TeamMemberRoleAdmin, + CreatedAt: time.Now(), + } - if !errors.Is(err, user.ErrTeamNotFound) { - l.ErrorContext(r.Context(), "could not get team", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if err := member.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) + } - team.ID, err = uuid.NewV7() - if err != nil { - l.Error("could not generate UUID", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + err = h.teamStore.Tx(ctx, func(txCtx context.Context) error { + if err := h.teamStore.Create(txCtx, team); err != nil { + return err } - member := user.TeamMember{ - UserID: u.ID, - TeamID: team.ID, - Username: u.Username, - Role: user.TeamMemberRoleAdmin, - } - if err := member.IsValid(); err != nil { - // shouldn't ever happen, just check it anyway - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) + // add the current user as an admin to the team + if err := h.teamStore.AddTeamMember(txCtx, member); err != nil { + return err } - err = s.Tx(r.Context(), func(ctx context.Context) error { - if err := s.Create(ctx, team); err != nil { - return err - } + return nil + }) - // add the current user as an admin to the team - if err := s.AddTeamMember(ctx, member); err != nil { - return err - } + if err != nil { + h.logger.ErrorContext(ctx, "could not create team", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - return nil - }) + resp := mapTeamResponse(team) + return &resp, nil +} - if err != nil { - l.Error("could not create team", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h TeamHandler) ListTeams(ctx context.Context) ([]oas.TeamResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - response.WriteJSONResponse(w, http.StatusOK, team) + teams, err := h.teamStore.GetAllByUser(ctx, u.ID) + if err != nil { + h.logger.ErrorContext(ctx, "could not get teams for user", slog.Any("error", err)) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") } -} -func ListTeams(l *slog.Logger, s user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authenticated failed") - return - } + resp := make([]oas.TeamResponse, len(teams)) + for i := 0; i < len(teams); i++ { + resp[i] = mapTeamResponse(teams[i]) + } - teams, err := s.GetAllByUser(r.Context(), u.ID) - if err != nil { - l.ErrorContext(r.Context(), "could not get teams for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + return resp, nil +} - response.WriteJSONResponse(w, http.StatusOK, util.NilSliceToEmpty(teams)) +func (h TeamHandler) GetTeam(ctx context.Context, params oas.GetTeamParams) (*oas.TeamResponse, error) { + team, _, err := h.getTeamAndCurrentMemberFromRequest(ctx, params.Name) + if err != nil { + return nil, err } + + resp := mapTeamResponse(team) + return &resp, nil } -func GetTeam(l *slog.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - team, ok := teamFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h TeamHandler) DeleteTeam(ctx context.Context, params oas.DeleteTeamParams) error { + team, member, err := h.getTeamAndCurrentMemberFromRequest(ctx, params.Name) + if err != nil { + return err + } - response.WriteJSONResponse(w, http.StatusOK, team) + if member.Role != user.TeamMemberRoleAdmin { + return newErrorResponse(http.StatusForbidden, "insufficient permission") } + + if err := h.teamStore.DeleteByID(ctx, team.ID); err != nil { + h.logger.ErrorContext(ctx, "could not delete team", slog.Any("error", err)) + return newInternalServerErrorResponse() + } + + return nil } -func DeleteTeam(l *slog.Logger, s user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - currentMember, ok := teamMemberFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team member from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h TeamHandler) AddTeamMember(ctx context.Context, req *oas.TeamMemberRequest, params oas.AddTeamMemberParams) (*oas.TeamMemberResponse, error) { + team, currentMember, err := h.getTeamAndCurrentMemberFromRequest(ctx, params.Name) + if err != nil { + return nil, err + } - if currentMember.Role != user.TeamMemberRoleAdmin { - response.WriteJSONError(w, http.StatusForbidden, "insufficient permission") - return - } + if currentMember.Role != user.TeamMemberRoleAdmin { + return nil, newErrorResponse(http.StatusForbidden, "insufficient permission") + } - team, ok := teamFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + userToAdd, err := h.userStore.GetByUsername(ctx, req.Username) + if err != nil { + if errors.Is(err, user.ErrNotFound) { + return nil, newErrorResponse(http.StatusNotFound, "user not found") } - if err := s.DeleteByID(r.Context(), team.ID); err != nil { - l.ErrorContext(r.Context(), "could not delete team", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + h.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - w.WriteHeader(http.StatusNoContent) + newMember := user.TeamMember{ + UserID: userToAdd.ID, + TeamID: team.ID, + Username: userToAdd.Username, + Role: user.TeamMemberRole(req.Role), + CreatedAt: time.Now(), } -} -type addTeamMemberRequest struct { - Username string `json:"username"` - Role string `json:"role"` -} + if err := newMember.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) + } -func AddTeamMember(l *slog.Logger, teamStore user.TeamStore, userStore user.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - currentMember, ok := teamMemberFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team member from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if err := h.teamStore.AddTeamMember(ctx, newMember); err != nil { + h.logger.ErrorContext(ctx, "could not add team member", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - if currentMember.Role != user.TeamMemberRoleAdmin { - response.WriteJSONError(w, http.StatusForbidden, "insufficient permission") - return - } + resp := mapTeamMemberResponse(newMember) + return &resp, nil +} - var req addTeamMemberRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return - } +func (h TeamHandler) ListTeamMembers(ctx context.Context, params oas.ListTeamMembersParams) ([]oas.TeamMemberResponse, error) { + team, _, err := h.getTeamAndCurrentMemberFromRequest(ctx, params.Name) + if err != nil { + return nil, err + } - userToAdd, err := userStore.GetByUsername(r.Context(), req.Username) - if err != nil { - response.WriteJSONError(w, http.StatusNotFound, "user not found") - return - } + members, err := h.teamStore.GetTeamMembers(ctx, team.ID) + if err != nil { + h.logger.ErrorContext(ctx, "could not get team members", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - member := user.TeamMember{ - UserID: userToAdd.ID, - TeamID: currentMember.TeamID, - Username: userToAdd.Username, - Role: user.TeamMemberRole(req.Role), - } - if err := member.IsValid(); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) - } + resp := make([]oas.TeamMemberResponse, len(members)) + for i := 0; i < len(members); i++ { + resp[i] = mapTeamMemberResponse(members[i]) + } - if err := teamStore.AddTeamMember(r.Context(), member); err != nil { - l.ErrorContext(r.Context(), "could not add team member", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - } + return resp, nil +} - response.WriteJSONResponse(w, http.StatusOK, member) +func (h TeamHandler) RemoveTeamMember(ctx context.Context, params oas.RemoveTeamMemberParams) error { + team, currentMember, err := h.getTeamAndCurrentMemberFromRequest(ctx, params.Name) + if err != nil { + return err } -} -func ListTeamMembers(l *slog.Logger, s user.TeamStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - team, ok := teamFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if currentMember.Role != user.TeamMemberRoleAdmin { + return newErrorResponse(http.StatusForbidden, "insufficient permission") + } - members, err := s.GetTeamMembers(r.Context(), team.ID) - if err != nil { - l.ErrorContext(r.Context(), "could not get team members", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + userToRemove, err := h.userStore.GetByUsername(ctx, params.Username) + if err != nil { + if errors.Is(err, user.ErrNotFound) { + return newErrorResponse(http.StatusNotFound, "user not found") } - response.WriteJSONResponse(w, http.StatusOK, util.NilSliceToEmpty(members)) + h.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) + return newInternalServerErrorResponse() } -} -func RemoveTeamMember(l *slog.Logger, teamStore user.TeamStore, userStore user.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - currentMember, ok := teamMemberFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse team member from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if currentMember.UserID == userToRemove.ID { + return newErrorResponse(http.StatusBadRequest, "cannot remove current user from team") + } - if currentMember.Role != user.TeamMemberRoleAdmin { - response.WriteJSONError(w, http.StatusForbidden, "insufficient permission") - return - } + if err := h.teamStore.RemoveTeamMember(ctx, team.ID, userToRemove.ID); err != nil { + h.logger.ErrorContext(ctx, "could not remove team member", slog.Any("error", err)) + return newInternalServerErrorResponse() + } - username := chi.URLParam(r, "username") - if username == "" { - response.WriteJSONError(w, http.StatusBadRequest, "no username given") - return - } + return nil +} - userToRemove, err := userStore.GetByUsername(r.Context(), username) - if err != nil { - if errors.Is(err, user.ErrNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "user not found") - return - } +func (h TeamHandler) getTeamAndCurrentMemberFromRequest(ctx context.Context, name string) (user.Team, user.TeamMember, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - l.ErrorContext(r.Context(), "could not get user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + team, err := h.teamStore.GetByName(ctx, name) + if err != nil { + if errors.Is(err, user.ErrTeamNotFound) { + return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusNotFound, "team not found") } - if currentMember.UserID == userToRemove.ID { - response.WriteJSONError(w, http.StatusBadRequest, "cannot remove current user from team") - return - } + h.logger.ErrorContext(ctx, "could not get team", + slog.Any("error", err), + slog.String("name", name)) + return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - if err := teamStore.RemoveTeamMember(r.Context(), currentMember.TeamID, userToRemove.ID); err != nil { - l.ErrorContext(r.Context(), "could not remove team member", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return + member, err := h.teamStore.GetTeamMember(ctx, team.ID, u.ID) + if err != nil { + if errors.Is(err, user.ErrTeamMemberNotFound) { + return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusNotFound, "team not found") } - w.WriteHeader(http.StatusNoContent) + h.logger.ErrorContext(ctx, "could not get team member", + slog.Any("error", err), + slog.String("team", name), + slog.String("user", string(u.Username))) + return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + } + + return team, member, nil +} + +func mapTeamResponse(t user.Team) oas.TeamResponse { + return oas.TeamResponse{ + ID: t.ID, + Name: string(t.Name), + CreatedAt: t.CreatedAt, + } +} + +func mapTeamMemberResponse(tm user.TeamMember) oas.TeamMemberResponse { + return oas.TeamMemberResponse{ + UserId: tm.UserID, + Username: string(tm.Username), + Role: oas.TeamMemberResponseRole(tm.Role), + CreatedAt: tm.CreatedAt, } } diff --git a/server/handlers/token.go b/server/handlers/token.go index 672c2f3..edacd3c 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -4,170 +4,140 @@ import ( "context" "crypto/rand" "encoding/base64" - "encoding/json" "errors" - "github.com/evanebb/regauth/server/middleware" - "github.com/evanebb/regauth/server/response" + "github.com/evanebb/regauth/oas" "github.com/evanebb/regauth/token" - "github.com/evanebb/regauth/util" "github.com/google/uuid" "log/slog" "net/http" + "time" ) -type personalAccessTokenCtxKey struct{} - -func PersonalAccessTokenParser(l *slog.Logger, s token.Store) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - id, err := getUUIDFromRequest(r) - if err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid ID given") - return - } - - pat, err := s.GetByID(r.Context(), id) - if err != nil { - if errors.Is(err, token.ErrNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "personal access token not found") - return - } - - l.ErrorContext(r.Context(), "could not get personal access token", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - if pat.UserID != u.ID { - response.WriteJSONError(w, http.StatusNotFound, "personal access token not found") - return - } - - ctx := withPersonalAccessToken(r.Context(), pat) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) - } +type TokenHandler struct { + logger *slog.Logger + tokenStore token.Store } -// withPersonalAccessToken sets the given token.PersonalAccessToken in the context. -// Use personalAccessTokenFromContext to retrieve the token. -func withPersonalAccessToken(ctx context.Context, t token.PersonalAccessToken) context.Context { - return context.WithValue(ctx, personalAccessTokenCtxKey{}, t) -} - -// personalAccessTokenFromContext parses the current token.PersonalAccessToken from the given request context. -// This requires the token to have been previously set in the context, for example by the PersonalAccessTokenParser middleware. -func personalAccessTokenFromContext(ctx context.Context) (token.PersonalAccessToken, bool) { - val, ok := ctx.Value(personalAccessTokenCtxKey{}).(token.PersonalAccessToken) - return val, ok -} +func (h TokenHandler) CreatePersonalAccessToken(ctx context.Context, req *oas.PersonalAccessTokenRequest) (*oas.PersonalAccessTokenCreationResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -type tokenCreationResponse struct { - token.PersonalAccessToken - Token string `json:"token"` -} + id, err := uuid.NewV7() + if err != nil { + h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } -func CreateToken(l *slog.Logger, s token.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authenticated failed") - return - } + pat := token.PersonalAccessToken{ + ID: id, + Description: token.Description(req.Description), + Permission: token.Permission(req.Permission), + ExpirationDate: req.ExpirationDate, + UserID: u.ID, + CreatedAt: time.Now(), + } - var pat token.PersonalAccessToken - if err := json.NewDecoder(r.Body).Decode(&pat); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return - } + if err := pat.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) + } - var err error - pat.ID, err = uuid.NewV7() - if err != nil { - l.Error("could not generate UUID", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + randBytes := make([]byte, 42) + _, _ = rand.Read(randBytes) + plainTextToken := "registry_pat_" + base64.URLEncoding.EncodeToString(randBytes) - pat.UserID = u.ID + if err := h.tokenStore.Create(ctx, pat, plainTextToken); err != nil { + h.logger.ErrorContext(ctx, "could not create personal access token", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - if err := pat.IsValid(); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) - return - } + return &oas.PersonalAccessTokenCreationResponse{ + ID: pat.ID, + Description: string(pat.Description), + Permission: oas.PersonalAccessTokenCreationResponsePermission(pat.Permission), + ExpirationDate: pat.ExpirationDate, + Token: plainTextToken, + CreatedAt: time.Now(), + }, nil +} - randBytes := make([]byte, 42) - _, _ = rand.Read(randBytes) - plainTextToken := "registry_pat_" + base64.URLEncoding.EncodeToString(randBytes) +func (h TokenHandler) ListPersonalAccessTokens(ctx context.Context) ([]oas.PersonalAccessTokenResponse, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return nil, newInternalServerErrorResponse() + } - if err := s.Create(r.Context(), pat, plainTextToken); err != nil { - l.ErrorContext(r.Context(), "could not create personal access token", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + tokens, err := h.tokenStore.GetAllByUser(ctx, u.ID) + if err != nil { + h.logger.ErrorContext(ctx, "could not get personal access tokens for user", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - resp := tokenCreationResponse{PersonalAccessToken: pat, Token: plainTextToken} - response.WriteJSONResponse(w, http.StatusOK, resp) + resp := make([]oas.PersonalAccessTokenResponse, len(tokens)) + for i := 0; i < len(tokens); i++ { + resp[i] = mapPersonalAccessTokenResponse(tokens[i]) } + + return resp, nil } -func ListTokens(l *slog.Logger, s token.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } +func (h TokenHandler) GetPersonalAccessToken(ctx context.Context, params oas.GetPersonalAccessTokenParams) (*oas.PersonalAccessTokenResponse, error) { + pat, err := h.getPersonalAccessTokenFromRequest(ctx, params.ID) + if err != nil { + return nil, err + } - tokens, err := s.GetAllByUser(r.Context(), u.ID) - if err != nil { - l.ErrorContext(r.Context(), "could not get personal access tokens for user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + resp := mapPersonalAccessTokenResponse(pat) + return &resp, nil +} + +func (h TokenHandler) DeletePersonalAccessToken(ctx context.Context, params oas.DeletePersonalAccessTokenParams) error { + pat, err := h.getPersonalAccessTokenFromRequest(ctx, params.ID) + if err != nil { + return err + } - response.WriteJSONResponse(w, http.StatusOK, util.NilSliceToEmpty(tokens)) + if err := h.tokenStore.DeleteByID(ctx, pat.ID); err != nil { + h.logger.ErrorContext(ctx, "could not delete personal access token", slog.Any("error", err)) + return newInternalServerErrorResponse() } + + return nil } -func GetToken(l *slog.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - pat, ok := personalAccessTokenFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse personal access token from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return +func (h TokenHandler) getPersonalAccessTokenFromRequest(ctx context.Context, id uuid.UUID) (token.PersonalAccessToken, error) { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return token.PersonalAccessToken{}, newInternalServerErrorResponse() + } + + pat, err := h.tokenStore.GetByID(ctx, id) + if err != nil { + if errors.Is(err, token.ErrNotFound) { + return token.PersonalAccessToken{}, newErrorResponse(http.StatusNotFound, "personal access token not found") } - response.WriteJSONResponse(w, http.StatusOK, pat) + h.logger.ErrorContext(ctx, "could not get personal access token", slog.Any("error", err)) + return token.PersonalAccessToken{}, newInternalServerErrorResponse() } -} -func DeleteToken(l *slog.Logger, s token.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - pat, ok := personalAccessTokenFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse personal access token from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if pat.UserID != u.ID { + return token.PersonalAccessToken{}, newErrorResponse(http.StatusNotFound, "personal access token not found") + } - if err := s.DeleteByID(r.Context(), pat.ID); err != nil { - l.ErrorContext(r.Context(), "could not delete personal access token", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + return pat, nil +} - w.WriteHeader(http.StatusNoContent) +func mapPersonalAccessTokenResponse(t token.PersonalAccessToken) oas.PersonalAccessTokenResponse { + return oas.PersonalAccessTokenResponse{ + ID: t.ID, + Description: string(t.Description), + Permission: oas.PersonalAccessTokenResponsePermission(t.Permission), + ExpirationDate: t.ExpirationDate, + CreatedAt: t.CreatedAt, } } diff --git a/server/handlers/user.go b/server/handlers/user.go index 15abea3..30561b2 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -2,219 +2,177 @@ package handlers import ( "context" - "encoding/json" "errors" "github.com/evanebb/regauth/auth/local" - "github.com/evanebb/regauth/server/middleware" - "github.com/evanebb/regauth/server/response" + "github.com/evanebb/regauth/oas" "github.com/evanebb/regauth/user" - "github.com/evanebb/regauth/util" - "github.com/go-chi/chi/v5" "github.com/google/uuid" "log/slog" "net/http" + "time" ) -type userCtxKey struct{} +type UserHandler struct { + logger *slog.Logger + userStore user.Store + credentialsStore local.UserCredentialsStore +} -func RequireRole(l *slog.Logger, role user.Role) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - u, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } +func (h UserHandler) CreateUser(ctx context.Context, req *oas.UserRequest) (*oas.UserResponse, error) { + if err := h.requireRole(ctx, user.RoleAdmin); err != nil { + return nil, err + } - if u.Role != role { - response.WriteJSONError(w, http.StatusForbidden, "insufficient permission") - return - } + id, err := uuid.NewV7() + if err != nil { + h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) + return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) + newUser := user.User{ + ID: id, + Username: user.Username(req.Username), + Role: user.Role(req.Role), + CreatedAt: time.Now(), } -} -func UserParser(l *slog.Logger, s user.Store) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - username := chi.URLParam(r, "username") - if username == "" { - response.WriteJSONError(w, http.StatusBadRequest, "no username given") - return - } - - u, err := s.GetByUsername(r.Context(), username) - if err != nil { - if errors.Is(err, user.ErrNotFound) { - response.WriteJSONError(w, http.StatusNotFound, "user not found") - return - } - - l.ErrorContext(r.Context(), "could not get user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - ctx := withUser(r.Context(), u) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) + if err := newUser.IsValid(); err != nil { + return nil, newErrorResponse(http.StatusBadRequest, err.Error()) } -} -// withUser sets the given user.User in the context. -// Use userFromContext to retrieve the user. -func withUser(ctx context.Context, u user.User) context.Context { - return context.WithValue(ctx, userCtxKey{}, u) -} + _, err = h.userStore.GetByUsername(ctx, req.Username) + if err == nil { + return nil, newErrorResponse(http.StatusBadRequest, "user already exists") + } -// userFromContext parses the current user.User from the given request context. -// This requires the user to have been previously set in the context, for example by the UserParser middleware. -func userFromContext(ctx context.Context) (user.User, bool) { - val, ok := ctx.Value(userCtxKey{}).(user.User) - return val, ok -} + if !errors.Is(err, user.ErrNotFound) { + h.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } -func CreateUser(l *slog.Logger, s user.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var newUser user.User - if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return - } + if err := h.userStore.Create(ctx, newUser); err != nil { + h.logger.ErrorContext(ctx, "could not create user", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - _, err := s.GetByUsername(r.Context(), string(newUser.Username)) - if err == nil { - response.WriteJSONError(w, http.StatusBadRequest, "user already exists") - return - } + resp := mapUserResponse(newUser) + return &resp, nil +} - if !errors.Is(err, user.ErrNotFound) { - l.ErrorContext(r.Context(), "could not get user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h UserHandler) ListUsers(ctx context.Context) ([]oas.UserResponse, error) { + if err := h.requireRole(ctx, user.RoleAdmin); err != nil { + return nil, err + } - newUser.ID, err = uuid.NewV7() - if err != nil { - l.Error("could not generate UUID", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + users, err := h.userStore.GetAll(ctx) + if err != nil { + h.logger.ErrorContext(ctx, "could not get users", slog.Any("error", err)) + return nil, newInternalServerErrorResponse() + } - if err := newUser.IsValid(); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) - return - } + resp := make([]oas.UserResponse, len(users)) + for i := 0; i < len(users); i++ { + resp[i] = mapUserResponse(users[i]) + } - if err := s.Create(r.Context(), newUser); err != nil { - l.Error("could not create user", "error", err) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + return resp, nil +} + +func (h UserHandler) GetUser(ctx context.Context, params oas.GetUserParams) (*oas.UserResponse, error) { + if err := h.requireRole(ctx, user.RoleAdmin); err != nil { + return nil, err + } - response.WriteJSONResponse(w, http.StatusOK, newUser) + u, err := h.getUserFromRequest(ctx, params.Username) + if err != nil { + return nil, err } + + resp := mapUserResponse(u) + return &resp, nil } -func ListUsers(l *slog.Logger, s user.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - users, err := s.GetAll(r.Context()) - if err != nil { - l.ErrorContext(r.Context(), "could not get users", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h UserHandler) DeleteUser(ctx context.Context, params oas.DeleteUserParams) error { + currentUser, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return newInternalServerErrorResponse() + } - response.WriteJSONResponse(w, http.StatusOK, util.NilSliceToEmpty(users)) + u, err := h.getUserFromRequest(ctx, params.Username) + if err != nil { + return err } -} -func GetUser(l *slog.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - u, ok := userFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if currentUser.ID == u.ID { + return newErrorResponse(http.StatusBadRequest, "cannot delete current user") + } - response.WriteJSONResponse(w, http.StatusOK, u) + if err := h.userStore.DeleteByID(ctx, u.ID); err != nil { + h.logger.ErrorContext(ctx, "could not delete user", slog.Any("error", err)) + return newInternalServerErrorResponse() } -} -func DeleteUser(l *slog.Logger, s user.Store) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - currentUser, ok := middleware.AuthenticatedUserFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse authenticated user from request context") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } + return nil +} - u, ok := userFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } +func (h UserHandler) ChangeUserPassword(ctx context.Context, req *oas.UserPasswordChangeRequest, params oas.ChangeUserPasswordParams) error { + u, err := h.getUserFromRequest(ctx, params.Username) + if err != nil { + return err + } - if currentUser.ID == u.ID { - response.WriteJSONError(w, http.StatusBadRequest, "cannot delete current user") - return + credentials := local.UserCredentials{UserID: u.ID} + if err := credentials.SetPassword(req.Password); err != nil { + if errors.Is(err, local.ErrWeakPassword) { + return newErrorResponse(http.StatusBadRequest, err.Error()) } - if err := s.DeleteByID(r.Context(), u.ID); err != nil { - l.ErrorContext(r.Context(), "could not delete user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + h.logger.ErrorContext(ctx, "could not set password", slog.Any("error", err)) + return newInternalServerErrorResponse() + } - w.WriteHeader(http.StatusNoContent) + if err := h.credentialsStore.Save(ctx, credentials); err != nil { + h.logger.ErrorContext(ctx, "could not update user credentials", slog.Any("error", err)) + return newInternalServerErrorResponse() } -} -type userPasswordChangeRequest struct { - Password string `json:"password"` + return nil } -func ChangeUserPassword(l *slog.Logger, s local.UserCredentialsStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req userPasswordChangeRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - response.WriteJSONError(w, http.StatusBadRequest, "invalid JSON body given") - return +func (h UserHandler) getUserFromRequest(ctx context.Context, username string) (user.User, error) { + u, err := h.userStore.GetByUsername(ctx, username) + if err != nil { + if errors.Is(err, user.ErrNotFound) { + return user.User{}, newErrorResponse(http.StatusNotFound, "user not found") } - u, ok := userFromContext(r.Context()) - if !ok { - l.ErrorContext(r.Context(), "could not parse user from request context") - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + h.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) + return user.User{}, newInternalServerErrorResponse() + } + + return u, nil +} - credentials := local.UserCredentials{UserID: u.ID} - if err := credentials.SetPassword(req.Password); err != nil { - if errors.Is(err, local.ErrWeakPassword) { - response.WriteJSONError(w, http.StatusBadRequest, err.Error()) - return - } +func (h UserHandler) requireRole(ctx context.Context, role user.Role) error { + u, ok := AuthenticatedUserFromContext(ctx) + if !ok { + h.logger.ErrorContext(ctx, "could not parse user from request context") + return newInternalServerErrorResponse() + } - l.ErrorContext(r.Context(), "could not set password", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + if u.Role != role { + return newErrorResponse(http.StatusForbidden, "insufficient permission") + } - if err := s.Save(r.Context(), credentials); err != nil { - l.ErrorContext(r.Context(), "could not update user credentials", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } + return nil +} - w.WriteHeader(http.StatusNoContent) +func mapUserResponse(u user.User) oas.UserResponse { + return oas.UserResponse{ + ID: u.ID, + Username: string(u.Username), + Role: oas.UserResponseRole(u.Role), + CreatedAt: u.CreatedAt, } } diff --git a/server/handlers/util.go b/server/handlers/util.go deleted file mode 100644 index 5627568..0000000 --- a/server/handlers/util.go +++ /dev/null @@ -1,17 +0,0 @@ -package handlers - -import ( - "fmt" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "net/http" -) - -func getUUIDFromRequest(r *http.Request) (uuid.UUID, error) { - u, err := uuid.Parse(chi.URLParam(r, "id")) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to parse UUID from request: %w", err) - } - - return u, nil -} diff --git a/server/middleware/auth.go b/server/middleware/auth.go deleted file mode 100644 index ebfd1cd..0000000 --- a/server/middleware/auth.go +++ /dev/null @@ -1,161 +0,0 @@ -package middleware - -import ( - "context" - "errors" - "github.com/evanebb/regauth/auth/local" - "github.com/evanebb/regauth/server/response" - "github.com/evanebb/regauth/token" - "github.com/evanebb/regauth/user" - "log/slog" - "net/http" - "strings" -) - -type ExcludedRoute struct { - Path string - Method string -} - -type ExcludedRoutes []ExcludedRoute - -func (r ExcludedRoutes) IsExcluded(path, method string) bool { - for _, route := range r { - if route.Path == path && route.Method == method { - return true - } - } - - return false -} - -type authenticatedUserCtxKey struct{} - -func TokenAuthentication( - l *slog.Logger, - tokenStore token.Store, - userStore user.Store, - excludedRoutes ExcludedRoutes, -) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - - split := strings.Split(authHeader, " ") - if len(split) != 2 || split[0] != "Bearer" { - l.DebugContext(r.Context(), "no or invalid bearer token in authorization header") - // allow bypassing token authentication for certain routes, it is assumed that authentication will be - // handled separately - // it is probably better to create some kind of authenticator interface + a stack of authenticators to - // check, but eh, fine for now - if excludedRoutes.IsExcluded(r.URL.Path, r.Method) { - l.DebugContext(r.Context(), "bypassing token authentication for route", - slog.String("path", r.URL.Path), - slog.String("method", r.Method)) - - next.ServeHTTP(w, r) - return - } - - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - t, err := tokenStore.GetByPlainTextToken(r.Context(), split[1]) - if err != nil { - if errors.Is(err, token.ErrNotFound) { - l.DebugContext(r.Context(), "token does not exist") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - l.ErrorContext(r.Context(), "could not get personal access token", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - u, err := userStore.GetByID(r.Context(), t.UserID) - if err != nil { - l.ErrorContext(r.Context(), "could not get user for token", slog.Any("error", err), - slog.String("userId", t.UserID.String())) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - l.DebugContext(r.Context(), "token authentication successful") - ctx := WithAuthenticatedUser(r.Context(), u) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) - } -} - -func UsernamePasswordAuthentication(l *slog.Logger, userStore user.Store, credentialsStore local.UserCredentialsStore) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - // check if the user is already set in the request; if so, we do not have to do anything - if _, ok := AuthenticatedUserFromContext(r.Context()); ok { - l.DebugContext(r.Context(), "user already set in request, skipping basic authentication") - next.ServeHTTP(w, r) - return - } - - username, password, ok := r.BasicAuth() - if !ok { - l.DebugContext(r.Context(), "no or invalid basic authentication in authorization header") - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - u, err := userStore.GetByUsername(r.Context(), username) - if err != nil { - if errors.Is(err, user.ErrNotFound) { - l.DebugContext(r.Context(), "user not found", slog.String("username", username)) - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - l.ErrorContext(r.Context(), "could not get user", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - credentials, err := credentialsStore.GetByUserID(r.Context(), u.ID) - if err != nil { - if errors.Is(err, user.ErrNotFound) { - l.DebugContext(r.Context(), "no credentials set for user", slog.String("username", username)) - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - l.ErrorContext(r.Context(), "could not get credentials", slog.Any("error", err)) - response.WriteJSONError(w, http.StatusInternalServerError, "internal server error") - return - } - - if err := credentials.CheckPassword(password); err != nil { - l.DebugContext(r.Context(), "password does not match", slog.String("username", username)) - response.WriteJSONError(w, http.StatusUnauthorized, "authentication failed") - return - } - - l.DebugContext(r.Context(), "basic authentication successful") - ctx := WithAuthenticatedUser(r.Context(), u) - next.ServeHTTP(w, r.WithContext(ctx)) - } - return http.HandlerFunc(fn) - } -} - -// WithAuthenticatedUser sets the authenticated user.User in the context. -// Use AuthenticatedUserFromContext to retrieve the user. -func WithAuthenticatedUser(ctx context.Context, u user.User) context.Context { - return context.WithValue(ctx, authenticatedUserCtxKey{}, u) -} - -// AuthenticatedUserFromContext parses the authenticated user.User from the given request context. -// This requires the user to have been previously set in the context, for example by the TokenAuthentication middleware. -func AuthenticatedUserFromContext(ctx context.Context) (user.User, bool) { - val, ok := ctx.Value(authenticatedUserCtxKey{}).(user.User) - return val, ok -} diff --git a/server/routes.go b/server/routes.go index cf22059..48c4027 100644 --- a/server/routes.go +++ b/server/routes.go @@ -4,6 +4,7 @@ import ( "github.com/evanebb/regauth/api" "github.com/evanebb/regauth/auth" "github.com/evanebb/regauth/auth/local" + "github.com/evanebb/regauth/oas" "github.com/evanebb/regauth/repository" "github.com/evanebb/regauth/server/handlers" "github.com/evanebb/regauth/server/middleware" @@ -41,79 +42,14 @@ func baseRouter( r.Handle("/token", handlers.GenerateRegistryToken(logger, authenticator, authorizer, accessTokenConfig)) - r.Mount("/v1", v1ApiRouter(logger, repoStore, userStore, teamStore, tokenStore, credentialsStore)) + handler := handlers.NewHandler(logger, repoStore, userStore, teamStore, tokenStore, credentialsStore) + securityHandler := handlers.NewSecurityHandler(logger, tokenStore, userStore, credentialsStore) + apiServer, err := oas.NewServer(handler, securityHandler, oas.WithNotFound(handlers.NotFound)) + if err != nil { + panic(err) + } - return r -} - -func v1ApiRouter( - logger *slog.Logger, - repoStore repository.Store, - userStore user.Store, - teamStore user.TeamStore, - tokenStore token.Store, - credentialsStore local.UserCredentialsStore, -) chi.Router { - r := chi.NewRouter() - - r.NotFound(handlers.NotFound) - - excludedRoutes := middleware.ExcludedRoutes{{Path: "/v1/tokens", Method: "POST"}} - authMiddleware := middleware.TokenAuthentication(logger, tokenStore, userStore, excludedRoutes) - r.Use(authMiddleware) - - r.Route("/repositories", func(r chi.Router) { - r.Post("/", handlers.CreateRepository(logger, repoStore, teamStore)) - r.Get("/", handlers.ListRepositories(logger, repoStore, teamStore)) - - r.Route("/{namespace}/{name}", func(r chi.Router) { - r.Use(handlers.RepositoryParser(logger, repoStore, teamStore)) - r.Get("/", handlers.GetRepository(logger)) - r.Delete("/", handlers.DeleteRepository(logger, repoStore)) - }) - }) - - r.Route("/tokens", func(r chi.Router) { - // special case: since this is a fully API-driven application, users can create personal access tokens using - // their username and password. Otherwise there would be no way for a user to access the rest of the API :) - userPassMiddleware := middleware.UsernamePasswordAuthentication(logger, userStore, credentialsStore) - r.With(userPassMiddleware).Post("/", handlers.CreateToken(logger, tokenStore)) - - r.Get("/", handlers.ListTokens(logger, tokenStore)) - r.Route("/{id}", func(r chi.Router) { - r.Use(handlers.PersonalAccessTokenParser(logger, tokenStore)) - r.Get("/", handlers.GetToken(logger)) - r.Delete("/", handlers.DeleteToken(logger, tokenStore)) - }) - }) - - r.Route("/teams", func(r chi.Router) { - r.Post("/", handlers.CreateTeam(logger, teamStore)) - r.Get("/", handlers.ListTeams(logger, teamStore)) - r.Route("/{name}", func(r chi.Router) { - r.Use(handlers.TeamParser(logger, teamStore)) - r.Get("/", handlers.GetTeam(logger)) - r.Delete("/", handlers.DeleteTeam(logger, teamStore)) - - r.Route("/members", func(r chi.Router) { - r.Get("/", handlers.ListTeamMembers(logger, teamStore)) - r.Post("/", handlers.AddTeamMember(logger, teamStore, userStore)) - r.Delete("/{username}", handlers.RemoveTeamMember(logger, teamStore, userStore)) - }) - }) - }) - - r.Route("/users", func(r chi.Router) { - r.Use(handlers.RequireRole(logger, user.RoleAdmin)) - r.Post("/", handlers.CreateUser(logger, userStore)) - r.Get("/", handlers.ListUsers(logger, userStore)) - r.Route("/{username}", func(r chi.Router) { - r.Use(handlers.UserParser(logger, userStore)) - r.Get("/", handlers.GetUser(logger)) - r.Delete("/", handlers.DeleteUser(logger, userStore)) - r.Post("/password", handlers.ChangeUserPassword(logger, credentialsStore)) - }) - }) + r.Mount("/v1/", apiServer) return r } diff --git a/store/postgres/personal_access_token.go b/store/postgres/personal_access_token.go index 7fa7a65..9c44009 100644 --- a/store/postgres/personal_access_token.go +++ b/store/postgres/personal_access_token.go @@ -112,8 +112,8 @@ func (s PersonalAccessTokenStore) Create(ctx context.Context, t token.PersonalAc lastEight := plainTextToken[len(plainTextToken)-8:] hash := auth.HashTokenWithRandomSalt(plainTextToken) - query := "INSERT INTO personal_access_tokens (id, hash, last_eight ,description, permission, expiration_date, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7)" - _, err = s.QuerierFromContext(ctx).Exec(ctx, query, t.ID, hash, lastEight, t.Description, permissionToDatabaseMap[t.Permission], t.ExpirationDate, t.UserID) + query := "INSERT INTO personal_access_tokens (id, hash, last_eight ,description, permission, expiration_date, user_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" + _, err = s.QuerierFromContext(ctx).Exec(ctx, query, t.ID, hash, lastEight, t.Description, permissionToDatabaseMap[t.Permission], t.ExpirationDate, t.UserID, t.CreatedAt) return err } diff --git a/store/postgres/repository.go b/store/postgres/repository.go index 825aada..3141813 100644 --- a/store/postgres/repository.go +++ b/store/postgres/repository.go @@ -110,13 +110,13 @@ func (s RepositoryStore) Create(ctx context.Context, r repository.Repository) er } query := ` - INSERT INTO repositories (id, namespace_id, name, visibility) - SELECT $1, id, $2, $3 + INSERT INTO repositories (id, namespace_id, name, visibility, created_at) + SELECT $1, id, $2, $3, $4 FROM namespaces - WHERE name = $4 + WHERE name = $5 ` - _, err = s.QuerierFromContext(ctx).Exec(ctx, query, r.ID, r.Name, r.Visibility, r.Namespace) + _, err = s.QuerierFromContext(ctx).Exec(ctx, query, r.ID, r.Name, r.Visibility, r.CreatedAt, r.Namespace) return err } diff --git a/store/postgres/team.go b/store/postgres/team.go index 71e28b7..3b3d46f 100644 --- a/store/postgres/team.go +++ b/store/postgres/team.go @@ -92,8 +92,8 @@ func (s TeamStore) Create(ctx context.Context, t user.Team) error { return err } - query := "INSERT INTO teams (id, name) VALUES ($1, $2)" - if _, err := tx.Exec(ctx, query, t.ID, t.Name); err != nil { + query := "INSERT INTO teams (id, name, created_at) VALUES ($1, $2, $3)" + if _, err := tx.Exec(ctx, query, t.ID, t.Name, t.CreatedAt); err != nil { _ = tx.Rollback(ctx) return err } @@ -181,8 +181,8 @@ func (s TeamStore) AddTeamMember(ctx context.Context, m user.TeamMember) error { return err } - query := "INSERT INTO team_members (user_id, team_id, role) VALUES ($1, $2, $3)" - _, err = s.QuerierFromContext(ctx).Exec(ctx, query, m.UserID, m.TeamID, m.Role) + query := "INSERT INTO team_members (user_id, team_id, role, created_at) VALUES ($1, $2, $3, $4)" + _, err = s.QuerierFromContext(ctx).Exec(ctx, query, m.UserID, m.TeamID, m.Role, m.CreatedAt) return err } diff --git a/store/postgres/user.go b/store/postgres/user.go index ea13989..7a84ed3 100644 --- a/store/postgres/user.go +++ b/store/postgres/user.go @@ -84,8 +84,8 @@ func (s UserStore) Create(ctx context.Context, u user.User) error { return err } - query := "INSERT INTO users (id, username, role) VALUES ($1, $2, $3)" - if _, err = tx.Exec(ctx, query, u.ID, u.Username, u.Role); err != nil { + query := "INSERT INTO users (id, username, role, created_at) VALUES ($1, $2, $3, $4)" + if _, err = tx.Exec(ctx, query, u.ID, u.Username, u.Role, u.CreatedAt); err != nil { _ = tx.Rollback(ctx) return err } From 04d0a879a64b7430ae86b793c697cd6834f5cda4 Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 22:53:25 +0200 Subject: [PATCH 02/10] Remove JSON tags from models --- repository/repository.go | 10 +++++----- token/personalaccesstoken.go | 12 ++++++------ user/team.go | 16 ++++++++-------- user/user.go | 8 ++++---- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/repository/repository.go b/repository/repository.go index a1e42d6..ea77104 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -8,11 +8,11 @@ import ( ) type Repository struct { - ID uuid.UUID `json:"id"` - Namespace string `json:"namespace"` - Name Name `json:"name"` - Visibility Visibility `json:"visibility"` - CreatedAt time.Time `json:"createdAt"` + ID uuid.UUID + Namespace string + Name Name + Visibility Visibility + CreatedAt time.Time } func (r Repository) IsValid() error { diff --git a/token/personalaccesstoken.go b/token/personalaccesstoken.go index 96428cb..1fb1299 100644 --- a/token/personalaccesstoken.go +++ b/token/personalaccesstoken.go @@ -8,12 +8,12 @@ import ( ) type PersonalAccessToken struct { - ID uuid.UUID `json:"id"` - Description Description `json:"description"` - Permission Permission `json:"permission"` - ExpirationDate time.Time `json:"expirationDate"` - UserID uuid.UUID `json:"-"` - CreatedAt time.Time `json:"createdAt"` + ID uuid.UUID + Description Description + Permission Permission + ExpirationDate time.Time + UserID uuid.UUID + CreatedAt time.Time } func (t PersonalAccessToken) IsValid() error { diff --git a/user/team.go b/user/team.go index 440036b..a5be202 100644 --- a/user/team.go +++ b/user/team.go @@ -7,9 +7,9 @@ import ( ) type Team struct { - ID uuid.UUID `json:"id"` - Name TeamName `json:"name"` - CreatedAt time.Time `json:"createdAt"` + ID uuid.UUID + Name TeamName + CreatedAt time.Time } func (t Team) IsValid() error { @@ -42,11 +42,11 @@ func (n TeamName) IsValid() error { } type TeamMember struct { - UserID uuid.UUID `json:"userId"` - TeamID uuid.UUID `json:"-"` - Username Username `json:"username"` - Role TeamMemberRole `json:"role"` - CreatedAt time.Time `json:"createdAt"` + UserID uuid.UUID + TeamID uuid.UUID + Username Username + Role TeamMemberRole + CreatedAt time.Time } func (m TeamMember) IsValid() error { diff --git a/user/user.go b/user/user.go index 108638a..d109049 100644 --- a/user/user.go +++ b/user/user.go @@ -7,10 +7,10 @@ import ( ) type User struct { - ID uuid.UUID `json:"id"` - Username Username `json:"username"` - Role Role `json:"role"` - CreatedAt time.Time `json:"createdAt"` + ID uuid.UUID + Username Username + Role Role + CreatedAt time.Time } func (u User) IsValid() error { From 661cf08586a7b289c8ed63f1c5f06aabcc8e45bb Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 22:53:44 +0200 Subject: [PATCH 03/10] Remove unused nil slice conversion func --- util/nilslice.go | 12 ------------ util/nilslice_test.go | 25 ------------------------- 2 files changed, 37 deletions(-) delete mode 100644 util/nilslice.go delete mode 100644 util/nilslice_test.go diff --git a/util/nilslice.go b/util/nilslice.go deleted file mode 100644 index 17c5b65..0000000 --- a/util/nilslice.go +++ /dev/null @@ -1,12 +0,0 @@ -package util - -// NilSliceToEmpty will convert a nil slice to an empty slice. -// If the slice is not nil, it will return the original slice. -// Mostly useful before encoding the slice as JSON, to ensure that an empty array is returned instead of null. -func NilSliceToEmpty[T any](s []T) []T { - if s == nil { - return make([]T, 0) - } - - return s -} diff --git a/util/nilslice_test.go b/util/nilslice_test.go deleted file mode 100644 index f1bbe53..0000000 --- a/util/nilslice_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package util - -import "testing" - -func TestNilSliceToEmpty(t *testing.T) { - t.Parallel() - - t.Run("nil slice is converted to empty slice", func(t *testing.T) { - t.Parallel() - var s []string - if actual := NilSliceToEmpty(s); actual == nil { - t.Fatalf("expected slice to not be nil") - } - }) - - t.Run("non-nil slice is returned as-is", func(t *testing.T) { - t.Parallel() - s := make([]string, 0) - actual := NilSliceToEmpty(s) - // not really anything to check here, just check the length I guess - if len(actual) != 0 || actual == nil { - t.Fatalf("expected slice to not be changed") - } - }) -} From b6b3f86200f57070b797c44d99db9746a719855b Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 23:24:06 +0200 Subject: [PATCH 04/10] Make slice conversion shorter using generic function, rename functions to be clearer --- server/handlers/repository.go | 13 ++++--------- server/handlers/team.go | 24 +++++++----------------- server/handlers/token.go | 11 +++-------- server/handlers/user.go | 13 ++++--------- server/handlers/util.go | 12 ++++++++++++ 5 files changed, 30 insertions(+), 43 deletions(-) create mode 100644 server/handlers/util.go diff --git a/server/handlers/repository.go b/server/handlers/repository.go index ec788b0..973f0b4 100644 --- a/server/handlers/repository.go +++ b/server/handlers/repository.go @@ -61,7 +61,7 @@ func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.Reposi return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") } - resp := mapRepositoryResponse(repo) + resp := convertToRepositoryResponse(repo) return &resp, nil } @@ -84,12 +84,7 @@ func (h RepositoryHandler) ListRepositories(ctx context.Context) ([]oas.Reposito return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") } - resp := make([]oas.RepositoryResponse, len(repos)) - for i := 0; i < len(repos); i++ { - resp[i] = mapRepositoryResponse(repos[i]) - } - - return resp, nil + return convertSlice(repos, convertToRepositoryResponse), nil } func (h RepositoryHandler) GetRepository(ctx context.Context, params oas.GetRepositoryParams) (*oas.RepositoryResponse, error) { @@ -98,7 +93,7 @@ func (h RepositoryHandler) GetRepository(ctx context.Context, params oas.GetRepo return nil, err } - resp := mapRepositoryResponse(repo) + resp := convertToRepositoryResponse(repo) return &resp, nil } @@ -166,7 +161,7 @@ func (h RepositoryHandler) getRepositoryFromRequest(ctx context.Context, namespa return repo, nil } -func mapRepositoryResponse(r repository.Repository) oas.RepositoryResponse { +func convertToRepositoryResponse(r repository.Repository) oas.RepositoryResponse { return oas.RepositoryResponse{ ID: r.ID, Namespace: r.Namespace, diff --git a/server/handlers/team.go b/server/handlers/team.go index 5bd1a4c..934bb05 100644 --- a/server/handlers/team.go +++ b/server/handlers/team.go @@ -80,7 +80,7 @@ func (h TeamHandler) CreateTeam(ctx context.Context, req *oas.TeamRequest) (*oas return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") } - resp := mapTeamResponse(team) + resp := convertToTeamResponse(team) return &resp, nil } @@ -97,12 +97,7 @@ func (h TeamHandler) ListTeams(ctx context.Context) ([]oas.TeamResponse, error) return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") } - resp := make([]oas.TeamResponse, len(teams)) - for i := 0; i < len(teams); i++ { - resp[i] = mapTeamResponse(teams[i]) - } - - return resp, nil + return convertSlice(teams, convertToTeamResponse), nil } func (h TeamHandler) GetTeam(ctx context.Context, params oas.GetTeamParams) (*oas.TeamResponse, error) { @@ -111,7 +106,7 @@ func (h TeamHandler) GetTeam(ctx context.Context, params oas.GetTeamParams) (*oa return nil, err } - resp := mapTeamResponse(team) + resp := convertToTeamResponse(team) return &resp, nil } @@ -170,7 +165,7 @@ func (h TeamHandler) AddTeamMember(ctx context.Context, req *oas.TeamMemberReque return nil, newInternalServerErrorResponse() } - resp := mapTeamMemberResponse(newMember) + resp := convertToTeamMemberResponse(newMember) return &resp, nil } @@ -186,12 +181,7 @@ func (h TeamHandler) ListTeamMembers(ctx context.Context, params oas.ListTeamMem return nil, newInternalServerErrorResponse() } - resp := make([]oas.TeamMemberResponse, len(members)) - for i := 0; i < len(members); i++ { - resp[i] = mapTeamMemberResponse(members[i]) - } - - return resp, nil + return convertSlice(members, convertToTeamMemberResponse), nil } func (h TeamHandler) RemoveTeamMember(ctx context.Context, params oas.RemoveTeamMemberParams) error { @@ -261,7 +251,7 @@ func (h TeamHandler) getTeamAndCurrentMemberFromRequest(ctx context.Context, nam return team, member, nil } -func mapTeamResponse(t user.Team) oas.TeamResponse { +func convertToTeamResponse(t user.Team) oas.TeamResponse { return oas.TeamResponse{ ID: t.ID, Name: string(t.Name), @@ -269,7 +259,7 @@ func mapTeamResponse(t user.Team) oas.TeamResponse { } } -func mapTeamMemberResponse(tm user.TeamMember) oas.TeamMemberResponse { +func convertToTeamMemberResponse(tm user.TeamMember) oas.TeamMemberResponse { return oas.TeamMemberResponse{ UserId: tm.UserID, Username: string(tm.Username), diff --git a/server/handlers/token.go b/server/handlers/token.go index edacd3c..3c665ba 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -76,12 +76,7 @@ func (h TokenHandler) ListPersonalAccessTokens(ctx context.Context) ([]oas.Perso return nil, newInternalServerErrorResponse() } - resp := make([]oas.PersonalAccessTokenResponse, len(tokens)) - for i := 0; i < len(tokens); i++ { - resp[i] = mapPersonalAccessTokenResponse(tokens[i]) - } - - return resp, nil + return convertSlice(tokens, convertToPersonalAccessTokenResponse), nil } func (h TokenHandler) GetPersonalAccessToken(ctx context.Context, params oas.GetPersonalAccessTokenParams) (*oas.PersonalAccessTokenResponse, error) { @@ -90,7 +85,7 @@ func (h TokenHandler) GetPersonalAccessToken(ctx context.Context, params oas.Get return nil, err } - resp := mapPersonalAccessTokenResponse(pat) + resp := convertToPersonalAccessTokenResponse(pat) return &resp, nil } @@ -132,7 +127,7 @@ func (h TokenHandler) getPersonalAccessTokenFromRequest(ctx context.Context, id return pat, nil } -func mapPersonalAccessTokenResponse(t token.PersonalAccessToken) oas.PersonalAccessTokenResponse { +func convertToPersonalAccessTokenResponse(t token.PersonalAccessToken) oas.PersonalAccessTokenResponse { return oas.PersonalAccessTokenResponse{ ID: t.ID, Description: string(t.Description), diff --git a/server/handlers/user.go b/server/handlers/user.go index 30561b2..f05e148 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -55,7 +55,7 @@ func (h UserHandler) CreateUser(ctx context.Context, req *oas.UserRequest) (*oas return nil, newInternalServerErrorResponse() } - resp := mapUserResponse(newUser) + resp := convertToUserResponse(newUser) return &resp, nil } @@ -70,12 +70,7 @@ func (h UserHandler) ListUsers(ctx context.Context) ([]oas.UserResponse, error) return nil, newInternalServerErrorResponse() } - resp := make([]oas.UserResponse, len(users)) - for i := 0; i < len(users); i++ { - resp[i] = mapUserResponse(users[i]) - } - - return resp, nil + return convertSlice(users, convertToUserResponse), nil } func (h UserHandler) GetUser(ctx context.Context, params oas.GetUserParams) (*oas.UserResponse, error) { @@ -88,7 +83,7 @@ func (h UserHandler) GetUser(ctx context.Context, params oas.GetUserParams) (*oa return nil, err } - resp := mapUserResponse(u) + resp := convertToUserResponse(u) return &resp, nil } @@ -168,7 +163,7 @@ func (h UserHandler) requireRole(ctx context.Context, role user.Role) error { return nil } -func mapUserResponse(u user.User) oas.UserResponse { +func convertToUserResponse(u user.User) oas.UserResponse { return oas.UserResponse{ ID: u.ID, Username: string(u.Username), diff --git a/server/handlers/util.go b/server/handlers/util.go new file mode 100644 index 0000000..35c7144 --- /dev/null +++ b/server/handlers/util.go @@ -0,0 +1,12 @@ +package handlers + +// convertSlice will convert a slice of type []I to a slice of type []O, using the provided conversion function to +// convert each item in the input slice. +func convertSlice[I any, O any](in []I, conversionFunc func(in I) O) []O { + out := make([]O, len(in)) + for i := 0; i < len(in); i++ { + out[i] = conversionFunc(in[i]) + } + + return out +} From 4201b9e4073b2a27019b660224d49fc0423e9ea1 Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 23:30:57 +0200 Subject: [PATCH 05/10] Use internal server error helper function --- server/handlers/handlers.go | 2 +- server/handlers/repository.go | 22 +++++++++++----------- server/handlers/security.go | 8 ++++---- server/handlers/team.go | 18 +++++++++--------- server/handlers/token.go | 4 ++-- server/handlers/user.go | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/server/handlers/handlers.go b/server/handlers/handlers.go index 50a189d..e05ca3d 100644 --- a/server/handlers/handlers.go +++ b/server/handlers/handlers.go @@ -67,5 +67,5 @@ func (h Handler) NewError(ctx context.Context, err error) *oas.ErrorStatusCode { // log the error and return a generic internal server error by default, to avoid potentially leaking sensitive info h.logger.ErrorContext(ctx, "unhandled error occurred: "+err.Error(), slog.Any("error", err)) - return newErrorResponse(http.StatusInternalServerError, "internal server error") + return newInternalServerErrorResponse() } diff --git a/server/handlers/repository.go b/server/handlers/repository.go index 973f0b4..bc137f2 100644 --- a/server/handlers/repository.go +++ b/server/handlers/repository.go @@ -25,13 +25,13 @@ func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.Reposi u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } authorizedNamespaces, err := h.getUserNamespaces(ctx, u) if err != nil { h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } if !slices.Contains(authorizedNamespaces, req.Namespace) { @@ -41,7 +41,7 @@ func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.Reposi id, err := uuid.NewV7() if err != nil { h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } repo := repository.Repository{ @@ -58,7 +58,7 @@ func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.Reposi if err := h.repoStore.Create(ctx, repo); err != nil { h.logger.ErrorContext(ctx, "could not create repository", "error", err) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } resp := convertToRepositoryResponse(repo) @@ -69,19 +69,19 @@ func (h RepositoryHandler) ListRepositories(ctx context.Context) ([]oas.Reposito u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } namespaces, err := h.getUserNamespaces(ctx, u) if err != nil { h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } repos, err := h.repoStore.GetAllByNamespace(ctx, namespaces...) if err != nil { h.logger.ErrorContext(ctx, "failed to get repositories for user", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } return convertSlice(repos, convertToRepositoryResponse), nil @@ -105,7 +105,7 @@ func (h RepositoryHandler) DeleteRepository(ctx context.Context, params oas.Dele if err := h.repoStore.DeleteByID(ctx, repo.ID); err != nil { h.logger.ErrorContext(ctx, "could not delete repository", slog.Any("error", err)) - return newErrorResponse(http.StatusInternalServerError, "internal server error") + return newInternalServerErrorResponse() } return nil @@ -132,13 +132,13 @@ func (h RepositoryHandler) getRepositoryFromRequest(ctx context.Context, namespa u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return repository.Repository{}, newInternalServerErrorResponse() } authorizedNamespaces, err := h.getUserNamespaces(ctx, u) if err != nil { h.logger.ErrorContext(ctx, "failed to get namespaces for user", slog.Any("error", err)) - return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return repository.Repository{}, newInternalServerErrorResponse() } if !slices.Contains(authorizedNamespaces, namespace) { @@ -155,7 +155,7 @@ func (h RepositoryHandler) getRepositoryFromRequest(ctx context.Context, namespa slog.Any("error", err), slog.String("namespace", namespace), slog.String("name", name)) - return repository.Repository{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return repository.Repository{}, newInternalServerErrorResponse() } return repo, nil diff --git a/server/handlers/security.go b/server/handlers/security.go index 2d7cb3c..0d6cc06 100644 --- a/server/handlers/security.go +++ b/server/handlers/security.go @@ -41,12 +41,12 @@ func (s SecurityHandler) HandlePersonalAccessToken(ctx context.Context, operatio } s.logger.ErrorContext(ctx, "could not get personal access token", slog.Any("error", err)) - return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + return ctx, newInternalServerErrorResponse() } u, err := s.userStore.GetByID(ctx, tok.UserID) if err != nil { - return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + return ctx, newInternalServerErrorResponse() } s.logger.DebugContext(ctx, "token authentication successful") @@ -62,7 +62,7 @@ func (s SecurityHandler) HandleUsernamePassword(ctx context.Context, operationNa } s.logger.ErrorContext(ctx, "could not get user", slog.Any("error", err)) - return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + return ctx, newInternalServerErrorResponse() } credentials, err := s.credentialsStore.GetByUserID(ctx, u.ID) @@ -73,7 +73,7 @@ func (s SecurityHandler) HandleUsernamePassword(ctx context.Context, operationNa } s.logger.ErrorContext(ctx, "could not get credentials", slog.Any("error", err)) - return ctx, newErrorResponse(http.StatusInternalServerError, "internal server error") + return ctx, newInternalServerErrorResponse() } if err := credentials.CheckPassword(t.GetPassword()); err != nil { diff --git a/server/handlers/team.go b/server/handlers/team.go index 934bb05..3bce003 100644 --- a/server/handlers/team.go +++ b/server/handlers/team.go @@ -21,7 +21,7 @@ func (h TeamHandler) CreateTeam(ctx context.Context, req *oas.TeamRequest) (*oas u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } _, err := h.teamStore.GetByName(ctx, req.Name) @@ -31,13 +31,13 @@ func (h TeamHandler) CreateTeam(ctx context.Context, req *oas.TeamRequest) (*oas if !errors.Is(err, user.ErrTeamNotFound) { h.logger.ErrorContext(ctx, "could not get team", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } id, err := uuid.NewV7() if err != nil { h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } team := user.Team{ @@ -77,7 +77,7 @@ func (h TeamHandler) CreateTeam(ctx context.Context, req *oas.TeamRequest) (*oas if err != nil { h.logger.ErrorContext(ctx, "could not create team", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } resp := convertToTeamResponse(team) @@ -88,13 +88,13 @@ func (h TeamHandler) ListTeams(ctx context.Context) ([]oas.TeamResponse, error) u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } teams, err := h.teamStore.GetAllByUser(ctx, u.ID) if err != nil { h.logger.ErrorContext(ctx, "could not get teams for user", slog.Any("error", err)) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } return convertSlice(teams, convertToTeamResponse), nil @@ -220,7 +220,7 @@ func (h TeamHandler) getTeamAndCurrentMemberFromRequest(ctx context.Context, nam u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return user.Team{}, user.TeamMember{}, newInternalServerErrorResponse() } team, err := h.teamStore.GetByName(ctx, name) @@ -232,7 +232,7 @@ func (h TeamHandler) getTeamAndCurrentMemberFromRequest(ctx context.Context, nam h.logger.ErrorContext(ctx, "could not get team", slog.Any("error", err), slog.String("name", name)) - return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return user.Team{}, user.TeamMember{}, newInternalServerErrorResponse() } member, err := h.teamStore.GetTeamMember(ctx, team.ID, u.ID) @@ -245,7 +245,7 @@ func (h TeamHandler) getTeamAndCurrentMemberFromRequest(ctx context.Context, nam slog.Any("error", err), slog.String("team", name), slog.String("user", string(u.Username))) - return user.Team{}, user.TeamMember{}, newErrorResponse(http.StatusInternalServerError, "internal server error") + return user.Team{}, user.TeamMember{}, newInternalServerErrorResponse() } return team, member, nil diff --git a/server/handlers/token.go b/server/handlers/token.go index 3c665ba..217cf4e 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -22,13 +22,13 @@ func (h TokenHandler) CreatePersonalAccessToken(ctx context.Context, req *oas.Pe u, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } id, err := uuid.NewV7() if err != nil { h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } pat := token.PersonalAccessToken{ diff --git a/server/handlers/user.go b/server/handlers/user.go index f05e148..ccf3767 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -26,7 +26,7 @@ func (h UserHandler) CreateUser(ctx context.Context, req *oas.UserRequest) (*oas id, err := uuid.NewV7() if err != nil { h.logger.ErrorContext(ctx, "could not generate UUID", "error", err) - return nil, newErrorResponse(http.StatusInternalServerError, "internal server error") + return nil, newInternalServerErrorResponse() } newUser := user.User{ From d671b032f40b8ac4bd6164c13e50ca8974eb8343 Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 23:41:12 +0200 Subject: [PATCH 06/10] Pass oas.ErrorStatusCode through error handler --- server/handlers/handlers.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/handlers/handlers.go b/server/handlers/handlers.go index e05ca3d..5ef27bd 100644 --- a/server/handlers/handlers.go +++ b/server/handlers/handlers.go @@ -59,7 +59,16 @@ func NewHandler( } func (h Handler) NewError(ctx context.Context, err error) *oas.ErrorStatusCode { + var ( + errorStatusCode *oas.ErrorStatusCode + ) + switch { + case errors.As(err, &errorStatusCode): + // if this is already a status code error, just pass it through + // this should really only happen with errors returned from the SecurityHandler, since ogen will not check if + // those are *oas.ErrorStatusCode instances + return errorStatusCode case errors.Is(err, ogenerrors.ErrSecurityRequirementIsNotSatisfied): // no credentials given return newErrorResponse(http.StatusUnauthorized, "authentication failed") From 95512d698f5f481d8ceb7ca11d45a2298e8017bf Mon Sep 17 00:00:00 2001 From: evanebb Date: Sun, 6 Apr 2025 23:51:55 +0200 Subject: [PATCH 07/10] Remove UnimplementedHandler embed, we have no more unimplemented endpoints --- server/handlers/repository.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/handlers/repository.go b/server/handlers/repository.go index bc137f2..a37eec4 100644 --- a/server/handlers/repository.go +++ b/server/handlers/repository.go @@ -18,7 +18,6 @@ type RepositoryHandler struct { logger *slog.Logger repoStore repository.Store teamStore user.TeamStore - oas.UnimplementedHandler } func (h RepositoryHandler) CreateRepository(ctx context.Context, req *oas.RepositoryRequest) (*oas.RepositoryResponse, error) { From 60e57bf3e90325e8e9bd5d475f1508e1c57c627a Mon Sep 17 00:00:00 2001 From: evanebb Date: Mon, 7 Apr 2025 00:05:36 +0200 Subject: [PATCH 08/10] Return error when retrieving user credentials --- store/postgres/usercredentials.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/store/postgres/usercredentials.go b/store/postgres/usercredentials.go index ab9692d..c85bc8f 100644 --- a/store/postgres/usercredentials.go +++ b/store/postgres/usercredentials.go @@ -22,8 +22,12 @@ func (s UserCredentialsStore) GetByUserID(ctx context.Context, id uuid.UUID) (lo query := "SELECT id, password_hash FROM users WHERE id = $1" err := s.QuerierFromContext(ctx).QueryRow(ctx, query, id).Scan(&c.UserID, &c.PasswordHash) - if errors.Is(err, pgx.ErrNoRows) { - return local.UserCredentials{}, local.ErrNoCredentials + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return local.UserCredentials{}, local.ErrNoCredentials + } + + return local.UserCredentials{}, err } if len(c.PasswordHash) == 0 { From 34e2dac7dfb7ac6f444135f021f7ae5f26eae4ca Mon Sep 17 00:00:00 2001 From: evanebb Date: Mon, 7 Apr 2025 00:06:08 +0200 Subject: [PATCH 09/10] Use proper error instead of error that is never returned --- server/handlers/security.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handlers/security.go b/server/handlers/security.go index 0d6cc06..8fd5027 100644 --- a/server/handlers/security.go +++ b/server/handlers/security.go @@ -67,7 +67,7 @@ func (s SecurityHandler) HandleUsernamePassword(ctx context.Context, operationNa credentials, err := s.credentialsStore.GetByUserID(ctx, u.ID) if err != nil { - if errors.Is(err, user.ErrNotFound) { + if errors.Is(err, local.ErrNoCredentials) { s.logger.DebugContext(ctx, "no credentials set for user", slog.String("username", t.GetUsername())) return ctx, newErrorResponse(http.StatusUnauthorized, "authentication failed") } From 1a001d74daed50974fec01cdef37b49bcb9ab610 Mon Sep 17 00:00:00 2001 From: evanebb Date: Mon, 7 Apr 2025 00:09:57 +0200 Subject: [PATCH 10/10] Add missing require role for user endpoints --- server/handlers/user.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/handlers/user.go b/server/handlers/user.go index ccf3767..619b82d 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -88,6 +88,10 @@ func (h UserHandler) GetUser(ctx context.Context, params oas.GetUserParams) (*oa } func (h UserHandler) DeleteUser(ctx context.Context, params oas.DeleteUserParams) error { + if err := h.requireRole(ctx, user.RoleAdmin); err != nil { + return err + } + currentUser, ok := AuthenticatedUserFromContext(ctx) if !ok { h.logger.ErrorContext(ctx, "could not parse user from request context") @@ -112,6 +116,10 @@ func (h UserHandler) DeleteUser(ctx context.Context, params oas.DeleteUserParams } func (h UserHandler) ChangeUserPassword(ctx context.Context, req *oas.UserPasswordChangeRequest, params oas.ChangeUserPasswordParams) error { + if err := h.requireRole(ctx, user.RoleAdmin); err != nil { + return err + } + u, err := h.getUserFromRequest(ctx, params.Username) if err != nil { return err