imc-vibe/backend/cmd/server/main.go
Christoph Haas 6ef558d44d Simplify CLI: replace --setup with --reset-admin-password flag
- Remove --setup and --admin-user flags
- Add --reset-admin-password flag that generates random password and exits
- Admin username is now always 'admin'
2026-03-22 21:47:23 +01:00

205 lines
5.1 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() {
resetAdminPassword := flag.Bool("reset-admin-password", false, "Reset admin password to a random value and exit")
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 *resetAdminPassword {
password := generateRandomPassword(16)
fmt.Printf("Generated password: %s\n", password)
hash, err := auth.HashPassword(password)
if err != nil {
log.Fatalf("Failed to hash password: %v", err)
}
if err := database.EnsureAdminUser("admin", hash); err != nil {
log.Fatalf("Failed to reset admin password: %v", err)
}
fmt.Println("Admin password reset successfully.")
return
}
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"
}
}