package handler import ( "encoding/json" "net/http" "strconv" "strings" "secunda-test/internal/domain" "secunda-test/internal/httpx" "secunda-test/internal/service" ) type Handler struct { auth *service.AuthService teams *service.TeamService tasks *service.TaskService mw *httpx.Middleware } func New(auth *service.AuthService, teams *service.TeamService, tasks *service.TaskService, mw *httpx.Middleware) *Handler { return &Handler{auth: auth, teams: teams, tasks: tasks, mw: mw} } func (h *Handler) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("POST /api/v1/register", h.register) mux.HandleFunc("POST /api/v1/login", h.login) mux.Handle("POST /api/v1/teams", h.mw.Auth(http.HandlerFunc(h.createTeam))) mux.Handle("GET /api/v1/teams", h.mw.Auth(http.HandlerFunc(h.listTeams))) mux.Handle("POST /api/v1/teams/", h.mw.Auth(http.HandlerFunc(h.teamSubroutes))) mux.Handle("POST /api/v1/tasks", h.mw.Auth(http.HandlerFunc(h.createTask))) mux.Handle("GET /api/v1/tasks", h.mw.Auth(http.HandlerFunc(h.listTasks))) mux.Handle("PUT /api/v1/tasks/", h.mw.Auth(http.HandlerFunc(h.taskSubroutes))) mux.Handle("GET /api/v1/tasks/", h.mw.Auth(http.HandlerFunc(h.taskSubroutes))) mux.Handle("GET /api/v1/reports/team-summary", h.mw.Auth(http.HandlerFunc(h.teamSummary))) mux.Handle("GET /api/v1/reports/top-creators", h.mw.Auth(http.HandlerFunc(h.topCreators))) mux.Handle("GET /api/v1/reports/invalid-assignees", h.mw.Auth(http.HandlerFunc(h.invalidAssignees))) return h.mw.Chain(mux) } func (h *Handler) register(w http.ResponseWriter, r *http.Request) { var req struct{ Email, Password, Name string } if !decode(w, r, &req) { return } id, err := h.auth.Register(r.Context(), req.Email, req.Password, req.Name) if err != nil { httpx.WriteError(w, httpx.StatusFromError(err), err.Error()) return } httpx.WriteJSON(w, http.StatusCreated, map[string]int64{"id": id}) } func (h *Handler) login(w http.ResponseWriter, r *http.Request) { var req struct{ Email, Password string } if !decode(w, r, &req) { return } token, err := h.auth.Login(r.Context(), req.Email, req.Password) if err != nil { httpx.WriteError(w, httpx.StatusFromError(err), err.Error()) return } httpx.WriteJSON(w, http.StatusOK, map[string]string{"token": token}) } func (h *Handler) createTeam(w http.ResponseWriter, r *http.Request) { var req struct{ Name string } if !decode(w, r, &req) { return } team, err := h.teams.Create(r.Context(), mustUserID(r), req.Name) respond(w, team, http.StatusCreated, err) } func (h *Handler) listTeams(w http.ResponseWriter, r *http.Request) { teams, err := h.teams.List(r.Context(), mustUserID(r)) respond(w, teams, http.StatusOK, err) } func (h *Handler) teamSubroutes(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/teams/"), "/") if len(parts) == 2 && parts[1] == "invite" { teamID, _ := strconv.ParseInt(parts[0], 10, 64) var req struct{ Email string } if !decode(w, r, &req) { return } err := h.teams.Invite(r.Context(), mustUserID(r), teamID, req.Email) respond(w, map[string]string{"status": "invited"}, http.StatusOK, err) return } http.NotFound(w, r) } func (h *Handler) createTask(w http.ResponseWriter, r *http.Request) { var req domain.Task if !decode(w, r, &req) { return } task, err := h.tasks.Create(r.Context(), mustUserID(r), req) respond(w, task, http.StatusCreated, err) } func (h *Handler) listTasks(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() teamID, _ := strconv.ParseInt(q.Get("team_id"), 10, 64) page, _ := strconv.Atoi(q.Get("page")) pageSize, _ := strconv.Atoi(q.Get("page_size")) var assignee *int64 if q.Get("assignee_id") != "" { v, _ := strconv.ParseInt(q.Get("assignee_id"), 10, 64) assignee = &v } tasks, err := h.tasks.List(r.Context(), mustUserID(r), domain.TaskFilter{ TeamID: teamID, Status: domain.TaskStatus(q.Get("status")), AssigneeID: assignee, Page: page, PageSize: pageSize, }) respond(w, tasks, http.StatusOK, err) } func (h *Handler) taskSubroutes(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/v1/tasks/") parts := strings.Split(path, "/") id, _ := strconv.ParseInt(parts[0], 10, 64) if len(parts) == 2 && parts[1] == "history" && r.Method == http.MethodGet { history, err := h.tasks.History(r.Context(), mustUserID(r), id) respond(w, history, http.StatusOK, err) return } if len(parts) == 1 && r.Method == http.MethodPut { var req domain.Task if !decode(w, r, &req) { return } task, err := h.tasks.Update(r.Context(), mustUserID(r), id, req) respond(w, task, http.StatusOK, err) return } http.NotFound(w, r) } func (h *Handler) teamSummary(w http.ResponseWriter, r *http.Request) { out, err := h.tasks.TeamSummary(r.Context()) respond(w, out, http.StatusOK, err) } func (h *Handler) topCreators(w http.ResponseWriter, r *http.Request) { out, err := h.tasks.TopCreators(r.Context()) respond(w, out, http.StatusOK, err) } func (h *Handler) invalidAssignees(w http.ResponseWriter, r *http.Request) { out, err := h.tasks.InvalidAssignees(r.Context()) respond(w, out, http.StatusOK, err) } func decode(w http.ResponseWriter, r *http.Request, v any) bool { if err := json.NewDecoder(r.Body).Decode(v); err != nil { httpx.WriteError(w, http.StatusBadRequest, "invalid json") return false } return true } func respond(w http.ResponseWriter, v any, status int, err error) { if err != nil { httpx.WriteError(w, httpx.StatusFromError(err), err.Error()) return } httpx.WriteJSON(w, status, v) } func mustUserID(r *http.Request) int64 { id, _ := httpx.UserID(r.Context()) return id }