diff --git a/flag.go b/flag.go index 120eeee..fd27177 100644 --- a/flag.go +++ b/flag.go @@ -4,20 +4,20 @@ import ( _ "embed" "flag" "fmt" + "io" "log" "os" "path/filepath" + "syscall" "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/files" + "github.com/bouncepaw/mycorrhiza/user" + "golang.org/x/term" ) // CLI options are read and parsed here. -func init() { - flag.StringVar(&cfg.HTTPPort, "port", "", "Listen on another port. This option also updates the config file for your convenience.") - flag.Usage = printHelp -} - // printHelp prints the help message. func printHelp() { fmt.Fprintf( @@ -28,13 +28,22 @@ func printHelp() { flag.PrintDefaults() } +type CreateUserCommand struct { + name string +} + // parseCliArgs parses CLI options and sets several important global variables. Call it early. func parseCliArgs() { + var createAdminName string + + flag.StringVar(&createAdminName, "create-admin", "", "Create a new admin. The password will be prompted in the terminal.") + flag.StringVar(&cfg.HTTPPort, "port", "", "Listen on another port. This option also updates the config file for your convenience.") + flag.Usage = printHelp flag.Parse() args := flag.Args() if len(args) == 0 { - log.Fatal("Error: pass a wiki directory") + log.Fatal("error: pass a wiki directory") } wikiDir, err := filepath.Abs(args[0]) @@ -43,4 +52,46 @@ func parseCliArgs() { } cfg.WikiDir = wikiDir + + if createAdminName != "" { + createAdminCommand(createAdminName) + os.Exit(0) + } +} + +func createAdminCommand(name string) { + wr := log.Writer() + log.SetFlags(0) + + if err := files.PrepareWikiRoot(); err != nil { + log.Fatal("error: ", err) + } + cfg.UseAuth = true + cfg.AllowRegistration = true + + log.SetOutput(io.Discard) + user.InitUserDatabase() + log.SetOutput(wr) + + handle := int(syscall.Stdin) + if !term.IsTerminal(handle) { + log.Fatal("error: not a terminal") + } + + fmt.Print("Password: ") + passwordBytes, err := term.ReadPassword(handle) + fmt.Print("\n") + if err != nil { + log.Fatal("error: ", err) + } + + password := string(passwordBytes) + + log.SetOutput(io.Discard) + err = user.Register(name, password, "admin", true) + log.SetOutput(wr) + + if err != nil { + log.Fatal("error: ", err) + } } diff --git a/go.mod b/go.mod index 84b7ce3..4075f56 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/smartystreets/goconvey v1.6.4 // indirect github.com/valyala/quicktemplate v1.6.3 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b gopkg.in/ini.v1 v1.62.0 // indirect ) diff --git a/go.sum b/go.sum index f4c97aa..3e1c46b 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= diff --git a/user/net.go b/user/net.go index 2c00b12..5998fac 100644 --- a/user/net.go +++ b/user/net.go @@ -35,36 +35,31 @@ func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) { } // Register registers the given user. If it fails, a non-nil error is returned. -func Register(username, password string) error { +func Register(username, password, group string, force bool) error { username = util.CanonicalName(username) - log.Println("Attempt to register user", username) + switch { - case cfg.RegistrationLimit > 0 && Count() >= cfg.RegistrationLimit: - log.Printf("Limit reached: %d", cfg.RegistrationLimit) - return fmt.Errorf("Reached the limit of registered users: %d", cfg.RegistrationLimit) - case HasUsername(username): - log.Println("Username taken") - return fmt.Errorf("Username \"%s\" is taken already.", username) case !util.IsPossibleUsername(username): - log.Println("Illegal username:", username) - return fmt.Errorf("Illegal username \"%s\".", username) + return fmt.Errorf("illegal username \"%s\"", username) + case !force && cfg.RegistrationLimit > 0 && Count() >= cfg.RegistrationLimit: + return fmt.Errorf("reached the limit of registered users (%d)", cfg.RegistrationLimit) + case !force && HasUsername(username): + return fmt.Errorf("username \"%s\" is already taken", username) } + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } + u := User{ Name: username, - Group: "editor", + Group: group, Password: string(hash), RegisteredAt: time.Now(), } users.Store(username, &u) - err = SaveUserDatabase() - if err != nil { - return err - } - return nil + return SaveUserDatabase() } // LoginDataHTTP logs such user in and returns string representation of an error if there is any. diff --git a/web/auth.go b/web/auth.go index 94cc2c4..3e65f83 100644 --- a/web/auth.go +++ b/web/auth.go @@ -45,9 +45,10 @@ func handlerRegister(w http.ResponseWriter, rq *http.Request) { var ( username = rq.PostFormValue("username") password = rq.PostFormValue("password") - err = user.Register(username, password) + err = user.Register(username, password, "editor", false) ) if err != nil { + log.Printf("Failed to register \"%s\": %s", username, err.Error()) w.Header().Set("Content-Type", mime.TypeByExtension(".html")) w.WriteHeader(http.StatusBadRequest) fmt.Fprint( @@ -62,6 +63,7 @@ func handlerRegister(w http.ResponseWriter, rq *http.Request) { ), ) } else { + log.Printf("Successfully registered \"%s\"", username) user.LoginDataHTTP(w, rq, username, password) http.Redirect(w, rq, "/"+rq.URL.RawQuery, http.StatusSeeOther) }