package main import ( "context" "log" "os" "os/signal" "sort" "syscall" "time" "git.aiterp.net/stufflog/server/database" "git.aiterp.net/stufflog/server/graph" "git.aiterp.net/stufflog/server/internal/generate" "git.aiterp.net/stufflog/server/internal/slerrors" "git.aiterp.net/stufflog/server/internal/space" "git.aiterp.net/stufflog/server/models" "git.aiterp.net/stufflog/server/services" "github.com/99designs/gqlgen/graphql/playground" "github.com/gin-gonic/gin" "github.com/pkg/errors" "github.com/pressly/goose" "github.com/urfave/cli/v2" ) var dbDriver string var dbConnect string var listenAddress string var s3Host string var s3AccessKey string var s3SecretKey string var s3BucketName string var s3MaxFileSize int64 var s3Secure bool var s3RootDirectory string var s3UrlRoot string func main() { app := &cli.App{ Name: "stufflog", Usage: "Issue tracker for your home and hobbies", Flags: []cli.Flag{ &cli.StringFlag{ Name: "db-driver", Value: "mysql", Usage: "Database driver", EnvVars: []string{"DATABASE_DRIVER"}, Destination: &dbDriver, }, &cli.StringFlag{ Name: "db-connect", Value: "stufflog_user:stuff1234@(localhost:3306)/stufflog", Usage: "Database connection string or path", EnvVars: []string{"DATABASE_CONNECT"}, Destination: &dbConnect, }, &cli.StringFlag{ Name: "listenAddress", Value: ":8000", Usage: "Address to bind the server to.", EnvVars: []string{"SERVER_LISTEN"}, Destination: &listenAddress, }, &cli.StringFlag{ Name: "s3-host", Value: "", Usage: "S3 Host (without https://)", EnvVars: []string{"S3_HOST"}, Destination: &s3Host, }, &cli.StringFlag{ Name: "s3-access-key", Value: "", Usage: "S3 access key (not the secret key)", EnvVars: []string{"S3_ACCESS_KEY"}, Destination: &s3AccessKey, }, &cli.StringFlag{ Name: "s3-secret-key", Value: "", Usage: "S3 secret key", EnvVars: []string{"S3_SECRET_KEY"}, Destination: &s3SecretKey, }, &cli.StringFlag{ Name: "s3-bucket-name", Value: "stufflog", Usage: "S3 bucket name", EnvVars: []string{"S3_BUCKET"}, Destination: &s3BucketName, }, &cli.Int64Flag{ Name: "s3-max-file-size", Value: 1024 * 1024, Usage: "S3 maximum file size", EnvVars: []string{"S3_MAX_FILE_SIZE"}, Destination: &s3MaxFileSize, }, &cli.BoolFlag{ Name: "s3-secure", Usage: "Enable HTTPS for the s3 server", EnvVars: []string{"S3_HTTPS", "S3_SECURE"}, Value: false, Destination: &s3Secure, }, &cli.StringFlag{ Name: "s3-root-directory", Value: "stufflog/userdata", Usage: "Root directory under S3 server to use.", EnvVars: []string{"S3_ROOT_DIRECTORY"}, Destination: &s3RootDirectory, }, &cli.StringFlag{ Name: "s3-url-root", Value: "", Usage: "The URL root for public URLs; includes the root directory. It's generated if blank.", EnvVars: []string{"S3_URL_ROOT"}, Destination: &s3UrlRoot, }, }, Commands: []*cli.Command{ { Name: "reset-admin", Usage: "Reset the admin user (or create it)", Action: func(c *cli.Context) error { db, err := database.Open(dbDriver, dbConnect) if err != nil { return errors.Wrap(err, "Failed to connect to database") } timeout, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() newPass := generate.Generate(20, "") admin, err := db.Users().Find(timeout, "Admin") if slerrors.IsNotFound(err) { admin = &models.User{ ID: "Admin", Name: "Administrator", Active: true, Admin: true, } err = admin.SetPassword(newPass) if err != nil { return errors.Wrap(err, "Failed to set password") } _, err = db.Users().Insert(timeout, *admin) if err != nil { return errors.Wrap(err, "Failed to inset user") } } else if err != nil { return err } else { err = admin.SetPassword(newPass) if err != nil { return errors.Wrap(err, "Failed to set password") } err = db.Users().Save(timeout, *admin) if err != nil { return errors.Wrap(err, "Failed to save user") } } log.Println("Username:", admin.ID) log.Println("Password:", newPass) return nil }, }, { Name: "migrate", Usage: "Migrate the configured database", Action: func(c *cli.Context) error { db, err := database.Open(dbDriver, dbConnect) if err != nil { return errors.Wrap(err, "Failed to connect to database") } err = db.Migrate() if err == goose.ErrNoNextVersion { log.Println("No more migrations to run") } else if err != nil { return errors.Wrap(err, "Failed to run migration") } log.Println("Migration succeeded") return nil }, }, { Name: "server", Usage: "Run the server", Action: func(c *cli.Context) error { db, err := database.Open(dbDriver, dbConnect) if err != nil { return errors.Wrap(err, "Failed to connect to database") } var s3 *space.Space if s3Host != "" { s3, err = space.Connect( s3Host, s3AccessKey, s3SecretKey, s3BucketName, s3Secure, s3MaxFileSize, s3RootDirectory, s3UrlRoot, ) if err != nil { return err } } bundle := services.NewBundle(db, s3) server := gin.New() server.GET("/graphql", graph.Gin(bundle, db)) server.POST("/graphql", graph.Gin(bundle, db)) server.GET("/playground", gin.WrapH(playground.Handler("StuffLog GraphQL Playground", "/graphql"))) exitSignal := make(chan os.Signal) signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM) errCh := make(chan error) go func() { err := server.Run(listenAddress) if err != nil { errCh <- err } }() select { case sig := <-exitSignal: { log.Println("Received signal", sig) return nil } case err := <-errCh: { return err } } }, }, }, } sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) err := app.Run(os.Args) if err != nil { log.Fatal(err) } }