diff --git a/app.go b/app.go index 540b496..154943a 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,6 @@ package writefreely import ( "database/sql" - "flag" "fmt" "html/template" "net/http" @@ -190,184 +189,8 @@ func pageForReq(app *app, r *http.Request) page.StaticPage { var shttp = http.NewServeMux() var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") -func Serve() { - // General options usable with other commands - debugPtr := flag.Bool("debug", false, "Enables debug logging.") - configFile := flag.String("c", "config.ini", "The configuration file to use") - - // Setup actions - createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") - doConfig := flag.Bool("config", false, "Run the configuration process") - genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys") - createSchema := flag.Bool("init-db", false, "Initialize app database") - migrate := flag.Bool("migrate", false, "Migrate the database") - - // Admin actions - createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") - createUser := flag.String("create-user", "", "Create a regular user with the given username:password") - resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") - outputVersion := flag.Bool("v", false, "Output the current version") - flag.Parse() - - debugging = *debugPtr - - app := &app{ - cfgFile: *configFile, - } - - if *outputVersion { - fmt.Println(serverSoftware + " " + softwareVer) - os.Exit(0) - } else if *createConfig { - log.Info("Creating configuration...") - c := config.New() - log.Info("Saving configuration %s...", app.cfgFile) - err := config.Save(c, app.cfgFile) - if err != nil { - log.Error("Unable to save configuration: %v", err) - os.Exit(1) - } - os.Exit(0) - } else if *doConfig { - d, err := config.Configure(app.cfgFile) - if err != nil { - log.Error("Unable to configure: %v", err) - os.Exit(1) - } - if d.User != nil { - app.cfg = d.Config - connectToDatabase(app) - defer shutdown(app) - - if !app.db.DatabaseInitialized() { - err = adminInitDatabase(app) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - } - - u := &User{ - Username: d.User.Username, - HashedPass: d.User.HashedPass, - Created: time.Now().Truncate(time.Second).UTC(), - } - - // Create blog - log.Info("Creating user %s...\n", u.Username) - err = app.db.CreateUser(u, app.cfg.App.SiteName) - if err != nil { - log.Error("Unable to create user: %s", err) - os.Exit(1) - } - log.Info("Done!") - } - os.Exit(0) - } else if *genKeys { - errStatus := 0 - - // Read keys path from config - loadConfig(app) - - // Create keys dir if it doesn't exist yet - fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) - if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) { - err = os.Mkdir(fullKeysDir, 0700) - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - } - - // Generate keys - initKeyPaths(app) - err := generateKey(emailKeyPath) - if err != nil { - errStatus = 1 - } - err = generateKey(cookieAuthKeyPath) - if err != nil { - errStatus = 1 - } - err = generateKey(cookieKeyPath) - if err != nil { - errStatus = 1 - } - - os.Exit(errStatus) - } else if *createSchema { - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - err := adminInitDatabase(app) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *createAdmin != "" { - err := adminCreateUser(app, *createAdmin, true) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *createUser != "" { - err := adminCreateUser(app, *createUser, false) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } - os.Exit(0) - } else if *resetPassUser != "" { - // Connect to the database - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - - // Fetch user - u, err := app.db.GetUserForAuth(*resetPassUser) - if err != nil { - log.Error("Get user: %s", err) - os.Exit(1) - } - - // Prompt for new password - prompt := promptui.Prompt{ - Templates: &promptui.PromptTemplates{ - Success: "{{ . | bold | faint }}: ", - }, - Label: "New password", - Mask: '*', - } - newPass, err := prompt.Run() - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - - // Do the update - log.Info("Updating...") - err = adminResetPassword(app, u, newPass) - if err != nil { - log.Error("%s", err) - os.Exit(1) - } - log.Info("Success.") - os.Exit(0) - } else if *migrate { - loadConfig(app) - connectToDatabase(app) - defer shutdown(app) - - err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName)) - if err != nil { - log.Error("migrate: %s", err) - os.Exit(1) - } - - os.Exit(0) - } +func Serve(app *app, debug bool) { + debugging = debug log.Info("Initializing...") @@ -375,7 +198,7 @@ func Serve() { hostName = app.cfg.App.Host isSingleUser = app.cfg.App.SingleUser - app.cfg.Server.Dev = *debugPtr + app.cfg.Server.Dev = debugging err := initTemplates(app.cfg) if err != nil { @@ -488,6 +311,165 @@ func Serve() { } } +// OutputVersion prints out the version of the application. +func OutputVersion() { + fmt.Println(serverSoftware + " " + softwareVer) +} + +// NewApp creates a new app instance. +func NewApp(cfgFile string) *app { + return &app{ + cfgFile: cfgFile, + } +} + +// CreateConfig creates a default configuration and saves it to the app's cfgFile. +func CreateConfig(app *app) error { + log.Info("Creating configuration...") + c := config.New() + log.Info("Saving configuration %s...", app.cfgFile) + err := config.Save(c, app.cfgFile) + if err != nil { + return fmt.Errorf("Unable to save configuration: %v", err) + } + return nil +} + +// DoConfig runs the interactive configuration process. +func DoConfig(app *app) { + d, err := config.Configure(app.cfgFile) + if err != nil { + log.Error("Unable to configure: %v", err) + os.Exit(1) + } + if d.User != nil { + app.cfg = d.Config + connectToDatabase(app) + defer shutdown(app) + + if !app.db.DatabaseInitialized() { + err = adminInitDatabase(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + } + + u := &User{ + Username: d.User.Username, + HashedPass: d.User.HashedPass, + Created: time.Now().Truncate(time.Second).UTC(), + } + + // Create blog + log.Info("Creating user %s...\n", u.Username) + err = app.db.CreateUser(u, app.cfg.App.SiteName) + if err != nil { + log.Error("Unable to create user: %s", err) + os.Exit(1) + } + log.Info("Done!") + } + os.Exit(0) +} + +// GenerateKeys creates app encryption keys and saves them into the configured KeysParentDir. +func GenerateKeys(app *app) error { + // Read keys path from config + loadConfig(app) + + // Create keys dir if it doesn't exist yet + fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) + if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) { + err = os.Mkdir(fullKeysDir, 0700) + if err != nil { + return err + } + } + + // Generate keys + initKeyPaths(app) + var keyErrs error + err := generateKey(emailKeyPath) + if err != nil { + keyErrs = err + } + err = generateKey(cookieAuthKeyPath) + if err != nil { + keyErrs = err + } + err = generateKey(cookieKeyPath) + if err != nil { + keyErrs = err + } + + return keyErrs +} + +// CreateSchema creates all database tables needed for the application. +func CreateSchema(app *app) error { + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + err := adminInitDatabase(app) + if err != nil { + return err + } + return nil +} + +// Migrate runs all necessary database migrations. +func Migrate(app *app) error { + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + + err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName)) + if err != nil { + return fmt.Errorf("migrate: %s", err) + } + return nil +} + +// ResetPassword runs the interactive password reset process. +func ResetPassword(app *app, username string) error { + // Connect to the database + loadConfig(app) + connectToDatabase(app) + defer shutdown(app) + + // Fetch user + u, err := app.db.GetUserForAuth(username) + if err != nil { + log.Error("Get user: %s", err) + os.Exit(1) + } + + // Prompt for new password + prompt := promptui.Prompt{ + Templates: &promptui.PromptTemplates{ + Success: "{{ . | bold | faint }}: ", + }, + Label: "New password", + Mask: '*', + } + newPass, err := prompt.Run() + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + + // Do the update + log.Info("Updating...") + err = adminResetPassword(app, u, newPass) + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + log.Info("Success.") + return nil +} + func loadConfig(app *app) { log.Info("Loading %s configuration...", app.cfgFile) cfg, err := config.Load(app.cfgFile) @@ -533,7 +515,8 @@ func shutdown(app *app) { app.db.Close() } -func adminCreateUser(app *app, credStr string, isAdmin bool) error { +// CreateUser creates a new admin or normal user from the given username:password string. +func CreateUser(app *app, credStr string, isAdmin bool) error { // Create an admin user with --create-admin creds := strings.Split(credStr, ":") if len(creds) != 2 { diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index e0a293c..73866cb 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2019 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -11,9 +11,89 @@ package main import ( + "flag" + "github.com/writeas/web-core/log" "github.com/writeas/writefreely" + "os" ) func main() { - writefreely.Serve() + // General options usable with other commands + debugPtr := flag.Bool("debug", false, "Enables debug logging.") + configFile := flag.String("c", "config.ini", "The configuration file to use") + + // Setup actions + createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") + doConfig := flag.Bool("config", false, "Run the configuration process") + genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys") + createSchema := flag.Bool("init-db", false, "Initialize app database") + migrate := flag.Bool("migrate", false, "Migrate the database") + + // Admin actions + createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") + createUser := flag.String("create-user", "", "Create a regular user with the given username:password") + resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") + outputVersion := flag.Bool("v", false, "Output the current version") + flag.Parse() + + app := writefreely.NewApp(*configFile) + + if *outputVersion { + writefreely.OutputVersion() + os.Exit(0) + } else if *createConfig { + err := writefreely.CreateConfig(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *doConfig { + writefreely.DoConfig(app) + os.Exit(0) + } else if *genKeys { + err := writefreely.GenerateKeys(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createSchema { + err := writefreely.CreateSchema(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createAdmin != "" { + err := writefreely.CreateUser(app, *createAdmin, true) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *createUser != "" { + err := writefreely.CreateUser(app, *createUser, false) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *resetPassUser != "" { + err := writefreely.ResetPassword(app, *resetPassUser) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } else if *migrate { + err := writefreely.Migrate(app) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) + } + + writefreely.Serve(app, *debugPtr) }