diff --git a/internal/api/handlers/announcement.go b/internal/api/handlers/announcement.go index 98caf443..4e3191e6 100644 --- a/internal/api/handlers/announcement.go +++ b/internal/api/handlers/announcement.go @@ -5,7 +5,6 @@ package handlers import ( "database/sql" "errors" - "fmt" "log" "net/http" @@ -129,9 +128,9 @@ func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) { DiscordMessageID: msgID, } - fmt.Println("DTO ->", params, "\nDBMODEL->", dbParams) // TODO: error out if required fields aren't provided if err := h.announcementService.Create(ctx, dbParams); err != nil { + log.Printf("Failed to create announcement: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to create announcement", }) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index 8a516389..7da48e49 100644 --- a/internal/cli/announcements/delete.go +++ b/internal/cli/announcements/delete.go @@ -2,13 +2,13 @@ package announcements import ( "fmt" - "io" "net/http" + "os" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -31,29 +31,12 @@ func init() { func deleteAnnouncement(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) - // ----- Delete ----- - request, err := oauth.NewRequestWithAuth(http.MethodDelete, deleteUrl.String(), nil) - if err != nil { - fmt.Println("Error: failed to construct delete request:", err) - return + if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - - client := http.Client{} - response, err := client.Do(request) - if err != nil { - fmt.Println("Error: failed to send delete request:", err) - return - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", response.Status) - return - } - body, err := io.ReadAll(response.Body) - if err != nil { - fmt.Println("Error: failed to read response body:", err) - return - } - utils.PrettyPrintJSON(body) } diff --git a/internal/cli/announcements/get.go b/internal/cli/announcements/get.go index 603996fb..71e0ac86 100644 --- a/internal/cli/announcements/get.go +++ b/internal/cli/announcements/get.go @@ -2,11 +2,12 @@ package announcements import ( "fmt" - "io" "net/http" + "os" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -29,30 +30,12 @@ func init() { func getAnnouncement(uuid string, cfg *config.Config) { getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) - // ----- Requesting Get ----- - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, getUrl.String(), nil) - if err != nil { - fmt.Println("error with request:", err) - return + if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - - res, err := client.Do(req) - if err != nil { - fmt.Println("error getting announcements:", err) - return - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", res.Status) - return - } - - body, err := io.ReadAll(res.Body) - if err != nil { - fmt.Println("Error: failed to read response body:", err) - return - } - utils.PrettyPrintJSON(body) } diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index a1fa018f..02b95f47 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -1,18 +1,18 @@ package announcements import ( + "bytes" "encoding/json" "fmt" - "io" "net/http" - "strings" + "os" "github.com/charmbracelet/huh" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/forms" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -27,44 +27,28 @@ var PostAnnouncement = &cobra.Command{ } func postAnnouncement(cfg *config.Config) { - payload, err := postForm() - if err != nil { - fmt.Println("Error:", err) - return - } - - jsonPayload, err := json.Marshal(payload) - if err != nil { - fmt.Println("Error: could not marshal JSON:", err) - return - } + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements") - postURL := config.GetBaseURL(cfg).JoinPath("v1", "announcements") - client := http.Client{} - req, err := oauth.NewRequestWithAuth(http.MethodPost, postURL.String(), strings.NewReader(string(jsonPayload))) + payload, err := postForm() if err != nil { - fmt.Println("Error: could not create request:", err) + fmt.Fprintln(os.Stderr, "Error:", err) return } - - res, err := client.Do(req) + b, err := json.Marshal(payload) if err != nil { - fmt.Println("Error: could not send request:", err) + fmt.Fprintln(os.Stderr, "Error: could not marshal JSON:", err) return } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", res.Status) - return - } - - body, err := io.ReadAll(res.Body) - if err != nil { - fmt.Println("Error: could not read response body:", err) - return + if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } func postForm() (*dto.Announcement, error) { @@ -111,5 +95,5 @@ func postForm() (*dto.Announcement, error) { payload.DiscordChannelID = &channelIDStr payload.DiscordMessageID = &messageIDStr - return &payload, err + return &payload, nil } diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 14651e28..1ff2f33c 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -4,14 +4,14 @@ import ( "bytes" "encoding/json" "fmt" - "io" "net/http" + "os" "github.com/charmbracelet/huh" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -32,81 +32,56 @@ func init() { } func putAnnouncements(id string, cfg *config.Config) { - resourceURL := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) + resourceUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) - // ----- Get the Announcement We Want to Update ----- - client := http.Client{} - getReq, err := oauth.NewRequestWithAuth(http.MethodGet, resourceURL.String(), nil) - if err != nil { - fmt.Printf("Error: couldn't retrieve resource %s: %s", id, err) - return - } - getRes, err := client.Do(getReq) - if err != nil { - fmt.Println("Error: failed to send request:", err) - return - } - defer getRes.Body.Close() - if getRes.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", getRes.Status) - return - } - body, err := io.ReadAll(getRes.Body) - if err != nil { - fmt.Println("Error: failed to read response body:", err) + // ----- Get announcement we want to update ----- + var oldPayload dto.Announcement + if body, err := client.SendRequestAndReadResponse(resourceUrl, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } return + } else { + err = json.Unmarshal(body, &oldPayload) + if err != nil { + fmt.Fprintln(os.Stderr, "Error: failed to unmarshal response body:", err) + return + } } - // ----- Update found announceement ----- - var oldPayload dto.UpdateAnnouncement - err = json.Unmarshal(body, &oldPayload) + // ----- Update found announcement ----- + newPayload, err := putForm(&oldPayload) if err != nil { - fmt.Println("Error: failed to unmarshal response body:", err) + fmt.Fprintln(os.Stderr, "Error:", err) return } - newPayload, err := putForm(id) + b, err := json.Marshal(newPayload) if err != nil { - fmt.Println("Error:", err) + fmt.Fprintln(os.Stderr, "Error: failed to marshal data:", err) return } - jsonPayload, err := json.Marshal(newPayload) - if err != nil { - fmt.Println("Error: failed to marshal data:", err) - return - } - putRequest, err := oauth.NewRequestWithAuth(http.MethodPut, resourceURL.String(), bytes.NewBuffer(jsonPayload)) - if err != nil { - fmt.Println("Error: failed to contruct request:", err) - return - } - putResponse, err := client.Do(putRequest) - if err != nil { - fmt.Println("Error: failed to send request: ", err) - return - } - defer putResponse.Body.Close() - if putResponse.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", putResponse.Status) - return - } - body, err = io.ReadAll(putResponse.Body) - if err != nil { - fmt.Println("Error: failed to read response body", err) - return + // Update remote resource with new data + if body, err := client.SendRequestAndReadResponse(resourceUrl, true, http.MethodPut, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } -// TODO: Use DTO models instaad of dbmodels -func putForm(uuid string) (*dto.UpdateAnnouncement, error) { +func putForm(oldPayload *dto.Announcement) (*dto.UpdateAnnouncement, error) { var payload dto.UpdateAnnouncement var err error var ( - visibilityStr string - announceAtStr string - channelIDStr string - messageIDStr string + visibilityStr string = oldPayload.Visibility + announceAtStr string // no default for now bc its stored as a raw timestamp + channelIDStr string = *oldPayload.DiscordChannelID + messageIDStr string = *oldPayload.DiscordMessageID ) form := huh.NewForm( huh.NewGroup( @@ -131,8 +106,7 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { return nil, err } - payload.Uuid = uuid - // HACK: These conversions won't be necessary once we start using DTO models here + payload.Uuid = oldPayload.Uuid payload.Visibility = &visibilityStr if announceAtStr != "" { timestamp, err := utils.ByteSlicetoUnix([]byte(announceAtStr)) @@ -141,7 +115,6 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { } payload.AnnounceAt = ×tamp } - payload.DiscordChannelID = &channelIDStr payload.DiscordMessageID = &messageIDStr diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go new file mode 100644 index 00000000..8fe5fe5a --- /dev/null +++ b/internal/cli/client/client.go @@ -0,0 +1,39 @@ +package client + +import ( + "fmt" + "io" + "net/http" + "net/url" + + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" +) + +func SendRequestAndReadResponse(url *url.URL, enableAuth bool, method string, body io.Reader) ([]byte, error) { + client := http.Client{} + var req *http.Request + var err error + if enableAuth { + req, err = oauth.NewRequestWithAuth(method, url.String(), body) + } else { + req, err = http.NewRequest(method, url.String(), body) + } + if err != nil { + return nil, fmt.Errorf("failed to construct request: %w", err) + } + + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + if res.StatusCode != http.StatusOK { + return data, fmt.Errorf("HTTP %s", res.Status) + } + return data, nil +} diff --git a/internal/cli/oauth/request_with_auth.go b/internal/cli/oauth/request_with_auth.go index d845c9ef..58a772ec 100644 --- a/internal/cli/oauth/request_with_auth.go +++ b/internal/cli/oauth/request_with_auth.go @@ -36,6 +36,7 @@ type StoredToken struct { // enough to not run into port conflicts. const redirectAddr = ":61234" +// Wraps `http.NewRequest` by performing the OAuth2 flow and setting the Authorization header func NewRequestWithAuth(method, targetURL string, body io.Reader) (*http.Request, error) { cfg := config.Load() req, err := http.NewRequest(method, targetURL, body) diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index d69261e1..caac2586 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -2,16 +2,13 @@ package officers import ( "fmt" - "io" "net/http" - "net/url" "os" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -20,33 +17,9 @@ var DeleteOfficers = &cobra.Command{ Short: "Delete an officer with their id", Run: func(cmd *cobra.Command, args []string) { - var uuidVal string - cmd.Flags().Set("id", uuidVal) - err := huh.NewForm().Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - err = huh.NewInput(). - Title("ACMCSUF-CLI Board Delete:"). - Description("Please enter the officer's ID:"). - Prompt("> "). - Value(&uuidVal). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - cmd.Flags().Set("id", uuidVal) - - id, _ := cmd.Flags().GetString("id") - deleteOfficer(id, config.Cfg) + var uuid string + uuid, _ = cmd.Flags().GetString("id") + deleteOfficer(uuid, config.Cfg) }, } @@ -56,41 +29,14 @@ func init() { } func deleteOfficer(id string, cfg *config.Config) { - // prepare url - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - deleteURL := baseURL.JoinPath(fmt.Sprint("v1/board/officers/", id)) + deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - request, err := oauth.NewRequestWithAuth(http.MethodDelete, deleteURL.String(), nil) - if err != nil { - fmt.Println("Error making delete request:", err) - return - } - - client := &http.Client{} - response, err := client.Do(request) - if err != nil { - fmt.Println("Error with delete response:", err) - return - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - fmt.Println("Response status:", response.Status) - return - } - - body, err := io.ReadAll(response.Body) - if err != nil { - fmt.Println("Error reading delete response body:", err) - return + if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index 1c74900c..5b3ac40c 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -1,16 +1,13 @@ package officers import ( - "encoding/json" "fmt" "net/http" - "net/url" "os" - "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/utils" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) @@ -19,51 +16,8 @@ var GetOfficers = &cobra.Command{ Short: "Get Officers", Run: func(cmd *cobra.Command, args []string) { - blankUUID := "" - cmd.Flags().Set("id", blankUUID) - var flagsChosen []string - err := huh.NewForm( - huh.NewGroup( - huh.NewMultiSelect[string](). - //Ask the user what commands they want to use. - Title("ACMCSUF-CLI Officers Get"). - Description("Choose a command(s). Note: Use spacebar to select and if done click enter.\nTo get all officers, simply click enter."). - Options( - huh.NewOption("Get Specific ID", "id"), - ). - Value(&flagsChosen), - ), - ).Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - for _, flag := range flagsChosen { - var uuidVal string - switch flag { - case "id": - err = huh.NewInput(). - Title("ACMCSUF-CLI Officers Get:"). - Description("Please enter the officer's ID:"). - Prompt("> "). - Value(&uuidVal). - Run() - cmd.Flags().Set("id", uuidVal) - } - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - } - - id, _ := cmd.Flags().GetString("id") - getOfficers(id, config.Cfg) + uuid, _ := cmd.Flags().GetString("id") + getOfficers(uuid, config.Cfg) }, } @@ -71,60 +25,15 @@ func init() { GetOfficers.Flags().String("id", "", "Get a specific officer") } -func getOfficers(id string, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - // prepare url - path := fmt.Sprint("v1/board/officers/", id) - - getURL := baseURL.JoinPath(path) - - // getting officer(s) - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, getURL.String(), nil) - if err != nil { - fmt.Println("error getting the request: ", err) - return - } +func getOfficers(uuid string, cfg *config.Config) { + getUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", uuid) - res, err := client.Do(req) - if err != nil { - fmt.Println("error with getting response", err) - return - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - fmt.Println("response status:", res.Status) - return - } - - if id == "" { - var getPayload []dbmodels.GetOfficerRow - err = json.NewDecoder(res.Body).Decode(&getPayload) - if err != nil { - fmt.Println("Failed to read response body without id:", err) - return - } - - for i := range getPayload { - fmt.Println(utils.PrintStruct(getPayload[i])) + if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } } else { - var getPayload dbmodels.GetOfficerRow - err = json.NewDecoder(res.Body).Decode(&getPayload) - if err != nil { - fmt.Println("Failed to read response body with id:", err) - return - } - - fmt.Println(utils.PrintStruct(getPayload)) + utils.PrettyPrintJSON(body) } } diff --git a/internal/cli/officers/officers.go b/internal/cli/officers/officers.go deleted file mode 100644 index faee3b91..00000000 --- a/internal/cli/officers/officers.go +++ /dev/null @@ -1,69 +0,0 @@ -package officers - -import ( - "fmt" - "os" - - "github.com/charmbracelet/huh" - "github.com/spf13/cobra" -) - -type officerFlags struct { - uuid bool - fullname bool - picture bool - github bool - discord bool -} - -var CLIOfficers = &cobra.Command{ - Use: "officers HEADER", - Short: "A command to manage officers.", -} - -func init() { - CLIOfficers.AddCommand(GetOfficers) - CLIOfficers.AddCommand(DeleteOfficers) - CLIOfficers.AddCommand(PostOfficer) - CLIOfficers.AddCommand(PutOfficer) -} - -func ShowMenu(backCallback func()) { - var boardState string - boardMenu := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("ACMCSUF-CLI Officers"). - Description("Choose an option to your heart's content."). - Options( - huh.NewOption("Delete", "delete"), - huh.NewOption("Get", "get"), - huh.NewOption("Post", "post"), - huh.NewOption("Put", "put"), - huh.NewOption("Back", "back"), - ). - Value(&boardState), - ), - ) - err := boardMenu.Run() - if err != nil { - fmt.Println("Uh oh:", err) - os.Exit(1) - } - - if boardState == "delete" { - DeleteOfficers.Run(DeleteOfficers, []string{}) - backCallback() - } else if boardState == "get" { - GetOfficers.Run(GetOfficers, []string{}) - backCallback() - } else if boardState == "post" { - PostOfficer.Run(PostOfficer, []string{}) - backCallback() - } else if boardState == "put" { - PutOfficer.Run(PutOfficer, []string{}) - backCallback() - } else if boardState == "back" { - backCallback() - } -} diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index 83dc7976..2f2814ca 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -1,21 +1,19 @@ package officers import ( - "bufio" + "bytes" "encoding/json" "fmt" - "io" "net/http" - "net/url" "os" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/forms" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -24,34 +22,7 @@ var PostOfficer = &cobra.Command{ Short: "Post a new officer", Run: func(cmd *cobra.Command, args []string) { - var payload dbmodels.CreateOfficerParams - err := huh.NewForm().Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - - payload.Uuid, _ = cmd.Flags().GetString("uuid") - payload.FullName, _ = cmd.Flags().GetString("name") - pic, _ := cmd.Flags().GetString("picture") - payload.Picture = utils.StringtoNullString(pic) - git, _ := cmd.Flags().GetString("github") - payload.Github = utils.StringtoNullString(git) - disc, _ := cmd.Flags().GetString("discord") - payload.Discord = utils.StringtoNullString(disc) - - changedFlags := officerFlags{ - uuid: cmd.Flags().Lookup("uuid").Changed, - fullname: cmd.Flags().Lookup("name").Changed, - picture: cmd.Flags().Lookup("picture").Changed, - github: cmd.Flags().Lookup("github").Changed, - discord: cmd.Flags().Lookup("discord").Changed, - } - - postOfficer(&payload, &changedFlags, config.Cfg) + postOfficer(config.Cfg) }, } @@ -64,247 +35,63 @@ func init() { PostOfficer.Flags().StringP("discord", "d", "", "Set the discord of this officer") } -func postOfficer(payload *dbmodels.CreateOfficerParams, cf *officerFlags, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - // uuid - for { - if cf.uuid { - break - } - - var uuid string - err := huh.NewInput(). - Title("ACMCSUF-CLI Officer Post:"). - Description("Please enter officer's uuid:"). - Prompt("> "). - Value(&uuid). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(uuid)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - payload.Uuid = string(scanner.Bytes()) - break - } - - // full name - for { - if cf.fullname { - break - } - - var fullName string - err := huh.NewInput(). - Title("ACMCSUF-CLI Officer Post:"). - Description("Please enter officer's full name:"). - Prompt("> "). - Value(&fullName). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(fullName)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - payload.FullName = string(scanner.Bytes()) - break - } +func postOfficer(cfg *config.Config) { + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers") - // picture - for { - if cf.picture { - break - } + payload, err := postForm() + cobra.CheckErr(err) + b, err := json.Marshal(payload) + cobra.CheckErr(err) - var picLink string - err := huh.NewInput(). - Title("ACMCSUF-CLI Officer Post:"). - Description("Please enter the picture link for officer:"). - Prompt("> "). - Value(&picLink). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(picLink)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue + if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } - - payload.Picture = utils.StringtoNullString(string(scanner.Bytes())) - break + } else { + utils.PrettyPrintJSON(body) } +} - // github - for { - if cf.github { - break - } - - var githubLink string - err := huh.NewInput(). - Title("ACMCSUF-CLI Officer Post:"). - Description("Please enter the github link for officer:"). - Prompt("> "). - Value(&githubLink). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(githubLink)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - payload.Github = utils.StringtoNullString(string(scanner.Bytes())) - break - } - - // discord - for { - if cf.discord { - break - } - - var discordLink string - err := huh.NewInput(). - Title("ACMCSUF-CLI Officer Post:"). - Description("Please enter the discord link for officer"). - Prompt("> "). - Value(&discordLink). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(discordLink)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - payload.Discord = utils.StringtoNullString(string(scanner.Bytes())) - break - - } - - // confirmation - for { - var option string - description := "Is your board data correct?\n" + utils.PrintStruct(payload) - err := huh.NewSelect[string](). - Title("ACMCSUF-CLI Officer Post:"). - Description(description). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&option). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(option)) - utils.PrintStruct(payload) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - return - } - - confirmationBuffer := scanner.Bytes() - confirmationBool, err := utils.YesOrNo(confirmationBuffer, scanner) - if err != nil { - fmt.Println("error with reading confirmation:", err) - } - if !confirmationBool { - // Sorry :( - return - } else { - break - } - } - - // marshal to json, and prepare url - jsonPayload, err := json.Marshal(*payload) - if err != nil { - fmt.Println("error formating payload to json: ", err) - return - } - - postURL := baseURL.JoinPath("v1/board/officers/") - - // post payload - client := http.Client{} - req, err := oauth.NewRequestWithAuth(http.MethodPost, postURL.String(), strings.NewReader(string(jsonPayload))) - if err != nil { - fmt.Println("error with post: ", err) - return - } - - res, err := client.Do(req) - if err != nil { - fmt.Println("error getting response", res) - return - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - fmt.Println("response status:", res.Status) - return +func postForm() (*dbmodels.CreateOfficerParams, error) { + var payload dbmodels.CreateOfficerParams + var err error + var ( + picture string + github string + discord string + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Officer ID"). + Value(&payload.Uuid). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Full Name"). + Value(&payload.FullName). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Picture URL"). + Value(&picture), + huh.NewInput(). + Title("GitHub Username"). + Value(&github), + huh.NewInput(). + Title("Discord Username"). + Value(&discord), + ), + ) + if err = form.Run(); err != nil { + return nil, err } - body, err := io.ReadAll(res.Body) - if err != nil { - fmt.Println("error reading body: ", err) - return - } + // HACK: conversions required here due to lack of DTO models + payload.Picture = utils.StringtoNullString(picture) + payload.Github = utils.StringtoNullString(github) + payload.Discord = utils.StringtoNullString(discord) - fmt.Println(string(body)) + return &payload, nil } diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index c620b2db..3a77cb84 100644 --- a/internal/cli/officers/put.go +++ b/internal/cli/officers/put.go @@ -1,22 +1,18 @@ package officers import ( - "bufio" "bytes" "encoding/json" "fmt" - "io" "net/http" - "net/url" "os" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -25,268 +21,94 @@ var PutOfficer = &cobra.Command{ Short: "update an existing officer by id", Run: func(cmd *cobra.Command, args []string) { - payload := dbmodels.UpdateOfficerParams{} - var uuidVal string - cmd.Flags().Set("id", uuidVal) - huh.NewForm().Run() - huh.NewInput(). - Title("ACMCSUF-CLI Officer Put:"). - Description("Please enter the officer's ID:"). - Prompt("> "). - Value(&uuidVal). - Run() - cmd.Flags().Set("id", uuidVal) id, _ := cmd.Flags().GetString("id") - - fullname, _ := cmd.Flags().GetString("fullname") - picture, _ := cmd.Flags().GetString("picture") - github, _ := cmd.Flags().GetString("github") - discord, _ := cmd.Flags().GetString("discord") - uuid, _ := cmd.Flags().GetString("uuid") - - payload.FullName = fullname - payload.Picture = utils.StringtoNullString(picture) - payload.Github = utils.StringtoNullString(github) - payload.Discord = utils.StringtoNullString(discord) - payload.Uuid = uuid - - flags := officerFlags{ - fullname: cmd.Flags().Lookup("fullname").Changed, - picture: cmd.Flags().Lookup("picture").Changed, - github: cmd.Flags().Lookup("github").Changed, - discord: cmd.Flags().Lookup("discord").Changed, - uuid: cmd.Flags().Lookup("uuid").Changed, - } - - putOfficer(id, &payload, flags, config.Cfg) + putOfficer(id, config.Cfg) }, } func init() { PutOfficer.Flags().String("id", "", "Officer ID to update") - - PutOfficer.Flags().String("fullname", "", "Change full name") - PutOfficer.Flags().String("picture", "", "Change picture URL") - PutOfficer.Flags().String("github", "", "Change GitHub username") - PutOfficer.Flags().String("discord", "", "Change Discord tag") - PutOfficer.Flags().String("uuid", "", "Change uuid") - PutOfficer.MarkFlagRequired("id") } -func putOfficer(id string, payload *dbmodels.UpdateOfficerParams, flags officerFlags, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - if id == "" { - fmt.Println("Officer id required for put!") - return - } - - // construct url - u := baseURL.JoinPath("v1/board/officers/", id) - - // getting old officer - client := http.Client{} - getReq, err := oauth.NewRequestWithAuth(http.MethodGet, u.String(), nil) - if err != nil { - fmt.Printf("error making request %s: %s\n", id, err) - return - } - - resp, err := client.Do(getReq) - if err != nil { - fmt.Println("error getting response", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - fmt.Println("get response code:", resp.Status) - return - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println("error reading response body:", err) - return - } - - var old dbmodels.CreateOfficerParams - if err := json.Unmarshal(body, &old); err != nil { - fmt.Println("error unmarshaling previous officer data:", err) - return - } - - scanner := bufio.NewScanner(os.Stdin) - - // full name - for { - if flags.fullname { - break - } - - change, err := utils.ChangePrompt("full name", old.FullName, scanner, "officer") - if err != nil { - fmt.Println(err) - continue - } - if change != nil { - payload.FullName = string(change) - } else { - payload.FullName = old.FullName - } - break - } - - // picture - for { - if flags.picture { - break - } - - change, err := utils.ChangePrompt("picture", old.Picture.String, scanner, "officer") - if err != nil { - fmt.Println(err) - continue - } - if change != nil { - payload.Picture = utils.StringtoNullString(string(change)) - } else { - payload.Picture = old.Picture - } - break - } - - // github - for { - if flags.github { - break - } - - change, err := utils.ChangePrompt("github", old.Github.String, scanner, "officer") - if err != nil { - fmt.Println(err) - continue - } - if change != nil { - payload.Github = utils.StringtoNullString(string(change)) - } else { - payload.Github = old.Github - } - break - } - - // discord - for { - if flags.discord { - break - } - - change, err := utils.ChangePrompt("discord", old.Discord.String, scanner, "officer") - if err != nil { - fmt.Println(err) - continue - } - if change != nil { - payload.Discord = utils.StringtoNullString(string(change)) - } else { - payload.Discord = old.Discord - } - break - } - - // uuid - for { - if flags.uuid { - break - } - - change, err := utils.ChangePrompt("uuid", old.Uuid, scanner, "officer") - if err != nil { - fmt.Println(err) - continue - } - if change != nil { - payload.Uuid = string(change) - } else { - payload.Uuid = old.Uuid - } - break - } +func putOfficer(id string, cfg *config.Config) { + url := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - // Confirm - for { - var option string - description := "Is your board data correct?\n" + utils.PrintStruct(payload) - huh.NewSelect[string](). - Title("ACMCSUF-CLI Officer Put:"). - Description(description). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&option). - Run() - scanner := bufio.NewScanner(strings.NewReader(option)) - utils.PrintStruct(payload) - scanner.Scan() - confirmation := scanner.Bytes() - - ok, err := utils.YesOrNo(confirmation, scanner) - if err != nil { - fmt.Println(err) - continue + // ----- Get officer we want to update + var oldPayload dbmodels.CreateOfficerParams + if body, err := client.SendRequestAndReadResponse(url, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } - if !ok { + return + } else { + if err := json.Unmarshal(body, &oldPayload); err != nil { + fmt.Fprintln(os.Stderr, "Error: failed to unmarshal response body:", err) return } - break } - // marshal payload - jsonPayload, err := json.Marshal(*payload) + // ----- Update found officer ----- + // Read new data + newPayload, err := putForm(&oldPayload) if err != nil { - fmt.Println("Error marshaling data:", err) + fmt.Fprintln(os.Stderr, "Error:", err) return } - - // PUT - putReq, err := oauth.NewRequestWithAuth(http.MethodPut, u.String(), bytes.NewBuffer(jsonPayload)) - if err != nil { - fmt.Println("Problem with PUT:", err) - return - } - - putResp, err := client.Do(putReq) + b, err := json.Marshal(newPayload) if err != nil { - fmt.Println("Error with response:", err) - return - } - if putResp == nil { - fmt.Println("no response received") - return - } - defer putResp.Body.Close() - - if putResp.StatusCode != http.StatusOK { - fmt.Println("put response status:", putResp.Status) + fmt.Fprintln(os.Stderr, "Error: failed to marshal data:", err) return } - fmt.Println("PUT status:", putResp.Status) - - body, err = io.ReadAll(putResp.Body) - if err != nil { - fmt.Println("Error reading body:", err) - return + // Update remote resource with new data + if body, err := client.SendRequestAndReadResponse(url, true, http.MethodPut, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } + } else { + utils.PrettyPrintJSON(body) } +} - fmt.Println(string(body)) +func putForm(oldPayload *dbmodels.CreateOfficerParams) (*dbmodels.UpdateOfficerParams, error) { + var payload dbmodels.UpdateOfficerParams + var err error + var ( + name string = oldPayload.FullName + picture string = oldPayload.Picture.String + github string = oldPayload.Github.String + discord string = oldPayload.Discord.String + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Full Name"). + Value(&name), + huh.NewInput(). + Title("Picture URL"). + Value(&picture), + huh.NewInput(). + Title("GitHub URL"). + Value(&github), + huh.NewInput(). + Title("Discord URL"). + Value(&discord), + ), + ) + if err = form.Run(); err != nil { + return nil, err + } + + payload.Uuid = oldPayload.Uuid + payload.FullName = name + payload.Picture = utils.StringtoNullString(picture) + payload.Github = utils.StringtoNullString(github) + payload.Discord = utils.StringtoNullString(discord) + + return &payload, nil } diff --git a/internal/cli/officers/root.go b/internal/cli/officers/root.go new file mode 100644 index 00000000..1cc29605 --- /dev/null +++ b/internal/cli/officers/root.go @@ -0,0 +1,17 @@ +package officers + +import ( + "github.com/spf13/cobra" +) + +var CLIOfficers = &cobra.Command{ + Use: "officers", + Short: "A command to manage officers.", +} + +func init() { + CLIOfficers.AddCommand(GetOfficers) + CLIOfficers.AddCommand(DeleteOfficers) + CLIOfficers.AddCommand(PostOfficer) + CLIOfficers.AddCommand(PutOfficer) +} diff --git a/utils/output.go b/utils/output.go index 3dd7d72e..c23a27b9 100644 --- a/utils/output.go +++ b/utils/output.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "os" "github.com/tidwall/pretty" ) @@ -11,3 +12,9 @@ func PrettyPrintJSON(json []byte) { colorfulJSON := pretty.Color(prettyJSON, nil) fmt.Print(string(colorfulJSON)) } + +func PrettyPrintJSONErr(json []byte) { + prettyJSON := pretty.Pretty(json) + colorfulJSON := pretty.Color(prettyJSON, nil) + fmt.Fprint(os.Stderr, string(colorfulJSON)) +}