Skip to content

Commit 52afd6e

Browse files
committed
initial api client
1 parent 497402d commit 52afd6e

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed

api/client/client.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package client
2+
3+
import (
4+
"net/http"
5+
"net/mail"
6+
"net/url"
7+
"time"
8+
9+
"github.com/simpleauthlink/authapi/api"
10+
"github.com/simpleauthlink/authapi/api/io"
11+
"github.com/simpleauthlink/authapi/token"
12+
)
13+
14+
// DefaultClientTimeout is the default timeout for the client requests. It can
15+
// be overridden in the Config struct.
16+
const DefaultClientTimeout = 30 * time.Second
17+
18+
// DefaultAPIEndpoint is the default API endpoint for the client. It can be
19+
// overridden in the Config struct.
20+
const DefaultAPIEndpoint = "https://api.simpleauth.link"
21+
22+
// Config holds the configuration for the client. It includes the API endpoint,
23+
// application ID, application secret, and the timeout for requests. All fields
24+
// are optional, but some of them are required for certain operations. If no
25+
// API endpoint or timeout is provided, the default values will be used.
26+
type Config struct {
27+
APIEndpoint string
28+
AppID *token.AppID
29+
AppSecret string
30+
Timeout time.Duration
31+
}
32+
33+
// init method initializes the Config struct with default values if they are
34+
// not set.
35+
func (c *Config) init() {
36+
if c.APIEndpoint == "" {
37+
c.APIEndpoint = DefaultAPIEndpoint
38+
}
39+
if c.Timeout == 0 {
40+
c.Timeout = DefaultClientTimeout
41+
}
42+
}
43+
44+
// Validate method checks if the configuration is valid. It returns an error if
45+
// the configuration is invalid. It receives a boolean parameter to indicate if
46+
// it should validate or not the application ID and secret. If the parameter is
47+
// true, it will check if the AppID is not nil and the AppSecret is not empty.
48+
func (c *Config) Validate(requireApp bool) error {
49+
if c.APIEndpoint == "" {
50+
return ErrInvalidAPIEndpoint
51+
}
52+
if c.Timeout <= 0 {
53+
return ErrInvalidTimeout
54+
}
55+
if requireApp {
56+
if c.AppID == nil {
57+
return ErrInvalidAppID
58+
}
59+
if c.AppSecret == "" {
60+
return ErrInvalidAppSecret
61+
}
62+
}
63+
return nil
64+
}
65+
66+
// Client is the main struct for the API client. It holds the configuration and
67+
// allows to make requests to the API. It is initialized with a Config struct
68+
// that contains the API endpoint, application ID, application secret, and the
69+
// timeout for requests. The client can be used to create new application IDs,
70+
// request tokens, and verify tokens. It also checks if the API is reachable
71+
// during initialization.
72+
type Client struct {
73+
config *Config
74+
httpClient *http.Client
75+
}
76+
77+
// New method creates a new Client instance with the provided configuration.
78+
// It initializes the configuration with default values if not set, checks if
79+
// the API is reachable, and returns a new Client instance. If the API is not
80+
// reachable, it returns an error.
81+
func New(cfg *Config) (*Client, error) {
82+
if cfg == nil {
83+
cfg = &Config{}
84+
}
85+
// initialize the configuration with default values if not set
86+
cfg.init()
87+
// create a new HTTP client
88+
httpClient := &http.Client{Timeout: cfg.Timeout}
89+
// check if the api is reachable
90+
r, err := req[any](cfg, http.MethodGet, api.HealthCheckPath, nil)
91+
if err != nil {
92+
return nil, err
93+
}
94+
// make a request to the health check endpoint
95+
resp, err := httpClient.Do(r)
96+
if err != nil {
97+
return nil, err
98+
}
99+
defer resp.Body.Close()
100+
// check if the response is OK
101+
if resp.StatusCode != http.StatusOK {
102+
return nil, ErrAPIUnavailable
103+
}
104+
// if everything is fine, return a new client instance
105+
return &Client{
106+
config: cfg,
107+
httpClient: httpClient,
108+
}, nil
109+
}
110+
111+
// NewAppID method creates a new AppID with the provided name, redirect URI,
112+
// secret, and session duration. It requires the client configuration to be
113+
// valid, including the application ID and secret. The name, redirect URI,
114+
// secret, and session duration must be valid. If the configuration is not
115+
// valid, it returns an error. If the request is successful, it returns a new
116+
// AppID instance with the generated ID. If the request fails, it returns an
117+
// error.
118+
func (c *Client) NewAppID(name, redirectURI, secret string, sessionDuration time.Duration) (*token.AppID, error) {
119+
// validate the configuration before making the request, for this operation
120+
// the app ID and secret are not required
121+
if err := c.config.Validate(false); err != nil {
122+
return nil, err
123+
}
124+
// check if the parameters are valid
125+
if name == "" {
126+
return nil, ErrInvalidAppName
127+
} else if redirectURI == "" {
128+
return nil, ErrInvalidAppRedirectURI
129+
} else if secret == "" {
130+
return nil, ErrInvalidAppSecret
131+
} else if sessionDuration <= 0 {
132+
return nil, ErrInvalidAppSessionDuration
133+
}
134+
// create the request data
135+
data := &api.AppIDRequest{
136+
Name: name,
137+
RedirectURL: redirectURI,
138+
Secret: secret,
139+
Duration: sessionDuration.String(),
140+
}
141+
// generate the request
142+
req, err := req(c.config, http.MethodPost, api.AppsPath, data)
143+
if err != nil {
144+
return nil, err
145+
}
146+
// make the request to the API
147+
res, err := c.httpClient.Do(req)
148+
if err != nil {
149+
return nil, err
150+
}
151+
// read the response data, it also checks if the response is OK and closes
152+
// the response body
153+
resData, err := new(io.Response[api.AppIDResponse]).Read(res)
154+
if err != nil {
155+
return nil, err
156+
}
157+
// parse and return the AppID
158+
return new(token.AppID).SetString(resData.ID), nil
159+
}
160+
161+
// SetupAppID sets the application ID and secret in the client configuration.
162+
func (c *Client) SetupAppID(appID *token.AppID, appSecret string) {
163+
c.config.AppID = appID
164+
c.config.AppSecret = appSecret
165+
}
166+
167+
// RequestToken method requests a new token for the given email address. It
168+
// requires the client configuration to be valid, including the application
169+
// ID and secret. The email address must be a valid email format.
170+
func (c *Client) RequestToken(email string) error {
171+
// validate the configuration before making the request, for this operation
172+
// the app ID and secret are required
173+
if err := c.config.Validate(true); err != nil {
174+
return err
175+
}
176+
// check if the email is valid
177+
if _, err := mail.ParseAddress(email); err != nil {
178+
return ErrInvalidEmailAddress
179+
}
180+
// create the request with the email data
181+
data := &api.TokenRequest{Email: email}
182+
req, err := req(c.config, http.MethodPost, api.TokensPath, data)
183+
if err != nil {
184+
return err
185+
}
186+
// make the request to the API
187+
res, err := c.httpClient.Do(req)
188+
if err != nil {
189+
return err
190+
}
191+
defer res.Body.Close()
192+
// check if the response is OK
193+
if res.StatusCode != http.StatusOK {
194+
return ErrRequestToken
195+
}
196+
return nil
197+
}
198+
199+
// VerifyToken method verifies the provided token for the given email address.
200+
// It requires the client configuration to be valid, including the application
201+
// ID and secret. The token must be valid, and the email address must be a
202+
// valid email format. If the token is valid, it returns true and the
203+
// expiration time. If the token is invalid or the email address is not valid,
204+
// it returns false. If the request fails, it also returns an error.
205+
func (c *Client) VerifyToken(token *token.Token, email string) (bool, time.Time, error) {
206+
// validate the configuration before making the request, for this operation
207+
// the app ID and secret are required
208+
if err := c.config.Validate(true); err != nil {
209+
return false, time.Time{}, err
210+
}
211+
// check if the token is valid
212+
if token == nil {
213+
return false, time.Time{}, ErrInvalidToken
214+
}
215+
// check if the email is valid
216+
if _, err := mail.ParseAddress(email); err != nil {
217+
return false, time.Time{}, ErrInvalidEmailAddress
218+
}
219+
// create the request with the token and email data
220+
data := &api.TokenStatusRequest{
221+
Token: token.String(),
222+
Email: email,
223+
}
224+
req, err := req(c.config, http.MethodPut, api.TokensPath, data)
225+
if err != nil {
226+
return false, time.Time{}, err
227+
}
228+
// make the request to the API
229+
res, err := c.httpClient.Do(req)
230+
if err != nil {
231+
return false, time.Time{}, err
232+
}
233+
// decode the response data, it also checks if the response is OK and
234+
// closes the response body
235+
resData, err := new(io.Response[api.TokenStatusResponse]).Read(res)
236+
if err != nil {
237+
return false, time.Time{}, err
238+
}
239+
// return the verification result and expiration time
240+
return resData.Valid, resData.Expiration, nil
241+
}
242+
243+
// req internal method creates a new HTTP request with the provided config,
244+
// method, path, and data. It validates the configuration and sets the
245+
// necessary headers. If the data is provided, it writes the JSON data to the
246+
// request body. It returns the created request or an error if something goes
247+
// wrong. This method is used internally by the client to create requests for
248+
// the API.
249+
func req[T any](config *Config, method, path string, data *T) (*http.Request, error) {
250+
if err := config.Validate(false); err != nil {
251+
return nil, err
252+
}
253+
// create the request URL
254+
finalEndpoint, err := url.JoinPath(config.APIEndpoint, path)
255+
if err != nil {
256+
return nil, ErrInvalidAPIEndpoint
257+
}
258+
// create the request with the method and URL
259+
req, err := http.NewRequest(method, finalEndpoint, nil)
260+
if err != nil {
261+
return nil, ErrCreateRequest
262+
}
263+
// set the AppID and AppSecret headers if the configuration contains them
264+
if err := config.Validate(true); err == nil {
265+
req.Header.Set(api.AppIDHeader, config.AppID.String())
266+
req.Header.Set(api.AppSecretHeader, config.AppSecret)
267+
}
268+
// set the body and content type if data is provided
269+
if data != nil {
270+
// write json data if provided
271+
if err := io.RequestWith(&data).WriteJSON(req); err != nil {
272+
return nil, err
273+
}
274+
}
275+
return req, nil
276+
}

api/client/errors.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package client
2+
3+
import "fmt"
4+
5+
var (
6+
ErrInvalidAPIEndpoint = fmt.Errorf("invalid API endpoint")
7+
ErrInvalidTimeout = fmt.Errorf("invalid timeout value")
8+
ErrInvalidAppID = fmt.Errorf("invalid application ID")
9+
ErrInvalidAppName = fmt.Errorf("invalid application ID name")
10+
ErrInvalidAppRedirectURI = fmt.Errorf("invalid application redirect URI")
11+
ErrInvalidAppSessionDuration = fmt.Errorf("invalid application session duration")
12+
ErrInvalidAppSecret = fmt.Errorf("invalid application secret")
13+
ErrInvalidEmailAddress = fmt.Errorf("invalid email address")
14+
ErrAPIUnavailable = fmt.Errorf("API is unavailable")
15+
ErrRequestToken = fmt.Errorf("failed to request token")
16+
ErrInvalidToken = fmt.Errorf("invalid token provided")
17+
ErrCreateRequest = fmt.Errorf("failed to create request")
18+
)

0 commit comments

Comments
 (0)