Files
llink/go/cmd/orion/main.go
T
Arjun Patel 71fe83bd0f typo
2026-06-11 13:57:08 -07:00

218 lines
7.7 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"github.com/flowy-live/llink/internal/utils/flog"
"cloud.google.com/go/firestore"
"cloud.google.com/go/storage"
firebase "firebase.google.com/go/v4"
"github.com/redis/go-redis/v9"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal"
"github.com/flowy-live/llink/internal/auth"
"github.com/flowy-live/llink/internal/billing"
"github.com/flowy-live/llink/internal/db"
"github.com/flowy-live/llink/internal/depot"
"github.com/flowy-live/llink/internal/handler"
"github.com/flowy-live/llink/internal/human"
"github.com/flowy-live/llink/internal/human/pushnotify"
"github.com/flowy-live/llink/internal/livekit"
"github.com/flowy-live/llink/internal/livestore"
"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"
"github.com/flowy-live/llink/internal/waitlist"
)
func redisForAuth() *redis.Client {
return internal.ConnectAndTestRedis(db.RedisDBAuth)
}
func main() {
port := utils.MustGetEnv("PORT")
gcsBucket := utils.MustGetEnv("GCS_BUCKET")
db.Init()
defer db.Cleanup()
redisClient := redisForAuth()
ctx := context.Background()
storageClient, err := storage.NewClient(ctx)
if err != nil {
flog.Error("failed to create GCS client", "error", err)
os.Exit(1)
}
defer storageClient.Close()
aeroAddr := utils.MustGetEnv("AERO_ADDR")
if aeroAddr == "" {
flog.Error("must provide AERO_ADDR")
os.Exit(1)
}
aeroServer, err := grpc.NewClient(aeroAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
flog.Error("connection to aero server invalid", "error", err)
os.Exit(1)
}
defer aeroServer.Close()
aeroSvc := pbaero.NewPrimaryClient(aeroServer)
gcpProject := utils.MustGetEnv("GCP_PROJECT")
fbApp, err := firebase.NewApp(ctx, &firebase.Config{ProjectID: gcpProject})
if err != nil {
flog.Error("failed to init Firebase Admin app", "error", err)
os.Exit(1)
}
fbAuth, err := fbApp.Auth(ctx)
if err != nil {
flog.Error("failed to create Firebase auth client", "error", err)
os.Exit(1)
}
authSvc := auth.NewAuthService(redisClient, aeroSvc, fbAuth)
humanSvc := human.NewService(db.Pool())
billingSvc, err := billing.NewService(ctx, db.Pool(), billing.Config{
SecretKey: utils.MustGetEnv("STRIPE_SECRET_KEY"),
WebhookSecret: utils.MustGetEnv("STRIPE_WEBHOOK_SECRET"),
PriceMonthlyID: utils.MustGetEnv("STRIPE_PRICE_PRO_MONTHLY"),
PriceAnnualID: utils.MustGetEnv("STRIPE_PRICE_PRO_ANNUAL"),
SuccessURL: utils.MustGetEnv("BILLING_SUCCESS_URL"),
CancelURL: utils.MustGetEnv("BILLING_CANCEL_URL"),
})
if err != nil {
flog.Error("failed to initialize billing service", "error", err)
os.Exit(1)
}
firestoreClient, err := firestore.NewClient(ctx, gcpProject)
if err != nil {
flog.Error("failed to create Firestore client", "error", err)
os.Exit(1)
}
defer firestoreClient.Close()
networkSvc := network.NewService(db.Pool(), aeroSvc, billingSvc, livestore.NewMembershipPublisher(firestoreClient), humanSvc)
particleSvc := particle.NewService(db.Pool(), networkSvc)
depotSvc := depot.NewService(db.Pool(), storageClient, depot.Config{
GoogleServiceAccountEmail: utils.MustGetEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL"),
BucketName: gcsBucket,
})
waitlistSvc := waitlist.NewService(db.Pool(), aeroSvc)
pushTokenSvc := pushnotify.NewService(db.Pool())
livekitClient := livekit.NewClient()
h := handler.NewHandler(authSvc, humanSvc, networkSvc, particleSvc, depotSvc, waitlistSvc, billingSvc, pushTokenSvc, livekitClient, firestoreClient)
withAuth := func(hf http.HandlerFunc) http.Handler {
return middleware.Auth(authSvc)(http.HandlerFunc(hf))
}
mux := http.NewServeMux()
// ==========================================================================
// Public routes (no auth)
// ==========================================================================
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("POST /auth/request-code", h.RequestSignInCode)
mux.HandleFunc("POST /auth/sign-in", h.SignIn)
mux.HandleFunc("POST /waitlist", h.AddToWaitlist)
mux.HandleFunc("POST /livekit/webhook", h.HandleLivekitWebhook)
mux.HandleFunc("POST /webhooks/stripe", h.HandleStripeWebhook)
// ==========================================================================
// Protected routes (auth required)
// ==========================================================================
// Auth
mux.Handle("POST /auth/sign-out", withAuth(h.SignOut))
mux.Handle("GET /auth/me", withAuth(h.GetCurrentHuman))
mux.Handle("POST /auth/firebase-token", withAuth(h.FirebaseToken))
// Settings
mux.Handle("PATCH /humans/me/settings", withAuth(h.UpdateSettings))
mux.Handle("PUT /humans/me/avatar", withAuth(h.UpdateAvatar))
mux.Handle("DELETE /humans/me/avatar", withAuth(h.DeleteAvatar))
// Get avatar download url, given objectId
mux.Handle("GET /humans/avatar/{id}", withAuth(h.GetObjectDownloadUrl))
// Push notification tokens (per-device)
mux.Handle("POST /humans/me/push-tokens", withAuth(h.RegisterPushToken))
mux.Handle("DELETE /humans/me/push-tokens", withAuth(h.UnregisterPushToken))
// Networks
mux.Handle("POST /networks", withAuth(h.CreateNetwork))
mux.Handle("GET /networks", withAuth(h.ListNetworks))
mux.Handle("GET /networks/{id}", withAuth(h.GetNetwork))
mux.Handle("POST /networks/{id}/members", withAuth(h.AddMembersToNetwork))
mux.Handle("DELETE /networks/{id}/members/{humanId}", withAuth(h.RemoveMemberFromNetwork))
// Billing (network admin only; admin check happens inside each handler)
mux.Handle("GET /networks/{id}/billing", withAuth(h.GetNetworkBilling))
mux.Handle("POST /networks/{id}/billing/checkout-session", withAuth(h.CreateCheckoutSession))
mux.Handle("POST /networks/{id}/billing/portal-session", withAuth(h.CreatePortalSession))
// Freemium usage (any network member)
mux.Handle("GET /networks/{id}/usage", withAuth(h.GetNetworkUsage))
// Network Invitations
mux.Handle("GET /networks/{id}/invitations", withAuth(h.ListInvitationsForNetwork))
mux.Handle("DELETE /networks/{id}/invitations", withAuth(h.RevokeInvitation))
mux.Handle("GET /invitations", withAuth(h.ListMyInvitations))
mux.Handle("POST /invitations/accept", withAuth(h.AcceptInvitation))
// Particles
mux.Handle("GET /particles/{id}/download", withAuth(h.GetObjectDownloadUrl))
// Link metadata
mux.Handle("GET /metadata", withAuth(h.GetLinkMetadata))
// Depot
mux.Handle("POST /depot/upload", withAuth(h.PrepareUpload))
mux.Handle("POST /depot/objects/{id}/confirm", withAuth(h.ConfirmUpload))
// LiveKit
mux.Handle("POST /livekit/token", withAuth(h.GetLivekitToken))
// Waitlist (admin-only)
mux.Handle("GET /waitlist", withAuth(h.GetWaitlist))
mux.Handle("GET /waitlist/{email}", withAuth(h.GetWaitlistEntry))
mux.Handle("POST /waitlist/invite", withAuth(h.InviteWaitlistEntrant))
// CORS_ALLOWED_ORIGINS is a comma-separated whitelist for the web client.
// Empty / unset = allow all
var allowedOrigins []string
if raw := os.Getenv("CORS_ALLOWED_ORIGINS"); raw != "" {
for _, o := range strings.Split(raw, ",") {
if o = strings.TrimSpace(o); o != "" {
allowedOrigins = append(allowedOrigins, o)
}
}
}
muxWithCors := middleware.CORS(allowedOrigins)(mux)
addr := fmt.Sprintf("0.0.0.0:%s", port)
flog.Info("running server", "addr", addr)
if err := http.ListenAndServe(addr, muxWithCors); err != nil {
flog.Error("server failed", "error", err)
os.Exit(1)
}
}