Compare commits
	
		
			6 Commits
		
	
	
		
			v0.3.4
			...
			733c0410fe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 733c0410fe | |||
| 8e88f09709 | |||
| d44801b0ae | |||
| a4bf701ac3 | |||
| 99bddcd03f | |||
| 6fdd55def8 | 
@@ -2,8 +2,8 @@ pipeline:
 | 
			
		||||
  test:
 | 
			
		||||
    image: golang:latest
 | 
			
		||||
    commands:
 | 
			
		||||
      - go build ./cmd/client/client.go
 | 
			
		||||
      - go build ./cmd/server/server.go
 | 
			
		||||
      - go build -o gpaste-client ./cmd/client/client.go
 | 
			
		||||
      - go build -o gpaste-server ./cmd/server/server.go
 | 
			
		||||
      - go test -v ./...
 | 
			
		||||
      - go vet ./...
 | 
			
		||||
    when:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								api/http.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								api/http.go
									
									
									
									
									
								
							@@ -119,13 +119,8 @@ func (s *HTTPServer) HandlerAPIFileGet(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	reqID := middleware.GetReqID(r.Context())
 | 
			
		||||
	type resp struct {
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
		ID      string `json:"id"`
 | 
			
		||||
		URL     string `json:"url"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var responses []resp
 | 
			
		||||
	var responses []ResponseAPIFilePost
 | 
			
		||||
 | 
			
		||||
	if err := r.ParseMultipartForm(1024 * 1024 * 10); err != nil {
 | 
			
		||||
		s.Logger.Warnw("Error parsing multipart form.", "req_id", reqID, "err", err)
 | 
			
		||||
@@ -149,7 +144,7 @@ func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.R
 | 
			
		||||
		}
 | 
			
		||||
		s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "filename", f.OriginalFilename, "remote_addr", r.RemoteAddr)
 | 
			
		||||
 | 
			
		||||
		responses = append(responses, resp{Message: "OK", ID: f.ID, URL: "TODO"})
 | 
			
		||||
		responses = append(responses, ResponseAPIFilePost{Message: "OK", ID: f.ID, URL: "TODO"})
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -162,10 +157,7 @@ func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.R
 | 
			
		||||
 | 
			
		||||
func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	reqID := middleware.GetReqID(r.Context())
 | 
			
		||||
	expectedRequest := struct {
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
	}{}
 | 
			
		||||
	var expectedRequest RequestAPILogin
 | 
			
		||||
	decoder := json.NewDecoder(r.Body)
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
	if err := decoder.Decode(&expectedRequest); err != nil {
 | 
			
		||||
@@ -179,9 +171,7 @@ func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := struct {
 | 
			
		||||
		Token string `json:"token"`
 | 
			
		||||
	}{
 | 
			
		||||
	response := ResponseAPILogin{
 | 
			
		||||
		Token: token,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -193,17 +183,12 @@ func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequestAPIUserCreate struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	reqID := middleware.GetReqID(r.Context())
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
 | 
			
		||||
	level, err := AuthLevelFromRequest(r)
 | 
			
		||||
	if err != nil || level < gpaste.AuthLevelAdmin {
 | 
			
		||||
	role, err := RoleFromRequest(r)
 | 
			
		||||
	if err != nil || role != users.RoleAdmin {
 | 
			
		||||
		w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -229,5 +214,6 @@ func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	w.WriteHeader(http.StatusAccepted)
 | 
			
		||||
	s.Logger.Infow("Created user.", "req_id", reqID, "remote_addr", r.RemoteAddr, "username", req.Username)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								api/json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/json.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
type RequestAPIUserCreate struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequestAPILogin struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ResponseAPILogin struct {
 | 
			
		||||
	Token string `json:"token"`
 | 
			
		||||
}
 | 
			
		||||
type ResponseAPIFilePost struct {
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
	ID      string `json:"id"`
 | 
			
		||||
	URL     string `json:"url"`
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/users"
 | 
			
		||||
	"github.com/go-chi/chi/v5/middleware"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +17,7 @@ type authCtxKey int
 | 
			
		||||
const (
 | 
			
		||||
	authCtxUsername authCtxKey = iota
 | 
			
		||||
	authCtxAuthLevel
 | 
			
		||||
	authCtxClaims
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *HTTPServer) MiddlewareAccessLogger(next http.Handler) http.Handler {
 | 
			
		||||
@@ -66,7 +68,8 @@ func (s *HTTPServer) MiddlewareAuthentication(next http.Handler) http.Handler {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx := context.WithValue(r.Context(), authCtxUsername, claims.Subject)
 | 
			
		||||
		ctx = context.WithValue(ctx, authCtxAuthLevel, gpaste.AuthLevelUser)
 | 
			
		||||
		ctx = context.WithValue(ctx, authCtxAuthLevel, claims.Role)
 | 
			
		||||
		ctx = context.WithValue(ctx, authCtxClaims, claims)
 | 
			
		||||
		withCtx := r.WithContext(ctx)
 | 
			
		||||
		s.Logger.Debugw("Request is authenticated.", "req_id", reqID, "username", claims.Subject)
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +82,6 @@ func (s *HTTPServer) MiddlewareAuthentication(next http.Handler) http.Handler {
 | 
			
		||||
func UsernameFromRequest(r *http.Request) (string, error) {
 | 
			
		||||
	rawUsername := r.Context().Value(authCtxUsername)
 | 
			
		||||
	if rawUsername == nil {
 | 
			
		||||
 | 
			
		||||
		return "", fmt.Errorf("no username")
 | 
			
		||||
	}
 | 
			
		||||
	username, ok := rawUsername.(string)
 | 
			
		||||
@@ -89,14 +91,26 @@ func UsernameFromRequest(r *http.Request) (string, error) {
 | 
			
		||||
	return username, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AuthLevelFromRequest(r *http.Request) (gpaste.AuthLevel, error) {
 | 
			
		||||
func RoleFromRequest(r *http.Request) (users.Role, error) {
 | 
			
		||||
	rawLevel := r.Context().Value(authCtxAuthLevel)
 | 
			
		||||
	if rawLevel == nil {
 | 
			
		||||
		return gpaste.AuthLevelUnset, fmt.Errorf("no username")
 | 
			
		||||
		return users.RoleUnset, fmt.Errorf("no username")
 | 
			
		||||
	}
 | 
			
		||||
	level, ok := rawLevel.(gpaste.AuthLevel)
 | 
			
		||||
	level, ok := rawLevel.(users.Role)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return gpaste.AuthLevelUnset, fmt.Errorf("no username")
 | 
			
		||||
		return users.RoleUnset, fmt.Errorf("no username")
 | 
			
		||||
	}
 | 
			
		||||
	return level, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ClaimsFromRequest(r *http.Request) *gpaste.Claims {
 | 
			
		||||
	rawClaims := r.Context().Value(authCtxAuthLevel)
 | 
			
		||||
	if rawClaims == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	claims, ok := rawClaims.(*gpaste.Claims)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return claims
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								auth.go
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								auth.go
									
									
									
									
									
								
							@@ -9,19 +9,17 @@ import (
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthLevel int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	AuthLevelUnset AuthLevel = iota
 | 
			
		||||
	AuthLevelUser
 | 
			
		||||
	AuthLevelAdmin
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthService struct {
 | 
			
		||||
	users      users.UserStore
 | 
			
		||||
	hmacSecret []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Claims struct {
 | 
			
		||||
	Role users.Role `json:"role,omitempty"`
 | 
			
		||||
 | 
			
		||||
	jwt.StandardClaims
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAuthService(store users.UserStore, signingSecret []byte) *AuthService {
 | 
			
		||||
	return &AuthService{users: store, hmacSecret: signingSecret}
 | 
			
		||||
}
 | 
			
		||||
@@ -37,13 +35,13 @@ func (as *AuthService) Login(username, password string) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: Set iss and aud
 | 
			
		||||
	claims := jwt.StandardClaims{
 | 
			
		||||
		Subject:   user.Username,
 | 
			
		||||
		ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(),
 | 
			
		||||
		NotBefore: time.Now().Unix(),
 | 
			
		||||
		IssuedAt:  time.Now().Unix(),
 | 
			
		||||
		Id:        uuid.NewString(),
 | 
			
		||||
	}
 | 
			
		||||
	claims := new(Claims)
 | 
			
		||||
	claims.Subject = user.Username
 | 
			
		||||
	claims.ExpiresAt = time.Now().Add(7 * 24 * time.Hour).Unix()
 | 
			
		||||
	claims.NotBefore = time.Now().Unix()
 | 
			
		||||
	claims.IssuedAt = time.Now().Unix()
 | 
			
		||||
	claims.Id = uuid.NewString()
 | 
			
		||||
	claims.Role = user.Role
 | 
			
		||||
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
 | 
			
		||||
	signed, err := token.SignedString(as.hmacSecret)
 | 
			
		||||
@@ -54,8 +52,8 @@ func (as *AuthService) Login(username, password string) (string, error) {
 | 
			
		||||
	return signed, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (as *AuthService) ValidateToken(rawToken string) (*jwt.StandardClaims, error) {
 | 
			
		||||
	claims := &jwt.StandardClaims{}
 | 
			
		||||
func (as *AuthService) ValidateToken(rawToken string) (*Claims, error) {
 | 
			
		||||
	claims := &Claims{}
 | 
			
		||||
	token, err := jwt.ParseWithClaims(rawToken, claims, func(t *jwt.Token) (interface{}, error) {
 | 
			
		||||
		return as.hmacSecret, nil
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/users"
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAuth(t *testing.T) {
 | 
			
		||||
@@ -17,7 +18,7 @@ func TestAuth(t *testing.T) {
 | 
			
		||||
		username := randomString(8)
 | 
			
		||||
		password := randomString(16)
 | 
			
		||||
 | 
			
		||||
		user := &users.User{Username: username}
 | 
			
		||||
		user := &users.User{Username: username, Role: users.RoleAdmin}
 | 
			
		||||
		if err := user.SetPassword(password); err != nil {
 | 
			
		||||
			t.Fatalf("error setting user password: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -30,9 +31,13 @@ func TestAuth(t *testing.T) {
 | 
			
		||||
			t.Fatalf("Error creating token: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := as.ValidateToken(token); err != nil {
 | 
			
		||||
		claims, err := as.ValidateToken(token)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatalf("Error validating token: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		if claims.Role != user.Role {
 | 
			
		||||
			t.Fatalf("Token role is not correct: %s", cmp.Diff(claims.Role, user.Role))
 | 
			
		||||
		}
 | 
			
		||||
		invalidToken := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDMyMjk3NjMsImp0aSI6ImUzNDk5NWI1LThiZmMtNDQyNy1iZDgxLWFmNmQ3OTRiYzM0YiIsImlhdCI6MTY0MjYyNDk2MywibmJmIjoxNjQyNjI0OTYzLCJzdWIiOiJYdE5Hemt5ZSJ9.VM6dkwSLaBv8cStkWRVVv9ADjdUrHGHrlB7GB7Ly7n8`
 | 
			
		||||
		if _, err := as.ValidateToken(invalidToken); err == nil {
 | 
			
		||||
			t.Fatalf("Invalid token passed validation")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										164
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
package client
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/api"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/files"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Client struct {
 | 
			
		||||
	BaseURL   string
 | 
			
		||||
	AuthToken string
 | 
			
		||||
 | 
			
		||||
	httpClient http.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Login(ctx context.Context, username, password string) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/login", c.BaseURL)
 | 
			
		||||
	// TODO: Change timeout
 | 
			
		||||
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	body := new(bytes.Buffer)
 | 
			
		||||
	requestData := api.RequestAPILogin{
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Password: password,
 | 
			
		||||
	}
 | 
			
		||||
	encoder := json.NewEncoder(body)
 | 
			
		||||
	if err := encoder.Encode(&requestData); err != nil {
 | 
			
		||||
		return fmt.Errorf("error encoding response: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating request: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := c.httpClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to perform request: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var responseData api.ResponseAPILogin
 | 
			
		||||
 | 
			
		||||
	decoder := json.NewDecoder(resp.Body)
 | 
			
		||||
	if err := decoder.Decode(&responseData); err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to parse response: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	c.AuthToken = responseData.Token
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) UserCreate(ctx context.Context, username, password string) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/user", c.BaseURL)
 | 
			
		||||
	body := new(bytes.Buffer)
 | 
			
		||||
 | 
			
		||||
	requestData := &api.RequestAPIUserCreate{
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Password: password,
 | 
			
		||||
	}
 | 
			
		||||
	encoder := json.NewEncoder(body)
 | 
			
		||||
	if err := encoder.Encode(requestData); err != nil {
 | 
			
		||||
		return fmt.Errorf("error encoding response: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating request: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AuthToken))
 | 
			
		||||
 | 
			
		||||
	resp, err := c.httpClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to perform request: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusAccepted {
 | 
			
		||||
		return fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Download(ctx context.Context, id string) (io.ReadCloser, error) {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/file/%s", c.BaseURL, id)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error creating request: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AuthToken))
 | 
			
		||||
 | 
			
		||||
	resp, err := c.httpClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to perform request: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return nil, fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resp.Body, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Upload(ctx context.Context, files ...*files.File) ([]api.ResponseAPIFilePost, error) {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/file", c.BaseURL)
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
 | 
			
		||||
	// TODO: Change timeout
 | 
			
		||||
	ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	// TODO: Improve buffering
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	mw := multipart.NewWriter(buf)
 | 
			
		||||
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		fw, err := mw.CreateFormFile(uuid.Must(uuid.NewRandom()).String(), file.OriginalFilename)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := io.Copy(fw, file.Body); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		file.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
	mw.Close()
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Add("Content-Type", mw.FormDataContentType())
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var expectedResp []api.ResponseAPIFilePost
 | 
			
		||||
 | 
			
		||||
	decoder := json.NewDecoder(resp.Body)
 | 
			
		||||
	if err := decoder.Decode(&expectedResp); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error decoding response: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return expectedResp, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								client/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								client/client_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
package client_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/api"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/client"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/files"
 | 
			
		||||
	"git.t-juice.club/torjus/gpaste/users"
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestClient(t *testing.T) {
 | 
			
		||||
	listener, err := net.Listen("tcp", ":0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	port := listener.Addr().(*net.TCPAddr).Port
 | 
			
		||||
	cfg := &gpaste.ServerConfig{
 | 
			
		||||
		LogLevel:      "ERROR",
 | 
			
		||||
		URL:           fmt.Sprintf("http://localhost:%d", port),
 | 
			
		||||
		SigningSecret: "TEST",
 | 
			
		||||
		Store:         &gpaste.ServerStoreConfig{Type: "memory"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	srv := api.NewHTTPServer(cfg)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		srv.Serve(listener)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	t.Cleanup(func() {
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		srv.Shutdown(ctx)
 | 
			
		||||
		listener.Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Add users
 | 
			
		||||
	username := "admin"
 | 
			
		||||
	password := "admin"
 | 
			
		||||
	user := &users.User{
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Role:     users.RoleAdmin,
 | 
			
		||||
	}
 | 
			
		||||
	if err := user.SetPassword(password); err != nil {
 | 
			
		||||
		t.Fatalf("Error setting password: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := srv.Users.Store(user); err != nil {
 | 
			
		||||
		t.Fatalf("Error storing user: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("Login", func(t *testing.T) {
 | 
			
		||||
		client := client.Client{BaseURL: cfg.URL}
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		if err := client.Login(ctx, username, password); err != nil {
 | 
			
		||||
			t.Fatalf("Error logging in: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		claims, err := srv.Auth.ValidateToken(client.AuthToken)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("unable to get claims from token: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if claims.Role != user.Role {
 | 
			
		||||
			t.Errorf("Claims have wrong role: %s", cmp.Diff(claims.Role, user.Role))
 | 
			
		||||
		}
 | 
			
		||||
		if claims.Subject != username {
 | 
			
		||||
			t.Errorf("Claims have wrong role: %s", cmp.Diff(claims.Subject, username))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t.Run("UserCreate", func(t *testing.T) {
 | 
			
		||||
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
			defer cancel()
 | 
			
		||||
			username := "user"
 | 
			
		||||
			password := "user"
 | 
			
		||||
 | 
			
		||||
			if err := client.UserCreate(ctx, username, password); err != nil {
 | 
			
		||||
				t.Fatalf("Error creating user: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			user, err := srv.Users.Get(username)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("Error getting new user: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if user.Username != username {
 | 
			
		||||
				t.Errorf("Username does not match.")
 | 
			
		||||
			}
 | 
			
		||||
			if err := user.ValidatePassword(password); err != nil {
 | 
			
		||||
				t.Errorf("Unable to validate password: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Upload", func(t *testing.T) {
 | 
			
		||||
		client := client.Client{BaseURL: cfg.URL}
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		fileContents := "this is the test file"
 | 
			
		||||
		fileBody := io.NopCloser(strings.NewReader(fileContents))
 | 
			
		||||
		file := &files.File{
 | 
			
		||||
			OriginalFilename: "filename.txt",
 | 
			
		||||
			Body:             fileBody,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := client.Upload(ctx, file)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatalf("Error uploading: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		retrieved, err := srv.Files.Get(resp[0].ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatalf("Error getting uploaded file from store: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer retrieved.Body.Close()
 | 
			
		||||
 | 
			
		||||
		buf := new(strings.Builder)
 | 
			
		||||
		if _, err := io.Copy(buf, retrieved.Body); err != nil {
 | 
			
		||||
			t.Fatalf("error reading body from store: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		if buf.String() != fileContents {
 | 
			
		||||
			t.Errorf("File contents does not match: %s", cmp.Diff(buf.String(), fileContents))
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Download", func(t *testing.T) {
 | 
			
		||||
		client := client.Client{BaseURL: cfg.URL}
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		fileContents := "this is the test file"
 | 
			
		||||
		fileBody := io.NopCloser(strings.NewReader(fileContents))
 | 
			
		||||
		file := &files.File{
 | 
			
		||||
			ID:               uuid.NewString(),
 | 
			
		||||
			OriginalFilename: "filename.txt",
 | 
			
		||||
			Body:             fileBody,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := srv.Files.Store(file); err != nil {
 | 
			
		||||
			t.Fatalf("Error putting file in store: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		body, err := client.Download(ctx, file.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatalf("Error uploading: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer body.Close()
 | 
			
		||||
 | 
			
		||||
		buf := new(strings.Builder)
 | 
			
		||||
		if _, err := io.Copy(buf, body); err != nil {
 | 
			
		||||
			t.Fatalf("error reading body from store: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		if buf.String() != fileContents {
 | 
			
		||||
			t.Errorf("File contents does not match: %s", cmp.Diff(buf.String(), fileContents))
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,7 @@ const (
 | 
			
		||||
type User struct {
 | 
			
		||||
	Username       string `json:"username"`
 | 
			
		||||
	HashedPassword []byte `json:"hashed_password"`
 | 
			
		||||
	Roles          []Role `json:"roles"`
 | 
			
		||||
	Role           Role   `json:"role"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserStore interface {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ func RunUserStoreTest(newFunc func() (func(), users.UserStore), t *testing.T) {
 | 
			
		||||
			passwordMap[username] = password
 | 
			
		||||
			user := &users.User{
 | 
			
		||||
				Username: username,
 | 
			
		||||
				Roles:    []users.Role{users.RoleAdmin},
 | 
			
		||||
				Role:     users.RoleAdmin,
 | 
			
		||||
			}
 | 
			
		||||
			if err := user.SetPassword(password); err != nil {
 | 
			
		||||
				t.Fatalf("Error setting password: %s", err)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user