Move monitoring PF infrastructure to Go
This commit is contained in:
414
internal/pf/http.go
Normal file
414
internal/pf/http.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package pf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
App *App
|
||||
}
|
||||
|
||||
type listingPayload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type bulkPayload struct {
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := s.apiPath(r.URL.Path)
|
||||
switch {
|
||||
case path == "/healthz":
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
case path == "/":
|
||||
writeJSON(w, http.StatusOK, map[string]string{"service": "monitoring-pf", "ui": "portal", "api": "go"})
|
||||
case !strings.HasPrefix(path, "/api/v1"):
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
case path == "/api/v1/access/me" && r.Method == http.MethodGet:
|
||||
s.accessMe(w, r)
|
||||
case path == "/api/v1/summary" && r.Method == http.MethodGet:
|
||||
s.summary(w, r)
|
||||
case path == "/api/v1/employees":
|
||||
s.employees(w, r)
|
||||
case strings.HasPrefix(path, "/api/v1/employees/"):
|
||||
s.employeeItem(w, r, path)
|
||||
case path == "/api/v1/projects":
|
||||
s.projects(w, r)
|
||||
case strings.HasPrefix(path, "/api/v1/projects/"):
|
||||
s.projectItem(w, r, path)
|
||||
case strings.HasPrefix(path, "/api/v1/listings/"):
|
||||
s.listingItem(w, r, path)
|
||||
default:
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) apiPath(path string) string {
|
||||
base := s.App.Cfg.PublicBasePath
|
||||
if base != "" && path == base {
|
||||
return "/"
|
||||
}
|
||||
if base != "" && strings.HasPrefix(path, base+"/") {
|
||||
return strings.TrimPrefix(path, base)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (s Server) accessMe(w http.ResponseWriter, r *http.Request) {
|
||||
portalID := portalUserID(r)
|
||||
emp, err := s.App.CurrentEmployee(r.Context(), portalID, false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var link *string
|
||||
if s.App.Cfg.TGBotUsername != "" && portalID != "" {
|
||||
v := "https://t.me/" + s.App.Cfg.TGBotUsername + "?start=" + portalID
|
||||
link = &v
|
||||
}
|
||||
var command *string
|
||||
if portalID != "" {
|
||||
v := "/start " + portalID
|
||||
command = &v
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"is_admin": isAdmin(r),
|
||||
"portal_user_id": nullablePlain(portalID),
|
||||
"telegram_linked": emp != nil && emp.TGChatID != nil && *emp.TGChatID != "",
|
||||
"employee": emp,
|
||||
"telegram_bot_username": nullablePlain(s.App.Cfg.TGBotUsername),
|
||||
"telegram_start_command": command,
|
||||
"telegram_start_link": link,
|
||||
})
|
||||
}
|
||||
|
||||
func (s Server) summary(w http.ResponseWriter, r *http.Request) {
|
||||
emp, err := s.App.CurrentEmployee(r.Context(), portalUserID(r), false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if emp != nil && emp.TGChatID == nil {
|
||||
emp = nil
|
||||
}
|
||||
out, err := s.App.Summary(r.Context(), emp)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s Server) employees(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
emp, err := s.App.CurrentEmployee(r.Context(), portalUserID(r), false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
items, err := s.App.ListEmployees(r.Context(), isAdmin(r), emp)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, items)
|
||||
case http.MethodPost:
|
||||
if !isAdmin(r) {
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
var payload EmployeePayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
emp, err := s.App.CreateEmployee(r.Context(), payload)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, emp)
|
||||
default:
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) employeeItem(w http.ResponseWriter, r *http.Request, path string) {
|
||||
id, ok := pathID(w, strings.TrimPrefix(path, "/api/v1/employees/"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !isAdmin(r) {
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPatch:
|
||||
var payload EmployeePayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
emp, err := s.App.UpdateEmployee(r.Context(), id, payload)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, emp)
|
||||
case http.MethodDelete:
|
||||
err := s.App.DeleteEmployee(r.Context(), id)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
items, err := s.App.ListProjects(r.Context(), emp.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, items)
|
||||
case http.MethodPost:
|
||||
var payload ProjectPayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
project, err := s.App.CreateProject(r.Context(), emp.ID, payload)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, project)
|
||||
default:
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
projectID, ok := pathID(w, parts[0])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
s.projectCRUD(w, r, emp.ID, 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 {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
changes, err := s.App.Worker.CheckProject(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
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 {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
out, err := s.App.Worker.Suggest(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
case len(parts) == 2 && parts[1] == "listings" && r.Method == http.MethodPost:
|
||||
s.addListing(w, r, emp.ID, projectID)
|
||||
case len(parts) == 3 && parts[1] == "listings" && parts[2] == "bulk" && r.Method == http.MethodPost:
|
||||
s.addListings(w, r, emp.ID, projectID)
|
||||
default:
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) projectCRUD(w http.ResponseWriter, r *http.Request, ownerID, projectID int64) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
project, err := s.App.ProjectByID(r.Context(), ownerID, projectID, true)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, project)
|
||||
case http.MethodPatch:
|
||||
var payload ProjectPayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
project, err := s.App.UpdateProject(r.Context(), ownerID, projectID, payload)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, project)
|
||||
case http.MethodDelete:
|
||||
if err := s.App.DeleteProject(r.Context(), ownerID, projectID); err != nil {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) addListing(w http.ResponseWriter, r *http.Request, ownerID, projectID int64) {
|
||||
if _, err := s.App.ProjectByID(r.Context(), ownerID, projectID, false); err != nil {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
var payload listingPayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
id, err := s.App.Worker.AddListing(r.Context(), projectID, payload.URL)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
listing, err := s.App.ListingByID(r.Context(), id, true)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, listing)
|
||||
}
|
||||
|
||||
func (s Server) addListings(w http.ResponseWriter, r *http.Request, ownerID, projectID int64) {
|
||||
if _, err := s.App.ProjectByID(r.Context(), ownerID, projectID, false); err != nil {
|
||||
writeError(w, http.StatusNotFound, "project not found")
|
||||
return
|
||||
}
|
||||
var payload bulkPayload
|
||||
if !decodeJSON(w, r, &payload) {
|
||||
return
|
||||
}
|
||||
out, err := s.App.Worker.AddListings(r.Context(), projectID, payload.URLs)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if r.Method != http.MethodDelete {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
if err := s.App.DeleteListing(r.Context(), emp.ID, id); err != nil {
|
||||
writeError(w, http.StatusNotFound, "listing not found")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s Server) requireEmployee(w http.ResponseWriter, r *http.Request) (*Employee, bool) {
|
||||
emp, err := s.App.CurrentEmployee(r.Context(), portalUserID(r), true)
|
||||
if errors.Is(err, ErrTelegramRequired) {
|
||||
writeError(w, http.StatusForbidden, "Сначала авторизуйтесь в Telegram-боте Monitoring PF")
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return nil, false
|
||||
}
|
||||
return emp, true
|
||||
}
|
||||
|
||||
func portalUserID(r *http.Request) string {
|
||||
return strings.TrimSpace(r.Header.Get("X-User-Id"))
|
||||
}
|
||||
|
||||
func isAdmin(r *http.Request) bool {
|
||||
return r.Header.Get("X-User-Is-Admin") == "1"
|
||||
}
|
||||
|
||||
func nullablePlain(value string) *string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func pathID(w http.ResponseWriter, value string) (int64, bool) {
|
||||
if strings.Contains(value, "/") {
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
return 0, false
|
||||
}
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, out any) bool {
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(out); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, detail string) {
|
||||
writeJSON(w, status, map[string]string{"detail": detail})
|
||||
}
|
||||
Reference in New Issue
Block a user