From 85f8ac60e7a5fa847e8ab30bdd3eb9783c2c1885 Mon Sep 17 00:00:00 2001 From: Alexey Date: Sun, 21 Dec 2025 00:00:03 +0200 Subject: [PATCH] some changes --- api/acl_admin/errors.go | 31 ++ api/acl_admin/handle.go | 11 +- api/acl_admin/resources.go | 302 +++++++------------ api/acl_admin/resources_models.go | 59 +--- api/acl_admin/roles.go | 348 +++++++++------------- api/acl_admin/roles_models.go | 64 +--- api/auth/handle.go | 2 + api/router.go | 2 +- docs/docs.go | 475 ++++++++---------------------- docs/swagger.json | 475 ++++++++---------------------- docs/swagger.yaml | 359 +++++++--------------- internal/acl/models.go | 10 +- internal/acl/roles.go | 4 +- internal/user/model.go | 11 +- 14 files changed, 678 insertions(+), 1475 deletions(-) diff --git a/api/acl_admin/errors.go b/api/acl_admin/errors.go index dee7889..642042d 100644 --- a/api/acl_admin/errors.go +++ b/api/acl_admin/errors.go @@ -1,5 +1,11 @@ package api_acladmin +import ( + "encoding/json" + "log/slog" + "net/http" +) + const ( ErrorInvalidRequestBody = "INVALID_REQUEST_BODY" ErrorInternalServerError = "INTERNAL_SERVER_ERROR" @@ -26,3 +32,28 @@ const ( const ( ErrorACLServiceNotInitialized = "ACL service is not initialized" ) + +// RFC-7807 (Problem Details) +type ProblemDetails struct { + Type string `json:"type" example:"https://api.triggerssmith.com/errors/role-not-found"` + Title string `json:"title" example:"Role not found"` + Status int `json:"status" example:"404"` + Detail string `json:"detail" example:"No role with ID 42"` + Instance string `json:"instance" example:"/api/acl/roles/42"` +} + +var typeDomain = "https://api.triggerssmith.com" + +func writeProblem(w http.ResponseWriter, status int, typ, title, detail string, r *http.Request) { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(status) + prob := ProblemDetails{ + Type: typeDomain + typ, + Title: title, + Status: status, + Detail: detail, + Instance: r.URL.Path, + } + slog.Warn("new problem", "type", typ, "title", title, "detail", detail, "instance", r.URL.Path, "status", status) + _ = json.NewEncoder(w).Encode(prob) +} diff --git a/api/acl_admin/handle.go b/api/acl_admin/handle.go index 851f766..a4c1c71 100644 --- a/api/acl_admin/handle.go +++ b/api/acl_admin/handle.go @@ -51,11 +51,12 @@ func MustRoute(config *config.Config, aclService *acl.Service, authService *auth // DELETE /roles/{roleId}/resources/{resId} — убрать ресурс return func(r chi.Router) { // Roles - r.Get("/roles", h.getRoles) // list all roles - r.Post("/roles", h.createRole) // create a new role - r.Get("/roles/{roleId}", h.getRole) // get a role by ID - r.Patch("/roles/{roleId}", h.updateRole) // update a role by ID - r.Delete("/roles/{roleId}", h.deleteRole) // delete a role by ID + r.Get("/roles", h.getRoles) // list all roles + r.Post("/roles", h.createRole) // create a new role + r.Get("/roles/{roleId}", h.getRole) // get a role by ID + r.Get("/roles/{roleId}/users", h.getRoleUsers) // get all assigned users to a role + r.Patch("/roles/{roleId}", h.updateRole) // update a role by ID + r.Delete("/roles/{roleId}", h.deleteRole) // delete a role by ID // // Resources r.Get("/resources", h.getResources) // list all resources diff --git a/api/acl_admin/resources.go b/api/acl_admin/resources.go index 5c88b96..6986ebb 100644 --- a/api/acl_admin/resources.go +++ b/api/acl_admin/resources.go @@ -10,68 +10,56 @@ import ( "github.com/go-chi/chi/v5" ) -// @Summary Get all resources -// @Tags resources -// @Produce json -// @Success 200 {object} getResourcesResponse -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/resources [get] +// @Summary Get all resources +// @Tags resources +// @Produce json +// @Success 200 {array} getResourcesResponse +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/resources [get] func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + resources, err := h.a.GetResources() if err != nil { switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to get resources", - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } - _ = json.NewEncoder(w).Encode(func() getResourcesResponse { - resp := make(getResourcesResponse, 0, len(resources)) - for _, res := range resources { - resp = append(resp, struct { - ID uint `json:"id" example:"1"` - Key string `json:"key" example:"html.view"` - }{ - ID: res.ID, - Key: res.Key, - }) - } - return resp - }()) + type R struct { + ID uint `json:"id" example:"1"` + Key string `json:"key" example:"html.view"` + } + + resp := make([]R, 0, len(resources)) + for _, res := range resources { + resp = append(resp, R{ID: res.ID, Key: res.Key}) + } + + _ = json.NewEncoder(w).Encode(resp) } -// @Summary Get resource by ID -// @Tags resources -// @Produce json -// @Param resourceId path int true "Resource ID" example(1) -// @Success 200 {object} getResourceResponse -// @Failure 400 {object} getResourceErrorInvalidResourceID -// @Failure 404 {object} getResourceErrorResourceNotFound -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/resources/{resourceId} [get] +// @Summary Get resource by ID +// @Tags resources +// @Produce json +// @Param resourceId path int true "Resource ID" example(1) +// @Success 200 {object} getResourceResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/resources/{resourceId} [get] func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + resourceIDStr := chi.URLParam(r, "resourceId") resourceID, err := strconv.Atoi(resourceIDStr) if err != nil || resourceID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(getResourceErrorInvalidResourceID{ - Error: ErrorInvalidResourceID, - Details: "Resource ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) return } @@ -79,27 +67,14 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) case acl.ErrResourceNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(getResourceErrorResourceNotFound{ - Error: ErrorResourceNotFound, - Details: "No resource with ID " + resourceIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to get resource with ID " + resourceIDStr, - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } _ = json.NewEncoder(w).Encode(getResourceResponse{ @@ -108,62 +83,41 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) { }) } -// @Summary Create resource -// @Tags resources -// @Accept json -// @Produce json -// @Param request body createResourceRequest true "Resource" -// @Success 201 {object} createResourceResponse -// @Failure 400 {object} errorInvalidRequestBody -// @Failure 400 {object} createResourceErrorInvalidResourceKey -// @Failure 409 {object} createResourceErrorResourceAlreadyExists -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/resources [post] +// @Summary Create resource +// @Tags resources +// @Accept json +// @Produce json +// @Param request body createResourceRequest true "Resource" +// @Success 201 {object} createResourceResponse +// @Failure 400 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/resources [post] func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + var req createResourceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errorInvalidRequestBody{ - Error: ErrorInvalidRequestBody, - Details: "Request body is not valid JSON", - }) + writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) return } resourceID, err := h.a.CreateResource(req.Key) if err != nil { - slog.Error("Failed to create resource", "error", err.Error()) + slog.Error("Failed to create resource", "error", err) + switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) case acl.ErrInvalidResourceKey: - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(createResourceErrorInvalidResourceKey{ - Error: ErrorFailedToCreateResource, - Details: "Resource key must be non-empty", - }) - return + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r) case acl.ErrResourceAlreadyExists: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(createResourceErrorResourceAlreadyExists{ - Error: ErrorFailedToCreateResource, - Details: "Resource with key '" + req.Key + "' already exists", - }) - return + writeProblem(w, http.StatusConflict, "/errors/acl/resource-already-exists", "Resource already exists", "Resource '"+req.Key+"' already exists", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to create resource with key '" + req.Key + "'", - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } w.WriteHeader(http.StatusCreated) @@ -173,148 +127,96 @@ func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) }) } -// @Summary Update resource -// @Tags resources -// @Accept json -// @Produce json -// @Param resourceId path int true "Resource ID" example(1) -// @Param request body updateResourceRequest true "Resource" -// @Success 200 {object} updateResourceResponse -// @Failure 400 {object} errorInvalidRequestBody -// @Failure 400 {object} updateResourceErrorInvalidResourceID -// @Failure 400 {object} updateResourceErrorInvalidResourceKey -// @Failure 404 {object} updateResourceErrorResourceNotFound -// @Failure 409 {object} updateResourceErrorResourceKeyAlreadyExists -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/resources/{resourceId} [patch] +// @Summary Update resource +// @Tags resources +// @Accept json +// @Produce json +// @Param resourceId path int true "Resource ID" example(1) +// @Param request body updateResourceRequest true "Resource" +// @Success 200 {object} updateResourceResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/resources/{resourceId} [patch] func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + var req updateResourceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errorInvalidRequestBody{ - Error: ErrorInvalidRequestBody, - Details: "Request body is not valid JSON", - }) + writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) return } resourceIDStr := chi.URLParam(r, "resourceId") resourceID, err := strconv.Atoi(resourceIDStr) if err != nil || resourceID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(updateResourceErrorInvalidResourceID{ - Error: ErrorInvalidResourceID, - Details: "Resource ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) return } err = h.a.UpdateResource(uint(resourceID), req.Key) if err != nil { - slog.Error("Failed to update resource", "error", err.Error()) + slog.Error("Failed to update resource", "error", err) + switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) case acl.ErrInvalidResourceKey: - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(updateResourceErrorInvalidResourceKey{ - Error: ErrorFailedToUpdateResource, - Details: "Invalid resource key", - }) - return + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r) case acl.ErrResourceNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(updateResourceErrorResourceNotFound{ - Error: ErrorFailedToUpdateResource, - Details: "No resource with ID " + resourceIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) case acl.ErrSameResourceKey: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(updateResourceErrorResourceKeyAlreadyExists{ - Error: ErrorFailedToUpdateResource, - Details: "Resource with key '" + req.Key + "' already exists", - }) - return + writeProblem(w, http.StatusConflict, "/errors/acl/resource-key-already-exists", "Resource key already exists", "Resource key '"+req.Key+"' already exists", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to update resource with key '" + req.Key + "'", - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } - w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(updateResourceResponse{ ID: uint(resourceID), Key: req.Key, }) } -// @Summary Delete resource -// @Tags resources -// @Produce json -// @Param resourceId path int true "Resource ID" example(1) -// @Success 200 -// @Failure 400 {object} deleteResourceErrorInvalidResourceID -// @Failure 404 {object} deleteResourceErrorResourceNotFound -// @Failure 409 {object} deleteResourceErrorResourceInUse -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/resources/{resourceId} [delete] +// @Summary Delete resource +// @Tags resources +// @Produce json +// @Param resourceId path int true "Resource ID" example(1) +// @Success 200 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/resources/{resourceId} [delete] func (h *aclAdminHandler) deleteResource(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + resourceIDStr := chi.URLParam(r, "resourceId") resourceID, err := strconv.Atoi(resourceIDStr) if err != nil || resourceID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(deleteResourceErrorInvalidResourceID{ - Error: ErrorInvalidResourceID, - Details: "Resource ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) return } err = h.a.DeleteResource(uint(resourceID)) if err != nil { - slog.Error("Failed to delete resource", "error", err.Error()) + slog.Error("Failed to delete resource", "error", err) + switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) case acl.ErrResourceNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(deleteResourceErrorResourceNotFound{ - Error: ErrorFailedToDeleteResource, - Details: "No resource with ID " + resourceIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) case acl.ErrResourceInUse: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(deleteResourceErrorResourceInUse{ - Error: ErrorFailedToDeleteResource, - Details: "Resource with ID " + resourceIDStr + " is used and cannot be deleted", - }) - return + writeProblem(w, http.StatusConflict, "/errors/acl/resource-in-use", "Resource in use", "Resource "+resourceIDStr+" is in use", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to delete resource with ID '" + resourceIDStr + "'", - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } w.WriteHeader(http.StatusOK) diff --git a/api/acl_admin/resources_models.go b/api/acl_admin/resources_models.go index 8ae2e41..ca433f3 100644 --- a/api/acl_admin/resources_models.go +++ b/api/acl_admin/resources_models.go @@ -7,6 +7,8 @@ type getResourcesResponse []struct { Key string `json:"key" example:"html.view"` } +var _ getResourcesResponse // for documentation + /*******************************************************************/ // used in getResource() type getResourceResponse struct { @@ -14,16 +16,6 @@ type getResourceResponse struct { Key string `json:"key" example:"html.view"` } -type getResourceErrorInvalidResourceID struct { - Error string `json:"error" example:"INVALID_RESOURCE_ID"` - Details string `json:"details" example:"Resource ID must be positive integer"` -} - -type getResourceErrorResourceNotFound struct { - Error string `json:"error" example:"RESOURCE_NOT_FOUND"` - Details string `json:"details" example:"No resource with ID 123"` -} - /*******************************************************************/ // used in createResource() type createResourceRequest struct { @@ -35,16 +27,6 @@ type createResourceResponse struct { Key string `json:"key" example:"html.view"` } -type createResourceErrorResourceAlreadyExists struct { - Error string `json:"error" example:"FAILED_TO_CREATE_RESOURCE"` - Details string `json:"details" example:"Resource with key 'html.view' already exists"` -} - -type createResourceErrorInvalidResourceKey struct { - Error string `json:"error" example:"FAILED_TO_CREATE_RESOURCE"` - Details string `json:"details" example:"Invalid resource key"` -} - /*******************************************************************/ // used in updateResource() type updateResourceRequest struct { @@ -55,40 +37,3 @@ type updateResourceResponse struct { ID uint `json:"id" example:"1"` Key string `json:"key" example:"html.view"` } - -type updateResourceErrorResourceNotFound struct { - Error string `json:"error" example:"RESOURCE_NOT_FOUND"` - Details string `json:"details" example:"No resource with ID 123"` -} - -type updateResourceErrorInvalidResourceID struct { - Error string `json:"error" example:"INVALID_RESOURCE_ID"` - Details string `json:"details" example:"Resource ID must be positive integer"` -} - -type updateResourceErrorInvalidResourceKey struct { - Error string `json:"error" example:"FAILED_TO_UPDATE_RESOURCE"` - Details string `json:"details" example:"Invalid resource key"` -} - -type updateResourceErrorResourceKeyAlreadyExists struct { - Error string `json:"error" example:"FAILED_TO_UPDATE_RESOURCE"` - Details string `json:"details" example:"Resource with key 'html.view' already exists"` -} - -/*******************************************************************/ -// used in deleteResource() -type deleteResourceErrorResourceNotFound struct { - Error string `json:"error" example:"RESOURCE_NOT_FOUND"` - Details string `json:"details" example:"No resource with ID 123"` -} - -type deleteResourceErrorInvalidResourceID struct { - Error string `json:"error" example:"INVALID_RESOURCE_ID"` - Details string `json:"details" example:"Resource ID must be positive integer"` -} - -type deleteResourceErrorResourceInUse struct { - Error string `json:"error" example:"FAILED_TO_DELETE_RESOURCE"` - Details string `json:"details" example:"Resource with ID 123 is used and cannot be deleted"` -} diff --git a/api/acl_admin/roles.go b/api/acl_admin/roles.go index b35e883..c0dbb88 100644 --- a/api/acl_admin/roles.go +++ b/api/acl_admin/roles.go @@ -10,158 +10,158 @@ import ( "github.com/go-chi/chi/v5" ) -// @Summary Get all roles -// @Tags roles -// @Produce json -// @Success 200 {object} getRolesResponse -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/roles [get] +// @Summary Get all roles +// @Tags roles +// @Produce json +// @Success 200 {array} getRolesResponse +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles [get] func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") roles, err := h.a.GetRoles() if err != nil { switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to get roles", - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } - _ = json.NewEncoder(w).Encode(func() getRolesResponse { - // Transform acl.Role to getRolesResponse - resp := make(getRolesResponse, 0, len(roles)) - for _, role := range roles { - resp = append(resp, struct { - ID uint `json:"id" example:"1"` - Name string `json:"name" example:"admin"` - }{ - ID: role.ID, - Name: role.Name, - }) - } - return resp - }()) + + type R struct { + ID uint `json:"id" example:"1"` + Name string `json:"name" example:"admin"` + } + + resp := make([]R, 0, len(roles)) + for _, role := range roles { + resp = append(resp, R{ID: role.ID, Name: role.Name}) + } + + _ = json.NewEncoder(w).Encode(resp) } -// @Summary Get role by ID -// @Tags roles -// @Produce json -// @Param roleId path int true "Role ID" example(1) -// @Success 200 {object} getRoleResponse -// @Failure 400 {object} getRoleErrorInvalidRoleID -// @Failure 404 {object} getRoleErrorRoleNotFound -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/roles/{roleId} [get] +// @Summary Get role users +// @Tags roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Success 200 {array} getRoleUsersResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId}/users [get] +func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + roleIDStr := chi.URLParam(r, "roleId") + roleID, err := strconv.Atoi(roleIDStr) + if err != nil || roleID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) + return + } + + role, err := h.a.GetRoleByID(uint(roleID)) + if err != nil { + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) + default: + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + if len(role.Users) == 0 { + writeProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r) + return + } + var respUsers getRoleUsersResponse + for _, user := range role.Users { + respUsers = append(respUsers, getRoleUser{ + ID: user.ID, + Name: user.Username, + Email: user.Email, + }) + } + _ = json.NewEncoder(w).Encode(respUsers) +} + +// @Summary Get role by ID +// @Tags roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Success 200 {object} getRoleResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId} [get] func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") roleIDStr := chi.URLParam(r, "roleId") roleID, err := strconv.Atoi(roleIDStr) if err != nil || roleID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(getRoleErrorInvalidRoleID{ - Error: ErrorInvalidRoleID, - Details: "Role ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) return } + role, err := h.a.GetRoleByID(uint(roleID)) if err != nil { switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) case acl.ErrRoleNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(getRoleErrorRoleNotFound{ - Error: ErrorRoleNotFound, - Details: "No role with ID " + roleIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to get role with ID " + roleIDStr, - }) - return + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } + _ = json.NewEncoder(w).Encode(getRoleResponse{ ID: role.ID, Name: role.Name, }) } -// @Summary Create role -// @Tags roles -// @Accept json -// @Produce json -// @Param request body createRoleRequest true "Role" -// @Success 201 {object} createRoleResponse -// @Failure 400 {object} errorInvalidRequestBody -// @Failure 401 {object} createRoleErrorInvalidRoleName -// @Failure 409 {object} createRoleErrorRoleAlreadyExists -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/roles [post] +// @Summary Create role +// @Tags roles +// @Accept json +// @Produce json +// @Param request body createRoleRequest true "Role" +// @Success 201 {object} createRoleResponse +// @Failure 400 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles [post] func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + var req createRoleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errorInvalidRequestBody{ - Error: ErrorInvalidRequestBody, - Details: "Request body is not valid JSON", - }) + writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) return } + roleID, err := h.a.CreateRole(req.Name) if err != nil { slog.Error("Failed to create role", "error", err.Error()) switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return - case acl.ErrRoleAlreadyExists: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(createRoleErrorRoleAlreadyExists{ - Error: ErrorFailedToCreateRole, - Details: "Role with name '" + req.Name + "' already exists", - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) case acl.ErrInvalidRoleName: - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(createRoleErrorInvalidRoleName{ - Error: ErrorFailedToCreateRole, - Details: "Role name must be non-empty string", - }) - return + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) + case acl.ErrRoleAlreadyExists: + writeProblem(w, http.StatusConflict, "/errors/acl/role-already-exists", "Role already exists", "Role '"+req.Name+"' already exists", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to create role with name '" + req.Name + "'", - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } + w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(createRoleResponse{ ID: roleID, @@ -169,146 +169,92 @@ func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) { }) } -// @Summary Update role -// @Tags roles -// @Accept json -// @Produce json -// @Param roleId path int true "Role ID" example(1) -// @Param request body updateRoleRequest true "Role" -// @Success 200 {object} updateRoleResponse -// @Failure 400 {object} errorInvalidRequestBody -// @Failure 400 {object} updateRoleErrorInvalidRoleID -// @Failure 400 {object} updateRoleErrorInvalidRoleName -// @Failure 404 {object} updateRoleErrorRoleNotFound -// @Failure 409 {object} updateRoleErrorRoleNameAlreadyExists -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/roles/{roleId} [patch] +// @Summary Update role +// @Tags roles +// @Accept json +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Param request body updateRoleRequest true "Role" +// @Success 200 {object} updateRoleResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId} [patch] func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + var req updateRoleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errorInvalidRequestBody{ - Error: ErrorInvalidRequestBody, - Details: "Request body is not valid JSON", - }) + writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) return } + roleIDStr := chi.URLParam(r, "roleId") roleID, err := strconv.Atoi(roleIDStr) if err != nil || roleID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(updateRoleErrorInvalidRoleID{ - Error: ErrorInvalidRoleID, - Details: "Role ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) return } + err = h.a.UpdateRole(uint(roleID), req.Name) - // TODO: make error handling more specific in acl service if err != nil { slog.Error("Failed to update role", "error", err.Error()) switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) case acl.ErrInvalidRoleName: - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(updateRoleErrorInvalidRoleName{ - Error: ErrorFailedToUpdateRole, - Details: "Invalid role name", - }) - return + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) case acl.ErrRoleNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(updateRoleErrorRoleNotFound{ - Error: ErrorFailedToUpdateRole, - Details: "No role with ID " + roleIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) case acl.ErrSameRoleName: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(updateRoleErrorRoleNameAlreadyExists{ - Error: ErrorFailedToUpdateRole, - Details: "Role with name '" + req.Name + "' already exists", - }) - return + writeProblem(w, http.StatusConflict, "/errors/acl/role-name-already-exists", "Role name already exists", "Role '"+req.Name+"' already exists", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to update role with name '" + req.Name + "'", - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } - w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(updateRoleResponse{ ID: uint(roleID), Name: req.Name, }) } -// @Summary Delete role -// @Tags roles -// @Produce json -// @Param roleId path int true "Role ID" example(1) -// @Success 200 -// @Failure 400 {object} deleteRoleErrorInvalidRoleID -// @Failure 404 {object} deleteRoleErrorRoleNotFound -// @Failure 409 {object} deleteRoleErrorRoleInUse -// @Failure 500 {object} errorInternalServerError -// @Router /api/acl/roles/{roleId} [delete] +// @Summary Delete role +// @Tags roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Success 200 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId} [delete] func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") roleIDStr := chi.URLParam(r, "roleId") roleID, err := strconv.Atoi(roleIDStr) if err != nil || roleID < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(deleteRoleErrorInvalidRoleID{ - Error: ErrorInvalidRoleID, - Details: "Role ID must be positive integer", - }) + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) return } + err = h.a.DeleteRole(uint(roleID)) - // TODO: make error handling more specific in acl service if err != nil { slog.Error("Failed to delete role", "error", err.Error()) switch err { case acl.ErrNotInitialized: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: ErrorACLServiceNotInitialized, - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) case acl.ErrRoleNotFound: - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(deleteRoleErrorRoleNotFound{ - Error: ErrorFailedToDeleteRole, - Details: "No role with ID " + roleIDStr, - }) - return + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) case acl.ErrRoleInUse: - w.WriteHeader(http.StatusConflict) - _ = json.NewEncoder(w).Encode(deleteRoleErrorRoleInUse{ - Error: ErrorFailedToDeleteRole, - Details: "Role with ID " + roleIDStr + " is assigned to users and cannot be deleted", - }) - return + writeProblem(w, http.StatusConflict, "/errors/acl/role-in-use", "Role in use", "Role "+roleIDStr+" is assigned to at least one user and cannot be deleted", r) default: - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errorInternalServerError{ - Error: ErrorInternalServerError, - Details: "Failed to delete role with ID '" + roleIDStr + "'", - }) - return + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } + return } + w.WriteHeader(http.StatusOK) } diff --git a/api/acl_admin/roles_models.go b/api/acl_admin/roles_models.go index e538fd7..47e03d1 100644 --- a/api/acl_admin/roles_models.go +++ b/api/acl_admin/roles_models.go @@ -7,6 +7,8 @@ type getRolesResponse []struct { Name string `json:"name" example:"admin"` } +var _ getRolesResponse + /*******************************************************************/ // used in getRole() type getRoleResponse struct { @@ -14,15 +16,14 @@ type getRoleResponse struct { Name string `json:"name" example:"admin"` } -type getRoleErrorInvalidRoleID struct { - Error string `json:"error" example:"INVALID_ROLE_ID"` - Details string `json:"details" example:"Role ID must be positive integer"` -} - -type getRoleErrorRoleNotFound struct { - Error string `json:"error" example:"ROLE_NOT_FOUND"` - Details string `json:"details" example:"No role with ID 123"` +/*******************************************************************/ +// used in getRoleUsers() +type getRoleUser struct { + ID uint `json:"id" example:"1"` + Name string `json:"username" example:"admin"` + Email string `json:"email" example:"admin@triggerssmith.com"` } +type getRoleUsersResponse []getRoleUser /*******************************************************************/ // used in createRole() @@ -35,16 +36,6 @@ type createRoleResponse struct { Name string `json:"name" example:"admin"` } -type createRoleErrorRoleAlreadyExists struct { - Error string `json:"error" example:"FAILED_TO_CREATE_ROLE"` - Details string `json:"details" example:"Role with name 'admin' already exists"` -} - -type createRoleErrorInvalidRoleName struct { - Error string `json:"error" example:"FAILED_TO_CREATE_ROLE"` - Details string `json:"details" example:"Invalid role name"` -} - /*******************************************************************/ // used in updateRole() type updateRoleRequest struct { @@ -55,40 +46,3 @@ type updateRoleResponse struct { ID uint `json:"id" example:"1"` Name string `json:"name" example:"admin"` } - -type updateRoleErrorRoleNotFound struct { - Error string `json:"error" example:"ROLE_NOT_FOUND"` - Details string `json:"details" example:"No role with ID 123"` -} - -type updateRoleErrorInvalidRoleID struct { - Error string `json:"error" example:"INVALID_ROLE_ID"` - Details string `json:"details" example:"Role ID must be positive integer"` -} - -type updateRoleErrorInvalidRoleName struct { - Error string `json:"error" example:"FAILED_TO_UPDATE_ROLE"` - Details string `json:"details" example:"Invalid role name"` -} - -type updateRoleErrorRoleNameAlreadyExists struct { - Error string `json:"error" example:"FAILED_TO_UPDATE_ROLE"` - Details string `json:"details" example:"Role with name 'admin' already exists"` -} - -/*******************************************************************/ -// used in deleteRole() -type deleteRoleErrorRoleNotFound struct { - Error string `json:"error" example:"ROLE_NOT_FOUND"` - Details string `json:"details" example:"No role with ID 123"` -} - -type deleteRoleErrorInvalidRoleID struct { - Error string `json:"error" example:"INVALID_ROLE_ID"` - Details string `json:"details" example:"Role ID must be positive integer"` -} - -type deleteRoleErrorRoleInUse struct { - Error string `json:"error" example:"FAILED_TO_DELETE_ROLE"` - Details string `json:"details" example:"Role with ID 123 is assigned to users and cannot be deleted"` -} diff --git a/api/auth/handle.go b/api/auth/handle.go index 05c6a09..c6b717f 100644 --- a/api/auth/handle.go +++ b/api/auth/handle.go @@ -5,6 +5,7 @@ package api_auth import ( "encoding/json" "fmt" + "log/slog" "net/http" "time" @@ -79,6 +80,7 @@ func (h *authHandler) handleRegister(w http.ResponseWriter, r *http.Request) { user, err := h.a.Register(req.Username, req.Email, req.Password) if err != nil { + slog.Error("Failed to register user", "error", err) http.Error(w, "Registration failed", http.StatusInternalServerError) return } diff --git a/api/router.go b/api/router.go index 5be8494..6eea7d2 100644 --- a/api/router.go +++ b/api/router.go @@ -90,7 +90,7 @@ func (r *Router) MustRoute() chi.Router { api.Route("/block", api_block.MustRoute(r.cfg)) authRoute := api_auth.MustRoute(r.cfg, r.authService) api.Route("/auth", authRoute) - api.Route("/users", authRoute) // legacy support + //api.Route("/users", authRoute) // legacy support aclAdminRoute := api_acladmin.MustRoute(r.cfg, r.aclService, r.authService) api.Route("/acl", aclAdminRoute) api.Route("/acl-admin", aclAdminRoute) // legacy support diff --git a/docs/docs.go b/docs/docs.go index f4fe8fb..cfc731f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -30,15 +30,18 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "key": { - "type": "string", - "example": "html.view" + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "key": { + "type": "string", + "example": "html.view" + } } } } @@ -47,7 +50,7 @@ const docTemplate = `{ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -84,19 +87,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.createResourceErrorInvalidResourceKey" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.createResourceErrorResourceAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -131,19 +134,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.getResourceErrorInvalidResourceID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.getResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -173,25 +176,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorInvalidResourceID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorResourceInUse" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -236,25 +239,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorInvalidResourceKey" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorResourceKeyAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -275,15 +278,18 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "admin" + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "admin" + } } } } @@ -292,7 +298,7 @@ const docTemplate = `{ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -329,25 +335,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.errorInvalidRequestBody" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api_acladmin.createRoleErrorInvalidRoleName" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.createRoleErrorRoleAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -382,19 +382,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.getRoleErrorInvalidRoleID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.getRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -424,25 +424,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorInvalidRoleID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorRoleInUse" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -487,25 +487,78 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorInvalidRoleName" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorRoleNameAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/roles/{roleId}/users": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "roles" + ], + "summary": "Get role users", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getRoleUser" + } + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -513,29 +566,28 @@ const docTemplate = `{ } }, "definitions": { - "api_acladmin.createResourceErrorInvalidResourceKey": { + "api_acladmin.ProblemDetails": { "type": "object", "properties": { - "details": { + "detail": { "type": "string", - "example": "Invalid resource key" + "example": "No role with ID 42" }, - "error": { + "instance": { "type": "string", - "example": "FAILED_TO_CREATE_RESOURCE" - } - } - }, - "api_acladmin.createResourceErrorResourceAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with key 'html.view' already exists" + "example": "/api/acl/roles/42" }, - "error": { + "status": { + "type": "integer", + "example": 404 + }, + "title": { "type": "string", - "example": "FAILED_TO_CREATE_RESOURCE" + "example": "Role not found" + }, + "type": { + "type": "string", + "example": "https://api.triggerssmith.com/errors/role-not-found" } } }, @@ -561,32 +613,6 @@ const docTemplate = `{ } } }, - "api_acladmin.createRoleErrorInvalidRoleName": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid role name" - }, - "error": { - "type": "string", - "example": "FAILED_TO_CREATE_ROLE" - } - } - }, - "api_acladmin.createRoleErrorRoleAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with name 'admin' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_CREATE_ROLE" - } - } - }, "api_acladmin.createRoleRequest": { "type": "object", "properties": { @@ -609,134 +635,6 @@ const docTemplate = `{ } } }, - "api_acladmin.deleteResourceErrorInvalidResourceID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.deleteResourceErrorResourceInUse": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with ID 123 is used and cannot be deleted" - }, - "error": { - "type": "string", - "example": "FAILED_TO_DELETE_RESOURCE" - } - } - }, - "api_acladmin.deleteResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" - } - } - }, - "api_acladmin.deleteRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.deleteRoleErrorRoleInUse": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with ID 123 is assigned to users and cannot be deleted" - }, - "error": { - "type": "string", - "example": "FAILED_TO_DELETE_ROLE" - } - } - }, - "api_acladmin.deleteRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, - "api_acladmin.errorInternalServerError": { - "type": "object", - "properties": { - "details": { - "type": "string" - }, - "error": { - "type": "string" - } - } - }, - "api_acladmin.errorInvalidRequestBody": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Request body is not valid JSON" - }, - "error": { - "type": "string", - "example": "INVALID_REQUEST_BODY" - } - } - }, - "api_acladmin.getResourceErrorInvalidResourceID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.getResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" - } - } - }, "api_acladmin.getResourceResponse": { "type": "object", "properties": { @@ -750,32 +648,6 @@ const docTemplate = `{ } } }, - "api_acladmin.getRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.getRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, "api_acladmin.getRoleResponse": { "type": "object", "properties": { @@ -789,55 +661,20 @@ const docTemplate = `{ } } }, - "api_acladmin.updateResourceErrorInvalidResourceID": { + "api_acladmin.getRoleUser": { "type": "object", "properties": { - "details": { + "userEmail": { "type": "string", - "example": "Resource ID must be positive integer" + "example": "admin@triggerssmith.com" }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.updateResourceErrorInvalidResourceKey": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid resource key" + "userId": { + "type": "integer", + "example": 1 }, - "error": { + "userName": { "type": "string", - "example": "FAILED_TO_UPDATE_RESOURCE" - } - } - }, - "api_acladmin.updateResourceErrorResourceKeyAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with key 'html.view' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_RESOURCE" - } - } - }, - "api_acladmin.updateResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" + "example": "admin" } } }, @@ -863,58 +700,6 @@ const docTemplate = `{ } } }, - "api_acladmin.updateRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.updateRoleErrorInvalidRoleName": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid role name" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_ROLE" - } - } - }, - "api_acladmin.updateRoleErrorRoleNameAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with name 'admin' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_ROLE" - } - } - }, - "api_acladmin.updateRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, "api_acladmin.updateRoleRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 41c22c7..69f3f69 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -19,15 +19,18 @@ "schema": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "key": { - "type": "string", - "example": "html.view" + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "key": { + "type": "string", + "example": "html.view" + } } } } @@ -36,7 +39,7 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -73,19 +76,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.createResourceErrorInvalidResourceKey" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.createResourceErrorResourceAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -120,19 +123,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.getResourceErrorInvalidResourceID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.getResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -162,25 +165,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorInvalidResourceID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.deleteResourceErrorResourceInUse" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -225,25 +228,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorInvalidResourceKey" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorResourceNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.updateResourceErrorResourceKeyAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -264,15 +267,18 @@ "schema": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "admin" + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "admin" + } } } } @@ -281,7 +287,7 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -318,25 +324,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.errorInvalidRequestBody" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/api_acladmin.createRoleErrorInvalidRoleName" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.createRoleErrorRoleAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -371,19 +371,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.getRoleErrorInvalidRoleID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.getRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -413,25 +413,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorInvalidRoleID" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.deleteRoleErrorRoleInUse" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -476,25 +476,78 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorInvalidRoleName" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorRoleNotFound" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/api_acladmin.updateRoleErrorRoleNameAlreadyExists" + "$ref": "#/definitions/api_acladmin.ProblemDetails" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/api_acladmin.errorInternalServerError" + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/roles/{roleId}/users": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "roles" + ], + "summary": "Get role users", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getRoleUser" + } + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" } } } @@ -502,29 +555,28 @@ } }, "definitions": { - "api_acladmin.createResourceErrorInvalidResourceKey": { + "api_acladmin.ProblemDetails": { "type": "object", "properties": { - "details": { + "detail": { "type": "string", - "example": "Invalid resource key" + "example": "No role with ID 42" }, - "error": { + "instance": { "type": "string", - "example": "FAILED_TO_CREATE_RESOURCE" - } - } - }, - "api_acladmin.createResourceErrorResourceAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with key 'html.view' already exists" + "example": "/api/acl/roles/42" }, - "error": { + "status": { + "type": "integer", + "example": 404 + }, + "title": { "type": "string", - "example": "FAILED_TO_CREATE_RESOURCE" + "example": "Role not found" + }, + "type": { + "type": "string", + "example": "https://api.triggerssmith.com/errors/role-not-found" } } }, @@ -550,32 +602,6 @@ } } }, - "api_acladmin.createRoleErrorInvalidRoleName": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid role name" - }, - "error": { - "type": "string", - "example": "FAILED_TO_CREATE_ROLE" - } - } - }, - "api_acladmin.createRoleErrorRoleAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with name 'admin' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_CREATE_ROLE" - } - } - }, "api_acladmin.createRoleRequest": { "type": "object", "properties": { @@ -598,134 +624,6 @@ } } }, - "api_acladmin.deleteResourceErrorInvalidResourceID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.deleteResourceErrorResourceInUse": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with ID 123 is used and cannot be deleted" - }, - "error": { - "type": "string", - "example": "FAILED_TO_DELETE_RESOURCE" - } - } - }, - "api_acladmin.deleteResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" - } - } - }, - "api_acladmin.deleteRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.deleteRoleErrorRoleInUse": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with ID 123 is assigned to users and cannot be deleted" - }, - "error": { - "type": "string", - "example": "FAILED_TO_DELETE_ROLE" - } - } - }, - "api_acladmin.deleteRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, - "api_acladmin.errorInternalServerError": { - "type": "object", - "properties": { - "details": { - "type": "string" - }, - "error": { - "type": "string" - } - } - }, - "api_acladmin.errorInvalidRequestBody": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Request body is not valid JSON" - }, - "error": { - "type": "string", - "example": "INVALID_REQUEST_BODY" - } - } - }, - "api_acladmin.getResourceErrorInvalidResourceID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.getResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" - } - } - }, "api_acladmin.getResourceResponse": { "type": "object", "properties": { @@ -739,32 +637,6 @@ } } }, - "api_acladmin.getRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.getRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, "api_acladmin.getRoleResponse": { "type": "object", "properties": { @@ -778,55 +650,20 @@ } } }, - "api_acladmin.updateResourceErrorInvalidResourceID": { + "api_acladmin.getRoleUser": { "type": "object", "properties": { - "details": { + "userEmail": { "type": "string", - "example": "Resource ID must be positive integer" + "example": "admin@triggerssmith.com" }, - "error": { - "type": "string", - "example": "INVALID_RESOURCE_ID" - } - } - }, - "api_acladmin.updateResourceErrorInvalidResourceKey": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid resource key" + "userId": { + "type": "integer", + "example": 1 }, - "error": { + "userName": { "type": "string", - "example": "FAILED_TO_UPDATE_RESOURCE" - } - } - }, - "api_acladmin.updateResourceErrorResourceKeyAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Resource with key 'html.view' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_RESOURCE" - } - } - }, - "api_acladmin.updateResourceErrorResourceNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No resource with ID 123" - }, - "error": { - "type": "string", - "example": "RESOURCE_NOT_FOUND" + "example": "admin" } } }, @@ -852,58 +689,6 @@ } } }, - "api_acladmin.updateRoleErrorInvalidRoleID": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role ID must be positive integer" - }, - "error": { - "type": "string", - "example": "INVALID_ROLE_ID" - } - } - }, - "api_acladmin.updateRoleErrorInvalidRoleName": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Invalid role name" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_ROLE" - } - } - }, - "api_acladmin.updateRoleErrorRoleNameAlreadyExists": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "Role with name 'admin' already exists" - }, - "error": { - "type": "string", - "example": "FAILED_TO_UPDATE_ROLE" - } - } - }, - "api_acladmin.updateRoleErrorRoleNotFound": { - "type": "object", - "properties": { - "details": { - "type": "string", - "example": "No role with ID 123" - }, - "error": { - "type": "string", - "example": "ROLE_NOT_FOUND" - } - } - }, "api_acladmin.updateRoleRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 45e0c12..9af92f5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,20 +1,20 @@ definitions: - api_acladmin.createResourceErrorInvalidResourceKey: + api_acladmin.ProblemDetails: properties: - details: - example: Invalid resource key + detail: + example: No role with ID 42 type: string - error: - example: FAILED_TO_CREATE_RESOURCE + instance: + example: /api/acl/roles/42 type: string - type: object - api_acladmin.createResourceErrorResourceAlreadyExists: - properties: - details: - example: Resource with key 'html.view' already exists + status: + example: 404 + type: integer + title: + example: Role not found type: string - error: - example: FAILED_TO_CREATE_RESOURCE + type: + example: https://api.triggerssmith.com/errors/role-not-found type: string type: object api_acladmin.createResourceRequest: @@ -32,24 +32,6 @@ definitions: example: html.view type: string type: object - api_acladmin.createRoleErrorInvalidRoleName: - properties: - details: - example: Invalid role name - type: string - error: - example: FAILED_TO_CREATE_ROLE - type: string - type: object - api_acladmin.createRoleErrorRoleAlreadyExists: - properties: - details: - example: Role with name 'admin' already exists - type: string - error: - example: FAILED_TO_CREATE_ROLE - type: string - type: object api_acladmin.createRoleRequest: properties: name: @@ -65,94 +47,6 @@ definitions: example: admin type: string type: object - api_acladmin.deleteResourceErrorInvalidResourceID: - properties: - details: - example: Resource ID must be positive integer - type: string - error: - example: INVALID_RESOURCE_ID - type: string - type: object - api_acladmin.deleteResourceErrorResourceInUse: - properties: - details: - example: Resource with ID 123 is used and cannot be deleted - type: string - error: - example: FAILED_TO_DELETE_RESOURCE - type: string - type: object - api_acladmin.deleteResourceErrorResourceNotFound: - properties: - details: - example: No resource with ID 123 - type: string - error: - example: RESOURCE_NOT_FOUND - type: string - type: object - api_acladmin.deleteRoleErrorInvalidRoleID: - properties: - details: - example: Role ID must be positive integer - type: string - error: - example: INVALID_ROLE_ID - type: string - type: object - api_acladmin.deleteRoleErrorRoleInUse: - properties: - details: - example: Role with ID 123 is assigned to users and cannot be deleted - type: string - error: - example: FAILED_TO_DELETE_ROLE - type: string - type: object - api_acladmin.deleteRoleErrorRoleNotFound: - properties: - details: - example: No role with ID 123 - type: string - error: - example: ROLE_NOT_FOUND - type: string - type: object - api_acladmin.errorInternalServerError: - properties: - details: - type: string - error: - type: string - type: object - api_acladmin.errorInvalidRequestBody: - properties: - details: - example: Request body is not valid JSON - type: string - error: - example: INVALID_REQUEST_BODY - type: string - type: object - api_acladmin.getResourceErrorInvalidResourceID: - properties: - details: - example: Resource ID must be positive integer - type: string - error: - example: INVALID_RESOURCE_ID - type: string - type: object - api_acladmin.getResourceErrorResourceNotFound: - properties: - details: - example: No resource with ID 123 - type: string - error: - example: RESOURCE_NOT_FOUND - type: string - type: object api_acladmin.getResourceResponse: properties: id: @@ -162,24 +56,6 @@ definitions: example: html.view type: string type: object - api_acladmin.getRoleErrorInvalidRoleID: - properties: - details: - example: Role ID must be positive integer - type: string - error: - example: INVALID_ROLE_ID - type: string - type: object - api_acladmin.getRoleErrorRoleNotFound: - properties: - details: - example: No role with ID 123 - type: string - error: - example: ROLE_NOT_FOUND - type: string - type: object api_acladmin.getRoleResponse: properties: id: @@ -189,40 +65,16 @@ definitions: example: admin type: string type: object - api_acladmin.updateResourceErrorInvalidResourceID: + api_acladmin.getRoleUser: properties: - details: - example: Resource ID must be positive integer + userEmail: + example: admin@triggerssmith.com type: string - error: - example: INVALID_RESOURCE_ID - type: string - type: object - api_acladmin.updateResourceErrorInvalidResourceKey: - properties: - details: - example: Invalid resource key - type: string - error: - example: FAILED_TO_UPDATE_RESOURCE - type: string - type: object - api_acladmin.updateResourceErrorResourceKeyAlreadyExists: - properties: - details: - example: Resource with key 'html.view' already exists - type: string - error: - example: FAILED_TO_UPDATE_RESOURCE - type: string - type: object - api_acladmin.updateResourceErrorResourceNotFound: - properties: - details: - example: No resource with ID 123 - type: string - error: - example: RESOURCE_NOT_FOUND + userId: + example: 1 + type: integer + userName: + example: admin type: string type: object api_acladmin.updateResourceRequest: @@ -240,42 +92,6 @@ definitions: example: html.view type: string type: object - api_acladmin.updateRoleErrorInvalidRoleID: - properties: - details: - example: Role ID must be positive integer - type: string - error: - example: INVALID_ROLE_ID - type: string - type: object - api_acladmin.updateRoleErrorInvalidRoleName: - properties: - details: - example: Invalid role name - type: string - error: - example: FAILED_TO_UPDATE_ROLE - type: string - type: object - api_acladmin.updateRoleErrorRoleNameAlreadyExists: - properties: - details: - example: Role with name 'admin' already exists - type: string - error: - example: FAILED_TO_UPDATE_ROLE - type: string - type: object - api_acladmin.updateRoleErrorRoleNotFound: - properties: - details: - example: No role with ID 123 - type: string - error: - example: ROLE_NOT_FOUND - type: string - type: object api_acladmin.updateRoleRequest: properties: name: @@ -303,19 +119,21 @@ paths: description: OK schema: items: - properties: - id: - example: 1 - type: integer - key: - example: html.view - type: string - type: object + items: + properties: + id: + example: 1 + type: integer + key: + example: html.view + type: string + type: object + type: array type: array "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get all resources tags: - resources @@ -339,15 +157,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.createResourceErrorInvalidResourceKey' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.createResourceErrorResourceAlreadyExists' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Create resource tags: - resources @@ -368,19 +186,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.deleteResourceErrorInvalidResourceID' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.deleteResourceErrorResourceNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.deleteResourceErrorResourceInUse' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Delete resource tags: - resources @@ -402,15 +220,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.getResourceErrorInvalidResourceID' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.getResourceErrorResourceNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get resource by ID tags: - resources @@ -440,19 +258,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.updateResourceErrorInvalidResourceKey' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.updateResourceErrorResourceNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.updateResourceErrorResourceKeyAlreadyExists' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Update resource tags: - resources @@ -465,19 +283,21 @@ paths: description: OK schema: items: - properties: - id: - example: 1 - type: integer - name: - example: admin - type: string - type: object + items: + properties: + id: + example: 1 + type: integer + name: + example: admin + type: string + type: object + type: array type: array "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get all roles tags: - roles @@ -501,19 +321,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.errorInvalidRequestBody' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/api_acladmin.createRoleErrorInvalidRoleName' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.createRoleErrorRoleAlreadyExists' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Create role tags: - roles @@ -534,19 +350,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.deleteRoleErrorInvalidRoleID' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.deleteRoleErrorRoleNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.deleteRoleErrorRoleInUse' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Delete role tags: - roles @@ -568,15 +384,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.getRoleErrorInvalidRoleID' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.getRoleErrorRoleNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get role by ID tags: - roles @@ -606,20 +422,55 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/api_acladmin.updateRoleErrorInvalidRoleName' + $ref: '#/definitions/api_acladmin.ProblemDetails' "404": description: Not Found schema: - $ref: '#/definitions/api_acladmin.updateRoleErrorRoleNotFound' + $ref: '#/definitions/api_acladmin.ProblemDetails' "409": description: Conflict schema: - $ref: '#/definitions/api_acladmin.updateRoleErrorRoleNameAlreadyExists' + $ref: '#/definitions/api_acladmin.ProblemDetails' "500": description: Internal Server Error schema: - $ref: '#/definitions/api_acladmin.errorInternalServerError' + $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Update role tags: - roles + /api/acl/roles/{roleId}/users: + get: + parameters: + - description: Role ID + example: 1 + in: path + name: roleId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + items: + $ref: '#/definitions/api_acladmin.getRoleUser' + type: array + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Get role users + tags: + - roles swagger: "2.0" diff --git a/internal/acl/models.go b/internal/acl/models.go index d2033a4..ccfe275 100644 --- a/internal/acl/models.go +++ b/internal/acl/models.go @@ -1,11 +1,13 @@ package acl +import "git.oblat.lv/alex/triggerssmith/internal/user" + type UserRole struct { UserID uint `gorm:"index;not null;uniqueIndex:ux_user_role"` RoleID uint `gorm:"index;not null;uniqueIndex:ux_user_role"` - Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"` - //User user.User `gorm:"constraint:OnDelete:CASCADE;foreignKey:UserID;references:ID"` + Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"` + User user.User `gorm:"constraint:OnDelete:CASCADE;foreignKey:UserID;references:ID"` } type Resource struct { @@ -17,8 +19,8 @@ type Role struct { ID uint `gorm:"primaryKey;autoIncrement" json:"id"` Name string `gorm:"unique;not null" json:"name"` - Resources []Resource `gorm:"many2many:role_resources" json:"resources"` - //Users []user.User `gorm:"many2many:user_roles"` + Resources []Resource `gorm:"many2many:role_resources" json:"resources"` + Users []user.User `gorm:"many2many:user_roles"` } type RoleResource struct { diff --git a/internal/acl/roles.go b/internal/acl/roles.go index 8404d38..c813a93 100644 --- a/internal/acl/roles.go +++ b/internal/acl/roles.go @@ -16,7 +16,7 @@ func (s *Service) GetRoles() ([]Role, error) { } var roles []Role - if err := s.db.Preload("Resources").Order("id").Find(&roles).Error; err != nil { + if err := s.db.Preload("Resources").Preload("Users").Order("id").Find(&roles).Error; err != nil { return nil, fmt.Errorf("db error: %w", err) } @@ -60,7 +60,7 @@ func (s *Service) GetRoleByID(roleID uint) (*Role, error) { return nil, ErrNotInitialized } var role Role - err := s.db.Preload("Resources").First(&role, roleID).Error + err := s.db.Preload("Resources").Preload("Users").First(&role, roleID).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrRoleNotFound diff --git a/internal/user/model.go b/internal/user/model.go index f7fcb71..06c40c3 100644 --- a/internal/user/model.go +++ b/internal/user/model.go @@ -1,15 +1,14 @@ package user import ( - "git.oblat.lv/alex/triggerssmith/internal/acl" "gorm.io/gorm" ) type User struct { - ID uint `gorm:"primaryKey"` - Username string `gorm:"uniqueIndex;not null"` - Email string `gorm:"uniqueIndex;not null"` - Password string `gorm:"not null"` - Roles []acl.Role `gorm:"many2many:user_roles"` + ID uint `gorm:"primaryKey"` + Username string `gorm:"uniqueIndex;not null"` + Email string `gorm:"uniqueIndex;not null"` + Password string `gorm:"not null"` + //Roles []acl.Role `gorm:"many2many:user_roles"` DeletedAt gorm.DeletedAt `gorm:"index"` }