Allow PF managers to manage subordinate projects
All checks were successful
CI / go (push) Successful in 22s
CI / python (push) Successful in 1s
Build and Deploy / build-and-deploy (push) Successful in 34s

This commit is contained in:
Grendgi
2026-06-11 16:56:23 +03:00
parent c763ff423d
commit 974090df4f
2 changed files with 160 additions and 29 deletions

View File

@@ -134,7 +134,15 @@ func (s Server) employees(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
items, err := s.App.ListEmployees(r.Context(), isAdmin(r), emp)
var items []Employee
if isAdmin(r) {
items, err = s.App.ListEmployees(r.Context(), true, emp)
} else if canViewTeam(r) {
ids := append(subordinatePortalIDs(r), portalUserID(r))
items, err = s.App.ListEmployeesByPortalUserIDs(r.Context(), ids)
} else {
items, err = s.App.ListEmployees(r.Context(), false, emp)
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -202,12 +210,12 @@ func (s Server) employeeItem(w http.ResponseWriter, r *http.Request, path string
}
func (s Server) projects(w http.ResponseWriter, r *http.Request) {
emp, ok := s.requireEmployee(w, r)
if !ok {
return
}
switch r.Method {
case http.MethodGet:
emp, ok := s.requireEmployee(w, r)
if !ok {
return
}
items, err := s.App.ListProjects(r.Context(), emp.ID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
@@ -219,7 +227,11 @@ func (s Server) projects(w http.ResponseWriter, r *http.Request) {
if !decodeJSON(w, r, &payload) {
return
}
project, err := s.App.CreateProject(r.Context(), emp.ID, payload)
owner, ok := s.resolveProjectOwner(w, r, payload.OwnerPortalUserID)
if !ok {
return
}
project, err := s.App.CreateProject(r.Context(), owner.ID, payload)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -231,10 +243,6 @@ func (s Server) projects(w http.ResponseWriter, r *http.Request) {
}
func (s Server) projectItem(w http.ResponseWriter, r *http.Request, path string) {
emp, ok := s.requireEmployee(w, r)
if !ok {
return
}
rest := strings.TrimPrefix(path, "/api/v1/projects/")
parts := strings.Split(strings.Trim(rest, "/"), "/")
if len(parts) == 0 {
@@ -245,13 +253,17 @@ func (s Server) projectItem(w http.ResponseWriter, r *http.Request, path string)
if !ok {
return
}
ownerID, ok := s.projectOwnerIDForAccess(w, r, projectID)
if !ok {
return
}
if len(parts) == 1 {
s.projectCRUD(w, r, emp.ID, projectID)
s.projectCRUD(w, r, ownerID, projectID)
return
}
switch {
case len(parts) == 2 && parts[1] == "check" && r.Method == http.MethodPost:
if _, err := s.App.ProjectByID(r.Context(), emp.ID, projectID, false); err != nil {
if _, err := s.App.ProjectByID(r.Context(), ownerID, projectID, false); err != nil {
writeError(w, http.StatusNotFound, "project not found")
return
}
@@ -262,7 +274,7 @@ func (s Server) projectItem(w http.ResponseWriter, r *http.Request, path string)
}
writeJSON(w, http.StatusOK, map[string]int{"changes": changes})
case len(parts) == 2 && parts[1] == "suggest" && r.Method == http.MethodGet:
if _, err := s.App.ProjectByID(r.Context(), emp.ID, projectID, false); err != nil {
if _, err := s.App.ProjectByID(r.Context(), ownerID, projectID, false); err != nil {
writeError(w, http.StatusNotFound, "project not found")
return
}
@@ -273,9 +285,9 @@ func (s Server) projectItem(w http.ResponseWriter, r *http.Request, path string)
}
writeJSON(w, http.StatusOK, out)
case len(parts) == 2 && parts[1] == "listings" && r.Method == http.MethodPost:
s.addListing(w, r, emp.ID, projectID)
s.addListing(w, r, ownerID, projectID)
case len(parts) == 3 && parts[1] == "listings" && parts[2] == "bulk" && r.Method == http.MethodPost:
s.addListings(w, r, emp.ID, projectID)
s.addListings(w, r, ownerID, projectID)
default:
writeError(w, http.StatusNotFound, "not found")
}
@@ -356,10 +368,6 @@ func (s Server) addListings(w http.ResponseWriter, r *http.Request, ownerID, pro
}
func (s Server) listingItem(w http.ResponseWriter, r *http.Request, path string) {
emp, ok := s.requireEmployee(w, r)
if !ok {
return
}
id, ok := pathID(w, strings.TrimPrefix(path, "/api/v1/listings/"))
if !ok {
return
@@ -368,7 +376,11 @@ func (s Server) listingItem(w http.ResponseWriter, r *http.Request, path string)
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if err := s.App.DeleteListing(r.Context(), emp.ID, id); err != nil {
ownerID, ok := s.listingOwnerIDForAccess(w, r, id)
if !ok {
return
}
if err := s.App.DeleteListing(r.Context(), ownerID, id); err != nil {
writeError(w, http.StatusNotFound, "listing not found")
return
}
@@ -388,6 +400,59 @@ func (s Server) requireEmployee(w http.ResponseWriter, r *http.Request) (*Employ
return emp, true
}
func (s Server) resolveProjectOwner(w http.ResponseWriter, r *http.Request, requested *string) (*Employee, bool) {
targetPortalID := strings.TrimSpace(portalUserID(r))
if requested != nil && strings.TrimSpace(*requested) != "" {
targetPortalID = strings.TrimSpace(*requested)
}
if targetPortalID == "" {
writeError(w, http.StatusForbidden, "Сначала авторизуйтесь в Telegram-боте Monitoring PF")
return nil, false
}
if !canManagePortalUser(r, targetPortalID) {
writeError(w, http.StatusForbidden, "Нет прав на создание проекта для этого сотрудника")
return nil, false
}
owner, err := s.App.CurrentEmployee(r.Context(), targetPortalID, true)
if errors.Is(err, ErrTelegramRequired) {
writeError(w, http.StatusBadRequest, "У владельца проекта не подключен Telegram-бот Monitoring PF")
return nil, false
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return nil, false
}
return owner, true
}
func (s Server) projectOwnerIDForAccess(w http.ResponseWriter, r *http.Request, projectID int64) (int64, bool) {
owner, err := s.App.ProjectOwner(r.Context(), projectID)
if errors.Is(err, ErrNotFound) {
writeError(w, http.StatusNotFound, "project not found")
return 0, false
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return 0, false
}
if owner.PortalUserID == nil || !canManagePortalUser(r, *owner.PortalUserID) {
writeError(w, http.StatusNotFound, "project not found")
return 0, false
}
return owner.ID, true
}
func (s Server) listingOwnerIDForAccess(w http.ResponseWriter, r *http.Request, listingID int64) (int64, bool) {
var projectID int64
err := s.App.DB.QueryRowContext(r.Context(), `
SELECT project_id FROM competitor_listings WHERE id = ?`, listingID).Scan(&projectID)
if err != nil {
writeError(w, http.StatusNotFound, "listing not found")
return 0, false
}
return s.projectOwnerIDForAccess(w, r, projectID)
}
func portalUserID(r *http.Request) string {
return strings.TrimSpace(r.Header.Get("X-User-Id"))
}
@@ -400,6 +465,22 @@ func canViewTeam(r *http.Request) bool {
return isAdmin(r) || r.Header.Get("X-User-Is-Department-Head") == "1"
}
func canManagePortalUser(r *http.Request, targetPortalID string) bool {
targetPortalID = strings.TrimSpace(targetPortalID)
if targetPortalID == "" {
return false
}
if targetPortalID == portalUserID(r) || isAdmin(r) {
return true
}
for _, id := range subordinatePortalIDs(r) {
if id == targetPortalID {
return true
}
}
return false
}
func subordinatePortalIDs(r *http.Request) []string {
raw := strings.TrimSpace(r.Header.Get("X-User-Subordinates"))
if raw == "" {