mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-28 07:57:03 +00:00
feat: enhance authentication with JWT support and clean up unused code
This commit is contained in:
@@ -46,6 +46,7 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gofiber/schema v1.6.0 // indirect
|
github.com/gofiber/schema v1.6.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.3 // indirect
|
github.com/gofiber/utils/v2 v2.0.0-rc.3 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/google/gopacket v1.1.19 // indirect
|
github.com/google/gopacket v1.1.19 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ github.com/gofiber/utils/v2 v2.0.0-rc.2 h1:NvJTf7yMafTq16lUOJv70nr+HIOLNQcvGme/X
|
|||||||
github.com/gofiber/utils/v2 v2.0.0-rc.2/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
github.com/gofiber/utils/v2 v2.0.0-rc.2/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.3 h1:gOL5jAEGUT2UbQkTkgMJctYt4rYewnTIt0Y7YaDATDc=
|
github.com/gofiber/utils/v2 v2.0.0-rc.3 h1:gOL5jAEGUT2UbQkTkgMJctYt4rYewnTIt0Y7YaDATDc=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.3/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
github.com/gofiber/utils/v2 v2.0.0-rc.3/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type AuthService struct {
|
|||||||
oidcProvider *oidc.Provider
|
oidcProvider *oidc.Provider
|
||||||
oidcVerifier *oidc.IDTokenVerifier
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
oauth2Config *oauth2.Config
|
oauth2Config *oauth2.Config
|
||||||
|
jwtService *JWTService
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInfo represents authenticated user information
|
// UserInfo represents authenticated user information
|
||||||
@@ -31,8 +32,14 @@ type UserInfo struct {
|
|||||||
|
|
||||||
// NewAuthService creates a new authentication service
|
// NewAuthService creates a new authentication service
|
||||||
func NewAuthService(cfg *config.AuthConfig) (*AuthService, error) {
|
func NewAuthService(cfg *config.AuthConfig) (*AuthService, error) {
|
||||||
|
jwtService, err := NewJWTService()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize JWT service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
service := &AuthService{
|
service := &AuthService{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
jwtService: jwtService,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize OIDC if enabled
|
// Initialize OIDC if enabled
|
||||||
@@ -265,7 +272,6 @@ func extractRoles(claims map[string]interface{}, path string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is the last part, it should be the roles array
|
|
||||||
if i == len(parts)-1 {
|
if i == len(parts)-1 {
|
||||||
return extractStringArray(value)
|
return extractStringArray(value)
|
||||||
}
|
}
|
||||||
@@ -301,3 +307,33 @@ func extractStringArray(value interface{}) []string {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateStateToken generates a secure CSRF state token
|
||||||
|
func (a *AuthService) GenerateStateToken() (string, error) {
|
||||||
|
return a.jwtService.GenerateStateToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndConsumeState validates and consumes a CSRF state token
|
||||||
|
func (a *AuthService) ValidateAndConsumeState(token string) bool {
|
||||||
|
return a.jwtService.ValidateAndConsumeState(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSessionToken generates a JWT session token for the user
|
||||||
|
func (a *AuthService) GenerateSessionToken(userInfo *UserInfo) (string, error) {
|
||||||
|
return a.jwtService.GenerateToken(userInfo, a.config.OIDC.SessionMaxAge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSessionToken validates a JWT session token and returns user info
|
||||||
|
func (a *AuthService) ValidateSessionToken(tokenString string) (*UserInfo, error) {
|
||||||
|
claims, err := a.jwtService.ValidateToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
Username: claims.Username,
|
||||||
|
Email: claims.Email,
|
||||||
|
Name: claims.Name,
|
||||||
|
Roles: claims.Roles,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ func (h *BucketHandler) ListBuckets(c fiber.Ctx) error {
|
|||||||
// @Tags Buckets
|
// @Tags Buckets
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body models.CreateBucketRequest true "Bucket creation payload"
|
// @Param payload body models.CreateBucketRequest true "Bucket creation payload"
|
||||||
// @Success 201 {object} models.APIResponse{data=object{bucket=string,message=string}} "Bucket created successfully"
|
// @Success 201 {object} models.APIResponse{data=object{bucket=string,message=string}} "Bucket created successfully"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request body or bucket name is required"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request body or bucket name is required"
|
||||||
// @Failure 409 {object} models.APIResponse{error=models.APIError} "Bucket already exists"
|
// @Failure 409 {object} models.APIResponse{error=models.APIError} "Bucket already exists"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to create bucket"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to create bucket"
|
||||||
// @Router /api/v1/buckets [post]
|
// @Router /api/v1/buckets [post]
|
||||||
func (h *BucketHandler) CreateBucket(c fiber.Ctx) error {
|
func (h *BucketHandler) CreateBucket(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ func NewClusterHandler(adminService *services.GarageAdminService) *ClusterHandle
|
|||||||
// @Tags Cluster
|
// @Tags Cluster
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} models.APIResponse{data=object} "Successfully retrieved cluster health"
|
// @Success 200 {object} models.APIResponse{data=object} "Successfully retrieved cluster health"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to get cluster health"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to get cluster health"
|
||||||
// @Router /api/v1/cluster/health [get]
|
// @Router /api/v1/cluster/health [get]
|
||||||
func (h *ClusterHandler) GetHealth(c fiber.Ctx) error {
|
func (h *ClusterHandler) GetHealth(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -49,8 +49,8 @@ func (h *ClusterHandler) GetHealth(c fiber.Ctx) error {
|
|||||||
// @Tags Cluster
|
// @Tags Cluster
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} models.APIResponse{data=object} "Successfully retrieved cluster status"
|
// @Success 200 {object} models.APIResponse{data=object} "Successfully retrieved cluster status"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to get cluster status"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to get cluster status"
|
||||||
// @Router /api/v1/cluster/status [get]
|
// @Router /api/v1/cluster/status [get]
|
||||||
func (h *ClusterHandler) GetStatus(c fiber.Ctx) error {
|
func (h *ClusterHandler) GetStatus(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func NewMonitoringHandler(adminService *services.GarageAdminService, s3Service *
|
|||||||
// @Tags Monitoring
|
// @Tags Monitoring
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce text/plain
|
// @Produce text/plain
|
||||||
// @Success 200 {string} string "System metrics in plain text format"
|
// @Success 200 {string} string "System metrics in plain text format"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to retrieve metrics"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to retrieve metrics"
|
||||||
// @Router /api/v1/monitoring/metrics [get]
|
// @Router /api/v1/monitoring/metrics [get]
|
||||||
func (h *MonitoringHandler) GetMetrics(c fiber.Ctx) error {
|
func (h *MonitoringHandler) GetMetrics(c fiber.Ctx) error {
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ func NewObjectHandler(s3Service *services.S3Service) *ObjectHandler {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket to list objects from"
|
// @Param bucket path string true "Name of the bucket to list objects from"
|
||||||
// @Param prefix query string false "Filter objects by prefix"
|
// @Param prefix query string false "Filter objects by prefix"
|
||||||
// @Param max_keys query int false "Maximum number of objects to return (default: 100)"
|
// @Param max_keys query int false "Maximum number of objects to return (default: 100)"
|
||||||
// @Param continuation_token query string false "Token for pagination to retrieve next page of results"
|
// @Param continuation_token query string false "Token for pagination to retrieve next page of results"
|
||||||
// @Success 200 {object} models.APIResponse{data=models.ObjectListResponse} "Successfully retrieved list of objects and prefixes"
|
// @Success 200 {object} models.APIResponse{data=models.ObjectListResponse} "Successfully retrieved list of objects and prefixes"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to list objects"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to list objects"
|
||||||
// @Router /api/v1/buckets/{bucket}/objects [get]
|
// @Router /api/v1/buckets/{bucket}/objects [get]
|
||||||
func (h *ObjectHandler) ListObjects(c fiber.Ctx) error {
|
func (h *ObjectHandler) ListObjects(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -80,13 +80,13 @@ func (h *ObjectHandler) ListObjects(c fiber.Ctx) error {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept multipart/form-data
|
// @Accept multipart/form-data
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket to upload the object to"
|
// @Param bucket path string true "Name of the bucket to upload the object to"
|
||||||
// @Param file formData file true "File to upload"
|
// @Param file formData file true "File to upload"
|
||||||
// @Param key formData string false "Object key (path in bucket). If not provided, the filename will be used"
|
// @Param key formData string false "Object key (path in bucket). If not provided, the filename will be used"
|
||||||
// @Success 201 {object} models.APIResponse{data=models.ObjectUploadResponse} "Object uploaded successfully"
|
// @Success 201 {object} models.APIResponse{data=models.ObjectUploadResponse} "Object uploaded successfully"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to upload object"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to upload object"
|
||||||
// @Router /api/v1/buckets/{bucket}/objects [post]
|
// @Router /api/v1/buckets/{bucket}/objects [post]
|
||||||
func (h *ObjectHandler) UploadObject(c fiber.Ctx) error {
|
func (h *ObjectHandler) UploadObject(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -195,12 +195,12 @@ func (h *ObjectHandler) GetObject(c fiber.Ctx) error {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket containing the object"
|
// @Param bucket path string true "Name of the bucket containing the object"
|
||||||
// @Param key path string true "Key (path) of the object"
|
// @Param key path string true "Key (path) of the object"
|
||||||
// @Success 200 {object} models.APIResponse{data=models.ObjectDeleteResponse} "Successfully deleted the object"
|
// @Success 200 {object} models.APIResponse{data=models.ObjectDeleteResponse} "Successfully deleted the object"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Bucket name and object key are required"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Bucket name and object key are required"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Object not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Object not found"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete object"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete object"
|
||||||
// @Router /api/v1/buckets/{bucket}/objects/{key} [delete]
|
// @Router /api/v1/buckets/{bucket}/objects/{key} [delete]
|
||||||
func (h *ObjectHandler) DeleteObject(c fiber.Ctx) error {
|
func (h *ObjectHandler) DeleteObject(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -290,9 +290,9 @@ func (h *ObjectHandler) GetObjectMetadata(c fiber.Ctx) error {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket containing the object"
|
// @Param bucket path string true "Name of the bucket containing the object"
|
||||||
// @Param key path string true "Key (path) of the object"
|
// @Param key path string true "Key (path) of the object"
|
||||||
// @Param expires_in query int false "Expiration time in seconds for the pre-signed URL (default: 3600 seconds)"
|
// @Param expires_in query int false "Expiration time in seconds for the pre-signed URL (default: 3600 seconds)"
|
||||||
// @Success 200 {object} models.APIResponse{data=models.PresignedURLResponse} "Successfully generated pre-signed URL"
|
// @Success 200 {object} models.APIResponse{data=models.PresignedURLResponse} "Successfully generated pre-signed URL"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Object not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Object not found"
|
||||||
@@ -366,12 +366,12 @@ func (h *ObjectHandler) GetPresignedURL(c fiber.Ctx) error {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket containing the objects"
|
// @Param bucket path string true "Name of the bucket containing the objects"
|
||||||
// @Param request body object{keys=[]string,prefix=string} true "List of object keys to delete and optional prefix for path context"
|
// @Param request body object{keys=[]string,prefix=string} true "List of object keys to delete and optional prefix for path context"
|
||||||
// @Success 200 {object} models.APIResponse{data=models.ObjectDeleteMultipleResponse} "Successfully deleted the objects"
|
// @Success 200 {object} models.APIResponse{data=models.ObjectDeleteMultipleResponse} "Successfully deleted the objects"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete objects"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete objects"
|
||||||
// @Router /api/v1/buckets/{bucket}/objects/delete-multiple [post]
|
// @Router /api/v1/buckets/{bucket}/objects/delete-multiple [post]
|
||||||
func (h *ObjectHandler) DeleteMultipleObjects(c fiber.Ctx) error {
|
func (h *ObjectHandler) DeleteMultipleObjects(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -417,52 +417,6 @@ func (h *ObjectHandler) DeleteMultipleObjects(c fiber.Ctx) error {
|
|||||||
return c.JSON(models.SuccessResponse(response))
|
return c.JSON(models.SuccessResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadObjectStream uploads an object from request body stream (for large files)
|
|
||||||
//
|
|
||||||
// @Summary Upload object via stream
|
|
||||||
// @Description Uploads an object directly from the request body stream, suitable for large files
|
|
||||||
// @Tags Objects
|
|
||||||
// @Accept application/octet-stream
|
|
||||||
// @Produce json
|
|
||||||
// @Param bucket path string true "Name of the bucket to upload the object to"
|
|
||||||
// @Param key path string true "Object key (path in bucket)"
|
|
||||||
// @Param Content-Type header string false "Content type of the object being uploaded"
|
|
||||||
// @Param body body string true "Raw binary data of the object"
|
|
||||||
// @Success 201 {object} models.APIResponse{data=models.ObjectUploadResponse} "Object uploaded successfully"
|
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Bucket name and object key are required"
|
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to upload object"
|
|
||||||
// @Router /api/v1/buckets/{bucket}/objects/{key} [put]
|
|
||||||
func (h *ObjectHandler) UploadObjectStream(c fiber.Ctx) error {
|
|
||||||
ctx := c.Context()
|
|
||||||
|
|
||||||
// Get bucket name and object key from URL parameters
|
|
||||||
bucketName := c.Params("bucket")
|
|
||||||
key := c.Params("key")
|
|
||||||
|
|
||||||
if bucketName == "" || key == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(
|
|
||||||
models.ErrorResponse(models.ErrCodeBadRequest, "Bucket name and object key are required"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get content type from header
|
|
||||||
contentType := c.Get("Content-Type", "application/octet-stream")
|
|
||||||
|
|
||||||
// Get request body as reader
|
|
||||||
bodyReader := c.Request().BodyStream()
|
|
||||||
|
|
||||||
// Upload to Garage
|
|
||||||
uploadResult, err := h.s3Service.UploadObject(ctx, bucketName, key, io.NopCloser(bodyReader), contentType)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(
|
|
||||||
models.ErrorResponse(models.ErrCodeUploadFailed, "Failed to upload object: "+err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(models.SuccessResponse(uploadResult))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadMultipleObjects uploads multiple objects to a bucket
|
// UploadMultipleObjects uploads multiple objects to a bucket
|
||||||
//
|
//
|
||||||
// @Summary Upload multiple objects to bucket
|
// @Summary Upload multiple objects to bucket
|
||||||
@@ -470,12 +424,12 @@ func (h *ObjectHandler) UploadObjectStream(c fiber.Ctx) error {
|
|||||||
// @Tags Objects
|
// @Tags Objects
|
||||||
// @Accept multipart/form-data
|
// @Accept multipart/form-data
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param bucket path string true "Name of the bucket to upload the objects to"
|
// @Param bucket path string true "Name of the bucket to upload the objects to"
|
||||||
// @Param files formData file true "Files to upload (can be multiple)"
|
// @Param files formData file true "Files to upload (can be multiple)"
|
||||||
// @Success 201 {object} models.APIResponse{data=models.ObjectUploadMultipleResponse} "Objects uploaded successfully (including partial failures)"
|
// @Success 201 {object} models.APIResponse{data=models.ObjectUploadMultipleResponse} "Objects uploaded successfully (including partial failures)"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Invalid request parameters"
|
||||||
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
// @Failure 404 {object} models.APIResponse{error=models.APIError} "Bucket not found"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to upload objects"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to upload objects"
|
||||||
// @Router /api/v1/buckets/{bucket}/objects/upload-multiple [post]
|
// @Router /api/v1/buckets/{bucket}/objects/upload-multiple [post]
|
||||||
func (h *ObjectHandler) UploadMultipleObjects(c fiber.Ctx) error {
|
func (h *ObjectHandler) UploadMultipleObjects(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -496,7 +450,6 @@ func (h *ObjectHandler) UploadMultipleObjects(c fiber.Ctx) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all files from the form (they should all be under "files" field)
|
|
||||||
files := form.File["files"]
|
files := form.File["files"]
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(
|
return c.Status(fiber.StatusBadRequest).JSON(
|
||||||
|
|||||||
@@ -170,10 +170,10 @@ func (h *UserHandler) CreateUser(c fiber.Ctx) error {
|
|||||||
// @Description Deletes a specific user/access key
|
// @Description Deletes a specific user/access key
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param access_key path string true "Access key of the user to delete"
|
// @Param access_key path string true "Access key of the user to delete"
|
||||||
// @Success 200 {object} models.APIResponse{data=map[string]interface{}} "User deleted successfully"
|
// @Success 200 {object} models.APIResponse{data=map[string]interface{}} "User deleted successfully"
|
||||||
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Access key is required"
|
// @Failure 400 {object} models.APIResponse{error=models.APIError} "Access key is required"
|
||||||
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete user"
|
// @Failure 500 {object} models.APIResponse{error=models.APIError} "Failed to delete user"
|
||||||
// @Router /api/v1/users/{access_key} [delete]
|
// @Router /api/v1/users/{access_key} [delete]
|
||||||
func (h *UserHandler) DeleteUser(c fiber.Ctx) error {
|
func (h *UserHandler) DeleteUser(c fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|||||||
@@ -78,18 +78,8 @@ func handleOIDCAuth(c fiber.Ctx, authService *auth.AuthService, oidcCfg *config.
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In a production implementation, you would:
|
// Validate JWT session token
|
||||||
// 1. Validate the session token from the cookie
|
userInfo, err := authService.ValidateSessionToken(sessionCookie)
|
||||||
// 2. Look up the session in a session store (Redis, memory, etc.)
|
|
||||||
// 3. Verify the session hasn't expired
|
|
||||||
// 4. Extract user information from the session
|
|
||||||
//
|
|
||||||
// For now, we'll implement a basic token verification
|
|
||||||
// You should extend this based on your session management strategy
|
|
||||||
|
|
||||||
// Verify ID token if it's stored in the session
|
|
||||||
ctx := c.Context()
|
|
||||||
userInfo, err := authService.VerifyIDToken(ctx, sessionCookie)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(
|
return c.Status(fiber.StatusUnauthorized).JSON(
|
||||||
models.ErrorResponse(models.ErrCodeUnauthorized, "Invalid or expired session"),
|
models.ErrorResponse(models.ErrCodeUnauthorized, "Invalid or expired session"),
|
||||||
@@ -104,8 +94,6 @@ func handleOIDCAuth(c fiber.Ctx, authService *auth.AuthService, oidcCfg *config.
|
|||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequireAuth is a simpler middleware that just checks if auth is enabled
|
|
||||||
// Use this for routes that should only be accessible when any auth is active
|
|
||||||
func RequireAuth(cfg *config.AuthConfig) fiber.Handler {
|
func RequireAuth(cfg *config.AuthConfig) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
if cfg.Mode == "none" {
|
if cfg.Mode == "none" {
|
||||||
@@ -117,10 +105,8 @@ func RequireAuth(cfg *config.AuthConfig) fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequireAdmin is middleware that checks if the user has admin role (OIDC only)
|
|
||||||
func RequireAdmin(authService *auth.AuthService) fiber.Handler {
|
func RequireAdmin(authService *auth.AuthService) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
// Get user info from context (set by AuthMiddleware)
|
|
||||||
userInfoInterface := c.Locals("userInfo")
|
userInfoInterface := c.Locals("userInfo")
|
||||||
if userInfoInterface == nil {
|
if userInfoInterface == nil {
|
||||||
return c.Status(fiber.StatusForbidden).JSON(
|
return c.Status(fiber.StatusForbidden).JSON(
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type ListObjectsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UploadObjectRequest represents metadata for an object upload
|
// UploadObjectRequest represents metadata for an object upload
|
||||||
// Note: The actual file data comes from multipart form or request body
|
|
||||||
type UploadObjectRequest struct {
|
type UploadObjectRequest struct {
|
||||||
Bucket string `json:"bucket" validate:"required"`
|
Bucket string `json:"bucket" validate:"required"`
|
||||||
Key string `json:"key" validate:"required"`
|
Key string `json:"key" validate:"required"`
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ func SetupRoutes(
|
|||||||
objects.Post("/upload-multiple", objectHandler.UploadMultipleObjects) // Upload multiple objects
|
objects.Post("/upload-multiple", objectHandler.UploadMultipleObjects) // Upload multiple objects
|
||||||
objects.Post("/delete-multiple", objectHandler.DeleteMultipleObjects) // Delete multiple objects
|
objects.Post("/delete-multiple", objectHandler.DeleteMultipleObjects) // Delete multiple objects
|
||||||
objects.Get("/:key", objectHandler.GetObject) // Download object
|
objects.Get("/:key", objectHandler.GetObject) // Download object
|
||||||
objects.Put("/:key", objectHandler.UploadObjectStream) // Upload object (stream)
|
|
||||||
objects.Delete("/:key", objectHandler.DeleteObject) // Delete object
|
objects.Delete("/:key", objectHandler.DeleteObject) // Delete object
|
||||||
objects.Head("/:key", objectHandler.GetObjectMetadata) // Get object metadata
|
objects.Head("/:key", objectHandler.GetObjectMetadata) // Get object metadata
|
||||||
objects.Post("/:key/presign", objectHandler.GetPresignedURL) // Generate pre-signed URL
|
objects.Post("/:key/presign", objectHandler.GetPresignedURL) // Generate pre-signed URL
|
||||||
@@ -103,8 +102,13 @@ func SetupRoutes(
|
|||||||
{
|
{
|
||||||
// Login endpoint - redirects to OIDC provider
|
// Login endpoint - redirects to OIDC provider
|
||||||
authRoutes.Get("/login", func(c fiber.Ctx) error {
|
authRoutes.Get("/login", func(c fiber.Ctx) error {
|
||||||
// Generate state token for CSRF protection
|
state, err := authService.GenerateStateToken()
|
||||||
state := "random-state-token" // In production, use a secure random token
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Failed to generate state token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
authURL, err := authService.GetAuthorizationURL(state)
|
authURL, err := authService.GetAuthorizationURL(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
@@ -116,6 +120,14 @@ func SetupRoutes(
|
|||||||
|
|
||||||
// Callback endpoint - handles OIDC redirect after login
|
// Callback endpoint - handles OIDC redirect after login
|
||||||
authRoutes.Get("/callback", func(c fiber.Ctx) error {
|
authRoutes.Get("/callback", func(c fiber.Ctx) error {
|
||||||
|
// Get and validate state token
|
||||||
|
state := c.Query("state")
|
||||||
|
if !authService.ValidateAndConsumeState(state) {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Invalid or expired state token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Get authorization code from query
|
// Get authorization code from query
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
@@ -149,15 +161,18 @@ func SetupRoutes(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production, you should:
|
// Generate JWT session token
|
||||||
// 1. Create a session and store it in Redis/memory
|
sessionToken, err := authService.GenerateSessionToken(userInfo)
|
||||||
// 2. Set a secure session cookie
|
if err != nil {
|
||||||
// 3. Redirect to the frontend with the session
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Failed to create session",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// For now, just set the ID token as a cookie (not recommended for production)
|
// Set JWT session token as secure cookie
|
||||||
c.Cookie(&fiber.Cookie{
|
c.Cookie(&fiber.Cookie{
|
||||||
Name: cfg.Auth.OIDC.CookieName,
|
Name: cfg.Auth.OIDC.CookieName,
|
||||||
Value: rawIDToken,
|
Value: sessionToken,
|
||||||
MaxAge: cfg.Auth.OIDC.SessionMaxAge,
|
MaxAge: cfg.Auth.OIDC.SessionMaxAge,
|
||||||
Secure: cfg.Auth.OIDC.CookieSecure,
|
Secure: cfg.Auth.OIDC.CookieSecure,
|
||||||
HTTPOnly: cfg.Auth.OIDC.CookieHTTPOnly,
|
HTTPOnly: cfg.Auth.OIDC.CookieHTTPOnly,
|
||||||
|
|||||||
@@ -174,37 +174,40 @@ func (s *S3Service) ListObjects(ctx context.Context, bucketName, prefix string,
|
|||||||
|
|
||||||
// Set default max keys if not specified
|
// Set default max keys if not specified
|
||||||
if maxKeys <= 0 {
|
if maxKeys <= 0 {
|
||||||
maxKeys = 100
|
maxKeys = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create list objects options
|
// Use ListObjectsV2 for proper pagination support
|
||||||
opts := minio.ListObjectsOptions{
|
opts := minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Recursive: false, // Use delimiter to get folders
|
Recursive: false,
|
||||||
MaxKeys: maxKeys,
|
MaxKeys: maxKeys,
|
||||||
|
StartAfter: continuationToken,
|
||||||
|
UseV1: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: MinIO SDK v7 doesn't directly support continuation tokens in the same way
|
|
||||||
// We'll use the ListObjects which returns a channel
|
|
||||||
|
|
||||||
objects := make([]models.ObjectInfo, 0)
|
objects := make([]models.ObjectInfo, 0)
|
||||||
prefixes := make(map[string]bool) // Use map to deduplicate prefixes
|
prefixesMap := make(map[string]bool)
|
||||||
|
|
||||||
// List objects
|
var lastKey string
|
||||||
objectCh := client.ListObjects(ctx, bucketName, opts)
|
isTruncated := false
|
||||||
|
itemCount := 0
|
||||||
|
|
||||||
count := 0
|
// List objects using the channel-based API
|
||||||
for object := range objectCh {
|
for object := range client.ListObjects(ctx, bucketName, opts) {
|
||||||
if object.Err != nil {
|
if object.Err != nil {
|
||||||
return nil, fmt.Errorf("failed to list objects in bucket %s: %w", bucketName, object.Err)
|
return nil, fmt.Errorf("failed to list objects in bucket %s: %w", bucketName, object.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a prefix (directory)
|
// Check if this is a prefix (directory)
|
||||||
if object.Key[len(object.Key)-1:] == "/" && object.Size == 0 {
|
if len(object.Key) > 0 && object.Key[len(object.Key)-1:] == "/" && object.Size == 0 {
|
||||||
prefixes[object.Key] = true
|
prefixesMap[object.Key] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track the last key for pagination
|
||||||
|
lastKey = object.Key
|
||||||
|
|
||||||
// Add to objects list
|
// Add to objects list
|
||||||
objects = append(objects, models.ObjectInfo{
|
objects = append(objects, models.ObjectInfo{
|
||||||
Key: object.Key,
|
Key: object.Key,
|
||||||
@@ -215,25 +218,32 @@ func (s *S3Service) ListObjects(ctx context.Context, bucketName, prefix string,
|
|||||||
StorageClass: object.StorageClass,
|
StorageClass: object.StorageClass,
|
||||||
})
|
})
|
||||||
|
|
||||||
count++
|
itemCount++
|
||||||
if count >= maxKeys {
|
if itemCount >= maxKeys {
|
||||||
|
isTruncated = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert prefixes map to slice
|
// Convert prefixes map to slice
|
||||||
prefixList := make([]string, 0, len(prefixes))
|
prefixList := make([]string, 0, len(prefixesMap))
|
||||||
for p := range prefixes {
|
for p := range prefixesMap {
|
||||||
prefixList = append(prefixList, p)
|
prefixList = append(prefixList, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare next continuation token
|
||||||
|
var nextToken string
|
||||||
|
if isTruncated && lastKey != "" {
|
||||||
|
nextToken = lastKey
|
||||||
|
}
|
||||||
|
|
||||||
return &models.ObjectListResponse{
|
return &models.ObjectListResponse{
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
Prefixes: prefixList,
|
Prefixes: prefixList,
|
||||||
Count: len(objects),
|
Count: len(objects),
|
||||||
IsTruncated: count >= maxKeys,
|
IsTruncated: isTruncated,
|
||||||
NextContinuationToken: "", // MinIO SDK handles this differently
|
NextContinuationToken: nextToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export function ObjectBrowserView({
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (item.isFile) {
|
if (item.isFile) {
|
||||||
item.file((file: File) => {
|
item.file((file: File) => {
|
||||||
// Add the relative path to the file object
|
|
||||||
const fullPath = path + file.name;
|
const fullPath = path + file.name;
|
||||||
Object.defineProperty(file, 'webkitRelativePath', {
|
Object.defineProperty(file, 'webkitRelativePath', {
|
||||||
value: fullPath,
|
value: fullPath,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import type {
|
|||||||
StorageMetrics,
|
StorageMetrics,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
// Configure axios instance with base URL
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api',
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -24,7 +23,6 @@ const api = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add request interceptor for authentication
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('auth-token');
|
const token = localStorage.getItem('auth-token');
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -33,7 +31,6 @@ api.interceptors.request.use((config) => {
|
|||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add response interceptor for error handling
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
// If response has success=false in data, treat it as an error
|
// If response has success=false in data, treat it as an error
|
||||||
@@ -119,7 +116,6 @@ export const bucketsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSettings: async (name: string, settings: Partial<BucketDetails>): Promise<void> => {
|
updateSettings: async (name: string, settings: Partial<BucketDetails>): Promise<void> => {
|
||||||
// TODO: Implement when backend endpoint is ready
|
|
||||||
await api.patch(`/v1/buckets/${name}/settings`, settings);
|
await api.patch(`/v1/buckets/${name}/settings`, settings);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -185,12 +181,6 @@ export const objectsApi = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadStream: async (bucket: string, key: string, data: Blob | File, contentType?: string): Promise<void> => {
|
|
||||||
await api.put(`/v1/buckets/${bucket}/objects/${encodeURIComponent(key)}`, data, {
|
|
||||||
headers: { 'Content-Type': contentType || 'application/octet-stream' },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
uploadMultiple: async (bucket: string, files: File[]): Promise<any> => {
|
uploadMultiple: async (bucket: string, files: File[]): Promise<any> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
@@ -199,10 +199,8 @@ export function AccessControl() {
|
|||||||
try {
|
try {
|
||||||
const updates: { status?: string; expiration?: string } = {};
|
const updates: { status?: string; expiration?: string } = {};
|
||||||
|
|
||||||
// Add status change
|
|
||||||
updates.status = keyStatus;
|
updates.status = keyStatus;
|
||||||
|
|
||||||
// Add expiration if set and not "never expires"
|
|
||||||
if (!neverExpires && expirationDate) {
|
if (!neverExpires && expirationDate) {
|
||||||
updates.expiration = new Date(expirationDate).toISOString();
|
updates.expiration = new Date(expirationDate).toISOString();
|
||||||
} else if (neverExpires) {
|
} else if (neverExpires) {
|
||||||
|
|||||||
Reference in New Issue
Block a user