package handler import ( "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/google/uuid" "learning-service/internal/model" "learning-service/internal/repository" ) type AccessGrantHandler struct { repo *repository.AccessGrantRepository testRepo *repository.TestRepository courseRepo *repository.CourseRepository } func NewAccessGrantHandler( repo *repository.AccessGrantRepository, testRepo *repository.TestRepository, courseRepo *repository.CourseRepository, ) *AccessGrantHandler { return &AccessGrantHandler{repo: repo, testRepo: testRepo, courseRepo: courseRepo} } // authorizeResourceOwner — проверка прав на управление access_grants: // только owner ресурса (или admin через bypass на портале). Owner // автоматически имеет доступ ко всему, гранты для него не нужны — // они только для «других». func (h *AccessGrantHandler) authorizeResourceOwner(w http.ResponseWriter, r *http.Request, resourceType string, resourceID uuid.UUID) (uuid.UUID, bool) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return uuid.Nil, false } var owner uuid.UUID switch resourceType { case "test": t, err := h.testRepo.Get(r.Context(), resourceID) if err != nil { writeRepoError(w, r, err, "get test") return uuid.Nil, false } owner = t.OwnerUserID case "course": c, err := h.courseRepo.Get(r.Context(), resourceID) if err != nil { writeRepoError(w, r, err, "get course") return uuid.Nil, false } owner = c.OwnerUserID default: writeError(w, http.StatusBadRequest, "resource_type must be test|course") return uuid.Nil, false } if owner != uid { writeError(w, http.StatusForbidden, "only owner can manage access") return uuid.Nil, false } return uid, true } // List — GET /access/{resourceType}/{resourceId}. func (h *AccessGrantHandler) List(w http.ResponseWriter, r *http.Request) { resourceType := strings.ToLower(chi.URLParam(r, "resourceType")) resourceID, err := parseUUID(chi.URLParam(r, "resourceId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid resource id") return } if _, ok := h.authorizeResourceOwner(w, r, resourceType, resourceID); !ok { return } items, err := h.repo.List(r.Context(), resourceType, resourceID) if err != nil { writeRepoError(w, r, err, "list access grants") return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) } // Create — POST /access/{resourceType}/{resourceId} {subject_type, subject_id, can_manage}. func (h *AccessGrantHandler) Create(w http.ResponseWriter, r *http.Request) { resourceType := strings.ToLower(chi.URLParam(r, "resourceType")) resourceID, err := parseUUID(chi.URLParam(r, "resourceId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid resource id") return } uid, ok := h.authorizeResourceOwner(w, r, resourceType, resourceID) if !ok { return } var req model.CreateAccessGrantRequest if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } if req.SubjectType != "user" && req.SubjectType != "role" && req.SubjectType != "department" && req.SubjectType != "position" && req.SubjectType != "public" { writeError(w, http.StatusBadRequest, "subject_type must be user|role|department|position|public") return } g, err := h.repo.Create(r.Context(), resourceType, resourceID, req, uid) if err != nil { writeRepoError(w, r, err, "create access grant") return } writeJSON(w, http.StatusCreated, g) } // Delete — DELETE /access/{resourceType}/{resourceId}/grants/{grantId}. // resourceType/resourceId дублируются в URL'е для удобства проверки прав // (handler читает их из path-params, не из body). func (h *AccessGrantHandler) Delete(w http.ResponseWriter, r *http.Request) { resourceType := strings.ToLower(chi.URLParam(r, "resourceType")) resourceID, err := parseUUID(chi.URLParam(r, "resourceId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid resource id") return } grantID, err := parseUUID(chi.URLParam(r, "grantId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid grant id") return } if _, ok := h.authorizeResourceOwner(w, r, resourceType, resourceID); !ok { return } // Дополнительно проверим, что grant действительно принадлежит этому // ресурсу — иначе owner ресурса A мог бы удалить грант ресурса B // просто подсунув grantId. g, err := h.repo.Get(r.Context(), grantID) if err != nil { writeRepoError(w, r, err, "get grant") return } if g.ResourceType != resourceType || g.ResourceID != resourceID { writeError(w, http.StatusBadRequest, "grant does not belong to this resource") return } if err := h.repo.Delete(r.Context(), grantID); err != nil { writeRepoError(w, r, err, "delete grant") return } w.WriteHeader(http.StatusNoContent) }