town/cmd/welcome/main.go

280 lines
5.9 KiB
Go
Raw Normal View History

package main
2023-02-26 08:57:46 +00:00
import (
2023-02-28 19:30:07 +00:00
"errors"
2023-02-26 08:57:46 +00:00
"fmt"
2023-03-01 00:10:45 +00:00
"net/mail"
2023-02-26 08:57:46 +00:00
"os"
2023-02-28 23:44:34 +00:00
"regexp"
2023-02-28 05:50:55 +00:00
"strings"
2023-02-26 08:57:46 +00:00
2023-02-28 19:30:07 +00:00
"git.tilde.town/tildetown/town/invites"
2023-03-01 05:22:09 +00:00
"git.tilde.town/tildetown/town/sshkey"
2023-02-28 23:18:11 +00:00
"git.tilde.town/tildetown/town/stats"
2023-02-28 05:50:55 +00:00
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
2023-02-26 08:57:46 +00:00
_ "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
}
2023-02-28 05:50:55 +00:00
func surveyIconSet(icons *survey.IconSet) {
icons.Question.Text = "~"
icons.Question.Format = "magenta:b"
}
2023-02-26 08:57:46 +00:00
2023-02-28 19:30:07 +00:00
func promptCode() (code string, err error) {
2023-03-01 05:22:09 +00:00
err = survey.AskOne(&survey.Input{
2023-02-28 05:50:55 +00:00
Message: "invite code?",
2023-03-01 05:22:09 +00:00
}, &code,
2023-02-28 05:50:55 +00:00
survey.WithValidator(survey.Required),
survey.WithIcons(surveyIconSet))
2023-02-28 19:30:07 +00:00
code = strings.TrimSpace(code)
2023-02-28 05:50:55 +00:00
return
}
2023-02-26 08:57:46 +00:00
func confirmContinue() (conf bool, err error) {
err = survey.AskOne(
&survey.Confirm{
Message: "Does the above look ok?",
}, &conf,
survey.WithValidator(survey.Required),
survey.WithIcons(surveyIconSet))
return
}
type asker struct {
UserData *newUserData
Style lipgloss.Style
Invite invites.Invite
TownData stats.TildeData
}
func (a *asker) Ask() (err error) {
// TODO somehow un and email getting set to "" but pubkey works fine?
if err = a.promptUsername(); err != nil {
return err
}
if err = a.promptEmail(); err != nil {
return err
}
if err = a.promptKey(); err != nil {
return err
}
s := a.Style.SetString(
fmt.Sprintf(`ok! your account is about to be created with the following details:
username: %s
email: %s
pubkey: %s`, a.UserData.Username, a.UserData.Email, a.UserData.PubKey)).Bold(true).MaxWidth(80)
fmt.Println(s)
return nil
}
func (a *asker) promptUsername() (err error) {
2023-03-01 05:22:09 +00:00
// copied from /etc/adduser.conf
2023-02-28 23:44:34 +00:00
usernameRE := regexp.MustCompile(`^[a-z][-a-z0-9_]*$`)
2023-03-01 05:22:09 +00:00
err = survey.AskOne(
&survey.Input{
Message: "desired username?",
Default: a.UserData.Username,
}, &a.UserData.Username,
2023-02-28 19:30:07 +00:00
survey.WithValidator(survey.Required),
survey.WithIcons(surveyIconSet),
2023-02-28 23:44:34 +00:00
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
}),
2023-02-28 19:30:07 +00:00
survey.WithValidator(func(val interface{}) error {
un := val.(string)
for _, v := range a.TownData.Users {
2023-02-28 23:18:11 +00:00
if v.Username == un {
2023-02-28 23:44:34 +00:00
return fmt.Errorf("username '%s' is already in use", un)
2023-02-28 23:18:11 +00:00
}
}
2023-02-28 19:30:07 +00:00
return nil
}))
return
2023-02-28 19:30:07 +00:00
}
func (a *asker) promptEmail() (err error) {
2023-03-01 05:22:09 +00:00
err = survey.AskOne(
&survey.Input{
Message: "e-mail (for account recovery only)?",
Default: a.UserData.Email,
}, &a.UserData.Email,
2023-03-01 00:10:45 +00:00
survey.WithValidator(survey.Required),
survey.WithIcons(surveyIconSet),
survey.WithValidator(func(val interface{}) error {
email := val.(string)
_, err := mail.ParseAddress(email)
if err != nil {
2023-03-01 00:33:08 +00:00
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)
2023-03-01 00:10:45 +00:00
}
return nil
}))
return
2023-03-01 00:10:45 +00:00
}
func (a *asker) promptKey() (err error) {
2023-03-01 05:22:09 +00:00
err = survey.AskOne(
&survey.Input{
Message: "SSH public key?",
Default: a.UserData.PubKey,
}, &a.UserData.PubKey,
2023-03-01 05:22:09 +00:00
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
}
2023-02-28 05:50:55 +00:00
func _main() error {
2023-02-28 19:30:07 +00:00
inviteDB, err := invites.ConnectDB()
if err != nil {
return err
}
2023-02-28 05:50:55 +00:00
s := lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{
Light: "#7D19BD",
Dark: "#E0B0FF",
})
2023-02-26 08:57:46 +00:00
2023-02-28 05:50:55 +00:00
s = s.SetString(welcomeArt)
fmt.Println(s)
2023-02-26 08:57:46 +00:00
2023-02-28 19:30:07 +00:00
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)
townData, err := stats.Stats()
2023-02-28 05:50:55 +00:00
if err != nil {
return err
}
data := &newUserData{
Email: invite.Email,
}
2023-02-26 08:57:46 +00:00
a := &asker{
UserData: data,
Invite: *invite,
TownData: townData,
Style: s,
}
if err = a.Ask(); err != nil {
2023-03-01 00:10:45 +00:00
return err
}
conf, err := confirmContinue()
2023-03-01 05:22:09 +00:00
if err != nil {
return err
}
if !conf {
for !conf {
if err = a.Ask(); err != nil {
return err
}
if conf, err = confirmContinue(); err != nil {
return err
}
}
}
s = s.SetString("cool, awesome, going to make your account now...")
fmt.Println(s)
2023-03-01 05:22:09 +00:00
2023-03-03 21:29:13 +00:00
if err = createUser(*data); err != nil {
s = s.SetString(fmt.Sprintf(`augh, I'm sorry. the account creation failed.
Please email root@tilde.town and paste the following error:
%s
Your invite code has not been marked as used and you're welcome to try again, though if there is a system issue you might need to wait for word from an admin.`, err.Error()))
fmt.Println(s)
return nil
}
2023-02-28 05:50:55 +00:00
// TODO assuming account creation succeeded, mark invite as used
2023-02-26 08:57:46 +00:00
2023-02-28 05:50:55 +00:00
return nil
2023-02-26 08:57:46 +00:00
}
2023-03-03 21:29:13 +00:00
func createUser(data newUserData) error {
// TODO adduser
// TODO add to town group
// TODO create keyfile by running helper (helper should also make ~/.ssh)
// TODO generate welcome gift
// TODO any alerts
return errors.New("not implemented")
}
func main() {
2023-02-28 19:30:07 +00:00
// TODO friendlier error handling
2023-02-26 08:57:46 +00:00
err := _main()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}