OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// luminary-recover: emergency recovery CLI (reset password, clear IP rules, restart).
|
||||
// Intended for docker exec when admin login is locked out.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/rose_cat707/luminary/internal/auth"
|
||||
"github.com/rose_cat707/luminary/internal/model"
|
||||
"github.com/rose_cat707/luminary/internal/settings"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run())
|
||||
}
|
||||
|
||||
func run() int {
|
||||
dataDir := flag.String("data-dir", envOr("LUMINARY_DATA", "./data"), "directory containing luminary.db")
|
||||
deprecatedConfig := flag.String("config", "", "deprecated: ignored; use -data-dir / LUMINARY_DATA")
|
||||
username := flag.String("username", "", "admin username (default: admin)")
|
||||
password := flag.String("password", "", "new admin password (default: admin123)")
|
||||
doAll := flag.Bool("all", false, "reset password, clear IP rules and sessions, then restart")
|
||||
doPassword := flag.Bool("reset-password", false, "reset admin password")
|
||||
doIP := flag.Bool("clear-ip", false, "delete all IP rules")
|
||||
doSessions := flag.Bool("clear-sessions", false, "delete all admin sessions")
|
||||
doRestart := flag.Bool("restart", false, "send SIGTERM to PID 1 to restart server (clears login cooldown)")
|
||||
flag.Parse()
|
||||
if *deprecatedConfig != "" {
|
||||
fmt.Fprintln(os.Stderr, "warning: -config is deprecated and ignored; use -data-dir / LUMINARY_DATA")
|
||||
}
|
||||
|
||||
if !*doAll && !*doPassword && !*doIP && !*doSessions && !*doRestart {
|
||||
fmt.Fprintln(os.Stderr, "usage: luminary-recover --all [--password NEW] [--restart]")
|
||||
fmt.Fprintln(os.Stderr, " luminary-recover --reset-password --clear-ip --clear-sessions [--restart]")
|
||||
flag.PrintDefaults()
|
||||
return 2
|
||||
}
|
||||
|
||||
user := *username
|
||||
pass := *password
|
||||
if user == "" || pass == "" {
|
||||
legacyUser, legacyPass := settings.LegacyAdminCredentials()
|
||||
if user == "" {
|
||||
user = legacyUser
|
||||
}
|
||||
if pass == "" {
|
||||
pass = legacyPass
|
||||
}
|
||||
}
|
||||
|
||||
if *doAll {
|
||||
*doPassword = true
|
||||
*doIP = true
|
||||
*doSessions = true
|
||||
*doRestart = true
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(*dataDir, "luminary.db")
|
||||
db, err := openDB(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open database: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if *doPassword {
|
||||
if pass == "" {
|
||||
fmt.Fprintln(os.Stderr, "password is required (use --password)")
|
||||
return 1
|
||||
}
|
||||
if err := resetPassword(db, user, pass); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "reset password: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("admin password reset for user %q\n", user)
|
||||
}
|
||||
|
||||
if *doIP {
|
||||
res := db.Exec("DELETE FROM ip_rules")
|
||||
if res.Error != nil {
|
||||
fmt.Fprintf(os.Stderr, "clear IP rules: %v\n", res.Error)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("cleared %d IP rule(s)\n", res.RowsAffected)
|
||||
}
|
||||
|
||||
if *doSessions {
|
||||
res := db.Exec("DELETE FROM admin_sessions")
|
||||
if res.Error != nil {
|
||||
fmt.Fprintf(os.Stderr, "clear sessions: %v\n", res.Error)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("cleared %d admin session(s)\n", res.RowsAffected)
|
||||
}
|
||||
|
||||
if *doRestart {
|
||||
fmt.Println("sending SIGTERM to PID 1 (restart server to clear in-memory login cooldown)...")
|
||||
if err := syscall.Kill(1, syscall.SIGTERM); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "restart: %v (you may need to restart the container manually)\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("done")
|
||||
return 0
|
||||
}
|
||||
|
||||
func openDB(path string) (*gorm.DB, error) {
|
||||
dsn := path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func resetPassword(db *gorm.DB, username, password string) error {
|
||||
hash, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var user model.AdminUser
|
||||
err = db.Where("username = ?", username).First(&user).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return db.Create(&model.AdminUser{
|
||||
Username: username,
|
||||
PasswordHash: hash,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&user).Update("password_hash", hash).Error
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
webembed "github.com/rose_cat707/luminary/web"
|
||||
"github.com/rose_cat707/luminary/internal/gateway"
|
||||
"github.com/rose_cat707/luminary/internal/handler/admin"
|
||||
"github.com/rose_cat707/luminary/internal/handler/files"
|
||||
"github.com/rose_cat707/luminary/internal/handler/proxy"
|
||||
"github.com/rose_cat707/luminary/internal/handler/replicate"
|
||||
"github.com/rose_cat707/luminary/internal/handler/static"
|
||||
"github.com/rose_cat707/luminary/internal/middleware"
|
||||
"github.com/rose_cat707/luminary/internal/model"
|
||||
"github.com/rose_cat707/luminary/internal/monitor"
|
||||
"github.com/rose_cat707/luminary/internal/prediction"
|
||||
"github.com/rose_cat707/luminary/internal/settings"
|
||||
"github.com/rose_cat707/luminary/internal/storage/imagestore"
|
||||
"github.com/rose_cat707/luminary/internal/store/cache"
|
||||
sqlitestore "github.com/rose_cat707/luminary/internal/store/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dataDir := flag.String("data-dir", envOr("LUMINARY_DATA", "./data"), "directory for SQLite database")
|
||||
addr := flag.String("addr", envOr("LUMINARY_ADDR", ":8293"), "HTTP listen address")
|
||||
deprecatedConfig := flag.String("config", "", "deprecated: ignored; settings are stored in the database")
|
||||
flag.Parse()
|
||||
if *deprecatedConfig != "" {
|
||||
log.Printf("warning: -config is deprecated and ignored; use -data-dir / LUMINARY_DATA and admin UI settings")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(*dataDir, 0o755); err != nil {
|
||||
log.Fatalf("data dir: %v", err)
|
||||
}
|
||||
dbPath := filepath.Join(*dataDir, "luminary.db")
|
||||
|
||||
db, err := sqlitestore.New(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("database: %v", err)
|
||||
}
|
||||
|
||||
rt := settings.NewRuntime(db.DB())
|
||||
if err := rt.Load(); err != nil {
|
||||
log.Fatalf("settings: %v", err)
|
||||
}
|
||||
|
||||
memCache := cache.New()
|
||||
repo := sqlitestore.NewRepository(db, memCache)
|
||||
if err := repo.EnsureAdmin(); err != nil {
|
||||
log.Fatalf("admin bootstrap: %v", err)
|
||||
}
|
||||
if err := repo.BootstrapFromDB(); err != nil {
|
||||
log.Fatalf("cache bootstrap: %v", err)
|
||||
}
|
||||
rt.AfterLoad()
|
||||
|
||||
imageDir := filepath.Join(*dataDir, rt.ImageStoragePath())
|
||||
publicURL := func() string {
|
||||
if u := rt.PublicBaseURL(); u != "" {
|
||||
return u
|
||||
}
|
||||
a := *addr
|
||||
if strings.HasPrefix(a, ":") {
|
||||
a = "localhost" + a
|
||||
}
|
||||
return "http://" + a
|
||||
}
|
||||
imgStore := imagestore.New(db.DB(), imageDir, publicURL(), rt.EncryptionKey(), rt.SignedURLTTL())
|
||||
_ = imgStore.EnsureDir()
|
||||
|
||||
predSvc := prediction.NewService(db.DB(), memCache, imgStore, publicURL(), rt.PredictionWaitSeconds())
|
||||
|
||||
gw := gateway.New(memCache, rt)
|
||||
mon := monitor.New(gw, memCache, db, rt)
|
||||
mon.Start(repo.FlushUsage)
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(middleware.Logger(), gin.Recovery())
|
||||
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
adminHandler := admin.New(repo, db, memCache, gw, rt, mon, predSvc, imgStore, publicURL)
|
||||
adminGroup := r.Group("/api/admin", middleware.IPFilter(memCache, model.IPScopeAdmin))
|
||||
adminHandler.Register(adminGroup)
|
||||
|
||||
proxyHandler := proxy.New(gw)
|
||||
v1 := r.Group("/v1", middleware.IPFilter(memCache, model.IPScopeProxy), middleware.IngressAuth(gw))
|
||||
proxyHandler.Register(v1)
|
||||
|
||||
replicateHandler := replicate.New(predSvc, publicURL)
|
||||
replicateHandler.Register(v1)
|
||||
|
||||
filesHandler := files.New(imgStore)
|
||||
filesHandler.Register(r)
|
||||
|
||||
staticHandler := static.New(webembed.WebFS)
|
||||
staticHandler.Register(r)
|
||||
|
||||
go func() {
|
||||
log.Printf("Luminary listening on %s (data=%s)", *addr, *dataDir)
|
||||
if err := r.Run(*addr); err != nil {
|
||||
log.Fatalf("server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("shutting down...")
|
||||
mon.Stop()
|
||||
repo.FlushUsage(memCache.DrainUsageDeltas())
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user