From 2e0abfb3fa9d1b2d71e05868914d8006252b3011 Mon Sep 17 00:00:00 2001 From: Ojas Tyagi Date: Tue, 24 Dec 2024 22:43:55 +0530 Subject: [PATCH] feat: cleanup --- cmd/create.go | 22 ++-- cmd/form.go | 9 +- cmd/forms.go | 8 +- cmd/root.go | 10 +- config.yaml | 5 +- internal/config/config.go | 9 +- internal/constants/api.go | 6 ++ internal/constants/messages.go | 22 ++-- internal/models/error.go | 21 ++++ internal/models/form.go | 4 +- internal/services/client.go | 177 +++++++++++++++------------------ internal/ui/create/create.go | 25 ++--- internal/ui/form/form.go | 28 ++++-- main.go | 2 +- 14 files changed, 181 insertions(+), 167 deletions(-) create mode 100644 internal/constants/api.go create mode 100644 internal/models/error.go diff --git a/cmd/create.go b/cmd/create.go index 4bb5cf4..54aac43 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -1,11 +1,11 @@ package cmd import ( - "fmt" "os" "strconv" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/log" "github.com/devmegablaster/bashform/internal/constants" "github.com/devmegablaster/bashform/internal/services" "github.com/devmegablaster/bashform/internal/styles" @@ -13,14 +13,13 @@ import ( "github.com/spf13/cobra" ) -func (c *CLI) CreateForm() *cobra.Command { +func (c *CLI) createForm() *cobra.Command { newFormCmd := &cobra.Command{ Use: "create [number of questions] [share code]", Args: cobra.ExactArgs(2), Short: "Create a new form with a specific number of questions and shareable code", Aliases: []string{"c"}, RunE: func(cmd *cobra.Command, args []string) error { - n, err := strconv.Atoi(args[0]) if err != nil { return err @@ -39,17 +38,16 @@ func (c *CLI) CreateForm() *cobra.Command { code = args[1] } - client := services.NewClient(c.cfg.Api.BaseUrl, c.PubKey) - - check, err := client.GetForm(code) + client := services.NewClient(c.cfg.Api.BaseURL, c.PubKey) + check, err := client.CheckCode(code) - if err != nil { - cmd.Println(styles.Error.Render("\nError checking code\n")) + if !check { + cmd.Println(styles.Error.Render(constants.MessageCodeNotAvailable)) + return nil } - if check.Code != "" { - cmd.Println(styles.Error.Render(constants.MessageErrorCodeAlreadyExists)) - return nil + if err != nil { + cmd.Println(styles.Error.Render("Error checking code")) } cr := create.NewModel(c.cfg, c.Session, n, code, client) @@ -60,7 +58,7 @@ func (c *CLI) CreateForm() *cobra.Command { tea.WithOutput(c.Session)) if _, err := p.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error starting program: %v", err) + log.Error("Could not run program", "error", err) os.Exit(1) } diff --git a/cmd/form.go b/cmd/form.go index 3f66ece..f701af3 100644 --- a/cmd/form.go +++ b/cmd/form.go @@ -2,14 +2,13 @@ package cmd import ( tea "github.com/charmbracelet/bubbletea" - "github.com/devmegablaster/bashform/internal/constants" "github.com/devmegablaster/bashform/internal/services" "github.com/devmegablaster/bashform/internal/styles" "github.com/devmegablaster/bashform/internal/ui/form" "github.com/spf13/cobra" ) -func (c *CLI) FillForm() *cobra.Command { +func (c *CLI) fillForm() *cobra.Command { formCmd := &cobra.Command{ Use: "form [code]", Short: "Fill out a form using form code", @@ -17,16 +16,16 @@ func (c *CLI) FillForm() *cobra.Command { Aliases: []string{"f"}, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - - client := services.NewClient(c.cfg.Api.BaseUrl, c.PubKey) + client := services.NewClient(c.cfg.Api.BaseURL, c.PubKey) formData, err := client.GetForm(args[0]) if err != nil { - cmd.Println(styles.Error.Render(constants.MessageFormNotFound)) + cmd.Println(styles.Error.Render(err.Error())) return nil } model := form.NewModel(formData, client, c.Session) p := tea.NewProgram(model, + tea.WithAltScreen(), tea.WithInput(cmd.InOrStdin()), tea.WithOutput(cmd.OutOrStdout()), ) diff --git a/cmd/forms.go b/cmd/forms.go index 249f87a..30302f3 100644 --- a/cmd/forms.go +++ b/cmd/forms.go @@ -2,7 +2,6 @@ package cmd import ( tea "github.com/charmbracelet/bubbletea" - "github.com/devmegablaster/bashform/internal/constants" "github.com/devmegablaster/bashform/internal/models" "github.com/devmegablaster/bashform/internal/services" "github.com/devmegablaster/bashform/internal/styles" @@ -10,17 +9,16 @@ import ( "github.com/spf13/cobra" ) -func (c *CLI) GetForms() *cobra.Command { +func (c *CLI) getForms() *cobra.Command { formCmd := &cobra.Command{ Use: "forms", Short: "Get your forms", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - - client := services.NewClient(c.cfg.Api.BaseUrl, c.PubKey) + client := services.NewClient(c.cfg.Api.BaseURL, c.PubKey) formsResp, err := client.GetForms() if err != nil { - cmd.Println(styles.Error.Render(constants.MessageFormNotFound)) + cmd.Println(styles.Error.Render(err.Error())) return nil } diff --git a/cmd/root.go b/cmd/root.go index c08ab31..839f752 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/ssh" "github.com/devmegablaster/bashform/internal/config" + "github.com/devmegablaster/bashform/internal/constants" "github.com/spf13/cobra" ) @@ -18,14 +19,15 @@ type CLI struct { func NewCLI(cfg config.Config, session ssh.Session) *CLI { + // Encode the public key encoded := bytes.Buffer{} - enc := base64.NewEncoder(base64.StdEncoding, &encoded) enc.Write(session.PublicKey().Marshal()) rootCmd := &cobra.Command{ Use: "bashform", RunE: func(cmd *cobra.Command, args []string) error { + cmd.Println(constants.Logo) cmd.Help() return nil }, @@ -51,9 +53,9 @@ func (c *CLI) Init() { c.RootCmd.SetContext(c.Session.Context()) // Add Commands - c.AddCommand(c.FillForm()) - c.AddCommand(c.CreateForm()) - c.AddCommand(c.GetForms()) + c.AddCommand(c.fillForm()) + c.AddCommand(c.createForm()) + c.AddCommand(c.getForms()) } func (c *CLI) Run() error { diff --git a/config.yaml b/config.yaml index cdc891f..df092e9 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,8 @@ ssh: url: bashform.me host: 0.0.0.0 - port: 23 + port: 22 + key_path: ./.ssh/id_ed25519 api: - base_url: http://localhost:8000 + base_url: http://bashform-api:8000 diff --git a/internal/config/config.go b/internal/config/config.go index cc0bf21..de5104d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,13 +13,14 @@ type Config struct { } type SSHConfig struct { - URL string `mapstructure:"url"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` + URL string `mapstructure:"url"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + KeyPath string `mapstructure:"key_path"` } type ApiConfig struct { - BaseUrl string `mapstructure:"base_url"` + BaseURL string `mapstructure:"base_url"` } func New() Config { diff --git a/internal/constants/api.go b/internal/constants/api.go new file mode 100644 index 0000000..6b366af --- /dev/null +++ b/internal/constants/api.go @@ -0,0 +1,6 @@ +package constants + +const ( + API_AUTH_HEADER = "PubKey" + API_FORMS_PATH = "/forms" +) diff --git a/internal/constants/messages.go b/internal/constants/messages.go index ca7747b..eacaa7a 100644 --- a/internal/constants/messages.go +++ b/internal/constants/messages.go @@ -1,15 +1,15 @@ package constants const ( - MessageFormNotFound = "Form not found!" - MessageFormSubmitted = "Your response has been submitted!" - MessageFormSubmitting = "Submitting Your response..." - MessageSizeError = "Please resize your terminal to a minimum of %dx%d and run again | Current session size - %dx%d" - MessageHelpExit = "q or ctrl+c to exit" - MessageFormCreated = "Form created successfully!" - MessageFormCreating = "Creating your form..." - MessageFormCreateError = "Error creating form: %s" - MessageCommandHeader = "Command to fill this form:" - MessageCommand = "ssh -t %s f %s" - MessageErrorCodeAlreadyExists = "Form with the same code already exists!" + MessageFormNotFound = "Form not found!" + MessageFormSubmitted = "Your response has been submitted!" + MessageFormSubmitting = "Submitting Your response..." + MessageSizeError = "Please resize your terminal to a minimum of %dx%d and run again | Current session size - %dx%d" + MessageHelpExit = "q or ctrl+c to exit" + MessageFormCreated = "Form created successfully!" + MessageFormCreating = "Creating your form..." + MessageFormCreateError = "Error creating form: %s" + MessageCommandHeader = "Command to fill this form:" + MessageCommand = "ssh -t %s f %s" + MessageCodeNotAvailable = "Form with the same code already exists!" ) diff --git a/internal/models/error.go b/internal/models/error.go new file mode 100644 index 0000000..d9dade1 --- /dev/null +++ b/internal/models/error.go @@ -0,0 +1,21 @@ +package models + +type ApiError struct { + Error string `json:"error"` + Data ApiErrorData `json:"data"` +} + +type ApiErrorData struct { + Error string `json:"error"` + Details string `json:"details"` +} + +func ErrorToApiError(err error) *ApiError { + return &ApiError{ + Error: err.Error(), + Data: ApiErrorData{ + Error: err.Error(), + Details: err.Error(), + }, + } +} diff --git a/internal/models/form.go b/internal/models/form.go index f433b15..2ad9c24 100644 --- a/internal/models/form.go +++ b/internal/models/form.go @@ -13,10 +13,12 @@ type Form struct { Questions []Question `json:"questions"` Responses []Response `json:"responses"` Multiple bool `json:"multiple"` + Error string `json:"error"` } type FormResponse struct { - Data Form `json:"data"` + Data Form `json:"data"` + Error string `json:"error"` } type FormRequest struct { diff --git a/internal/services/client.go b/internal/services/client.go index 04df240..8dd613c 100644 --- a/internal/services/client.go +++ b/internal/services/client.go @@ -6,156 +6,139 @@ import ( "fmt" "net/http" + "github.com/charmbracelet/log" + "github.com/devmegablaster/bashform/internal/constants" "github.com/devmegablaster/bashform/internal/models" ) type Client struct { - BaseUrl string - PubKey string + baseURL string + pubKey string httpClient *http.Client } -func NewClient(baseUrl string, pubKey string) *Client { +func NewClient(baseURL string, pubKey string) *Client { return &Client{ - BaseUrl: baseUrl, - PubKey: pubKey, + baseURL: baseURL, + pubKey: pubKey, httpClient: &http.Client{}, } } +// Get form by code func (c *Client) GetForm(code string) (models.Form, error) { - req, err := http.NewRequest("GET", c.BaseUrl+"/forms/"+code, nil) - if err != nil { - return models.Form{}, err - } - - req.Header.Add("PubKey", c.PubKey) - - resp, err := c.httpClient.Do(req) - if err != nil { - fmt.Println(err) - return models.Form{}, err - } - - defer resp.Body.Close() - var formResponse models.FormResponse - if err := json.NewDecoder(resp.Body).Decode(&formResponse); err != nil { - fmt.Println(err) - return models.Form{}, err + if err := c.doRequest(http.MethodGet, fmt.Sprintf("%s/%s", constants.API_FORMS_PATH, code), nil, &formResponse, true); err != nil { + log.Error(err) + return models.Form{}, fmt.Errorf(err.Data.Error) } return formResponse.Data, nil } -func (c *Client) GetForms() ([]models.Form, error) { - req, err := http.NewRequest("GET", c.BaseUrl+"/forms", nil) - if err != nil { - return nil, err - } - - req.Header.Add("PubKey", c.PubKey) +// CodeResponse is the response from the checkCode endpoint +type CodeResponse struct { + CodeData struct { + Avaliable bool `json:"available"` + } `json:"data"` +} - resp, err := c.httpClient.Do(req) - if err != nil { - fmt.Println(err) - return nil, err +// Check if code is available +func (c *Client) CheckCode(code string) (bool, error) { + var codeResponse CodeResponse + if err := c.doRequest(http.MethodGet, fmt.Sprintf("%s/%s/available", constants.API_FORMS_PATH, code), nil, &codeResponse, true); err != nil { + log.Error(err) + return false, fmt.Errorf(err.Data.Error) } - defer resp.Body.Close() + return codeResponse.CodeData.Avaliable, nil +} +// Get all the forms created by the user +func (c *Client) GetForms() ([]models.Form, error) { var formResponse models.FormsResponse - if err := json.NewDecoder(resp.Body).Decode(&formResponse); err != nil { - fmt.Println(err) - return nil, err + if err := c.doRequest(http.MethodGet, constants.API_FORMS_PATH, nil, &formResponse, true); err != nil { + log.Error(err) + return nil, fmt.Errorf(err.Data.Error) } return formResponse.Data, nil } -func (c *Client) SubmitForm(code string, response models.Response) { - reqBody, err := json.Marshal(response) - if err != nil { - fmt.Println(err) - return - } - - req, err := http.NewRequest("POST", c.BaseUrl+"/forms/"+code, bytes.NewBuffer(reqBody)) - if err != nil { - fmt.Println(err) - return +// Submit a response to a form using formID and response +func (c *Client) SubmitForm(id string, response models.Response) error { + if err := c.doRequest(http.MethodPost, fmt.Sprintf("%s/%s", constants.API_FORMS_PATH, id), response, nil, true); err != nil { + log.Error(err) + return fmt.Errorf(err.Data.Error) } - req.Header.Add("PubKey", c.PubKey) - req.Header.Add("Content-Type", "application/json") + return nil +} - resp, err := c.httpClient.Do(req) - if err != nil { - fmt.Println(err) - return +// Create a form +func (c *Client) CreateForm(form models.FormRequest) (*models.FormResponse, error) { + var formResponse models.FormResponse + if err := c.doRequest(http.MethodPost, constants.API_FORMS_PATH, form, &formResponse, true); err != nil { + log.Error(err) + return nil, fmt.Errorf(err.Data.Error) } - defer resp.Body.Close() + return &formResponse, nil } -func (c *Client) CreateForm(form models.FormRequest) (*models.FormResponse, error) { - reqBody, err := json.Marshal(form) +// Get form with responses for a form using formID +func (c *Client) GetResponses(formID string) (*models.Form, error) { + var responseResponse models.FormResponse + err := c.doRequest(http.MethodGet, fmt.Sprintf("%s/%s/responses", constants.API_FORMS_PATH, formID), nil, &responseResponse, true) if err != nil { - fmt.Println(err) - return nil, err + log.Error(err) + return nil, fmt.Errorf(err.Data.Error) } - req, err := http.NewRequest("POST", c.BaseUrl+"/forms", bytes.NewBuffer(reqBody)) - if err != nil { - fmt.Println(err) - return nil, err - } + return &responseResponse.Data, nil +} - req.Header.Add("PubKey", c.PubKey) - req.Header.Add("Content-Type", "application/json") +// doRequest performs the HTTP request +func (s *Client) doRequest(method, path string, data interface{}, response interface{}, auth bool) *models.ApiError { + url := fmt.Sprintf("%s%s", s.baseURL, path) - resp, err := c.httpClient.Do(req) - if err != nil { - fmt.Println(err) - return nil, err + var buf bytes.Buffer + if data != nil { + if err := json.NewEncoder(&buf).Encode(data); err != nil { + return models.ErrorToApiError(err) + } } - if resp.StatusCode != http.StatusCreated { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + req, err := http.NewRequest(method, url, &buf) + if err != nil { + return models.ErrorToApiError(err) } - defer resp.Body.Close() - - var formResponse models.FormResponse - if err := json.NewDecoder(resp.Body).Decode(&formResponse); err != nil { - fmt.Println(err) - return nil, err + req.Header.Set("Content-Type", "application/json") + if auth { + req.Header.Set(constants.API_AUTH_HEADER, s.pubKey) } - return &formResponse, nil -} - -func (c *Client) GetResponses(formID string) (*models.Form, error) { - req, err := http.NewRequest("GET", c.BaseUrl+"/forms/"+formID+"/responses", nil) + resp, err := s.httpClient.Do(req) if err != nil { - return nil, err + return models.ErrorToApiError(err) } + defer resp.Body.Close() - req.Header.Add("PubKey", c.PubKey) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + var apiErr models.ApiError + if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil { + return models.ErrorToApiError(err) + } - resp, err := c.httpClient.Do(req) - if err != nil { - fmt.Println(err) - return nil, err + return &apiErr } - defer resp.Body.Close() - - var responseResponse models.FormResponse - if err := json.NewDecoder(resp.Body).Decode(&responseResponse); err != nil { - fmt.Println(err) - return nil, err + if response != nil { + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return models.ErrorToApiError(err) + } } - return &responseResponse.Data, nil + return nil } diff --git a/internal/ui/create/create.go b/internal/ui/create/create.go index 1df00ae..1e93d92 100644 --- a/internal/ui/create/create.go +++ b/internal/ui/create/create.go @@ -59,19 +59,14 @@ func (m *Model) Init() tea.Cmd { } func (m *Model) View() string { - if m.sizeError { - return styles.Error.Render(fmt.Sprintf(constants.MessageSizeError, 50, 30, m.width, m.height)) - } - var content string - content = m.questionsForm.View() - if m.isCreating { - content = styles.Succes.Render(constants.MessageFormCreating) - } + switch { + case m.sizeError: + return styles.Error.Render(fmt.Sprintf(constants.MessageSizeError, 50, 30, m.width, m.height)) - if m.isCreated { + case m.isCreated: content = styles.Succes.Render(constants.MessageFormCreated) + "\n\n" + styles.Description.Render(constants.MessageCommandHeader) + @@ -79,13 +74,14 @@ func (m *Model) View() string { styles.Heading.Render(fmt.Sprintf(constants.MessageCommand, m.cfg.SSH.URL, m.formResp.Data.Code)) + "\n\n" + styles.Description.Render(constants.MessageHelpExit) - } - if m.err != nil { + case m.isCreating: + content = styles.Heading.Render(constants.MessageFormCreating) + + case m.err != nil: content = styles.Error.Render(fmt.Sprintf(constants.MessageFormCreateError, m.err.Error())) - } - if m.init { + case m.init: return styles.PlaceCenter(m.width, m.height, constants.Logo) } @@ -120,7 +116,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.questionsForm = f } - if m.questionsForm.State == huh.StateCompleted && !m.isCreating { + if m.questionsForm.State == huh.StateCompleted && !m.isCreating && !m.isCreated { m.CreateRequest() } @@ -146,4 +142,5 @@ func (m *Model) CreateRequest() { m.formResp = form m.isCreated = true + m.isCreating = false } diff --git a/internal/ui/form/form.go b/internal/ui/form/form.go index f6beb5b..4026fcc 100644 --- a/internal/ui/form/form.go +++ b/internal/ui/form/form.go @@ -57,7 +57,7 @@ func NewModel(form models.Form, client *services.Client, session ssh.Session) *M } func (m *Model) Init() tea.Cmd { - return tea.Batch(m.spinner.Tick, m.huhForm.Init(), tea.EnterAltScreen) + return tea.Batch(m.spinner.Tick, m.huhForm.Init()) } func (m *Model) View() string { @@ -69,16 +69,18 @@ func (m *Model) View() string { content = m.huhForm.View() - if m.isSubmitting { - content = m.spinner.View() + "\n" + styles.Description.Render(constants.MessageFormSubmitting) - } + switch { + case m.submitError != nil: + content = styles.Error.Render(m.submitError.Error()) - if m.submitSuccess { + case m.init: + return styles.PlaceCenter(m.width, m.height, constants.Logo) + + case m.submitSuccess: content = styles.Succes.Render(constants.MessageFormSubmitted) + "\n\n" + styles.Description.Render(constants.MessageHelpExit) - } - if m.init { - return styles.PlaceCenter(m.width, m.height, constants.Logo) + case m.isSubmitting: + content = m.spinner.View() + "\n" + styles.Description.Render(constants.MessageFormSubmitting) } box := styles.Box(m.width, content) @@ -92,7 +94,6 @@ func (m *Model) View() string { } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - // Handle key presses for exit switch msg := msg.(type) { case tea.KeyMsg: @@ -117,7 +118,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.huhForm = f } - if m.huhForm.State == huh.StateCompleted && !m.isSubmitting { + if m.huhForm.State == huh.StateCompleted && !m.isSubmitting && !m.submitSuccess { m.Submit() } @@ -146,7 +147,12 @@ func (m *Model) Submit() { Answers: answer, } - m.client.SubmitForm(m.Form.Code, response) + err := m.client.SubmitForm(m.Form.ID, response) + if err != nil { + m.submitError = err + m.isSubmitting = false + } + m.isSubmitting = false m.submitSuccess = true } diff --git a/main.go b/main.go index 6424eec..78ca61c 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ func main() { s, err := wish.NewServer( wish.WithAddress(net.JoinHostPort(cfg.SSH.Host, strconv.Itoa(cfg.SSH.Port))), - wish.WithHostKeyPath(".ssh/id_ed25519"), + wish.WithHostKeyPath(cfg.SSH.KeyPath), wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { return true }),