mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add authentication and personal user endpoint (#29)
* feat: Add authentication and personal user endpoint This contribution adds a lot of scaffolding for the database fake and testability of coderd. A new endpoint "/user" is added to return the currently authenticated user to the requester. * Use TestMain to catch leak instead * Add userpassword package * Add WIP * Add user auth * Fix test * Add comments * Fix login response * Fix order * Fix generated code * Update httpapi/httpapi.go Co-authored-by: Bryan <bryan@coder.com> Co-authored-by: Bryan <bryan@coder.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// New creates a Coder client for the provided URL.
|
||||
func New(url *url.URL) *Client {
|
||||
return &Client{
|
||||
url: url,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// Client is an HTTP caller for methods to the Coder API.
|
||||
type Client struct {
|
||||
url *url.URL
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// SetSessionToken applies the provided token to the current client.
|
||||
func (c *Client) SetSessionToken(token string) error {
|
||||
if c.httpClient.Jar == nil {
|
||||
var err error
|
||||
c.httpClient.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.httpClient.Jar.SetCookies(c.url, []*http.Cookie{{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: token,
|
||||
}})
|
||||
return nil
|
||||
}
|
||||
|
||||
// request performs an HTTP request with the body provided.
|
||||
// The caller is responsible for closing the response body.
|
||||
func (c *Client) request(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
|
||||
url, err := c.url.Parse(path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse url: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if body != nil {
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
err = enc.Encode(body)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("encode body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url.String(), &buf)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create request: %w", err)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("do: %w", err)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// readBodyAsError reads the response as an httpapi.Message, and
|
||||
// wraps it in a codersdk.Error type for easy marshalling.
|
||||
func readBodyAsError(res *http.Response) error {
|
||||
var m httpapi.Response
|
||||
err := json.NewDecoder(res.Body).Decode(&m)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// If no body is sent, we'll just provide the status code.
|
||||
return &Error{
|
||||
statusCode: res.StatusCode,
|
||||
}
|
||||
}
|
||||
return xerrors.Errorf("decode body: %w", err)
|
||||
}
|
||||
return &Error{
|
||||
Response: m,
|
||||
statusCode: res.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// Error represents an unaccepted or invalid request to the API.
|
||||
type Error struct {
|
||||
httpapi.Response
|
||||
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.statusCode
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("status code %d: %s", e.statusCode, e.Message)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
)
|
||||
|
||||
// CreateInitialUser attempts to create the first user on a Coder deployment.
|
||||
// This initial user has superadmin privileges. If >0 users exist, this request
|
||||
// will fail.
|
||||
func (c *Client) CreateInitialUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// User returns a user for the ID provided.
|
||||
// If the ID string is empty, the current user will be returned.
|
||||
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusOK {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// LoginWithPassword creates a session token authenticating with an email and password.
|
||||
// Call `SetSessionToken()` to apply the newly acquired token to the client.
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/login", req)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.LoginWithPasswordResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.LoginWithPasswordResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package codersdk_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUsers(t *testing.T) {
|
||||
t.Run("MultipleInitial", func(t *testing.T) {
|
||||
server := coderdtest.New(t)
|
||||
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateUserRequest{
|
||||
Email: "wowie@coder.com",
|
||||
Username: "tester",
|
||||
Password: "moo",
|
||||
})
|
||||
var cerr *codersdk.Error
|
||||
require.ErrorAs(t, err, &cerr)
|
||||
require.Equal(t, cerr.StatusCode(), http.StatusConflict)
|
||||
require.Greater(t, len(cerr.Error()), 0)
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
server := coderdtest.New(t)
|
||||
_, err := server.Client.User(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user