harryagasi
feat(storage): add Supabase object storage and reduce exposed API surface
e4eafe1
Raw
History Blame Contribute Delete
10.3 kB
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"project/internal/app"
"project/internal/domain"
"project/internal/framework"
)
type HTTPHandler struct {
Registry framework.Registry
AppService *app.Service
}
func NewHTTPHandler(registry framework.Registry, appService *app.Service) *HTTPHandler {
return &HTTPHandler{
Registry: registry,
AppService: appService,
}
}
// Health godoc
//
// @Summary Health check
// @Description Returns holistic readiness and dependency health for the service.
// @Tags system
// @Produce json
// @Success 200 {object} map[string]interface{} "Health report"
// @Failure 503 {object} map[string]interface{} "Critical dependency unavailable"
// @Router /health [get]
func (h *HTTPHandler) Health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
}
// ListFrameworks godoc
//
// @Produce json
// @Success 200 {array} object{id=string,name=string}
func (h *HTTPHandler) ListFrameworks(w http.ResponseWriter, r *http.Request) {
items := h.Registry.List()
type item struct {
ID string `json:"id"`
Name string `json:"name"`
}
out := []item{}
for _, fw := range items {
out = append(out, item{ID: fw.Framework.ID, Name: fw.Framework.Name})
}
writeJSON(w, http.StatusOK, out)
}
// CreateSession godoc
//
// @Accept json
// @Produce json
// @Param request body CreateSessionRequest true "Request body"
// @Success 201 {object} CreateSessionResponse
// @Failure 400 {object} ErrorResponse
func (h *HTTPHandler) CreateSession(w http.ResponseWriter, r *http.Request) {
var req CreateSessionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.FrameworkID == "" {
writeError(w, http.StatusBadRequest, "framework_id is required")
return
}
if req.UserID == "" {
writeError(w, http.StatusBadRequest, "user_id is required")
return
}
if req.RoomID == "" {
writeError(w, http.StatusBadRequest, "room_id is required")
return
}
if req.Language == "" {
req.Language = "id-ID"
}
result, err := h.AppService.StartSession(r.Context(), req.FrameworkID, req.Language, req.UserID, req.RoomID, parseMode(req.Mode))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, CreateSessionResponse{
SessionID: result.SessionID,
FrameworkID: result.FrameworkID,
RoomID: result.RoomID,
Status: "active",
OpeningMessage: result.OpeningMessage,
FirstQuestion: result.FirstQuestion,
})
}
// SendMessage godoc
//
// @Accept json
// @Produce json
// @Param id path string true "Session ID"
// @Param request body SendTextRequest true "Request body"
// @Success 200 {object} SendTextResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
func (h *HTTPHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
if sessionID == "" {
writeError(w, http.StatusBadRequest, "session_id is required")
return
}
var req SendTextRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Message == "" {
writeError(w, http.StatusBadRequest, "message is required")
return
}
if strings.TrimSpace(req.Message) == "/finish" {
h.handleFinishCommand(w, r, sessionID)
return
}
result, err := h.AppService.SendMessage(r.Context(), sessionID, req.Message)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, SendTextResponse{Reply: result.Reply, Stage: result.Stage, Finished: result.Finished})
}
// FinishSession godoc
//
// @Produce json
// @Param id path string true "Session ID"
// @Success 200 {object} FinishSessionResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
func (h *HTTPHandler) FinishSession(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
if sessionID == "" {
writeError(w, http.StatusBadRequest, "session_id is required")
return
}
result, err := h.AppService.FinishSession(r.Context(), sessionID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, FinishSessionResponse{
SessionID: sessionID,
RoomID: result.RoomID,
Status: "completed",
Result: toInterviewResultDTO(result.Result),
})
}
// GetRoomSession godoc
//
// @Produce json
// @Param room_id path string true "Room ID"
// @Success 200 {object} RoomSessionResponse
// @Failure 404 {object} ErrorResponse
func (h *HTTPHandler) GetRoomSession(w http.ResponseWriter, r *http.Request) {
roomID := r.PathValue("room_id")
if roomID == "" {
writeError(w, http.StatusBadRequest, "room_id is required")
return
}
res, err := h.AppService.GetRoomSession(r.Context(), roomID)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
turns := make([]TurnDTO, 0, len(res.Turns))
for _, t := range res.Turns {
turns = append(turns, TurnDTO{
Speaker: t.Speaker,
Text: t.Text,
CreatedAt: t.CreatedAt,
})
}
writeJSON(w, http.StatusOK, RoomSessionResponse{
SessionID: res.Session.SessionID,
RoomID: res.Session.RoomID,
FrameworkID: res.Session.FrameworkID,
FrameworkName: res.Session.FrameworkName,
Status: string(res.Session.Status),
Mode: string(res.Session.Mode),
Language: res.Session.Language,
StartedAt: res.Session.StartedAt,
Turns: turns,
})
}
// GetRoomResult godoc
//
// @Produce json
// @Param room_id path string true "Room ID"
// @Success 200 {object} InterviewResultDTO
// @Failure 404 {object} ErrorResponse
func (h *HTTPHandler) GetRoomResult(w http.ResponseWriter, r *http.Request) {
roomID := r.PathValue("room_id")
if roomID == "" {
writeError(w, http.StatusBadRequest, "room_id is required")
return
}
res, err := h.AppService.GetRoomResult(roomID)
if err != nil {
if errors.Is(err, app.ErrInterviewNotFinished) {
writeError(w, http.StatusNotFound, err.Error())
} else {
writeError(w, http.StatusInternalServerError, "failed to retrieve interview result")
}
return
}
writeJSON(w, http.StatusOK, toInterviewResultDTO(res))
}
func (h *HTTPHandler) handleFinishCommand(w http.ResponseWriter, r *http.Request, sessionID string) {
result, err := h.AppService.FinishSession(r.Context(), sessionID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, FinishSessionResponse{
SessionID: sessionID,
RoomID: result.RoomID,
Status: "completed",
Result: toInterviewResultDTO(result.Result),
})
}
func toInterviewResultDTO(r *domain.InterviewResult) *InterviewResultDTO {
if r == nil {
return nil
}
sections := make([]SectionResultDTO, 0, len(r.SectionResults))
for _, s := range r.SectionResults {
pairs := make([]QAPairDTO, 0, len(s.QAPairs))
for _, p := range s.QAPairs {
pairs = append(pairs, toQAPairDTO(p))
}
sections = append(sections, SectionResultDTO{
SectionTitle: s.SectionTitle,
Objective: s.Objective,
QAPairs: pairs,
SectionSummary: s.SectionSummary,
})
}
return &InterviewResultDTO{
FrameworkName: r.FrameworkName,
Mode: r.Mode,
Language: r.Language,
StartedAt: r.StartedAt,
EndedAt: r.EndedAt,
Summary: r.Summary,
Goals: r.Goals,
SectionResults: sections,
KeyInsights: r.KeyInsights,
UnresolvedItems: r.UnresolvedItems,
}
}
func toQAPairDTO(p domain.QAPair) QAPairDTO {
followUps := make([]QAPairDTO, 0, len(p.FollowUps))
for _, f := range p.FollowUps {
followUps = append(followUps, toQAPairDTO(f))
}
dto := QAPairDTO{
QuestionText: p.QuestionText,
AnswerCleaned: p.AnswerCleaned,
}
if len(followUps) > 0 {
dto.FollowUps = followUps
}
return dto
}
// StreamMessage godoc
//
// @Accept json
// @Produce text/event-stream
// @Param id path string true "Session ID"
// @Param request body SendTextRequest true "Request body"
// @Success 200 {string} string "Server-Sent Events stream token per event, ending with JSON {\"finished\":bool,\"stage\":string}"
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
func (h *HTTPHandler) StreamMessage(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
if sessionID == "" {
writeError(w, http.StatusBadRequest, "session_id is required")
return
}
var req SendTextRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Message == "" {
writeError(w, http.StatusBadRequest, "message is required")
return
}
if strings.TrimSpace(req.Message) == "/finish" {
h.handleFinishCommand(w, r, sessionID)
return
}
flusher, ok := w.(http.Flusher)
if !ok {
writeError(w, http.StatusInternalServerError, "streaming not supported")
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK)
result, err := h.AppService.StreamMessage(r.Context(), sessionID, req.Message, func(chunk string) {
fmt.Fprintf(w, "data: %s\n\n", chunk)
flusher.Flush()
})
if err != nil {
fmt.Fprintf(w, "data: {\"error\":%q}\n\n", err.Error())
flusher.Flush()
return
}
meta, _ := json.Marshal(map[string]any{"finished": result.Finished, "stage": result.Stage})
fmt.Fprintf(w, "data: %s\n\n", string(meta))
flusher.Flush()
}
func parseMode(s string) domain.InterviewMode {
if s == "audio" {
return domain.ModeAudio
}
return domain.ModeText
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, ErrorResponse{Error: msg})
}