package handler import ( "context" "encoding/json" "errors" "log/slog" "net/http" "strings" "time" "github.com/flowy-live/llink/internal/auth" "github.com/flowy-live/llink/internal/depot" "github.com/flowy-live/llink/internal/human" "github.com/flowy-live/llink/internal/middleware" "github.com/flowy-live/llink/internal/network" "github.com/flowy-live/llink/internal/particle" "github.com/flowy-live/llink/internal/utils" ) type Handler struct { authSvc auth.AuthService humanSvc human.Service networkSvc network.Service particleSvc particle.Service depotSvc depot.Service } func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service) *Handler { return &Handler{ authSvc: authSvc, humanSvc: humanSvc, networkSvc: networkSvc, particleSvc: particleSvc, depotSvc: depotSvc, } } // Response DTOs type Human struct { // Id will be nil if this human is not registered Id *string `json:"id"` Email string `json:"email"` EmailPrefix string `json:"email_prefix"` // CreatedAt will be nil if this human is not registered CreatedAt *time.Time `json:"created_at"` } type Network struct { Id string `json:"id"` Name string `json:"name"` AdminHuman Human `json:"admin_human"` Humans []Human `json:"humans"` CreatedAt time.Time `json:"created_at"` } // Auth Request/Response DTOs type RequestSignInCodeRequest struct { Email string `json:"email"` } type SignInRequest struct { Email string `json:"email"` Code string `json:"code"` } type SignInResponse struct { Human Human `json:"human"` Token string `json:"token"` } // Network Request DTOs type CreateNetworkRequest struct { Name string `json:"name"` } type AddMembersToNetworkRequest struct { EmailAddresses []string `json:"email_addresses"` } type SetOpenStreamCapacityRequest struct { Capacity int `json:"capacity"` } type MembersRequest struct { Emails []string `json:"emails"` } // Depot DTOs type PrepareUploadRequest struct { NetworkId string `json:"network_id"` Name string `json:"name"` ContentType string `json:"content_type"` ContentLength int64 `json:"content_length"` } type PrepareUploadResponse struct { ObjectID string `json:"object_id"` UploadURL string `json:"upload_url"` UploadHeaders map[string]string `json:"upload_headers"` } type DepotObject struct { ID string `json:"id"` Name string `json:"name"` ContentType string `json:"content_type"` ContentLength int64 `json:"content_length"` ContainsContent bool `json:"contains_content"` DownloadURL string `json:"download_url,omitempty"` CreatedAt time.Time `json:"created_at"` } // ============================================================================ // Auth Handlers // ============================================================================ // RequestSignInCode creates a human account if not already existent and sends a sign-in code func (h *Handler) RequestSignInCode(w http.ResponseWriter, r *http.Request) { var req RequestSignInCodeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } if req.Email == "" { http.Error(w, "email is required", http.StatusBadRequest) return } // Auto-create human if doesn't exist _, err := h.humanSvc.GetOrCreateByEmail(r.Context(), req.Email) if err != nil { slog.Error("failed to get or create human", "error", err, "email", req.Email) http.Error(w, "internal server error", http.StatusInternalServerError) return } // Request sign-in code if err := h.authSvc.RequestSignInCode(r.Context(), req.Email); err != nil { slog.Error("failed to request sign-in code", "error", err, "email", req.Email) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // SignIn verifies the code and returns a session token func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) { var req SignInRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } if req.Email == "" || req.Code == "" { http.Error(w, "email and code are required", http.StatusBadRequest) return } token, err := h.authSvc.VerifySignInCode(r.Context(), req.Email, req.Code) if err != nil { if errors.Is(err, auth.ErrInvalidCode) { http.Error(w, "invalid code", http.StatusUnauthorized) return } slog.Error("failed to verify sign-in code", "error", err, "email", req.Email) http.Error(w, "internal server error", http.StatusInternalServerError) return } // Get the human hum, err := h.humanSvc.GetByEmail(r.Context(), req.Email) if err != nil { slog.Error("failed to get human after sign-in", "error", err, "email", req.Email) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp := SignInResponse{ Human: humanToDTO(hum), Token: token, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // SignOut deletes the session from the token in headers func (h *Handler) SignOut(w http.ResponseWriter, r *http.Request) { token := extractBearerToken(r) if token == "" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } if err := h.authSvc.SignOut(r.Context(), token); err != nil { slog.Error("failed to sign out", "error", err) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // GetCurrentHuman returns the authenticated human func (h *Handler) GetCurrentHuman(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } hum, err := h.humanSvc.GetByEmail(r.Context(), email) if err != nil { if errors.Is(err, human.ErrNotFound) { http.Error(w, "human not found", http.StatusNotFound) return } slog.Error("failed to get current human", "error", err, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") dto := humanToDTO(hum) json.NewEncoder(w).Encode(dto) } // ============================================================================ // Network Handlers // ============================================================================ // CreateNetwork creates a new network func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req CreateNetworkRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } net, err := h.networkSvc.Create(r.Context(), req.Name, email) if err != nil { if errors.Is(err, network.ErrInvalidName) { http.Error(w, "name cannot be empty", http.StatusBadRequest) return } slog.Error("failed to create network", "error", err, "email", email, "name", req.Name) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp, err := h.networkToDTO(r.Context(), net) if err != nil { slog.Error("failed to convert network to DTO", "error", err, "network_id", net.ID) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(resp) } // ListNetworks retrieves networks for the authenticated human func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } networks, err := h.networkSvc.ListForEmail(r.Context(), email) if err != nil { slog.Error("failed to list networks", "error", err, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp := make([]Network, 0, len(networks)) for _, net := range networks { dto, err := h.networkToDTO(r.Context(), net) if err != nil { slog.Warn("failed to convert network to DTO in list", "error", err, "network_id", net.ID) continue } resp = append(resp, dto) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // GetNetwork retrieves a specific network func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } networkID := r.PathValue("id") if networkID == "" { http.Error(w, "network id is required", http.StatusBadRequest) return } // Check membership isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email) if err != nil { slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } if !isMember { http.Error(w, "access denied", http.StatusForbidden) return } net, err := h.networkSvc.GetByID(r.Context(), networkID) if err != nil { if errors.Is(err, network.ErrNotFound) { http.Error(w, "network not found", http.StatusNotFound) return } slog.Error("failed to get network", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp, err := h.networkToDTO(r.Context(), net) if err != nil { slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // AddMembersToNetwork adds members to a network func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } networkID := r.PathValue("id") if networkID == "" { http.Error(w, "network id is required", http.StatusBadRequest) return } // Check membership isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email) if err != nil { slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } if !isMember { http.Error(w, "access denied", http.StatusForbidden) return } var req AddMembersToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } if len(req.EmailAddresses) == 0 { http.Error(w, "email addresses are required", http.StatusBadRequest) return } if err := h.networkSvc.AddMembers(r.Context(), networkID, req.EmailAddresses); err != nil { slog.Error("failed to add members to network", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } // Return updated network net, err := h.networkSvc.GetByID(r.Context(), networkID) if err != nil { slog.Error("failed to get network after adding members", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp, err := h.networkToDTO(r.Context(), net) if err != nil { slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // RemoveMemberFromNetwork removes a member from a network func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } networkID := r.PathValue("id") memberEmail := r.PathValue("email") if networkID == "" || memberEmail == "" { http.Error(w, "network id and member email are required", http.StatusBadRequest) return } // Check membership isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email) if err != nil { slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } if !isMember { http.Error(w, "access denied", http.StatusForbidden) return } if err := h.networkSvc.RemoveMember(r.Context(), networkID, memberEmail); err != nil { slog.Error("failed to remove member from network", "error", err, "network_id", networkID, "member_email", memberEmail) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // DownloadParticleMedia returns a fresh signed download URL for media/file particles func (h *Handler) DownloadParticleMedia(w http.ResponseWriter, r *http.Request) { _, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // TODO: integrate firebase to fetch particle, and verify visibility for this particle's media objectID := r.PathValue("id") downloadURL, err := h.depotSvc.GetDownloadURL(r.Context(), objectID) if err != nil { slog.Error("failed to get download URL", "error", err, "object_id", objectID) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"url": downloadURL}) } // ============================================================================ // Depot Handlers // ============================================================================ // PrepareUpload prepares an upload and returns a signed URL for direct upload to GCS func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) { email, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req PrepareUploadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } if req.NetworkId == "" { http.Error(w, "network_id is required", http.StatusBadRequest) return } // Check network membership isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, email) if err != nil { slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "email", email) http.Error(w, "internal server error", http.StatusInternalServerError) return } if !isMember { http.Error(w, "access denied", http.StatusForbidden) return } input := depot.PrepareUploadInput{ Prefix: req.NetworkId, Name: req.Name, ContentType: req.ContentType, ContentLength: req.ContentLength, } result, err := h.depotSvc.PrepareUpload(r.Context(), input) if err != nil { if errors.Is(err, depot.ErrInvalidInput) { http.Error(w, err.Error(), http.StatusBadRequest) return } slog.Error("failed to prepare upload", "error", err, "network_id", req.NetworkId, "name", req.Name) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp := PrepareUploadResponse{ ObjectID: result.ObjectID, UploadURL: result.UploadURL, UploadHeaders: result.UploadHeaders, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // ConfirmUpload confirms that an upload has been completed func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) { _, ok := middleware.EmailFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } objectID := r.PathValue("id") if objectID == "" { http.Error(w, "object id is required", http.StatusBadRequest) return } obj, err := h.depotSvc.ConfirmUpload(r.Context(), objectID) if err != nil { if errors.Is(err, depot.ErrNotFound) { http.Error(w, "object not found", http.StatusNotFound) return } if errors.Is(err, depot.ErrInvalidInput) { http.Error(w, err.Error(), http.StatusBadRequest) return } slog.Error("failed to confirm upload", "error", err, "object_id", objectID) http.Error(w, "internal server error", http.StatusInternalServerError) return } resp := DepotObject{ ID: obj.ID, Name: obj.Name, ContentType: obj.ContentType, ContentLength: obj.ContentLength, ContainsContent: obj.ContainsContent, DownloadURL: "", CreatedAt: obj.CreatedAt, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // ============================================================================ // Helper Functions // ============================================================================ func humanToDTO(h *human.Human) Human { return Human{ Id: utils.CreateOptionalString(h.ID), Email: h.Email, EmailPrefix: h.EmailPrefix, CreatedAt: &h.CreatedAt, } } func emailPrefix(email string) string { return strings.Split(email, "@")[0] } func (h *Handler) networkToDTO(ctx context.Context, n *network.Network) (Network, error) { adminHuman, err := h.humanSvc.GetByEmail(ctx, n.AdminEmail) if err != nil { return Network{}, err } // Get member humans humans := make([]Human, 0, len(n.MemberEmails)) for _, email := range n.MemberEmails { hum, err := h.humanSvc.GetByEmail(ctx, email) if err != nil { if err == human.ErrNotFound { humans = append(humans, Human{ Id: nil, CreatedAt: nil, Email: email, EmailPrefix: emailPrefix(email), }) } continue } else { humans = append(humans, humanToDTO(hum)) } } return Network{ Id: n.ID, Name: n.Name, AdminHuman: humanToDTO(adminHuman), Humans: humans, CreatedAt: n.CreatedAt, }, nil } func extractBearerToken(r *http.Request) string { authHeader := r.Header.Get("Authorization") if authHeader == "" { return "" } const prefix = "Bearer " if len(authHeader) > len(prefix) && authHeader[:len(prefix)] == prefix { return authHeader[len(prefix):] } return "" }