From cf350a8a39935a437c6190a3678752cc328851bd Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:22:51 -0800 Subject: [PATCH 01/32] cleanup officer delete --- internal/cli/officers/delete.go | 55 ++++++--------------------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index d69261e1..48e049c3 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -4,10 +4,7 @@ import ( "fmt" "io" "net/http" - "net/url" - "os" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" @@ -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,40 +29,30 @@ 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) + request, err := oauth.NewRequestWithAuth(http.MethodDelete, deleteUrl.String(), nil) if err != nil { - fmt.Println("Error making delete request:", err) + fmt.Println("Error: failed to construct delete request:", err) return } client := &http.Client{} response, err := client.Do(request) if err != nil { - fmt.Println("Error with delete response:", err) + fmt.Println("Error: failed to send delete request:", err) return } defer response.Body.Close() if response.StatusCode != http.StatusOK { - fmt.Println("Response status:", response.Status) + fmt.Println("Error: HTTP", response.Status) return } body, err := io.ReadAll(response.Body) if err != nil { - fmt.Println("Error reading delete response body:", err) + fmt.Println("Error: failed to read response body:", err) return } utils.PrettyPrintJSON(body) From e718a413158a5f7e55ddc2f8777ddc73ca09442e Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:29:19 -0800 Subject: [PATCH 02/32] clean up officer get --- internal/cli/officers/get.go | 53 ++++++++---------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index 1c74900c..dfa53b1b 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -1,13 +1,12 @@ package officers import ( - "encoding/json" "fmt" + "io" "net/http" "net/url" "os" - "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/utils" "github.com/charmbracelet/huh" @@ -71,60 +70,32 @@ 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) +func getOfficers(uuid string, cfg *config.Config) { + getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) - getURL := baseURL.JoinPath(path) - - // getting officer(s) client := http.Client{} - req, err := http.NewRequest(http.MethodGet, getURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, getUrl.String(), nil) if err != nil { - fmt.Println("error getting the request: ", err) + fmt.Println("Error: failed to construct request:", err) return } res, err := client.Do(req) if err != nil { - fmt.Println("error with getting response", err) + fmt.Println("Error: failed to send request:", err) return } defer res.Body.Close() if res.StatusCode != http.StatusOK { - fmt.Println("response status:", res.Status) + fmt.Println("Error: HTTP", 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])) - } - } 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)) + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println("Error: failed to read response body:", err) + return } + utils.PrettyPrintJSON(body) } From fdeef719511276ae955a6f75df8a754f85a8036e Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:43:39 -0800 Subject: [PATCH 03/32] create resuable function --- internal/cli/client/client.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 internal/cli/client/client.go diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go new file mode 100644 index 00000000..eb8dc08d --- /dev/null +++ b/internal/cli/client/client.go @@ -0,0 +1,32 @@ +package client + +import ( + "fmt" + "io" + "net/http" + "net/url" +) + +func SendRequestAndReadResponse(url *url.URL, method string) ([]byte, error) { + client := http.Client{} + req, err := http.NewRequest(method, url.String(), nil) + 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() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %s", res.Status) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return body, nil +} From 2b9d53f9e987dd8c73037a5f37ea7d6146e80374 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:47:56 -0800 Subject: [PATCH 04/32] use reusable function once --- internal/cli/announcements/delete.go | 32 ++++++---------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index 8a516389..ad2af925 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,9 @@ 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, http.MethodDelete); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } 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) } From d3fe15e7f46e5f7ab464f41c3b84bc6a842d4744 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:54:20 -0800 Subject: [PATCH 05/32] more simplification --- internal/cli/announcements/delete.go | 2 +- internal/cli/announcements/get.go | 32 ++++++---------------------- internal/cli/client/client.go | 8 +++---- internal/cli/officers/delete.go | 32 ++++++---------------------- 4 files changed, 17 insertions(+), 57 deletions(-) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index ad2af925..313bdfe9 100644 --- a/internal/cli/announcements/delete.go +++ b/internal/cli/announcements/delete.go @@ -31,7 +31,7 @@ func init() { func deleteAnnouncement(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) - if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete); err != nil { + if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/get.go b/internal/cli/announcements/get.go index 603996fb..28fa2739 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,9 @@ 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, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } 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/client/client.go b/internal/cli/client/client.go index eb8dc08d..62e63f7f 100644 --- a/internal/cli/client/client.go +++ b/internal/cli/client/client.go @@ -7,9 +7,9 @@ import ( "net/url" ) -func SendRequestAndReadResponse(url *url.URL, method string) ([]byte, error) { +func SendRequestAndReadResponse(url *url.URL, method string, body io.Reader) ([]byte, error) { client := http.Client{} - req, err := http.NewRequest(method, url.String(), nil) + req, err := http.NewRequest(method, url.String(), body) if err != nil { return nil, fmt.Errorf("failed to construct request: %w", err) } @@ -24,9 +24,9 @@ func SendRequestAndReadResponse(url *url.URL, method string) ([]byte, error) { return nil, fmt.Errorf("HTTP %s", res.Status) } - body, err := io.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return body, nil + return data, nil } diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index 48e049c3..544b820c 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -2,13 +2,13 @@ package officers 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,9 @@ func init() { func deleteOfficer(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - 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, http.MethodDelete); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } 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) } From a4f423c7ed2dd1390ef67925f1a03749a36952c2 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 11:57:12 -0800 Subject: [PATCH 06/32] use new function in announcements post --- internal/cli/announcements/post.go | 32 +++++++----------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index a1fa018f..1bef7772 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -3,8 +3,8 @@ package announcements import ( "encoding/json" "fmt" - "io" "net/http" + "os" "strings" "github.com/charmbracelet/huh" @@ -39,32 +39,14 @@ func postAnnouncement(cfg *config.Config) { return } - postURL := config.GetBaseURL(cfg).JoinPath("v1", "announcements") - client := http.Client{} - req, err := oauth.NewRequestWithAuth(http.MethodPost, postURL.String(), strings.NewReader(string(jsonPayload))) - if err != nil { - fmt.Println("Error: could not create request:", err) - return - } - - res, err := client.Do(req) - if err != nil { - fmt.Println("Error: could not send request:", err) - return - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - fmt.Println("Error: HTTP", res.Status) - return - } + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements") - 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, http.MethodDelete, + strings.NewReader(string(jsonPayload))); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } func postForm() (*dto.Announcement, error) { From 811167e713c77532854432e0f22e9bcc3cccdfc2 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:10:05 -0800 Subject: [PATCH 07/32] add godoc comment --- internal/cli/oauth/request_with_auth.go | 1 + 1 file changed, 1 insertion(+) 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) From 78b59d87653519dcdae83add9c71adc511f349d8 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:13:47 -0800 Subject: [PATCH 08/32] update announcements put --- internal/cli/announcements/put.go | 67 ++++++++++--------------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 14651e28..8d46ab9a 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -4,8 +4,8 @@ import ( "bytes" "encoding/json" "fmt" - "io" "net/http" + "os" "github.com/charmbracelet/huh" "github.com/spf13/cobra" @@ -32,29 +32,18 @@ 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) - return + // ----- Get announcement we want to update ----- + var oldPayload dbmodels.CreateAnnouncementParams + if body, err := client.SendRequestAndReadResponse(resourceUrl, http.MethodDelete, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + err = json.Unmarshal(body, &oldPayload) + if err != nil { + fmt.Fprintln(os.Stderr, "Error: failed to unmarshal response body:", err) + return + } } // ----- Update found announceement ----- @@ -66,36 +55,22 @@ func putAnnouncements(id string, cfg *config.Config) { } newPayload, err := putForm(id) if err != nil { - fmt.Println("Error:", err) - return - } - jsonPayload, err := json.Marshal(newPayload) - if err != nil { - fmt.Println("Error: failed to marshal data:", err) + fmt.Fprintln(os.Stderr, "Error:", err) return } - putRequest, err := oauth.NewRequestWithAuth(http.MethodPut, resourceURL.String(), bytes.NewBuffer(jsonPayload)) + b, err := json.Marshal(newPayload) if err != nil { - fmt.Println("Error: failed to contruct request:", err) + fmt.Fprintln(os.Stderr, "Error: failed to marshal data:", 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, http.MethodDelete, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } // TODO: Use DTO models instaad of dbmodels From 42e2830e297a97beb08f8a090ea76b4bccc6c031 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:15:10 -0800 Subject: [PATCH 09/32] use bytes.NewBuffer --- internal/cli/announcements/post.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index 1bef7772..b9e8fa4d 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -1,11 +1,11 @@ package announcements import ( + "bytes" "encoding/json" "fmt" "net/http" "os" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" @@ -27,22 +27,21 @@ var PostAnnouncement = &cobra.Command{ } func postAnnouncement(cfg *config.Config) { + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements") + payload, err := postForm() if err != nil { fmt.Println("Error:", err) return } - - jsonPayload, err := json.Marshal(payload) + b, err := json.Marshal(payload) if err != nil { fmt.Println("Error: could not marshal JSON:", err) return } - postUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements") - if body, err := client.SendRequestAndReadResponse(postUrl, http.MethodDelete, - strings.NewReader(string(jsonPayload))); err != nil { + bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) From 90c671b2809cbb9267ea0c965e70049cebe1aabe Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:23:03 -0800 Subject: [PATCH 10/32] fixup officers delete and get --- internal/cli/officers/delete.go | 2 +- internal/cli/officers/get.go | 79 +++------------------------------ 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index 544b820c..1cda740f 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -31,7 +31,7 @@ func init() { func deleteOfficer(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete); err != nil { + if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index dfa53b1b..2d44d158 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -2,14 +2,12 @@ package officers import ( "fmt" - "io" "net/http" - "net/url" "os" + "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" ) @@ -18,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) }, } @@ -73,29 +28,9 @@ func init() { func getOfficers(uuid string, cfg *config.Config) { getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, getUrl.String(), nil) - if err != nil { - fmt.Println("Error: failed to construct request:", err) - return - } - - res, err := client.Do(req) - if err != nil { - fmt.Println("Error: failed to send request:", 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 + if body, err := client.SendRequestAndReadResponse(getUrl, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } From 0d4bc932cd57a646d0d6a6b6fb6991232362f49e Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:24:12 -0800 Subject: [PATCH 11/32] rename officers.go to root.go --- internal/cli/officers/{officers.go => root.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/cli/officers/{officers.go => root.go} (100%) diff --git a/internal/cli/officers/officers.go b/internal/cli/officers/root.go similarity index 100% rename from internal/cli/officers/officers.go rename to internal/cli/officers/root.go From 4c0fdd18ca18decc38991a64b47cc43378142815 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 12:24:45 -0800 Subject: [PATCH 12/32] remove top-level menu for now --- internal/cli/officers/root.go | 44 ----------------------------------- 1 file changed, 44 deletions(-) diff --git a/internal/cli/officers/root.go b/internal/cli/officers/root.go index faee3b91..a732dac7 100644 --- a/internal/cli/officers/root.go +++ b/internal/cli/officers/root.go @@ -1,10 +1,6 @@ package officers import ( - "fmt" - "os" - - "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) @@ -27,43 +23,3 @@ func init() { 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() - } -} From afe9e10c1923c57f3dec7ff2c9d3a2ba06016e44 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 13:38:49 -0800 Subject: [PATCH 13/32] cleanup officer post --- internal/cli/officers/post.go | 287 +++------------------------------- 1 file changed, 22 insertions(+), 265 deletions(-) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index 83dc7976..f08098a3 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -1,21 +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" ) @@ -24,34 +21,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 +34,34 @@ 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 - } - - // picture - for { - if cf.picture { - break - } - - 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 - } - - payload.Picture = utils.StringtoNullString(string(scanner.Bytes())) - break - } - - // github - for { - if cf.github { - break - } +func postOfficer(cfg *config.Config) { + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers") - 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) + payload, err := postForm() if err != nil { - fmt.Println("error formating payload to json: ", err) - return + fmt.Fprintln(os.Stderr, "Error:", err) } - - postURL := baseURL.JoinPath("v1/board/officers/") - - // post payload - client := http.Client{} - req, err := oauth.NewRequestWithAuth(http.MethodPost, postURL.String(), strings.NewReader(string(jsonPayload))) + b, err := json.Marshal(payload) if err != nil { - fmt.Println("error with post: ", err) - return + fmt.Fprintln(os.Stderr, "Error: failed to marshal JSON:", err) } - res, err := client.Do(req) - if err != nil { - fmt.Println("error getting response", res) - return + if body, err := client.SendRequestAndReadResponse(postUrl, http.MethodPost, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + utils.PrettyPrintJSON(body) } - 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 - body, err := io.ReadAll(res.Body) - if err != nil { - fmt.Println("error reading body: ", err) - return + form := huh.NewForm() + if err = form.Run(); err != nil { + return nil, err } - fmt.Println(string(body)) + return &payload, nil } From 6da3afc48aac1198b499b0886306673d255fbe02 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 13:50:38 -0800 Subject: [PATCH 14/32] implement officer post form --- internal/cli/officers/post.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index f08098a3..e9734c63 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -13,6 +13,7 @@ import ( "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/forms" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -57,11 +58,41 @@ func postOfficer(cfg *config.Config) { func postForm() (*dbmodels.CreateOfficerParams, error) { var payload dbmodels.CreateOfficerParams var err error + var ( + picture string + github string + discord string + ) - form := huh.NewForm() + 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 } + // 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) + return &payload, nil } From c7efaa34df1a245f00eaae04ed678c4ac3d18b31 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 14:17:54 -0800 Subject: [PATCH 15/32] fixup officer put --- internal/cli/officers/put.go | 305 +++++++---------------------------- 1 file changed, 59 insertions(+), 246 deletions(-) diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index c620b2db..41f1aa84 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,85 @@ 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 - } +func putOfficer(id string, cfg *config.Config) { + url := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - 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 - } - - // 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 - } - if !ok { + // ----- Get officer we want to update + var oldPayload dbmodels.UpdateOfficerParams + if body, err := client.SendRequestAndReadResponse(url, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } 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(id) 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)) + b, err := json.Marshal(newPayload) if err != nil { - fmt.Println("Problem with PUT:", err) + fmt.Fprintln(os.Stderr, "Error: failed to marshal data:", err) return } - putResp, err := client.Do(putReq) - 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) - 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, http.MethodDelete, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } else { + utils.PrettyPrintJSON(body) } +} - fmt.Println(string(body)) +func putForm(uuid string) (*dbmodels.UpdateOfficerParams, error) { + var payload dbmodels.UpdateOfficerParams + var err error + var ( + picture string + github string + discord string + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Full Name"). + Value(&payload.FullName), + 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 = uuid + payload.Picture = utils.StringtoNullString(picture) + payload.Github = utils.StringtoNullString(github) + payload.Discord = utils.StringtoNullString(discord) + + return &payload, nil } From 8917fe2a46efc17b63a5330a4f0ce7072779f5ca Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 14:32:33 -0800 Subject: [PATCH 16/32] rm unused flags --- internal/cli/officers/root.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/cli/officers/root.go b/internal/cli/officers/root.go index a732dac7..11ce40e0 100644 --- a/internal/cli/officers/root.go +++ b/internal/cli/officers/root.go @@ -4,14 +4,6 @@ import ( "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.", From b1a12b6fe72bce41f0baf756c9928ae32e8f75dd Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 14:46:13 -0800 Subject: [PATCH 17/32] restore auth to SendRequestAndReadResponse --- internal/cli/announcements/delete.go | 2 +- internal/cli/announcements/post.go | 2 +- internal/cli/announcements/put.go | 4 ++-- internal/cli/client/client.go | 12 ++++++++++-- internal/cli/officers/delete.go | 2 +- internal/cli/officers/get.go | 2 +- internal/cli/officers/post.go | 2 +- internal/cli/officers/put.go | 4 ++-- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index 313bdfe9..e64b9ca1 100644 --- a/internal/cli/announcements/delete.go +++ b/internal/cli/announcements/delete.go @@ -31,7 +31,7 @@ func init() { func deleteAnnouncement(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) - if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index b9e8fa4d..2efcfd7f 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -40,7 +40,7 @@ func postAnnouncement(cfg *config.Config) { return } - if body, err := client.SendRequestAndReadResponse(postUrl, http.MethodDelete, + if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 8d46ab9a..ba7a4956 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -36,7 +36,7 @@ func putAnnouncements(id string, cfg *config.Config) { // ----- Get announcement we want to update ----- var oldPayload dbmodels.CreateAnnouncementParams - if body, err := client.SendRequestAndReadResponse(resourceUrl, http.MethodDelete, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(resourceUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { err = json.Unmarshal(body, &oldPayload) @@ -65,7 +65,7 @@ func putAnnouncements(id string, cfg *config.Config) { } // Update remote resource with new data - if body, err := client.SendRequestAndReadResponse(resourceUrl, http.MethodDelete, + if body, err := client.SendRequestAndReadResponse(resourceUrl, true, http.MethodPut, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go index 62e63f7f..8cd9c835 100644 --- a/internal/cli/client/client.go +++ b/internal/cli/client/client.go @@ -5,11 +5,19 @@ import ( "io" "net/http" "net/url" + + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" ) -func SendRequestAndReadResponse(url *url.URL, method string, body io.Reader) ([]byte, error) { +func SendRequestAndReadResponse(url *url.URL, enableAuth bool, method string, body io.Reader) ([]byte, error) { client := http.Client{} - req, err := http.NewRequest(method, url.String(), body) + 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) } diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index 1cda740f..ed169830 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -31,7 +31,7 @@ func init() { func deleteOfficer(id string, cfg *config.Config) { deleteUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) - if body, err := client.SendRequestAndReadResponse(deleteUrl, http.MethodDelete, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index 2d44d158..c6f146e2 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -28,7 +28,7 @@ func init() { func getOfficers(uuid string, cfg *config.Config) { getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) - if body, err := client.SendRequestAndReadResponse(getUrl, http.MethodGet, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index e9734c63..8c220d3f 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -47,7 +47,7 @@ func postOfficer(cfg *config.Config) { fmt.Fprintln(os.Stderr, "Error: failed to marshal JSON:", err) } - if body, err := client.SendRequestAndReadResponse(postUrl, http.MethodPost, + if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index 41f1aa84..1aacdc5c 100644 --- a/internal/cli/officers/put.go +++ b/internal/cli/officers/put.go @@ -36,7 +36,7 @@ func putOfficer(id string, cfg *config.Config) { // ----- Get officer we want to update var oldPayload dbmodels.UpdateOfficerParams - if body, err := client.SendRequestAndReadResponse(url, http.MethodGet, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(url, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { if err := json.Unmarshal(body, &oldPayload); err != nil { @@ -59,7 +59,7 @@ func putOfficer(id string, cfg *config.Config) { } // Update remote resource with new data - if body, err := client.SendRequestAndReadResponse(url, http.MethodDelete, + if body, err := client.SendRequestAndReadResponse(url, true, http.MethodPut, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { From 273f77f18af53a70ba3168512f4cb0622bb8c3da Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 14:46:48 -0800 Subject: [PATCH 18/32] change usage string --- internal/cli/officers/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/officers/root.go b/internal/cli/officers/root.go index 11ce40e0..1cc29605 100644 --- a/internal/cli/officers/root.go +++ b/internal/cli/officers/root.go @@ -5,7 +5,7 @@ import ( ) var CLIOfficers = &cobra.Command{ - Use: "officers HEADER", + Use: "officers", Short: "A command to manage officers.", } From 3acf2c59d236336b8aec7055e39d5d210239a210 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:13:50 -0800 Subject: [PATCH 19/32] fixes --- internal/cli/announcements/get.go | 2 +- internal/cli/announcements/put.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/cli/announcements/get.go b/internal/cli/announcements/get.go index 28fa2739..ee426bc1 100644 --- a/internal/cli/announcements/get.go +++ b/internal/cli/announcements/get.go @@ -30,7 +30,7 @@ func init() { func getAnnouncement(uuid string, cfg *config.Config) { getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) - if body, err := client.SendRequestAndReadResponse(getUrl, http.MethodGet, nil); err != nil { + if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index ba7a4956..3f092912 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -78,7 +78,7 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { var payload dto.UpdateAnnouncement var err error var ( - visibilityStr string + visibilityStr string = "poop" announceAtStr string channelIDStr string messageIDStr string @@ -106,7 +106,6 @@ 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.Visibility = &visibilityStr if announceAtStr != "" { From 6bf04fb39563025791f2443d70fe808186eebe82 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:26:06 -0800 Subject: [PATCH 20/32] put old values in update form --- internal/cli/announcements/put.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 3f092912..7254c68d 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -78,10 +78,10 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { var payload dto.UpdateAnnouncement var err error var ( - visibilityStr string = "poop" - announceAtStr string - channelIDStr string - messageIDStr string + visibilityStr string = oldPayload.Visibility + announceAtStr string = fmt.Sprint(oldPayload.AnnounceAt) + channelIDStr string = oldPayload.DiscordChannelID.String + messageIDStr string = oldPayload.DiscordMessageID.String ) form := huh.NewForm( huh.NewGroup( From 9b0bb7e3fbaa28ef81c5376390a6f144478e8866 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:30:08 -0800 Subject: [PATCH 21/32] use default values in officer update --- internal/cli/officers/put.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index 1aacdc5c..f957ccea 100644 --- a/internal/cli/officers/put.go +++ b/internal/cli/officers/put.go @@ -35,7 +35,7 @@ func putOfficer(id string, cfg *config.Config) { url := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", id) // ----- Get officer we want to update - var oldPayload dbmodels.UpdateOfficerParams + var oldPayload dbmodels.CreateOfficerParams if body, err := client.SendRequestAndReadResponse(url, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { @@ -47,7 +47,7 @@ func putOfficer(id string, cfg *config.Config) { // ----- Update found officer ----- // Read new data - newPayload, err := putForm(id) + newPayload, err := putForm(&oldPayload) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) return @@ -67,20 +67,21 @@ func putOfficer(id string, cfg *config.Config) { } } -func putForm(uuid string) (*dbmodels.UpdateOfficerParams, error) { +func putForm(oldPayload *dbmodels.CreateOfficerParams) (*dbmodels.UpdateOfficerParams, error) { var payload dbmodels.UpdateOfficerParams var err error var ( - picture string - github string - discord string + 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(&payload.FullName), + Value(&name), huh.NewInput(). Title("Picture URL"). Value(&picture), @@ -96,7 +97,8 @@ func putForm(uuid string) (*dbmodels.UpdateOfficerParams, error) { return nil, err } - payload.Uuid = uuid + payload.Uuid = oldPayload.Uuid + payload.FullName = name payload.Picture = utils.StringtoNullString(picture) payload.Github = utils.StringtoNullString(github) payload.Discord = utils.StringtoNullString(discord) From e8fb875e02250a219d08915198b1bccce4b24797 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:36:09 -0800 Subject: [PATCH 22/32] fix bad route in officers get --- internal/cli/officers/get.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index c6f146e2..a9950514 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -26,7 +26,7 @@ func init() { } func getOfficers(uuid string, cfg *config.Config) { - getUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", uuid) + getUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers", uuid) if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) From 6ec4b51610c90198734bf1cf994091afd527fdf7 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:52:33 -0800 Subject: [PATCH 23/32] rm default value for announnceAt --- internal/cli/announcements/put.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 7254c68d..2b133ae7 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -79,7 +79,7 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { var err error var ( visibilityStr string = oldPayload.Visibility - announceAtStr string = fmt.Sprint(oldPayload.AnnounceAt) + announceAtStr string channelIDStr string = oldPayload.DiscordChannelID.String messageIDStr string = oldPayload.DiscordMessageID.String ) From 636115b335fa3cae116f59c6862d55ca6099d928 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 15:54:44 -0800 Subject: [PATCH 24/32] fix but in announcemnet put form --- internal/cli/announcements/put.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 2b133ae7..dea79b5b 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -106,6 +106,7 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { return nil, err } + payload.Uuid = oldPayload.Uuid // HACK: These conversions won't be necessary once we start using DTO models here payload.Visibility = &visibilityStr if announceAtStr != "" { From d7f5e5965a63fc83845ad15bb31778b7d43a05ed Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 16:15:16 -0800 Subject: [PATCH 25/32] ouput to stderr --- internal/cli/announcements/post.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index 2efcfd7f..38a41665 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -31,12 +31,12 @@ func postAnnouncement(cfg *config.Config) { payload, err := postForm() if err != nil { - fmt.Println("Error:", err) + fmt.Fprintln(os.Stderr, "Error:", err) return } b, err := json.Marshal(payload) if err != nil { - fmt.Println("Error: could not marshal JSON:", err) + fmt.Fprintln(os.Stderr, "Error: could not marshal JSON:", err) return } From 443e24bd47d136fd8daedcea34f3242667ca3b3f Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 16:35:38 -0800 Subject: [PATCH 26/32] error handling fixes --- internal/cli/announcements/delete.go | 3 +++ internal/cli/announcements/get.go | 3 +++ internal/cli/announcements/post.go | 6 +++++- internal/cli/announcements/put.go | 4 ++++ internal/cli/client/client.go | 7 +++---- internal/cli/officers/delete.go | 3 +++ internal/cli/officers/get.go | 3 +++ internal/cli/officers/post.go | 3 +++ internal/cli/officers/put.go | 7 +++++++ 9 files changed, 34 insertions(+), 5 deletions(-) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index e64b9ca1..31051411 100644 --- a/internal/cli/announcements/delete.go +++ b/internal/cli/announcements/delete.go @@ -33,6 +33,9 @@ func deleteAnnouncement(id string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } diff --git a/internal/cli/announcements/get.go b/internal/cli/announcements/get.go index ee426bc1..05cf9992 100644 --- a/internal/cli/announcements/get.go +++ b/internal/cli/announcements/get.go @@ -32,6 +32,9 @@ func getAnnouncement(uuid string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index 38a41665..b8d7c707 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -43,6 +43,10 @@ func postAnnouncement(cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + // NOTE: This isn't going to stderr. Should probably fix that at some point + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } @@ -92,5 +96,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 dea79b5b..99abc482 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -38,6 +38,10 @@ func putAnnouncements(id string, cfg *config.Config) { var oldPayload dbmodels.CreateAnnouncementParams 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 { diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go index 8cd9c835..8fe5fe5a 100644 --- a/internal/cli/client/client.go +++ b/internal/cli/client/client.go @@ -28,13 +28,12 @@ func SendRequestAndReadResponse(url *url.URL, enableAuth bool, method string, bo } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP %s", res.Status) - } - 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/officers/delete.go b/internal/cli/officers/delete.go index ed169830..2c824c09 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -33,6 +33,9 @@ func deleteOfficer(id string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index a9950514..a3e06717 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -30,6 +30,9 @@ func getOfficers(uuid string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index 8c220d3f..92d3c139 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -50,6 +50,9 @@ func postOfficer(cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSON(body) + } } else { utils.PrettyPrintJSON(body) } diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index f957ccea..525bd0a8 100644 --- a/internal/cli/officers/put.go +++ b/internal/cli/officers/put.go @@ -38,6 +38,10 @@ func putOfficer(id string, cfg *config.Config) { 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.PrettyPrintJSON(body) + } + return } else { if err := json.Unmarshal(body, &oldPayload); err != nil { fmt.Fprintln(os.Stderr, "Error: failed to unmarshal response body:", err) @@ -62,6 +66,9 @@ func putOfficer(id string, cfg *config.Config) { 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) } From 72f0412f61e97cccb0e523e4c501c5e8e0d78544 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Sat, 28 Feb 2026 16:41:39 -0800 Subject: [PATCH 27/32] add logging message --- internal/api/handlers/announcement.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/api/handlers/announcement.go b/internal/api/handlers/announcement.go index 98caf443..0fd60530 100644 --- a/internal/api/handlers/announcement.go +++ b/internal/api/handlers/announcement.go @@ -13,6 +13,7 @@ import ( "github.com/acmcsufoss/api.acmcsuf.com/internal/api/services" "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/gin-gonic/gin" + "log" ) type AnnouncementHandler struct { @@ -131,7 +132,8 @@ func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) { fmt.Println("DTO ->", params, "\nDBMODEL->", dbParams) // TODO: error out if required fields aren't provided - if err := h.announcementService.Create(ctx, dbParams); err != nil { + if err := h.announcementService.Create(ctx, params); err != nil { + log.Printf("Failed to create announcement: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to create announcement", }) From 25a1c69bdced4c208398d2937f1a501ff9b71a95 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Fri, 6 Mar 2026 16:46:41 -0800 Subject: [PATCH 28/32] fixes after rebase --- internal/api/handlers/announcement.go | 5 +---- internal/cli/announcements/post.go | 2 +- internal/cli/announcements/put.go | 23 +++++++---------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/internal/api/handlers/announcement.go b/internal/api/handlers/announcement.go index 0fd60530..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" @@ -13,7 +12,6 @@ import ( "github.com/acmcsufoss/api.acmcsuf.com/internal/api/services" "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/gin-gonic/gin" - "log" ) type AnnouncementHandler struct { @@ -130,9 +128,8 @@ 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, params); err != nil { + 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/post.go b/internal/cli/announcements/post.go index b8d7c707..6da2a43f 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -10,9 +10,9 @@ import ( "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" ) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 99abc482..238b6a74 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -10,8 +10,8 @@ import ( "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" ) @@ -35,7 +35,7 @@ func putAnnouncements(id string, cfg *config.Config) { resourceUrl := config.GetBaseURL(cfg).JoinPath("v1", "announcements", id) // ----- Get announcement we want to update ----- - var oldPayload dbmodels.CreateAnnouncementParams + 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 { @@ -51,13 +51,7 @@ func putAnnouncements(id string, cfg *config.Config) { } // ----- Update found announceement ----- - var oldPayload dto.UpdateAnnouncement - err = json.Unmarshal(body, &oldPayload) - if err != nil { - fmt.Println("Error: failed to unmarshal response body:", err) - return - } - newPayload, err := putForm(id) + newPayload, err := putForm(&oldPayload) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) return @@ -77,15 +71,14 @@ func putAnnouncements(id string, cfg *config.Config) { } } -// 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 = oldPayload.Visibility - announceAtStr string - channelIDStr string = oldPayload.DiscordChannelID.String - messageIDStr string = oldPayload.DiscordMessageID.String + 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( @@ -111,7 +104,6 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { } payload.Uuid = oldPayload.Uuid - // HACK: These conversions won't be necessary once we start using DTO models here payload.Visibility = &visibilityStr if announceAtStr != "" { timestamp, err := utils.ByteSlicetoUnix([]byte(announceAtStr)) @@ -120,7 +112,6 @@ func putForm(uuid string) (*dto.UpdateAnnouncement, error) { } payload.AnnounceAt = ×tamp } - payload.DiscordChannelID = &channelIDStr payload.DiscordMessageID = &messageIDStr From 65945a3b3a8ffad3a8f2730aace68078a5e1e45b Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 9 Mar 2026 08:20:30 -0700 Subject: [PATCH 29/32] exit on unrecoverable errors in cli officer post --- internal/cli/officers/post.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index 92d3c139..e4d15e01 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -39,13 +39,9 @@ func postOfficer(cfg *config.Config) { postUrl := config.GetBaseURL(cfg).JoinPath("v1", "board", "officers") payload, err := postForm() - if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) - } + cobra.CheckErr(err) b, err := json.Marshal(payload) - if err != nil { - fmt.Fprintln(os.Stderr, "Error: failed to marshal JSON:", err) - } + cobra.CheckErr(fmt.Sprintf("failed to marshal JSON: %v", err)) if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil { From 938405eca11b1e6aa85c957d77e8a3de9b1a0bf6 Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 9 Mar 2026 08:25:24 -0700 Subject: [PATCH 30/32] fix typo in comment --- internal/cli/announcements/put.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index 238b6a74..c3d0fb4b 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -50,7 +50,7 @@ func putAnnouncements(id string, cfg *config.Config) { } } - // ----- Update found announceement ----- + // ----- Update found announcement ----- newPayload, err := putForm(&oldPayload) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) From e70d8924b772571d7b438435c2fd2dd2f441da21 Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 9 Mar 2026 08:31:16 -0700 Subject: [PATCH 31/32] print non-nil json body on err --- internal/cli/announcements/delete.go | 2 +- internal/cli/announcements/get.go | 2 +- internal/cli/announcements/post.go | 3 +-- internal/cli/announcements/put.go | 3 +++ internal/cli/officers/delete.go | 2 +- internal/cli/officers/get.go | 2 +- internal/cli/officers/post.go | 2 +- internal/cli/officers/put.go | 2 +- utils/output.go | 7 +++++++ 9 files changed, 17 insertions(+), 8 deletions(-) diff --git a/internal/cli/announcements/delete.go b/internal/cli/announcements/delete.go index 31051411..7da48e49 100644 --- a/internal/cli/announcements/delete.go +++ b/internal/cli/announcements/delete.go @@ -34,7 +34,7 @@ func deleteAnnouncement(id string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/get.go b/internal/cli/announcements/get.go index 05cf9992..71e0ac86 100644 --- a/internal/cli/announcements/get.go +++ b/internal/cli/announcements/get.go @@ -33,7 +33,7 @@ func getAnnouncement(uuid string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index 6da2a43f..02b95f47 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -44,8 +44,7 @@ func postAnnouncement(cfg *config.Config) { bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - // NOTE: This isn't going to stderr. Should probably fix that at some point - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index c3d0fb4b..1ff2f33c 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -66,6 +66,9 @@ func putAnnouncements(id string, cfg *config.Config) { 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) } diff --git a/internal/cli/officers/delete.go b/internal/cli/officers/delete.go index 2c824c09..caac2586 100644 --- a/internal/cli/officers/delete.go +++ b/internal/cli/officers/delete.go @@ -34,7 +34,7 @@ func deleteOfficer(id string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(deleteUrl, true, http.MethodDelete, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/get.go b/internal/cli/officers/get.go index a3e06717..5b3ac40c 100644 --- a/internal/cli/officers/get.go +++ b/internal/cli/officers/get.go @@ -31,7 +31,7 @@ func getOfficers(uuid string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index e4d15e01..1592ba09 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -47,7 +47,7 @@ func postOfficer(cfg *config.Config) { bytes.NewBuffer(b)); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } } else { utils.PrettyPrintJSON(body) diff --git a/internal/cli/officers/put.go b/internal/cli/officers/put.go index 525bd0a8..3a77cb84 100644 --- a/internal/cli/officers/put.go +++ b/internal/cli/officers/put.go @@ -39,7 +39,7 @@ func putOfficer(id string, cfg *config.Config) { if body, err := client.SendRequestAndReadResponse(url, false, http.MethodGet, nil); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) if body != nil { - utils.PrettyPrintJSON(body) + utils.PrettyPrintJSONErr(body) } return } else { 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)) +} From df98c020680a31d06170e8ba3394a99c28280c01 Mon Sep 17 00:00:00 2001 From: Josh Holman Date: Mon, 9 Mar 2026 20:23:52 -0700 Subject: [PATCH 32/32] fix: CheckErr can now detect if err is nil --- internal/cli/officers/post.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/officers/post.go b/internal/cli/officers/post.go index 1592ba09..2f2814ca 100644 --- a/internal/cli/officers/post.go +++ b/internal/cli/officers/post.go @@ -41,7 +41,7 @@ func postOfficer(cfg *config.Config) { payload, err := postForm() cobra.CheckErr(err) b, err := json.Marshal(payload) - cobra.CheckErr(fmt.Sprintf("failed to marshal JSON: %v", err)) + cobra.CheckErr(err) if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, bytes.NewBuffer(b)); err != nil {