Mirrors the pattern sitesvc already proves: 32 random bytes, only the SHA-256 hash stored, a 24-hour expiry, and the token cleared on use -- so a leaked database yields no working links. Unverified login returns a distinct "verify your email address first" rather than the generic error. The address is already known to be theirs, so there is nothing to disclose and that is the only useful thing to say. Licence blobs are emailed inline. A blob is signed public data, not a secret: it is useless on any instance other than the one it names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"github.com/mrhid6/vantage/admin/internal/auth"
|
|
"github.com/mrhid6/vantage/admin/internal/config"
|
|
"github.com/mrhid6/vantage/admin/internal/db"
|
|
"github.com/mrhid6/vantage/admin/internal/inject"
|
|
"github.com/mrhid6/vantage/admin/internal/licensing"
|
|
"github.com/mrhid6/vantage/admin/internal/mail"
|
|
"github.com/mrhid6/vantage/admin/internal/models"
|
|
)
|
|
|
|
func main() {
|
|
godotenv.Load()
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatalf("configuration error: %v", err)
|
|
}
|
|
|
|
licensing.SetSigningKey(cfg.SigningKey)
|
|
|
|
mail.Init(mail.Config{
|
|
Host: cfg.SMTPHost, Port: cfg.SMTPPort, From: cfg.SMTPFrom,
|
|
Username: cfg.SMTPUsername, Password: cfg.SMTPPassword,
|
|
PublicURL: cfg.PublicURL,
|
|
})
|
|
if !mail.Enabled() {
|
|
log.Println("warning: SMTP not configured; verification and licence emails will fail")
|
|
}
|
|
|
|
auth.InitRedis(cfg.RedisAddr)
|
|
pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
if err := auth.Ping(pingCtx); err != nil {
|
|
pingCancel()
|
|
log.Fatalf("redis: %v", err)
|
|
}
|
|
pingCancel()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
if err := db.Connect(ctx, cfg); err != nil {
|
|
cancel()
|
|
log.Fatalf("database: %v", err)
|
|
}
|
|
cancel()
|
|
log.Printf("connected: admin=%s control=%s", cfg.AdminDBName, cfg.ControlDBName)
|
|
|
|
idxCtx, idxCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
if err := db.EnsureIndexes(idxCtx); err != nil {
|
|
idxCancel()
|
|
log.Fatalf("indexes: %v", err)
|
|
}
|
|
if err := models.SeedPlans(idxCtx); err != nil {
|
|
idxCancel()
|
|
log.Fatalf("plan seed: %v", err)
|
|
}
|
|
idxCancel()
|
|
|
|
reconcileCtx, stopReconcile := context.WithCancel(context.Background())
|
|
defer stopReconcile()
|
|
inject.StartReconciler(reconcileCtx)
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.Addr,
|
|
Handler: http.NotFoundHandler(), // replaced in Task 8
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
ReadTimeout: 20 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
log.Printf("admin listening on %s", cfg.Addr)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatalf("server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
stopCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
<-stopCtx.Done()
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer shutdownCancel()
|
|
_ = srv.Shutdown(shutdownCtx)
|
|
log.Println("admin stopped")
|
|
os.Exit(0)
|
|
}
|