refactor and documentation

This commit is contained in:
2026-01-03 15:41:21 +02:00
parent 1468937589
commit ca569d25bc
5 changed files with 159 additions and 312 deletions

View File

@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"git.oblat.lv/alex/triggerssmith/internal/acl" "git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -14,7 +15,7 @@ import (
// @Tags acl/resources // @Tags acl/resources
// @Produce json // @Produce json
// @Success 200 {object} getResourcesResponse // @Success 200 {object} getResourcesResponse
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/resources [get] // @Router /api/acl/resources [get]
func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -23,10 +24,10 @@ func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -49,9 +50,9 @@ func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param resourceId path int true "Resource ID" example(1) // @Param resourceId path int true "Resource ID" example(1)
// @Success 200 {object} getResourceResponse // @Success 200 {object} getResourceResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/resources/{resourceId} [get] // @Router /api/acl/resources/{resourceId} [get]
func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -59,7 +60,7 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) {
resourceIDStr := chi.URLParam(r, "resourceId") resourceIDStr := chi.URLParam(r, "resourceId")
resourceID, err := strconv.Atoi(resourceIDStr) resourceID, err := strconv.Atoi(resourceIDStr)
if err != nil || resourceID < 0 { if err != nil || resourceID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r)
return return
} }
@@ -67,12 +68,12 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r)
case acl.ErrResourceNotFound: case acl.ErrResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -89,16 +90,16 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param request body createResourceRequest true "Resource" // @Param request body createResourceRequest true "Resource"
// @Success 201 {object} createResourceResponse // @Success 201 {object} createResourceResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/resources [post] // @Router /api/acl/resources [post]
func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var req createResourceRequest var req createResourceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r)
return return
} }
@@ -108,14 +109,14 @@ func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request)
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r)
case acl.ErrInvalidResourceKey: case acl.ErrInvalidResourceKey:
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r)
case acl.ErrResourceAlreadyExists: case acl.ErrResourceAlreadyExists:
writeProblem(w, http.StatusConflict, "/errors/acl/resource-already-exists", "Resource already exists", "Resource '"+req.Key+"' already exists", r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/resource-already-exists", "Resource already exists", "Resource '"+req.Key+"' already exists", r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -134,24 +135,24 @@ func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request)
// @Param resourceId path int true "Resource ID" example(1) // @Param resourceId path int true "Resource ID" example(1)
// @Param request body updateResourceRequest true "Resource" // @Param request body updateResourceRequest true "Resource"
// @Success 200 {object} updateResourceResponse // @Success 200 {object} updateResourceResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/resources/{resourceId} [patch] // @Router /api/acl/resources/{resourceId} [patch]
func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var req updateResourceRequest var req updateResourceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r)
return return
} }
resourceIDStr := chi.URLParam(r, "resourceId") resourceIDStr := chi.URLParam(r, "resourceId")
resourceID, err := strconv.Atoi(resourceIDStr) resourceID, err := strconv.Atoi(resourceIDStr)
if err != nil || resourceID < 0 { if err != nil || resourceID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r)
return return
} }
@@ -161,16 +162,16 @@ func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request)
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r)
case acl.ErrInvalidResourceKey: case acl.ErrInvalidResourceKey:
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-key", "Invalid resource key", "Resource key must be non-empty", r)
case acl.ErrResourceNotFound: case acl.ErrResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r)
case acl.ErrSameResourceKey: case acl.ErrSameResourceKey:
writeProblem(w, http.StatusConflict, "/errors/acl/resource-key-already-exists", "Resource key already exists", "Resource key '"+req.Key+"' already exists", r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/resource-key-already-exists", "Resource key already exists", "Resource key '"+req.Key+"' already exists", r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -186,10 +187,10 @@ func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request)
// @Produce json // @Produce json
// @Param resourceId path int true "Resource ID" example(1) // @Param resourceId path int true "Resource ID" example(1)
// @Success 200 // @Success 200
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/resources/{resourceId} [delete] // @Router /api/acl/resources/{resourceId} [delete]
func (h *aclAdminHandler) deleteResource(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) deleteResource(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -197,7 +198,7 @@ func (h *aclAdminHandler) deleteResource(w http.ResponseWriter, r *http.Request)
resourceIDStr := chi.URLParam(r, "resourceId") resourceIDStr := chi.URLParam(r, "resourceId")
resourceID, err := strconv.Atoi(resourceIDStr) resourceID, err := strconv.Atoi(resourceIDStr)
if err != nil || resourceID < 0 { if err != nil || resourceID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r)
return return
} }
@@ -207,14 +208,14 @@ func (h *aclAdminHandler) deleteResource(w http.ResponseWriter, r *http.Request)
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "acl service is not initialized", r)
case acl.ErrResourceNotFound: case acl.ErrResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+resourceIDStr, r)
case acl.ErrResourceInUse: case acl.ErrResourceInUse:
writeProblem(w, http.StatusConflict, "/errors/acl/resource-in-use", "Resource in use", "Resource "+resourceIDStr+" is in use", r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/resource-in-use", "Resource in use", "Resource "+resourceIDStr+" is in use", r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }

View File

@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"git.oblat.lv/alex/triggerssmith/internal/acl" "git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -14,7 +15,7 @@ import (
// @Tags acl/roles // @Tags acl/roles
// @Produce json // @Produce json
// @Success 200 {array} getRolesResponse // @Success 200 {array} getRolesResponse
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles [get] // @Router /api/acl/roles [get]
func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -22,10 +23,10 @@ func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -48,16 +49,16 @@ func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Success 200 {object} getRoleResponse // @Success 200 {object} getRoleResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId} [get] // @Router /api/acl/roles/{roleId} [get]
func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
@@ -65,12 +66,12 @@ func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -86,16 +87,16 @@ func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Success 200 {array} getRoleUsersResponse // @Success 200 {array} getRoleUsersResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId}/users [get] // @Router /api/acl/roles/{roleId}/users [get]
func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
@@ -103,17 +104,17 @@ func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
if len(role.Users) == 0 { if len(role.Users) == 0 {
writeProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r)
return return
} }
var respUsers getRoleUsersResponse var respUsers getRoleUsersResponse
@@ -132,33 +133,33 @@ func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Success 200 {array} getRoleResourcesResponse // @Success 200 {array} getRoleResourcesResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId}/resources [get] // @Router /api/acl/roles/{roleId}/resources [get]
func (h *aclAdminHandler) getRoleResources(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getRoleResources(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
role, err := h.a.GetRoleByID(uint(roleID)) role, err := h.a.GetRoleByID(uint(roleID))
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
if len(role.Resources) == 0 { if len(role.Resources) == 0 {
writeProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r)
return return
} }
var respResources getRoleResourcesResponse var respResources getRoleResourcesResponse
@@ -177,16 +178,16 @@ func (h *aclAdminHandler) getRoleResources(w http.ResponseWriter, r *http.Reques
// @Produce json // @Produce json
// @Param request body createRoleRequest true "Role" // @Param request body createRoleRequest true "Role"
// @Success 201 {object} createRoleResponse // @Success 201 {object} createRoleResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles [post] // @Router /api/acl/roles [post]
func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var req createRoleRequest var req createRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r)
return return
} }
@@ -195,13 +196,13 @@ func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) {
slog.Error("Failed to create role", "error", err.Error()) slog.Error("Failed to create role", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrInvalidRoleName: case acl.ErrInvalidRoleName:
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r)
case acl.ErrRoleAlreadyExists: case acl.ErrRoleAlreadyExists:
writeProblem(w, http.StatusConflict, "/errors/acl/role-already-exists", "Role already exists", "Role '"+req.Name+"' already exists", r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/role-already-exists", "Role already exists", "Role '"+req.Name+"' already exists", r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -220,24 +221,24 @@ func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) {
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Param request body updateRoleRequest true "Role" // @Param request body updateRoleRequest true "Role"
// @Success 200 {object} updateRoleResponse // @Success 200 {object} updateRoleResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId} [patch] // @Router /api/acl/roles/{roleId} [patch]
func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var req updateRoleRequest var req updateRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r)
return return
} }
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
@@ -246,15 +247,15 @@ func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) {
slog.Error("Failed to update role", "error", err.Error()) slog.Error("Failed to update role", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrInvalidRoleName: case acl.ErrInvalidRoleName:
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
case acl.ErrSameRoleName: case acl.ErrSameRoleName:
writeProblem(w, http.StatusConflict, "/errors/acl/role-name-already-exists", "Role name already exists", "Role '"+req.Name+"' already exists", r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/role-name-already-exists", "Role name already exists", "Role '"+req.Name+"' already exists", r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -270,17 +271,17 @@ func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Success 204 // @Success 204
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId} [delete] // @Router /api/acl/roles/{roleId} [delete]
func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
@@ -289,13 +290,13 @@ func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) {
slog.Error("Failed to delete role", "error", err.Error()) slog.Error("Failed to delete role", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
case acl.ErrRoleInUse: case acl.ErrRoleInUse:
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) server.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: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -309,37 +310,37 @@ func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) {
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Param request body assignResourceToRoleRequest true "Resource" // @Param request body assignResourceToRoleRequest true "Resource"
// @Success 201 // @Success 201
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId}/resources [post] // @Router /api/acl/roles/{roleId}/resources [post]
func (h *aclAdminHandler) assignResourceToRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) assignResourceToRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
var req assignResourceToRoleRequest var req assignResourceToRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r)
return return
} }
if err := h.a.AssignResourceToRole(uint(roleID), req.ResourceID); err != nil { if err := h.a.AssignResourceToRole(uint(roleID), req.ResourceID); err != nil {
slog.Error("Failed to assign resource to role", "error", err.Error()) slog.Error("Failed to assign resource to role", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
case acl.ErrResourceNotFound: case acl.ErrResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(req.ResourceID)), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(req.ResourceID)), r)
case acl.ErrResourceAlreadyAssigned: case acl.ErrResourceAlreadyAssigned:
writeProblem(w, http.StatusConflict, "/errors/acl/resource-already-assigned", "Resource already assigned", "Resource with ID "+strconv.Itoa(int(req.ResourceID))+" is already assigned to role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/resource-already-assigned", "Resource already assigned", "Resource with ID "+strconv.Itoa(int(req.ResourceID))+" is already assigned to role with ID "+roleIDStr, r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -352,37 +353,37 @@ func (h *aclAdminHandler) assignResourceToRole(w http.ResponseWriter, r *http.Re
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Param resId path int true "Resource ID" example(1) // @Param resId path int true "Resource ID" example(1)
// @Success 204 // @Success 204
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/roles/{roleId}/resources/{resId} [delete] // @Router /api/acl/roles/{roleId}/resources/{resId} [delete]
func (h *aclAdminHandler) removeResourceFromRole(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) removeResourceFromRole(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
resourceIDStr := chi.URLParam(r, "resId") resourceIDStr := chi.URLParam(r, "resId")
resourceID, err := strconv.Atoi(resourceIDStr) resourceID, err := strconv.Atoi(resourceIDStr)
if err != nil || resourceID < 0 { if err != nil || resourceID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r)
return return
} }
if err := h.a.RemoveResourceFromRole(uint(roleID), uint(resourceID)); err != nil { if err := h.a.RemoveResourceFromRole(uint(roleID), uint(resourceID)); err != nil {
slog.Error("Failed to remove resource from role", "error", err.Error()) slog.Error("Failed to remove resource from role", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r)
case acl.ErrResourceNotFound: case acl.ErrResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(resourceID)), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(resourceID)), r)
case acl.ErrRoleResourceNotFound: case acl.ErrRoleResourceNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/role-resource-not-found", "Role resource not found", "No role-resource pair with role ID "+roleIDStr+" and resource ID "+strconv.Itoa(int(resourceID)), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/role-resource-not-found", "Role resource not found", "No role-resource pair with role ID "+roleIDStr+" and resource ID "+strconv.Itoa(int(resourceID)), r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }

View File

@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"git.oblat.lv/alex/triggerssmith/internal/acl" "git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -15,30 +16,30 @@ import (
// @Produce json // @Produce json
// @Param userId path int true "User ID" example(1) // @Param userId path int true "User ID" example(1)
// @Success 200 {object} getUserRolesResponse // @Success 200 {object} getUserRolesResponse
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/users/{userId}/roles [get] // @Router /api/acl/users/{userId}/roles [get]
func (h *aclAdminHandler) getUserRoles(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) getUserRoles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
userIDStr := chi.URLParam(r, "userId") userIDStr := chi.URLParam(r, "userId")
userID, err := strconv.Atoi(userIDStr) userID, err := strconv.Atoi(userIDStr)
if err != nil { if err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r)
return return
} }
roles, err := h.a.GetUserRoles(uint(userID)) roles, err := h.a.GetUserRoles(uint(userID))
if err != nil { if err != nil {
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrUserNotFound: case acl.ErrUserNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r)
default: default:
slog.Error("unexpected server error", "error", err.Error()) slog.Error("unexpected server error", "error", err.Error())
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -55,37 +56,37 @@ func (h *aclAdminHandler) getUserRoles(w http.ResponseWriter, r *http.Request) {
// @Param userId path int true "User ID" example(1) // @Param userId path int true "User ID" example(1)
// @Param body body assignRoleToUserRequest true "Role ID" // @Param body body assignRoleToUserRequest true "Role ID"
// @Success 201 // @Success 201
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 409 {object} ProblemDetails // @Failure 409 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/users/{userId}/roles [post] // @Router /api/acl/users/{userId}/roles [post]
func (h *aclAdminHandler) assignRoleToUser(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) assignRoleToUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
userIDStr := chi.URLParam(r, "userId") userIDStr := chi.URLParam(r, "userId")
userID, err := strconv.Atoi(userIDStr) userID, err := strconv.Atoi(userIDStr)
if err != nil || userID < 0 { if err != nil || userID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r)
return return
} }
var req assignRoleToUserRequest var req assignRoleToUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r)
return return
} }
if err := h.a.AssignRoleToUser(req.RoleID, uint(userID)); err != nil { if err := h.a.AssignRoleToUser(req.RoleID, uint(userID)); err != nil {
slog.Error("Failed to assign role to user", "error", err.Error()) slog.Error("Failed to assign role to user", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrUserNotFound: case acl.ErrUserNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r)
case acl.ErrRoleAlreadyAssigned: case acl.ErrRoleAlreadyAssigned:
writeProblem(w, http.StatusConflict, "/errors/acl/role-already-assigned", "Role already assigned", "Role with ID "+strconv.Itoa(int(req.RoleID))+" is already assigned to user "+strconv.Itoa(userID), r) server.WriteProblem(w, http.StatusConflict, "/errors/acl/role-already-assigned", "Role already assigned", "Role with ID "+strconv.Itoa(int(req.RoleID))+" is already assigned to user "+strconv.Itoa(userID), r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
return return
} }
@@ -98,22 +99,22 @@ func (h *aclAdminHandler) assignRoleToUser(w http.ResponseWriter, r *http.Reques
// @Param userId path int true "User ID" example(1) // @Param userId path int true "User ID" example(1)
// @Param roleId path int true "Role ID" example(1) // @Param roleId path int true "Role ID" example(1)
// @Success 204 // @Success 204
// @Failure 400 {object} ProblemDetails // @Failure 400 {object} server.ProblemDetails
// @Failure 404 {object} ProblemDetails // @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} ProblemDetails // @Failure 500 {object} server.ProblemDetails
// @Router /api/acl/users/{userId}/roles/{roleId} [delete] // @Router /api/acl/users/{userId}/roles/{roleId} [delete]
func (h *aclAdminHandler) removeRoleFromUser(w http.ResponseWriter, r *http.Request) { func (h *aclAdminHandler) removeRoleFromUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
userIDStr := chi.URLParam(r, "userId") userIDStr := chi.URLParam(r, "userId")
userID, err := strconv.Atoi(userIDStr) userID, err := strconv.Atoi(userIDStr)
if err != nil || userID < 0 { if err != nil || userID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r)
return return
} }
roleIDStr := chi.URLParam(r, "roleId") roleIDStr := chi.URLParam(r, "roleId")
roleID, err := strconv.Atoi(roleIDStr) roleID, err := strconv.Atoi(roleIDStr)
if err != nil || roleID < 0 { if err != nil || roleID < 0 {
writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) server.WriteProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r)
return return
} }
err = h.a.RemoveRoleFromUser(uint(roleID), uint(userID)) err = h.a.RemoveRoleFromUser(uint(roleID), uint(userID))
@@ -121,15 +122,15 @@ func (h *aclAdminHandler) removeRoleFromUser(w http.ResponseWriter, r *http.Requ
slog.Error("Failed to remove role from user", "error", err.Error()) slog.Error("Failed to remove role from user", "error", err.Error())
switch err { switch err {
case acl.ErrNotInitialized: case acl.ErrNotInitialized:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r)
case acl.ErrUserNotFound: case acl.ErrUserNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r)
case acl.ErrRoleNotFound: case acl.ErrRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r)
case acl.ErrUserRoleNotFound: case acl.ErrUserRoleNotFound:
writeProblem(w, http.StatusNotFound, "/errors/acl/user-role-not-found", "User role not found", "User "+strconv.Itoa(userID)+" does not have role "+strconv.Itoa(roleID), r) server.WriteProblem(w, http.StatusNotFound, "/errors/acl/user-role-not-found", "User role not found", "User "+strconv.Itoa(userID)+" does not have role "+strconv.Itoa(roleID), r)
default: default:
writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r)
} }
} }
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View File

@@ -3,24 +3,19 @@
package api_auth package api_auth
import ( import (
"encoding/json"
"fmt"
"log/slog"
"net/http" "net/http"
"time" "time"
"git.oblat.lv/alex/triggerssmith/internal/auth" "git.oblat.lv/alex/triggerssmith/internal/auth"
"git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
) )
func setRefreshCookie(w http.ResponseWriter, token string, ttl time.Duration, secure bool) { func setRefreshCookie(w http.ResponseWriter, token string, ttl time.Duration, secure bool) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "refresh_token", Name: "refresh_token",
Value: token, Value: token,
Path: "/api/auth/refresh", Path: "/api/auth/",
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
MaxAge: int(ttl.Seconds()), MaxAge: int(ttl.Seconds()),
@@ -58,165 +53,3 @@ func MustRoute(config *config.Config, authService *auth.Service) func(chi.Router
r.Post("/revoke", h.handleRevoke) // not implemented r.Post("/revoke", h.handleRevoke) // not implemented
} }
} }
type registerRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
type registerResponse struct {
UserID uint `json:"id"`
Username string `json:"username"`
}
func (h *authHandler) handleRegister(w http.ResponseWriter, r *http.Request) {
var req registerRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
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
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(registerResponse{
UserID: user.ID,
Username: user.Username,
})
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginResponse struct {
Token string `json:"accessToken"`
}
func (h *authHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
var req loginRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
tokens, err := h.a.Login(req.Username, req.Password)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
setRefreshCookie(w, tokens.Refresh, h.cfg.Auth.RefreshTokenTTL, false)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(loginResponse{Token: tokens.Access})
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
func (h *authHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
claims, err := h.a.AuthenticateRequest(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
rjti := claims.(jwt.MapClaims)["rjti"].(string)
err = h.a.Logout(rjti)
if err != nil {
http.Error(w, "Failed to logout, taking cookie anyways", http.StatusInternalServerError)
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: "",
MaxAge: -1,
Path: "/api/users/refresh",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
if err == nil {
w.WriteHeader(http.StatusOK)
}
}
type meResponse struct {
UserID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
func (h *authHandler) handleMe(w http.ResponseWriter, r *http.Request) {
refresh_token_cookie, err := r.Cookie("refresh_token")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
userID, err := h.a.ValidateRefreshToken(refresh_token_cookie.Value)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
user, err := h.a.Get("id", fmt.Sprint(userID))
if err != nil {
http.Error(w, "Failed to get user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(meResponse{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
})
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
type GetUserDataResponse meResponse
func (h *authHandler) handleGetUserData(w http.ResponseWriter, r *http.Request) {
by := r.URL.Query().Get("by")
value := r.URL.Query().Get("value")
if value == "" {
value = r.URL.Query().Get(by)
}
user, err := h.a.Get(by, value)
if err != nil {
http.Error(w, "Failed to get user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(meResponse{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
})
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
func (h *authHandler) handleRevoke(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *authHandler) handleRefresh(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}

View File

@@ -16,6 +16,7 @@ import (
"path/filepath" "path/filepath"
"git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -41,16 +42,26 @@ func MustRoute(config *config.Config) func(chi.Router) {
} }
} }
// @Summary Get block
// @Tags block
// @Produce json
// @Param blockPath path string true "Block Path" example(menu)
// @Success 200 {object} Block
// @Failure 403 {object} server.ProblemDetails
// @Failure 404 {object} server.ProblemDetails
// @Failure 500 {object} server.ProblemDetails
// @Router /api/block/{blockPath} [get]
func (h *blockHandler) handleBlock(w http.ResponseWriter, r *http.Request) { func (h *blockHandler) handleBlock(w http.ResponseWriter, r *http.Request) {
if !h.cfg.Server.BlockConfig.Enabled { if !h.cfg.Server.BlockConfig.Enabled {
http.Error(w, "Block serving is disabled", http.StatusForbidden) server.WriteProblem(w, http.StatusForbidden, "/errors/block/block-serving-disabled", "Block serving is disabled", "Block serving is disabled", r)
return return
} }
blockPath := r.URL.Path[len("/api/block/"):] blockPath := r.URL.Path[len("/api/block/"):]
block, err := LoadBlock(blockPath, h.cfg) block, err := LoadBlock(blockPath, h.cfg)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) slog.Error("failed to load block", slog.String("path", blockPath), slog.String("err", err.Error()))
server.WriteProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "failed to load block", r)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")