- Backend: Email producer (LavinMQ), EmailService interface - Backend: CRUD API for email_templates and email_settings - Backend: avatar_url field in users table + UpdateMyProfile support - Backend: StorageService for pre-signed URLs - NestJS: Email consumer with Nodemailer and Handlebars - Frontend: Email Templates admin pages (list/edit) - Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow - Frontend: New /post-job public page (company registration + job creation wizard) - Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
146 lines
4 KiB
Go
146 lines
4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
|
"github.com/rede5/gohorsejobs/backend/internal/services"
|
|
)
|
|
|
|
type ChatHandlers struct {
|
|
chatService *services.ChatService
|
|
}
|
|
|
|
func NewChatHandlers(s *services.ChatService) *ChatHandlers {
|
|
return &ChatHandlers{chatService: s}
|
|
}
|
|
|
|
// ListConversations lists all conversations for the authenticated user
|
|
// @Summary List Conversations
|
|
// @Description List chat conversations for candidate or company
|
|
// @Tags Chat
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {array} services.Conversation
|
|
// @Router /api/v1/conversations [get]
|
|
func (h *ChatHandlers) ListConversations(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
userID, _ := ctx.Value(middleware.ContextUserID).(string)
|
|
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
|
|
roles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles))
|
|
|
|
if userID == "" {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
isCandidate := false
|
|
for _, r := range roles {
|
|
if r == "candidate" || r == "CANDIDATE" {
|
|
isCandidate = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Logic: If user has company (tenantID), prefer company view?
|
|
// But a user might be both candidate and admin (unlikely in this domain model).
|
|
// If tenantID is present, assume acting as Company.
|
|
if tenantID != "" {
|
|
isCandidate = false
|
|
} else {
|
|
isCandidate = true
|
|
}
|
|
|
|
convs, err := h.chatService.ListConversations(ctx, userID, tenantID, isCandidate)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(convs)
|
|
}
|
|
|
|
// ListMessages lists messages in a conversation
|
|
// @Summary List Messages
|
|
// @Description Get message history for a conversation
|
|
// @Tags Chat
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Conversation ID"
|
|
// @Security BearerAuth
|
|
// @Success 200 {array} services.Message
|
|
// @Router /api/v1/conversations/{id}/messages [get]
|
|
func (h *ChatHandlers) ListMessages(w http.ResponseWriter, r *http.Request) {
|
|
conversationID := r.PathValue("id")
|
|
if conversationID == "" {
|
|
http.Error(w, "Conversation ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
msgs, err := h.chatService.ListMessages(r.Context(), conversationID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Mark "IsMine"
|
|
userID, _ := r.Context().Value(middleware.ContextUserID).(string)
|
|
for i := range msgs {
|
|
if msgs[i].SenderID == userID {
|
|
msgs[i].IsMine = true
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(msgs)
|
|
}
|
|
|
|
// SendMessage sends a new message
|
|
// @Summary Send Message
|
|
// @Description Send a message to a conversation
|
|
// @Tags Chat
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Conversation ID"
|
|
// @Param request body map[string]string true "Message Content"
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} services.Message
|
|
// @Router /api/v1/conversations/{id}/messages [post]
|
|
func (h *ChatHandlers) SendMessage(w http.ResponseWriter, r *http.Request) {
|
|
conversationID := r.PathValue("id")
|
|
if conversationID == "" {
|
|
http.Error(w, "Conversation ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
userID, _ := r.Context().Value(middleware.ContextUserID).(string)
|
|
if userID == "" {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Content == "" {
|
|
http.Error(w, "Content required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
msg, err := h.chatService.SendMessage(r.Context(), userID, conversationID, req.Content)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
msg.IsMine = true
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(msg)
|
|
}
|