136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/evilfox/wg-panel/internal/auth"
|
|
"github.com/evilfox/wg-panel/internal/config"
|
|
"github.com/evilfox/wg-panel/internal/db"
|
|
"github.com/evilfox/wg-panel/internal/handlers"
|
|
"github.com/evilfox/wg-panel/internal/mysqlimport"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
ctx := context.Background()
|
|
|
|
root := envOr("APP_ROOT", ".")
|
|
migrationsDir := envOr("MIGRATIONS_DIR", filepath.Join(root, "migrations"))
|
|
templatesDir := envOr("TEMPLATES_DIR", filepath.Join(root, "web", "templates"))
|
|
staticDir := envOr("STATIC_DIR", filepath.Join(root, "web", "static"))
|
|
|
|
pool, err := waitForDB(ctx, cfg.DatabaseURL, 60*time.Second)
|
|
if err != nil {
|
|
log.Fatalf("database: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := db.Migrate(ctx, pool, migrationsDir); err != nil {
|
|
log.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
if cfg.AutoImportDump {
|
|
if _, err := os.Stat(cfg.MySQLDumpPath); err == nil {
|
|
if err := mysqlimport.ImportFile(ctx, pool, cfg.MySQLDumpPath); err != nil {
|
|
log.Fatalf("mysql import: %v", err)
|
|
}
|
|
} else {
|
|
log.Printf("mysql dump not found at %s, skip import", cfg.MySQLDumpPath)
|
|
}
|
|
}
|
|
|
|
authStore := auth.NewStore(cfg, pool)
|
|
if err := authStore.EnsureAdmin(ctx); err != nil {
|
|
log.Fatalf("ensure admin: %v", err)
|
|
}
|
|
log.Printf("admin ready: username=%s email=%s", cfg.AdminUsername, cfg.AdminEmail)
|
|
|
|
app, err := handlers.New(pool, authStore, cfg, templatesDir)
|
|
if err != nil {
|
|
log.Fatalf("handlers: %v", err)
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(app.OfflineMiddleware)
|
|
|
|
fileServer(r, "/static/", http.Dir(staticDir))
|
|
|
|
r.Get("/", app.Home)
|
|
r.Get("/login", app.LoginPage)
|
|
r.Post("/login", app.LoginPost)
|
|
r.Get("/logout", app.Logout)
|
|
r.Get("/wg", app.PublicWG)
|
|
r.Get("/wg_config.php", app.PublicWG)
|
|
|
|
r.Route("/admin", func(r chi.Router) {
|
|
r.Use(authStore.RequireAdmin)
|
|
r.Get("/", app.AdminDashboard)
|
|
r.Post("/status", app.AdminStatusPost)
|
|
r.Get("/wg", app.AdminWG)
|
|
r.Post("/wg/upload", app.AdminWGUpload)
|
|
r.Post("/wg/delete", app.AdminWGDelete)
|
|
r.Post("/wg/settings", app.AdminWGSettings)
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.AppPort,
|
|
Handler: r,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
log.Printf("listening on :%s (%s)", cfg.AppPort, cfg.AppURL)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("server: %v", err)
|
|
}
|
|
}()
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
|
<-stop
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(shutdownCtx)
|
|
}
|
|
|
|
func waitForDB(ctx context.Context, databaseURL string, timeout time.Duration) (*pgxpool.Pool, error) {
|
|
deadline := time.Now().Add(timeout)
|
|
var last error
|
|
for time.Now().Before(deadline) {
|
|
pool, err := db.Connect(ctx, databaseURL)
|
|
if err == nil {
|
|
return pool, nil
|
|
}
|
|
last = err
|
|
log.Printf("waiting for postgres: %v", err)
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
return nil, last
|
|
}
|
|
|
|
func fileServer(r chi.Router, pathPrefix string, root http.FileSystem) {
|
|
r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(root)))
|
|
}
|
|
|
|
func envOr(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|