- Add --setup flag to create admin user - Add --admin-user and --admin-password for setup - Generate cryptographically random passwords for --setup - Add --bind and --port flags for server binding - Add BIND env var support - Add SMTP email service for password reset - Add password reset token storage - Add auth header to queue/logs frontend pages - Fix journalctl timestamp format - Fix logs to return empty array instead of error when no entries - Fix queue handler to return proper error message - Hide Users/Aliases nav links when no domain selected
225 lines
5.8 KiB
Go
225 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/imc-vibe/backend/internal/api"
|
|
"github.com/imc-vibe/backend/internal/auth"
|
|
"github.com/imc-vibe/backend/internal/config"
|
|
"github.com/imc-vibe/backend/internal/db"
|
|
)
|
|
|
|
func main() {
|
|
setup := flag.Bool("setup", false, "Create admin user and exit")
|
|
adminUser := flag.String("admin-user", "admin", "Admin username for setup")
|
|
adminPassword := flag.String("admin-password", "", "Admin password for setup (required with --setup)")
|
|
bind := flag.String("bind", "", "IP address to bind to (default: 0.0.0.0)")
|
|
port := flag.String("port", "", "Port to listen on (default: 8080)")
|
|
flag.Parse()
|
|
|
|
cfg := config.Load()
|
|
|
|
if *bind != "" {
|
|
cfg.Bind = *bind
|
|
}
|
|
if *port != "" {
|
|
cfg.Port = *port
|
|
}
|
|
|
|
database, err := db.Connect(cfg)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
|
|
if err := database.InitSchema(); err != nil {
|
|
log.Printf("Warning: Could not initialize schema: %v", err)
|
|
}
|
|
|
|
if *setup {
|
|
password := *adminPassword
|
|
if password == "" {
|
|
password = generateRandomPassword(16)
|
|
fmt.Printf("Generated random password: %s\n", password)
|
|
}
|
|
|
|
hash, err := auth.HashPassword(password)
|
|
if err != nil {
|
|
log.Fatalf("Failed to hash password: %v", err)
|
|
}
|
|
|
|
if err := database.EnsureAdminUser(*adminUser, hash); err != nil {
|
|
log.Fatalf("Failed to create admin user: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Admin user '%s' created successfully.\n", *adminUser)
|
|
fmt.Printf("You can now login with username: %s\n", *adminUser)
|
|
fmt.Printf("Password: %s\n", password)
|
|
return
|
|
}
|
|
|
|
if cfg.AdminUser != "" && cfg.AdminPassword != "" {
|
|
hash, err := auth.HashPassword(cfg.AdminPassword)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to hash admin password: %v", err)
|
|
} else {
|
|
if err := database.EnsureAdminUser(cfg.AdminUser, hash); err != nil {
|
|
log.Printf("Warning: Failed to create admin user: %v", err)
|
|
} else {
|
|
log.Printf("Admin user '%s' created or already exists", cfg.AdminUser)
|
|
}
|
|
}
|
|
}
|
|
|
|
useEmbedded := os.Getenv("USE_EMBEDDED") != "false"
|
|
|
|
frontendFS := FrontendFileSystem()
|
|
|
|
router := http.NewServeMux()
|
|
|
|
router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
})
|
|
|
|
apiRouter := api.New(database, cfg)
|
|
router.Handle("/api/", apiRouter.Handler())
|
|
|
|
if useEmbedded {
|
|
router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png"))
|
|
router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/")))
|
|
router.HandleFunc("/", serveSPA(frontendFS))
|
|
log.Println("Using embedded frontend")
|
|
} else {
|
|
router.Handle("/", http.FileServer(http.Dir("../frontend/build")))
|
|
log.Println("Using filesystem frontend")
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%s", cfg.Bind, cfg.Port)
|
|
log.Printf("Server starting on http://%s", addr)
|
|
log.Fatal(http.ListenAndServe(addr, router))
|
|
}
|
|
|
|
func generateRandomPassword(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
|
result := make([]byte, length)
|
|
if _, err := rand.Read(result); err != nil {
|
|
for i := range result {
|
|
result[i] = charset[i%len(charset)]
|
|
}
|
|
return string(result)
|
|
}
|
|
for i := range result {
|
|
result[i] = charset[int(result[i])%len(charset)]
|
|
}
|
|
return string(result)
|
|
}
|
|
|
|
func serveStaticPrefixed(fsys http.FileSystem, prefix string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
path := prefix + r.URL.Path
|
|
|
|
file, err := fsys.Open(path)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
contentType := mimeType(path)
|
|
w.Header().Set("Content-Type", contentType)
|
|
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
|
}
|
|
}
|
|
|
|
func serveStaticFile(fsys http.FileSystem, filename string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
file, err := fsys.Open(filename)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
contentType := mimeType(filename)
|
|
w.Header().Set("Content-Type", contentType)
|
|
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
|
}
|
|
}
|
|
|
|
func serveSPA(fsys http.FileSystem) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
|
|
if strings.HasPrefix(path, "/api/") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
path = "embed/index.html"
|
|
|
|
file, err := fsys.Open(path)
|
|
if err != nil {
|
|
log.Printf("Error opening %s: %v", path, err)
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
fi, _ := file.Stat()
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
|
}
|
|
}
|
|
|
|
func mimeType(path string) string {
|
|
switch {
|
|
case strings.HasSuffix(path, ".html"):
|
|
return "text/html; charset=utf-8"
|
|
case strings.HasSuffix(path, ".css"):
|
|
return "text/css"
|
|
case strings.HasSuffix(path, ".js"):
|
|
return "application/javascript"
|
|
case strings.HasSuffix(path, ".json"):
|
|
return "application/json"
|
|
case strings.HasSuffix(path, ".png"):
|
|
return "image/png"
|
|
case strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg"):
|
|
return "image/jpeg"
|
|
case strings.HasSuffix(path, ".svg"):
|
|
return "image/svg+xml"
|
|
case strings.HasSuffix(path, ".ico"):
|
|
return "image/x-icon"
|
|
case strings.HasSuffix(path, ".woff"):
|
|
return "font/woff"
|
|
case strings.HasSuffix(path, ".woff2"):
|
|
return "font/woff2"
|
|
case strings.HasSuffix(path, ".ttf"):
|
|
return "font/ttf"
|
|
case strings.HasSuffix(path, ".eot"):
|
|
return "application/vnd.ms-fontobject"
|
|
default:
|
|
return "text/plain"
|
|
}
|
|
}
|