6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
137 lines
3.7 KiB
Go
137 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
chimw "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/hlog"
|
|
|
|
"aamos-admin/config"
|
|
"aamos-admin/db"
|
|
"aamos-admin/handlers"
|
|
"aamos-admin/middleware"
|
|
)
|
|
|
|
func main() {
|
|
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
|
|
|
cfg := config.Load()
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "9090"
|
|
}
|
|
|
|
database, err := db.Connect(cfg.DBURL)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msg("database connect failed")
|
|
}
|
|
defer database.Close()
|
|
|
|
// Extend users table for password auth and soft-delete (idempotent).
|
|
if _, err := database.Exec(
|
|
`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT NOT NULL DEFAULT ''`,
|
|
); err != nil {
|
|
logger.Fatal().Err(err).Msg("password_hash migration failed")
|
|
}
|
|
usersH := &handlers.UsersHandler{DB: database}
|
|
if err := usersH.EnsureSchema(); err != nil {
|
|
logger.Fatal().Err(err).Msg("users deleted_at migration failed")
|
|
}
|
|
|
|
authH := &handlers.AuthHandler{
|
|
DB: database,
|
|
JWTSecret: []byte(cfg.JWTSecret),
|
|
}
|
|
|
|
// External data handler (quiXzoom, LandveX, Economy, CRM)
|
|
externalH := handlers.UnifiedDashboardHandler(database, cfg.AMOSBaseURL)
|
|
modulesH, err := handlers.NewModulesHandler(database)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msg("modules seed failed")
|
|
}
|
|
settingsH := handlers.NewSettingsHandler(database)
|
|
if err := settingsH.Init(); err != nil {
|
|
logger.Fatal().Err(err).Msg("settings init failed")
|
|
}
|
|
onboardingH := handlers.NewOnboardingHandler(database)
|
|
if err := onboardingH.Init(); err != nil {
|
|
logger.Fatal().Err(err).Msg("onboarding init failed")
|
|
}
|
|
hub := handlers.NewHub()
|
|
go hub.Run()
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.CORS)
|
|
r.Use(hlog.NewHandler(logger))
|
|
r.Use(hlog.RequestIDHandler("req_id", "X-Request-ID"))
|
|
r.Use(middleware.Logger(logger))
|
|
r.Use(chimw.Recoverer)
|
|
|
|
// Public
|
|
r.Handle("/health", handlers.NewHealthHandler())
|
|
r.Post("/api/v1/auth/login", authH.Login)
|
|
r.Post("/api/v1/auth/logout", authH.Logout)
|
|
|
|
// Protected
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(middleware.Auth(cfg))
|
|
|
|
r.Get("/api/v1/auth/me", authH.Me)
|
|
r.Get("/api/v1/dashboard", handlers.DashboardHandler(database))
|
|
r.Get("/api/v1/unified-dashboard", externalH)
|
|
r.Get("/api/v1/service-health", handlers.ServiceHealthHandler)
|
|
r.Get("/api/v1/metrics", handlers.MetricsHandler)
|
|
r.Get("/api/v1/audit", handlers.AuditHandler(database))
|
|
|
|
// UsersHandler.ServeHTTP dispatches on method and path suffix.
|
|
r.Handle("/api/v1/users", usersH)
|
|
r.Handle("/api/v1/users/{id}", usersH)
|
|
|
|
// ModulesHandler methods; Toggle parses the path internally.
|
|
r.Get("/api/v1/modules", modulesH.List)
|
|
r.Put("/api/v1/modules/{id}/toggle", modulesH.Toggle)
|
|
|
|
// SettingsHandler.ServeHTTP dispatches on GET/PUT.
|
|
r.Handle("/api/v1/settings", settingsH)
|
|
|
|
r.Post("/api/v1/onboarding/start", onboardingH.ServeHTTP)
|
|
|
|
// WebSocket — upgrade is rejected before handshake if token is missing.
|
|
r.Get("/ws/metrics", hub.ServeWS)
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: r,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
logger.Info().Str("addr", srv.Addr).Msg("server starting")
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
logger.Fatal().Err(err).Msg("listen error")
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
|
|
logger.Info().Msg("shutting down")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
logger.Error().Err(err).Msg("shutdown error")
|
|
}
|
|
}
|