package modules import ( "context" "errors" "fmt" "log/slog" "net/http" "strings" "github.com/jackc/pgx/v5/pgtype" "github.com/memohai/memoh/internal/boot" "github.com/memohai/memoh/internal/bots" "github.com/memohai/memoh/internal/config" dbsqlc "github.com/memohai/memoh/internal/db/sqlc" "github.com/memohai/memoh/internal/handlers" "github.com/memohai/memoh/internal/mcp" "github.com/memohai/memoh/internal/server" "github.com/memohai/memoh/internal/version" "go.uber.org/fx" "golang.org/x/crypto/bcrypt" ) var ServerModule = fx.Module( "server", fx.Provide( provideServer, ), fx.Invoke(startServer), ) // --------------------------------------------------------------------------- // server // --------------------------------------------------------------------------- type serverParams struct { fx.In Logger *slog.Logger RuntimeConfig *boot.RuntimeConfig Config config.Config ServerHandlers []server.Handler `group:"server_handlers"` ContainerdHandler *handlers.ContainerdHandler } func provideServer(params serverParams) *server.Server { allHandlers := make([]server.Handler, 0, len(params.ServerHandlers)+1) allHandlers = append(allHandlers, params.ServerHandlers...) allHandlers = append(allHandlers, params.ContainerdHandler) return server.NewServer(params.Logger, params.RuntimeConfig.ServerAddr, params.Config.Auth.JWTSecret, allHandlers...) } func startServer(lc fx.Lifecycle, logger *slog.Logger, srv *server.Server, shutdowner fx.Shutdowner, cfg config.Config, queries *dbsqlc.Queries, botService *bots.Service, containerdHandler *handlers.ContainerdHandler, mcpConnService *mcp.ConnectionService, toolGateway *mcp.ToolGatewayService) { fmt.Printf("Starting Memoh Agent %s\n", version.GetInfo()) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { if err := ensureAdminUser(ctx, logger, queries, cfg); err != nil { return err } botService.SetContainerLifecycle(containerdHandler) botService.AddRuntimeChecker(mcp.NewConnectionChecker(logger, mcpConnService, toolGateway)) go func() { if err := srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("server failed", slog.Any("error", err)) _ = shutdowner.Shutdown() } }() return nil }, OnStop: func(ctx context.Context) error { if err := srv.Stop(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("server stop: %w", err) } return nil }, }) } func ensureAdminUser(ctx context.Context, log *slog.Logger, queries *dbsqlc.Queries, cfg config.Config) error { if queries == nil { return fmt.Errorf("db queries not configured") } count, err := queries.CountAccounts(ctx) if err != nil { return err } if count > 0 { return nil } username := strings.TrimSpace(cfg.Admin.Username) password := strings.TrimSpace(cfg.Admin.Password) email := strings.TrimSpace(cfg.Admin.Email) if username == "" || password == "" { return fmt.Errorf("admin username/password required in config.toml") } if password == "change-your-password-here" { log.Warn("admin password uses default placeholder; please update config.toml") } hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } user, err := queries.CreateUser(ctx, dbsqlc.CreateUserParams{ IsActive: true, Metadata: []byte("{}"), }) if err != nil { return fmt.Errorf("create admin user: %w", err) } emailValue := pgtype.Text{Valid: false} if email != "" { emailValue = pgtype.Text{String: email, Valid: true} } displayName := pgtype.Text{String: username, Valid: true} dataRoot := pgtype.Text{String: cfg.MCP.DataRoot, Valid: cfg.MCP.DataRoot != ""} _, err = queries.CreateAccount(ctx, dbsqlc.CreateAccountParams{ UserID: user.ID, Username: pgtype.Text{String: username, Valid: true}, Email: emailValue, PasswordHash: pgtype.Text{String: string(hashed), Valid: true}, Role: "admin", DisplayName: displayName, AvatarUrl: pgtype.Text{Valid: false}, IsActive: true, DataRoot: dataRoot, }) if err != nil { return err } log.Info("Admin user created", slog.String("username", username)) return nil }