mirror of
				https://github.com/osmarks/mycorrhiza.git
				synced 2025-10-31 15:43:00 +00:00 
			
		
		
		
	Add -create-admin flag
It allows for interactive creation of admin account from the terminal for new wikis. Because we have a chicken-and-egg problem here with the user panel -- you need an admin account to appoint someone as an admin, right?
This commit is contained in:
		
							
								
								
									
										63
									
								
								flag.go
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								flag.go
									
									
									
									
									
								
							| @@ -4,20 +4,20 @@ import ( | |||||||
| 	_ "embed" | 	_ "embed" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"syscall" | ||||||
|  |  | ||||||
| 	"github.com/bouncepaw/mycorrhiza/cfg" | 	"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. | // 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. | // printHelp prints the help message. | ||||||
| func printHelp() { | func printHelp() { | ||||||
| 	fmt.Fprintf( | 	fmt.Fprintf( | ||||||
| @@ -28,13 +28,22 @@ func printHelp() { | |||||||
| 	flag.PrintDefaults() | 	flag.PrintDefaults() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type CreateUserCommand struct { | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  |  | ||||||
| // parseCliArgs parses CLI options and sets several important global variables. Call it early. | // parseCliArgs parses CLI options and sets several important global variables. Call it early. | ||||||
| func parseCliArgs() { | 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() | 	flag.Parse() | ||||||
|  |  | ||||||
| 	args := flag.Args() | 	args := flag.Args() | ||||||
| 	if len(args) == 0 { | 	if len(args) == 0 { | ||||||
| 		log.Fatal("Error: pass a wiki directory") | 		log.Fatal("error: pass a wiki directory") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	wikiDir, err := filepath.Abs(args[0]) | 	wikiDir, err := filepath.Abs(args[0]) | ||||||
| @@ -43,4 +52,46 @@ func parseCliArgs() { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cfg.WikiDir = wikiDir | 	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) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -10,6 +10,7 @@ require ( | |||||||
| 	github.com/smartystreets/goconvey v1.6.4 // indirect | 	github.com/smartystreets/goconvey v1.6.4 // indirect | ||||||
| 	github.com/valyala/quicktemplate v1.6.3 | 	github.com/valyala/quicktemplate v1.6.3 | ||||||
| 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 | 	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 | 	gopkg.in/ini.v1 v1.62.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								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-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-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-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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
| gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								user/net.go
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								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. | // 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) | 	username = util.CanonicalName(username) | ||||||
| 	log.Println("Attempt to register user", username) |  | ||||||
| 	switch { | 	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): | 	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) | 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	u := User{ | 	u := User{ | ||||||
| 		Name:         username, | 		Name:         username, | ||||||
| 		Group:        "editor", | 		Group:        group, | ||||||
| 		Password:     string(hash), | 		Password:     string(hash), | ||||||
| 		RegisteredAt: time.Now(), | 		RegisteredAt: time.Now(), | ||||||
| 	} | 	} | ||||||
| 	users.Store(username, &u) | 	users.Store(username, &u) | ||||||
| 	err = SaveUserDatabase() | 	return SaveUserDatabase() | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // LoginDataHTTP logs such user in and returns string representation of an error if there is any. | // LoginDataHTTP logs such user in and returns string representation of an error if there is any. | ||||||
|   | |||||||
| @@ -45,9 +45,10 @@ func handlerRegister(w http.ResponseWriter, rq *http.Request) { | |||||||
| 		var ( | 		var ( | ||||||
| 			username = rq.PostFormValue("username") | 			username = rq.PostFormValue("username") | ||||||
| 			password = rq.PostFormValue("password") | 			password = rq.PostFormValue("password") | ||||||
| 			err      = user.Register(username, password) | 			err      = user.Register(username, password, "editor", false) | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			log.Printf("Failed to register \"%s\": %s", username, err.Error()) | ||||||
| 			w.Header().Set("Content-Type", mime.TypeByExtension(".html")) | 			w.Header().Set("Content-Type", mime.TypeByExtension(".html")) | ||||||
| 			w.WriteHeader(http.StatusBadRequest) | 			w.WriteHeader(http.StatusBadRequest) | ||||||
| 			fmt.Fprint( | 			fmt.Fprint( | ||||||
| @@ -62,6 +63,7 @@ func handlerRegister(w http.ResponseWriter, rq *http.Request) { | |||||||
| 				), | 				), | ||||||
| 			) | 			) | ||||||
| 		} else { | 		} else { | ||||||
|  | 			log.Printf("Successfully registered \"%s\"", username) | ||||||
| 			user.LoginDataHTTP(w, rq, username, password) | 			user.LoginDataHTTP(w, rq, username, password) | ||||||
| 			http.Redirect(w, rq, "/"+rq.URL.RawQuery, http.StatusSeeOther) | 			http.Redirect(w, rq, "/"+rq.URL.RawQuery, http.StatusSeeOther) | ||||||
| 		} | 		} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 handlerug
					handlerug