diff --git a/cmd/serve.go b/cmd/serve.go index d5c4bd5..4483abe 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -13,16 +13,23 @@ import ( "git.oblat.lv/alex/triggerssmith/api" application "git.oblat.lv/alex/triggerssmith/internal/app" + "git.oblat.lv/alex/triggerssmith/internal/auth" "git.oblat.lv/alex/triggerssmith/internal/config" + "git.oblat.lv/alex/triggerssmith/internal/jwt" "git.oblat.lv/alex/triggerssmith/internal/server" + "git.oblat.lv/alex/triggerssmith/internal/token" + "git.oblat.lv/alex/triggerssmith/internal/user" "git.oblat.lv/alex/triggerssmith/internal/vars" "github.com/spf13/cobra" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) var optsServeCmd = struct { ConfigPath *string Debug *bool HideGreetings *bool + NoPIDFile *bool }{} // // simple middleware for request logging @@ -99,10 +106,8 @@ var serveCmd = &cobra.Command{ fmt.Fprintf(f, "Panic: %v\n", r) f.Write(stack) f.WriteString("\n\n") + slog.Error("Application panicked: the stack is flushed to disk", slog.Any("error", r)) } - - slog.Error("Application panicked: the stack is flushed to disk", slog.Any("error", r)) - os.Exit(-1) } }() @@ -114,13 +119,17 @@ var serveCmd = &cobra.Command{ slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelInfo}))) } - pid := os.Getpid() - slog.Debug("Starting server", slog.Int("pid", pid)) - if err := writePID(vars.PID_PATH); err != nil { - panic(err) + if !*optsServeCmd.NoPIDFile { + pid := os.Getpid() + slog.Debug("Starting server", slog.Int("pid", pid)) + if err := writePID(vars.PID_PATH); err != nil { + panic(err) + } + slog.Debug("created pid file", slog.String("path", vars.PID_PATH)) + defer os.Remove(vars.PID_PATH) + } else { + slog.Warn("Starting server without PID file as requested by --no-pidfile flag: this may complicate process management") } - slog.Debug("created pid file", slog.String("path", vars.PID_PATH)) - defer os.Remove(vars.PID_PATH) // load config slog.Debug("Reading configuration", slog.String("path", *optsServeCmd.ConfigPath)) @@ -140,9 +149,81 @@ var serveCmd = &cobra.Command{ app.LoadConfiguration(cfg) srv := app.Server() - - router := api.NewRouter(cfg) + // Services initialization + var jwtSigner jwt.Signer + // TODO: support more signing algorithms + // : support hot config reload for signing alg and secret + switch cfg.Auth.SignAlg { + case "HS256": + secretBytes, err := os.ReadFile(cfg.Auth.HMACSecretPath) + if err != nil { + slog.Error("Failed to read HMAC secret file", slog.String("path", cfg.Auth.HMACSecretPath), slog.String("error", err.Error())) + return + } + jwtSigner = jwt.NewHMACSigner(secretBytes) + default: + slog.Error("Unsupported JWT signing algorithm", slog.String("alg", cfg.Auth.SignAlg)) + return + } + jwtService := jwt.NewService(jwtSigner) + + err = os.MkdirAll(cfg.Data.DataPath, 0755) + if err != nil { + slog.Error("Failed to create data directory", slog.String("path", cfg.Data.DataPath), slog.String("error", err.Error())) + return + } + + tokenDb, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "tokens.sqlite3")), &gorm.Config{}) + if err != nil { + slog.Error("Failed to open token database", slog.String("error", err.Error())) + return + } + err = tokenDb.AutoMigrate(&token.Token{}) + if err != nil { + slog.Error("Failed to migrate token database", slog.String("error", err.Error())) + return + } + tokenStore, err := token.NewSQLiteTokenStore(tokenDb) + if err != nil { + slog.Error("Failed to create token store", slog.String("error", err.Error())) + return + } + tokenService, err := token.NewTokenService(&cfg.Auth, tokenStore) + + userDb, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "users.sqlite3")), &gorm.Config{}) + if err != nil { + slog.Error("Failed to open user database", slog.String("error", err.Error())) + return + } + err = userDb.AutoMigrate(&user.User{}) + if err != nil { + slog.Error("Failed to migrate user database", slog.String("error", err.Error())) + return + } + userStore, err := user.NewGormUserStore(userDb) + if err != nil { + slog.Error("Failed to create user store", slog.String("error", err.Error())) + return + } + userService, err := user.NewService(userStore) + + authService, err := auth.NewAuthService(auth.AuthServiceDependencies{ + Configuration: cfg, + + JWTService: jwtService, + UserService: userService, + TokenService: tokenService, + }) + if err != nil { + slog.Error("Failed to create auth service", slog.String("error", err.Error())) + return + } + + router := api.NewRouter(api.RouterDependencies{ + AuthService: authService, + Configuration: cfg, + }) srv.SetHandler(router.MustRoute()) srv.Init() @@ -204,5 +285,6 @@ func init() { optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") optsServeCmd.HideGreetings = serveCmd.Flags().BoolP("hide-greetings", "g", false, "Hide the welcome message and version when starting the server") + optsServeCmd.NoPIDFile = serveCmd.Flags().BoolP("no-pidfile", "p", false, "Do not write a PID file") rootCmd.AddCommand(serveCmd) }