2023-06-07 07:12:32 +00:00
package main
2023-10-18 00:48:37 +00:00
import (
2023-10-24 05:15:04 +00:00
"database/sql"
2023-10-25 01:40:37 +00:00
"errors"
2023-10-18 00:48:37 +00:00
"fmt"
"os"
"strconv"
2023-10-24 05:15:04 +00:00
"strings"
2023-10-18 00:48:37 +00:00
2023-10-24 06:22:21 +00:00
"git.tilde.town/tildetown/town/codes"
2023-10-25 01:40:37 +00:00
"git.tilde.town/tildetown/town/sshkey"
2023-10-24 05:15:04 +00:00
"git.tilde.town/tildetown/town/towndb"
2023-10-21 04:58:30 +00:00
"github.com/charmbracelet/lipgloss"
2023-10-24 05:15:04 +00:00
_ "github.com/mattn/go-sqlite3"
2023-10-18 00:48:37 +00:00
"github.com/mattn/go-tty"
)
2023-10-21 04:58:30 +00:00
// TODO consider local-only help command for renaming and adding emails to account
2023-10-24 19:10:13 +00:00
// TODO put colorscheme, prompting stuff into own packages for use in the other commands. would be good to get off of survey.
2023-10-21 04:58:30 +00:00
type colorScheme struct {
Header func ( string ) string
Subtitle func ( string ) string
Prompt func ( string ) string
Email func ( string ) string
Option func ( string ) string
2023-10-25 01:40:37 +00:00
Error func ( string ) string
2023-10-21 04:58:30 +00:00
}
func newColorScheme ( ) colorScheme {
s2r := func ( s lipgloss . Style ) func ( string ) string {
return s . Render
}
c := func ( s string ) lipgloss . Color {
return lipgloss . Color ( s )
}
s := lipgloss . NewStyle
return colorScheme {
Header : s2r ( s ( ) . Bold ( true ) . Foreground ( c ( "#E0B0FF" ) ) ) ,
Subtitle : s2r ( s ( ) . Italic ( true ) . Foreground ( c ( "gray" ) ) ) ,
Email : s2r ( s ( ) . Bold ( true ) . Underline ( true ) ) ,
Prompt : s2r ( s ( ) . Bold ( true ) . Foreground ( c ( "#00752d" ) ) ) ,
Option : s2r ( s ( ) . Bold ( true ) . Foreground ( c ( "#38747a" ) ) ) ,
2023-10-25 01:40:37 +00:00
Error : s2r ( s ( ) . Bold ( true ) . Foreground ( c ( "#f43124" ) ) ) ,
2023-10-21 04:58:30 +00:00
}
}
2023-10-24 19:10:13 +00:00
type Prompter struct {
cs colorScheme
tty * tty . TTY
}
func NewPrompter ( tty * tty . TTY , cs colorScheme ) * Prompter {
return & Prompter {
cs : cs ,
tty : tty ,
}
}
func ( p * Prompter ) String ( prompt string ) ( string , error ) {
2023-10-21 04:58:30 +00:00
fmt . Println ( "" )
2023-10-24 19:10:13 +00:00
fmt . Println ( p . cs . Prompt ( prompt ) )
fmt . Println ( p . cs . Subtitle ( "(type your answer below and press enter to submit)" ) )
s , err := p . tty . ReadString ( )
2023-10-21 04:58:30 +00:00
if err != nil {
return "" , fmt . Errorf ( "couldn't collect input: %w" , err )
}
return s , nil
}
2023-10-24 19:10:13 +00:00
func ( p * Prompter ) Select ( prompt string , opts [ ] string ) ( int , error ) {
2023-10-18 00:48:37 +00:00
fmt . Println ( )
2023-10-24 19:10:13 +00:00
fmt . Println ( p . cs . Prompt ( prompt ) )
fmt . Println ( p . cs . Subtitle ( "(pick an option using the corresponding number)" ) )
2023-10-18 00:48:37 +00:00
chosen := - 1
for chosen < 0 {
fmt . Println ( )
for ix , o := range opts {
2023-10-24 19:10:13 +00:00
fmt . Printf ( "%s: %s\n" , p . cs . Option ( fmt . Sprintf ( "%d" , ix + 1 ) ) , o )
2023-10-18 00:48:37 +00:00
}
2023-10-24 19:10:13 +00:00
r , err := p . tty . ReadRune ( )
2023-10-18 00:48:37 +00:00
if err != nil {
return - 1 , fmt . Errorf ( "could not collect answer for '%s': %w" , prompt , err )
}
c , err := strconv . Atoi ( string ( r ) )
if err != nil {
fmt . Println ( )
fmt . Printf ( "I could not understand '%s'. Try again, please.\n" , string ( r ) )
continue
}
if c > len ( opts ) || c == 0 {
fmt . Println ( )
fmt . Printf ( "%s is not an option. Try again, please.\n" , string ( r ) )
continue
}
chosen = c - 1
}
2023-10-21 04:58:30 +00:00
fmt . Println ( "" )
2023-10-18 00:48:37 +00:00
return chosen , nil
}
2023-10-25 01:40:37 +00:00
func _main ( cs colorScheme ) error {
2023-10-24 05:15:04 +00:00
db , err := towndb . ConnectDB ( )
if err != nil {
return fmt . Errorf ( "could not connect to database. please let root@tilde.town know about this." )
}
2023-10-21 04:58:30 +00:00
fmt . Println ( cs . Header ( "Hi, you have reached the tilde town help desk." ) )
2023-10-18 00:48:37 +00:00
fmt . Println ( )
2023-10-21 04:58:30 +00:00
fmt . Println ( "Please check out the options below." )
fmt . Printf ( "If none of them apply to your case, you can email %s. \n" , cs . Email ( "root@tilde.town" ) )
2023-10-18 00:48:37 +00:00
tty , err := tty . Open ( )
if err != nil {
return fmt . Errorf ( "could not open tty: %w" , err )
}
defer tty . Close ( )
2023-10-24 19:10:13 +00:00
p := NewPrompter ( tty , cs )
2023-10-18 00:48:37 +00:00
options := [ ] string {
2023-10-21 04:58:30 +00:00
"I need to request that a new SSH key be added to my account." ,
"I have a code from my e-mail to redeem for a new SSH key" ,
"I just want out of here" ,
2023-10-18 00:48:37 +00:00
}
2023-10-24 19:10:13 +00:00
c , err := p . Select ( "What do you need help with?" , options )
2023-10-18 00:48:37 +00:00
2023-10-24 05:15:04 +00:00
defer func ( ) {
fmt . Println ( )
fmt . Println ( cs . Header ( "bye~" ) )
} ( )
2023-10-21 04:58:30 +00:00
switch c {
case 0 :
2023-10-24 19:10:13 +00:00
return collectEmail ( db , cs , p )
2023-10-21 04:58:30 +00:00
case 1 :
2023-10-24 19:10:13 +00:00
return redeemCode ( db , cs , p )
2023-10-21 04:58:30 +00:00
case 2 :
2023-10-18 00:48:37 +00:00
return nil
}
2023-10-21 04:58:30 +00:00
return nil
}
2023-10-24 19:10:13 +00:00
func collectEmail ( db * sql . DB , cs colorScheme , p * Prompter ) error {
2023-10-24 05:15:04 +00:00
fmt . Println ( cs . Header ( "We can send a authorization code to an email associated with your town account." ) )
2023-10-24 19:10:13 +00:00
email , err := p . String ( "email to send reset code to?" )
2023-10-21 04:58:30 +00:00
if err != nil {
return err
}
2023-10-24 05:15:04 +00:00
fmt . Println ( )
fmt . Println ( cs . Header ( "thanks!" ) )
fmt . Println ( )
fmt . Printf ( "If %s is associated with a town account we'll email an authorization code.\n" , cs . Email ( email ) )
mustHave := [ ] string { "@" , "." }
found := 0
for _ , s := range mustHave {
if strings . Contains ( email , s ) {
found ++
}
}
if found != len ( mustHave ) {
// TODO log
return nil
}
user , err := towndb . UserForEmail ( db , email )
if err != nil {
// TODO log
return err
}
if user == nil {
// TODO log
return nil
}
2023-10-24 06:22:21 +00:00
code := codes . NewCode ( email )
fmt . Println ( code )
ac := & towndb . AuthCode {
Code : code ,
Email : email ,
}
2023-10-24 18:58:34 +00:00
if err = ac . Insert ( db ) ; err != nil {
// TODO log
return err
}
if err = sendAuthCodeEmail ( * ac ) ; err != nil {
2023-10-24 06:22:21 +00:00
// TODO log
return err
}
2023-10-24 05:15:04 +00:00
2023-10-21 04:58:30 +00:00
return nil
}
2023-10-18 00:48:37 +00:00
2023-10-24 19:10:13 +00:00
func redeemCode ( db * sql . DB , cs colorScheme , p * Prompter ) error {
fmt . Println ( cs . Header ( "redeem an auth code and add a new public key" ) )
fmt . Println ( )
2023-10-25 01:40:37 +00:00
c , err := p . String ( "paste your auth code and hit enter to submit:" )
2023-10-24 19:10:13 +00:00
if err != nil {
// TODO log
2023-10-25 01:40:37 +00:00
fmt . Println ( cs . Error ( "sorry, I couldn't read that." ) )
return nil
2023-10-24 19:10:13 +00:00
}
parts , err := codes . Decode ( c )
if err != nil {
// TODO log
2023-10-25 01:40:37 +00:00
fmt . Println ( cs . Error ( "sorry, that doesn't look like an auth code..." ) )
return nil
2023-10-24 19:10:13 +00:00
}
2023-10-24 19:23:35 +00:00
code := & towndb . AuthCode {
Code : parts [ 0 ] ,
Email : parts [ 1 ] ,
}
err = code . Hydrate ( db )
if err != nil {
// TODO log
2023-10-25 01:40:37 +00:00
return errors . New ( "the database is sad" )
2023-10-24 19:23:35 +00:00
}
if code . Used {
2023-10-25 01:40:37 +00:00
fmt . Println ( cs . Error ( "That code has already been redeemed. You'll have to request a new one." ) )
2023-10-24 19:23:35 +00:00
return nil
}
2023-10-24 19:10:13 +00:00
2023-10-25 01:40:37 +00:00
user , err := towndb . UserForEmail ( db , code . Email )
if err != nil || user == nil {
fmt . Println ( cs . Error ( "That code doesn't seem to match an account." ) )
// TODO log
return nil
}
key , err := p . String ( "paste your new public key and hit enter to submit:" )
if err != nil {
// TODO log
fmt . Println ( cs . Error ( "sorry, I couldn't read that." ) )
return nil
}
valid , err := sshkey . ValidKey ( key )
if err != nil {
return fmt . Errorf ( "failed to validate key: %w" , err )
}
if ! valid {
errMsg := fmt . Sprintf ( "that key is invalid: %s" , err . Error ( ) )
fmt . Println ( cs . Error ( errMsg ) )
return nil
}
// TODO need to create a new helper: appendkeyfile and use sudoers to allow help to call it. also need to add help user and add it to /etc/ssh/sshd_config
2023-10-21 04:58:30 +00:00
// TODO mark used
2023-10-18 00:48:37 +00:00
return nil
}
2023-06-07 07:12:32 +00:00
func main ( ) {
2023-10-25 01:40:37 +00:00
cs := newColorScheme ( )
err := _main ( cs )
2023-10-18 00:48:37 +00:00
if err != nil {
2023-10-25 01:40:37 +00:00
fmt . Println (
cs . Error ( fmt . Sprintf ( "sorry, something went wrong: %s" , err . Error ( ) ) ) )
2023-10-18 00:48:37 +00:00
fmt . Println ( "Please let an admin know by emailing a copy of this error to root@tilde.town" )
os . Exit ( 1 )
}
2023-06-07 07:12:32 +00:00
}