package main import ( "errors" "fmt" "net/mail" "os" "regexp" "strings" "git.tilde.town/tildetown/town/invites" "git.tilde.town/tildetown/town/sshkey" "git.tilde.town/tildetown/town/stats" "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/lipgloss" _ "embed" ) //go:embed welcome.txt var welcomeArt string // TODO move magic key machine to static page type newUserData struct { Username string DisplayName string Email string PubKey string } func surveyIconSet(icons *survey.IconSet) { icons.Question.Text = "~" icons.Question.Format = "magenta:b" } func promptCode() (code string, err error) { err = survey.AskOne(&survey.Input{ Message: "invite code?", }, &code, survey.WithValidator(survey.Required), survey.WithIcons(surveyIconSet)) code = strings.TrimSpace(code) return } func promptUsername(townData stats.TildeData) (un string, err error) { // copied from /etc/adduser.conf usernameRE := regexp.MustCompile(`^[a-z][-a-z0-9_]*$`) err = survey.AskOne( &survey.Input{ Message: "desired username?", }, &un, survey.WithValidator(survey.Required), survey.WithIcons(surveyIconSet), survey.WithValidator(func(val interface{}) error { un := val.(string) if len(un) > 32 { return fmt.Errorf("username '%s' is too long", un) } return nil }), survey.WithValidator(func(val interface{}) error { un := val.(string) if !usernameRE.MatchString(un) { return errors.New("usernames must start with a letter and only contain letters, nubers, - or _") } return nil }), survey.WithValidator(func(val interface{}) error { un := val.(string) for _, v := range townData.Users { if v.Username == un { return fmt.Errorf("username '%s' is already in use", un) } } return nil })) return "", nil } func promptEmail(defaultEmail string) (email string, err error) { err = survey.AskOne( &survey.Input{ Message: "e-mail (for account recovery only)?", Default: defaultEmail, }, &email, survey.WithValidator(survey.Required), survey.WithIcons(surveyIconSet), survey.WithValidator(func(val interface{}) error { email := val.(string) _, err := mail.ParseAddress(email) if err != nil { return fmt.Errorf("'%s' doesn't look like an email: %w", email, err) } if !strings.Contains(email, ".") { return fmt.Errorf("'%s' doesn't look like an email: domain not fully qualified", email) } return nil })) return "", nil } func promptKey() (key string, err error) { err = survey.AskOne( &survey.Input{ Message: "SSH public key?", }, &key, survey.WithValidator(survey.Required), survey.WithIcons(surveyIconSet), survey.WithValidator(func(v interface{}) error { key := v.(string) valid, err := sshkey.ValidKey(key) if err != nil { return fmt.Errorf("failed to validate key: %w", err) } if !valid { return errors.New("that doesn't seem like a valid SSH key. try another public key?") } return nil })) return } func _main() error { townData, err := stats.Stats() if err != nil { return err } inviteDB, err := invites.ConnectDB() if err != nil { return err } data := &newUserData{} s := lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{ Light: "#7D19BD", Dark: "#E0B0FF", }) s = s.SetString(welcomeArt) fmt.Println(s) code, err := promptCode() if err != nil { return err } invite, err := invites.Get(inviteDB, code) if err != nil { return fmt.Errorf("could not look up invite code: %w", err) } if invite.Used { return errors.New("that invite code has already been used.") } s = s.SetString("thanks!! just gotta collect some information now and then your account will be ready.") fmt.Println(s) data.Username, err = promptUsername(townData) if err != nil { return err } data.Email, err = promptEmail(invite.Email) if err != nil { return err } data.PubKey, err = promptKey() if err != nil { return err } // TODO should I allow a review+edit step? // TODO have enough to make account; can now do that // TODO assuming account creation succeeded, mark invite as used return nil } func main() { // TODO friendlier error handling err := _main() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }