210 lines
5.9 KiB
Go
210 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/flowy-live/llink/internal/utils/flog"
|
|
|
|
"github.com/flowy-live/llink/internal/billing"
|
|
"github.com/flowy-live/llink/internal/middleware"
|
|
"github.com/flowy-live/llink/internal/network"
|
|
)
|
|
|
|
type CreateCheckoutSessionRequest struct {
|
|
Cadence string `json:"cadence"`
|
|
}
|
|
|
|
type CheckoutSessionResponse struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type PortalSessionResponse struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// GetNetworkUsage reports today's usage, daily limit (nil on pro), and reset
|
|
// time. Open to any network member since the UI surfaces it to every sender.
|
|
func (h *Handler) GetNetworkUsage(w http.ResponseWriter, r *http.Request) {
|
|
humanId, ok := middleware.HumanIdFromContext(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
|
|
}
|
|
|
|
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
|
if err != nil {
|
|
flog.Error("failed to check network membership", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !isMember {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
usage, err := h.billingSvc.GetUsage(r.Context(), networkID)
|
|
if err != nil {
|
|
flog.Error("failed to get network usage", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, usage)
|
|
}
|
|
|
|
func (h *Handler) GetNetworkBilling(w http.ResponseWriter, r *http.Request) {
|
|
net, _, ok := h.loadNetworkForAdmin(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
status, err := h.billingSvc.GetStatus(r.Context(), net.ID)
|
|
if err != nil {
|
|
flog.Error("failed to get billing status", "error", err, "network_id", net.ID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, status)
|
|
}
|
|
|
|
func (h *Handler) CreateCheckoutSession(w http.ResponseWriter, r *http.Request) {
|
|
net, adminHumanId, ok := h.loadNetworkForAdmin(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req CreateCheckoutSessionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
cadence := billing.Cadence(req.Cadence)
|
|
if !cadence.IsValid() {
|
|
http.Error(w, "invalid cadence", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
adminHuman, err := h.humanSvc.GetByID(r.Context(), adminHumanId)
|
|
if err != nil {
|
|
flog.Error("failed to load admin human", "error", err, "human_id", adminHumanId)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
seats, err := h.networkSvc.CountSeats(r.Context(), net.ID)
|
|
if err != nil {
|
|
flog.Error("failed to count seats", "error", err, "network_id", net.ID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
url, err := h.billingSvc.CreateCheckoutSession(r.Context(), billing.CheckoutParams{
|
|
NetworkID: net.ID,
|
|
AdminHumanID: adminHumanId,
|
|
AdminEmail: adminHuman.Email,
|
|
Cadence: cadence,
|
|
Seats: seats,
|
|
})
|
|
if err != nil {
|
|
flog.Error("failed to create checkout session", "error", err, "network_id", net.ID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, CheckoutSessionResponse{URL: url})
|
|
}
|
|
|
|
func (h *Handler) CreatePortalSession(w http.ResponseWriter, r *http.Request) {
|
|
net, _, ok := h.loadNetworkForAdmin(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
url, err := h.billingSvc.CreatePortalSession(r.Context(), net.ID)
|
|
if errors.Is(err, billing.ErrNoActiveSubscription) {
|
|
http.Error(w, "no active subscription", http.StatusConflict)
|
|
return
|
|
}
|
|
if err != nil {
|
|
flog.Error("failed to create portal session", "error", err, "network_id", net.ID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, PortalSessionResponse{URL: url})
|
|
}
|
|
|
|
const maxStripeWebhookBytes = 1 << 20 // 1 MiB
|
|
|
|
func (h *Handler) HandleStripeWebhook(w http.ResponseWriter, r *http.Request) {
|
|
payload, err := io.ReadAll(io.LimitReader(r.Body, maxStripeWebhookBytes))
|
|
if err != nil {
|
|
flog.Warn("stripe webhook: failed to read body", "error", err)
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
signature := r.Header.Get("Stripe-Signature")
|
|
|
|
if err := h.billingSvc.HandleWebhook(r.Context(), payload, signature); err != nil {
|
|
flog.Error("stripe webhook failed", "error", err)
|
|
formattedErr := fmt.Errorf("webhook processing failed: %w", err)
|
|
http.Error(w, formattedErr.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// Resolves {id}, verifies the caller is admin. On failure writes the HTTP
|
|
// error and returns ok=false.
|
|
func (h *Handler) loadNetworkForAdmin(w http.ResponseWriter, r *http.Request) (*network.Network, string, bool) {
|
|
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return nil, "", false
|
|
}
|
|
|
|
networkID := r.PathValue("id")
|
|
if networkID == "" {
|
|
http.Error(w, "network id is required", http.StatusBadRequest)
|
|
return nil, "", false
|
|
}
|
|
|
|
net, err := h.networkSvc.GetByID(r.Context(), networkID)
|
|
if errors.Is(err, network.ErrNotFound) {
|
|
http.Error(w, "network not found", http.StatusNotFound)
|
|
return nil, "", false
|
|
}
|
|
if err != nil {
|
|
flog.Error("failed to load network", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return nil, "", false
|
|
}
|
|
|
|
if net.AdminHumanId != humanId {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return nil, "", false
|
|
}
|
|
|
|
return net, humanId, true
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(body); err != nil {
|
|
flog.Error("failed to write json", "error", err)
|
|
}
|
|
}
|