package main import ( "bytes" "encoding/json" "errors" "fmt" "os" "os/exec" "git.tilde.town/tildetown/town/invites" "git.tilde.town/tildetown/town/stats" "git.tilde.town/tildetown/town/towndb" "github.com/charmbracelet/lipgloss" _ "embed" ) // TODO mark on user table what signup id led to the account for forensics // TODO add logging like the signup tool has // TODO consider merging adduser, usermod, and createkeyfile into single createuser helper to limit sudoers list // TODO move magic key machine to static page // TODO add alerts for new users (mailing list post, irc post, etc(?)) //go:embed welcome.txt var welcomeArt string type newUserData struct { Username string DisplayName string Email string PubKey string } func defaultStyle() lipgloss.Style { return lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{ Light: "#7D19BD", Dark: "#E0B0FF", }) } func _main() error { inviteDB, err := invites.ConnectDB() if err != nil { return err } s := defaultStyle().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) townData, err := stats.Stats() if err != nil { return err } data := &newUserData{ Email: invite.Email, } a := &asker{ UserData: data, Invite: *invite, TownData: townData, Style: defaultStyle(), } if err = a.Ask(); err != nil { return err } conf, err := confirmContinue() 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("town users all agree to our code of conduct: https://tilde.town/wiki/conduct.html") fmt.Println(s) coc, err := confirmCoC() if err != nil { return err } if !coc { s = s.SetString("bummer. have a good one.") fmt.Println(s) return nil } s = s.SetString("cool, awesome, thank you, going to make your account now...") fmt.Println(s) 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 } s = s.SetString(fmt.Sprintf(`OK! your user account has been created. welcome, ~%[1]s <3 This program is going to exit and you are now free to ssh to town as yourself: ssh %[1]s@tilde.town if your public key isn't found by ssh, you'll need to explain to ssh how to find it with: ssh -i "replace with path to public key file" %[1]s@tilde.town for help with ssh, see: https://tilde.town/wiki/getting-started/ssh.html if you end up very stuck, you can email root@tilde.town .`, data.Username)) fmt.Println(s) if err = invite.MarkUsed(inviteDB); err != nil { return fmt.Errorf("could not mark invite as used: %w", err) } // TODO burn invite return nil } func createUser(data newUserData) (err error) { cmd := exec.Command("sudo", "/usr/sbin/adduser", "--quiet", "--disabled-password", data.Username) if err = cmd.Run(); err != nil { return fmt.Errorf("adduser failed: %w", err) } cmd = exec.Command("sudo", "/usr/sbin/usermod", "-a", "-G", "town", data.Username) if err = cmd.Run(); err != nil { return fmt.Errorf("usermod failed: %w", err) } cmd = exec.Command("sudo", "--user", data.Username, "/town/bin/createkeyfile", data.Username) cmd.Stdin = bytes.NewBufferString(keyfileText(data)) stdoutBuff := bytes.NewBuffer([]byte{}) cmd.Stdout = stdoutBuff if err = cmd.Run(); err != nil { return fmt.Errorf("createkeyfile failed with '%s': %w", string(stdoutBuff.Bytes()), err) } cmd = exec.Command("sudo", "/town/bin/generate_welcome_present.sh", data.Username) if err = cmd.Run(); err != nil { // TODO log this. no reason to bail out. } tu := towndb.TownUser{ Username: data.Username, Emails: []string{ data.Email, }, State: towndb.StateActive, } var out []byte if out, err = json.Marshal(tu); err != nil { return fmt.Errorf("could not serialize user data: %w", err) } cmd = exec.Command("sudo", "/town/bin/registeruser") stderrBuff := bytes.NewBuffer([]byte{}) cmd.Stderr = stderrBuff cmd.Stdin = bytes.NewBuffer(out) if err = cmd.Run(); err != nil { return fmt.Errorf("register user failed with '%s': %w", string(stderrBuff.Bytes()), err) } return nil } func keyfileText(data newUserData) string { header := `########## GREETINGS! ########## # Hi! This file was automatically generated by tilde.town when # your account was created. You can edit it if you want, but we # recommend adding stuff to ~/.ssh/authorized_keys instead.` return fmt.Sprintf("%s\n%s", header, data.PubKey) } func main() { // TODO friendlier error handling err := _main() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }