Skip to content

Commit e0df5c7

Browse files
authored
Merge pull request #13 from major-technology/3
Refactor the hiearchy of commands
2 parents de263e2 + bfe6553 commit e0df5c7

26 files changed

Lines changed: 1396 additions & 894 deletions

clients/api/client.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"time"
10+
11+
mjrToken "github.com/major-technology/cli/clients/token"
12+
"github.com/pkg/errors"
13+
)
14+
15+
// Client represents an API client for making authenticated requests
16+
type Client struct {
17+
baseURL string
18+
httpClient *http.Client
19+
}
20+
21+
// NewClient creates a new API client with the provided base URL and optional token
22+
func NewClient(baseURL string) *Client {
23+
return &Client{
24+
baseURL: baseURL,
25+
httpClient: &http.Client{
26+
Timeout: 30 * time.Second,
27+
},
28+
}
29+
}
30+
31+
// doRequestWithoutAuth is a helper method to make unauthenticated HTTP requests
32+
func (c *Client) doRequestWithoutAuth(method, path string, body interface{}, response interface{}) error {
33+
return c.doRequestInternal(method, path, body, response, false)
34+
}
35+
36+
// doRequest is a helper method to make HTTP requests with common error handling
37+
// It automatically gets the token from the keyring for each request
38+
func (c *Client) doRequest(method, path string, body interface{}, response interface{}) error {
39+
return c.doRequestInternal(method, path, body, response, true)
40+
}
41+
42+
// doRequestInternal is the internal implementation for making HTTP requests
43+
func (c *Client) doRequestInternal(method, path string, body interface{}, response interface{}, requireAuth bool) error {
44+
var token string
45+
if requireAuth {
46+
// Get token from keyring for this request
47+
t, err := mjrToken.GetToken()
48+
if err != nil {
49+
return &NoTokenError{OriginalError: err}
50+
}
51+
token = t
52+
}
53+
54+
var reqBody io.Reader
55+
if body != nil {
56+
jsonBody, err := json.Marshal(body)
57+
if err != nil {
58+
return errors.Wrap(err, "failed to marshal request body")
59+
}
60+
reqBody = bytes.NewBuffer(jsonBody)
61+
}
62+
63+
url := c.baseURL + path
64+
req, err := http.NewRequest(method, url, reqBody)
65+
if err != nil {
66+
return errors.Wrap(err, "failed to create request")
67+
}
68+
69+
req.Header.Set("Content-Type", "application/json")
70+
if token != "" {
71+
req.Header.Set("Authorization", "Bearer "+token)
72+
}
73+
74+
resp, err := c.httpClient.Do(req)
75+
if err != nil {
76+
return errors.Wrap(err, "failed to make request")
77+
}
78+
defer resp.Body.Close()
79+
80+
respBody, err := io.ReadAll(resp.Body)
81+
if err != nil {
82+
return errors.Wrap(err, "failed to read response")
83+
}
84+
85+
// Handle error responses
86+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
87+
var errResp ErrorResponse
88+
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Message != "" {
89+
return &APIError{
90+
StatusCode: resp.StatusCode,
91+
Message: errResp.Message,
92+
ErrorType: errResp.Error,
93+
}
94+
}
95+
return &APIError{
96+
StatusCode: resp.StatusCode,
97+
Message: string(respBody),
98+
}
99+
}
100+
101+
// Parse successful response if a response struct is provided
102+
if response != nil {
103+
if err := json.Unmarshal(respBody, response); err != nil {
104+
return errors.Wrap(err, "failed to parse response")
105+
}
106+
}
107+
108+
return nil
109+
}
110+
111+
// --- Authentication / User endpoints ---
112+
113+
// StartLogin initiates the device flow login process
114+
func (c *Client) StartLogin() (*LoginStartResponse, error) {
115+
var resp LoginStartResponse
116+
err := c.doRequestWithoutAuth("POST", "/login/start", map[string]interface{}{}, &resp)
117+
if err != nil {
118+
return nil, err
119+
}
120+
return &resp, nil
121+
}
122+
123+
// PollLogin polls the login endpoint to check if the user has authorized the device
124+
func (c *Client) PollLogin(deviceCode string) (*LoginPollResponse, error) {
125+
req := LoginPollRequest{DeviceCode: deviceCode}
126+
127+
var resp LoginPollResponse
128+
err := c.doRequestWithoutAuth("POST", "/login/poll", req, &resp)
129+
if err != nil {
130+
// Check if it's a bad request (expired/invalid codes)
131+
if IsBadRequest(err) {
132+
return nil, fmt.Errorf("invalid or expired device code")
133+
}
134+
return nil, err
135+
}
136+
137+
return &resp, nil
138+
}
139+
140+
// VerifyToken verifies the current token and returns user information
141+
func (c *Client) VerifyToken() (*VerifyTokenResponse, error) {
142+
var resp VerifyTokenResponse
143+
err := c.doRequest("GET", "/verify", nil, &resp)
144+
if err != nil {
145+
if IsUnauthorized(err) {
146+
return nil, fmt.Errorf("invalid or expired token - please login again")
147+
}
148+
return nil, err
149+
}
150+
151+
if !resp.Active {
152+
return nil, fmt.Errorf("token is not active - please login again")
153+
}
154+
155+
return &resp, nil
156+
}
157+
158+
// Logout revokes the current token
159+
func (c *Client) Logout() error {
160+
return c.doRequest("POST", "/logout", map[string]interface{}{}, nil)
161+
}
162+
163+
// --- Organization endpoints ---
164+
165+
// GetOrganizations retrieves the list of organizations for the authenticated user
166+
func (c *Client) GetOrganizations() (*OrganizationsResponse, error) {
167+
var resp OrganizationsResponse
168+
err := c.doRequest("GET", "/organizations", nil, &resp)
169+
if err != nil {
170+
return nil, err
171+
}
172+
return &resp, nil
173+
}
174+
175+
// --- Application endpoints ---
176+
177+
// CreateApplication creates a new application with a GitHub repository
178+
func (c *Client) CreateApplication(name, description, organizationID string) (*CreateApplicationResponse, error) {
179+
req := CreateApplicationRequest{
180+
Name: name,
181+
Description: description,
182+
OrganizationID: organizationID,
183+
}
184+
185+
var resp CreateApplicationResponse
186+
err := c.doRequest("POST", "/applications", req, &resp)
187+
if err != nil {
188+
return nil, err
189+
}
190+
return &resp, nil
191+
}
192+
193+
// GetApplicationByRepo retrieves an application by its repository owner and name
194+
func (c *Client) GetApplicationByRepo(owner, repo string) (*GetApplicationByRepoResponse, error) {
195+
req := GetApplicationByRepoRequest{
196+
Owner: owner,
197+
Repo: repo,
198+
}
199+
200+
var resp GetApplicationByRepoResponse
201+
err := c.doRequest("POST", "/application/from-repo", req, &resp)
202+
if err != nil {
203+
return nil, err
204+
}
205+
return &resp, nil
206+
}
207+
208+
// GetApplicationEnv retrieves environment variables for an application
209+
func (c *Client) GetApplicationEnv(organizationID, applicationID string) (map[string]string, error) {
210+
req := GetApplicationEnvRequest{
211+
OrganizationID: organizationID,
212+
ApplicationID: applicationID,
213+
}
214+
215+
var resp GetApplicationEnvResponse
216+
err := c.doRequest("POST", "/application/env", req, &resp)
217+
if err != nil {
218+
return nil, err
219+
}
220+
return resp.EnvVars, nil
221+
}

clients/api/errors.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// ErrorResponse represents an error response from the API
12+
type ErrorResponse struct {
13+
Error string `json:"error"`
14+
Message string `json:"message"`
15+
}
16+
17+
// APIError represents an API error with status code and message
18+
type APIError struct {
19+
StatusCode int
20+
Message string
21+
ErrorType string
22+
}
23+
24+
func (e *APIError) Error() string {
25+
if e.ErrorType != "" {
26+
return fmt.Sprintf("API error (status %d): %s - %s", e.StatusCode, e.ErrorType, e.Message)
27+
}
28+
return fmt.Sprintf("API error (status %d): %s", e.StatusCode, e.Message)
29+
}
30+
31+
// NoTokenError represents an error when no token is available
32+
type NoTokenError struct {
33+
OriginalError error
34+
}
35+
36+
func (e *NoTokenError) Error() string {
37+
return fmt.Sprintf("not logged in: %v", e.OriginalError)
38+
}
39+
40+
// IsUnauthorized checks if the error is an unauthorized error
41+
func IsUnauthorized(err error) bool {
42+
var apiErr *APIError
43+
if errors.As(err, &apiErr) {
44+
return apiErr.StatusCode == http.StatusUnauthorized
45+
}
46+
return false
47+
}
48+
49+
// IsNotFound checks if the error is a not found error
50+
func IsNotFound(err error) bool {
51+
var apiErr *APIError
52+
if errors.As(err, &apiErr) {
53+
return apiErr.StatusCode == http.StatusNotFound
54+
}
55+
return false
56+
}
57+
58+
// IsBadRequest checks if the error is a bad request error
59+
func IsBadRequest(err error) bool {
60+
var apiErr *APIError
61+
if errors.As(err, &apiErr) {
62+
return apiErr.StatusCode == http.StatusBadRequest
63+
}
64+
return false
65+
}
66+
67+
// IsNoToken checks if the error is a no token error
68+
func IsNoToken(err error) bool {
69+
var noTokenErr *NoTokenError
70+
return errors.As(err, &noTokenErr)
71+
}
72+
73+
// CheckErr checks for errors and prints appropriate messages using the command's output
74+
// Returns true if no error (ok to continue), false if there was an error
75+
func CheckErr(cmd *cobra.Command, err error) bool {
76+
if err == nil {
77+
return true
78+
}
79+
80+
// Check if it's a no token error
81+
if IsNoToken(err) {
82+
cmd.Println("Error: Not logged in. Please run 'major user login' first.")
83+
return false
84+
}
85+
86+
// Check if it's an API error
87+
var apiErr *APIError
88+
if errors.As(err, &apiErr) {
89+
cmd.Printf("Error: Failed to make request (status %d)\n", apiErr.StatusCode)
90+
if apiErr.ErrorType != "" {
91+
cmd.Printf(" Type: %s\n", apiErr.ErrorType)
92+
}
93+
cmd.Printf(" Message: %s\n", apiErr.Message)
94+
return false
95+
}
96+
97+
// Generic error
98+
cmd.Printf("Error: %v\n", err)
99+
return false
100+
}

0 commit comments

Comments
 (0)