Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
669 lines
20 KiB
Go
669 lines
20 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/support"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (s *Server) handleListSupportTickets(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": []any{}, "total": 0, "limit": 0, "offset": 0})
|
|
return
|
|
}
|
|
cid, ok := CompanyIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
limit, offset := ParseLimitOffset(r)
|
|
status := strings.TrimSpace(r.URL.Query().Get("status"))
|
|
items, total, err := s.Support.ListForUser(r.Context(), cid, uid, status, limit, offset)
|
|
if err != nil {
|
|
if support.IsMissingRelation(err) {
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": []any{}, "total": 0, "limit": limit, "offset": offset})
|
|
return
|
|
}
|
|
ClientOrLog(w, http.StatusBadRequest, "could not list tickets", err, support.ClientError)
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []support.Ticket{}
|
|
}
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": items, "total": total, "limit": limit, "offset": offset})
|
|
}
|
|
|
|
func (s *Server) handleCreateSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
cid, ok := CompanyIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
var body support.CreateInput
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
item, err := s.Support.Create(r.Context(), cid, uid, body)
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create ticket", err, support.ClientError)
|
|
return
|
|
}
|
|
// Stage A FAQ match (sync). Never awaits LLM — agent 4 owns AI fallback.
|
|
if updated, _, matchErr := s.Support.MaybeAutoReplyOnCreate(r.Context(), item); matchErr == nil {
|
|
item = updated
|
|
}
|
|
JSON(w, http.StatusCreated, item)
|
|
}
|
|
|
|
func (s *Server) handleGetSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
cid, ok := CompanyIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
item, err := s.Support.GetForUser(r.Context(), cid, uid, id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleReplySupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
cid, ok := CompanyIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
// Mass-assignment guard: customers cannot set is_internal_note / status.
|
|
var body support.UserReplyInput
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
item, err := s.Support.ReplyAsUser(r.Context(), cid, uid, id, support.ReplyInput{Body: body.Body})
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not reply", err, support.ClientError)
|
|
return
|
|
}
|
|
if updated, _, matchErr := s.Support.MaybeAutoReplyOnCustomerReply(r.Context(), item); matchErr == nil {
|
|
item = updated
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminListSupportTickets(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": []any{}, "total": 0, "limit": 0, "offset": 0})
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
limit, offset := ParseLimitOffset(r)
|
|
f := support.ListFilter{
|
|
Status: strings.TrimSpace(r.URL.Query().Get("status")),
|
|
Search: QuerySearch(r),
|
|
Scope: strings.TrimSpace(r.URL.Query().Get("scope")),
|
|
Flag: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("flag"))),
|
|
ActorID: uid,
|
|
FullAdmin: access.FullAdmin,
|
|
}
|
|
if f.Flag != "" && f.Flag != support.FlagNeedsHuman && f.Flag != support.FlagAIDraft {
|
|
Error(w, http.StatusBadRequest, "invalid flag")
|
|
return
|
|
}
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("company_id")); raw != "" {
|
|
cid, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid company_id")
|
|
return
|
|
}
|
|
f.CompanyID = &cid
|
|
}
|
|
if access.FullAdmin {
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("assignee_id")); raw != "" {
|
|
aid, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid assignee_id")
|
|
return
|
|
}
|
|
f.AssigneeID = &aid
|
|
}
|
|
}
|
|
items, total, err := s.Support.ListAdmin(r.Context(), f, limit, offset)
|
|
if err != nil {
|
|
if errors.Is(err, support.ErrForbidden) {
|
|
Error(w, http.StatusForbidden, "forbidden")
|
|
return
|
|
}
|
|
if support.IsMissingRelation(err) {
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": []any{}, "total": 0, "limit": limit, "offset": offset})
|
|
return
|
|
}
|
|
ClientOrLog(w, http.StatusBadRequest, "could not list tickets", err, support.ClientError)
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []support.Ticket{}
|
|
}
|
|
JSON(w, http.StatusOK, map[string]any{"tickets": items, "total": total, "limit": limit, "offset": offset, "scope": f.Scope})
|
|
}
|
|
|
|
func (s *Server) handleAdminGetSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
item, err := s.Support.GetAdmin(r.Context(), id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
if !s.staffMayAccessTicket(r, uid, item) {
|
|
// Anti-enumeration: same as missing for support_staff.
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminReplySupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
existing, err := s.Support.GetAdmin(r.Context(), id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
if !s.staffMayAccessTicket(r, uid, existing) {
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
if access.IsSupportOnly && existing.AssigneeAdminUserID != nil && *existing.AssigneeAdminUserID != uid {
|
|
CodedError(w, http.StatusConflict, "already_claimed", "assigned to another agent")
|
|
return
|
|
}
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
var body support.ReplyInput
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
item, err := s.Support.ReplyAsAgent(r.Context(), uid, id, body)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not reply", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminUpdateSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
existing, err := s.Support.GetAdmin(r.Context(), id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
if !s.staffMayAccessTicket(r, uid, existing) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
var body support.AdminUpdateInput
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
// Mass-assignment: only allowlisted fields; validate assignee is support-capable.
|
|
if body.AssigneeAdminUserID != nil && !body.ClearAssignee {
|
|
if *body.AssigneeAdminUserID == uuid.Nil {
|
|
Error(w, http.StatusBadRequest, "invalid assignee")
|
|
return
|
|
}
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
if access.IsSupportOnly && *body.AssigneeAdminUserID != uid {
|
|
// support_staff may only claim for self (or clear).
|
|
Error(w, http.StatusForbidden, "cannot assign to other staff")
|
|
return
|
|
}
|
|
ok, err := s.assigneeIsSupportCapable(r, *body.AssigneeAdminUserID)
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "authorization check failed", err)
|
|
return
|
|
}
|
|
if !ok {
|
|
Error(w, http.StatusBadRequest, "invalid assignee")
|
|
return
|
|
}
|
|
}
|
|
item, err := s.Support.UpdateAdmin(r.Context(), id, uid, body)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update ticket", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) staffActor(r *http.Request, uid uuid.UUID) support.AgentActor {
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
return support.AgentActor{UserID: uid, FullAdmin: access.FullAdmin}
|
|
}
|
|
|
|
func (s *Server) handleAdminClaimSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
item, err := s.Support.Claim(r.Context(), id, s.staffActor(r, uid))
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if errors.Is(err, support.ErrAlreadyClaimed) || errors.Is(err, support.ErrNotClaimable) {
|
|
Error(w, http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not claim ticket", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminReleaseSupportTicket(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
item, err := s.Support.Release(r.Context(), id, s.staffActor(r, uid))
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if errors.Is(err, support.ErrForbidden) {
|
|
Error(w, http.StatusForbidden, "forbidden")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not release ticket", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminApproveSupportAIDraft(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
existing, err := s.Support.GetAdmin(r.Context(), id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
if !s.staffMayAccessTicket(r, uid, existing) {
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
if access.IsSupportOnly && existing.AssigneeAdminUserID != nil && *existing.AssigneeAdminUserID != uid {
|
|
CodedError(w, http.StatusConflict, "already_claimed", "assigned to another agent")
|
|
return
|
|
}
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
var body support.ApproveAIDraftInput
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
item, err := s.Support.ApproveAIDraft(r.Context(), uid, id, body)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if errors.Is(err, support.ErrNoAIDraft) {
|
|
Error(w, http.StatusConflict, "no AI draft to approve")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not approve AI draft", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminDiscardSupportAIDraft(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
existing, err := s.Support.GetAdmin(r.Context(), id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "get failed", err)
|
|
return
|
|
}
|
|
if !s.staffMayAccessTicket(r, uid, existing) {
|
|
access, _ := StaffAccessFromContext(r.Context())
|
|
if access.IsSupportOnly && existing.AssigneeAdminUserID != nil && *existing.AssigneeAdminUserID != uid {
|
|
CodedError(w, http.StatusConflict, "already_claimed", "assigned to another agent")
|
|
return
|
|
}
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
item, err := s.Support.DiscardAIDraft(r.Context(), uid, id)
|
|
if errors.Is(err, support.ErrNotFound) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if errors.Is(err, support.ErrNoAIDraft) {
|
|
Error(w, http.StatusConflict, "no AI draft to discard")
|
|
return
|
|
}
|
|
if err != nil {
|
|
ClientOrLog(w, http.StatusBadRequest, "could not discard AI draft", err, support.ClientError)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (s *Server) handleAdminListSupportAgents(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
JSON(w, http.StatusOK, map[string]any{"agents": []any{}, "total": 0})
|
|
return
|
|
}
|
|
limit, offset := ParseLimitOffset(r)
|
|
includeAdmins := !QueryTruthy(r, "agents_only")
|
|
items, total, err := s.Support.ListAgents(r.Context(), includeAdmins, limit, offset)
|
|
if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "list failed", err)
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []support.SupportAgent{}
|
|
}
|
|
JSON(w, http.StatusOK, map[string]any{"agents": items, "total": total, "limit": limit, "offset": offset})
|
|
}
|
|
|
|
// staffMayAccessTicket enforces least-privilege visibility for support_staff.
|
|
func (s *Server) staffMayAccessTicket(r *http.Request, actor uuid.UUID, t support.Ticket) bool {
|
|
access, ok := StaffAccessFromContext(r.Context())
|
|
if !ok {
|
|
return false
|
|
}
|
|
if access.FullAdmin {
|
|
return true
|
|
}
|
|
if !access.SupportDesk {
|
|
return false
|
|
}
|
|
if t.AssigneeAdminUserID != nil {
|
|
return *t.AssigneeAdminUserID == actor
|
|
}
|
|
// Unassigned queue: claimable open/pending only.
|
|
return t.Status == "open" || t.Status == "pending"
|
|
}
|
|
|
|
func (s *Server) assigneeIsSupportCapable(r *http.Request, assignee uuid.UUID) (bool, error) {
|
|
if s.testStaffAccess != nil {
|
|
access, err := s.testStaffAccess(r.Context(), assignee)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return access.SupportDesk, nil
|
|
}
|
|
if s.Auth == nil {
|
|
return false, nil
|
|
}
|
|
return s.Auth.IsAssignableSupportStaff(r.Context(), assignee)
|
|
}
|
|
|
|
func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
JSON(w, http.StatusOK, map[string]any{"notifications": []any{}, "total": 0, "unread": 0, "limit": 0, "offset": 0})
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
limit, offset := ParseLimitOffset(r)
|
|
unreadOnly := QueryTruthy(r, "unread")
|
|
items, total, err := s.Support.ListNotifications(r.Context(), uid, unreadOnly, limit, offset)
|
|
if err != nil {
|
|
if support.IsMissingRelation(err) {
|
|
JSON(w, http.StatusOK, map[string]any{"notifications": []any{}, "total": 0, "unread": 0, "limit": limit, "offset": offset})
|
|
return
|
|
}
|
|
LogAndError(w, http.StatusInternalServerError, "list failed", err)
|
|
return
|
|
}
|
|
unread, err := s.Support.UnreadNotificationCount(r.Context(), uid)
|
|
if err != nil {
|
|
if support.IsMissingRelation(err) {
|
|
unread = 0
|
|
} else {
|
|
LogAndError(w, http.StatusInternalServerError, "list failed", err)
|
|
return
|
|
}
|
|
}
|
|
if items == nil {
|
|
items = []support.Notification{}
|
|
}
|
|
JSON(w, http.StatusOK, map[string]any{
|
|
"notifications": items,
|
|
"total": total,
|
|
"unread": unread,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMarkNotificationRead(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
if err := s.Support.MarkNotificationRead(r.Context(), uid, id); errors.Is(err, support.ErrNotificationGone) {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
} else if err != nil {
|
|
LogAndError(w, http.StatusInternalServerError, "update failed", err)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) handleMarkAllNotificationsRead(w http.ResponseWriter, r *http.Request) {
|
|
if s.Support == nil {
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
|
return
|
|
}
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
if !ok {
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
n, err := s.Support.MarkAllNotificationsRead(r.Context(), uid)
|
|
if err != nil {
|
|
if support.IsMissingRelation(err) {
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "updated": 0})
|
|
return
|
|
}
|
|
LogAndError(w, http.StatusInternalServerError, "update failed", err)
|
|
return
|
|
}
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "updated": n})
|
|
}
|