feat(storage): add S3 backend support and advanced upload limits
- Introduce S3-compatible storage backend support using minio-go. - Add configuration options for local storage limits, box limits, and rate limiting. - Implement storage backend selection (local vs S3) for anonymous and registered users. - Add an `/admin/storage` management interface. - Update documentation and environment examples with the new configuration variables.
This commit is contained in:
@@ -48,15 +48,27 @@ type AuthService struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
StorageQuotaMB *float64 `json:"storageQuotaMb,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
StorageQuotaMB *float64 `json:"storageQuotaMb,omitempty"`
|
||||
Policy UserPolicy `json:"policy,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type UserPolicy struct {
|
||||
MaxUploadMB *float64 `json:"maxUploadMb,omitempty"`
|
||||
DailyUploadMB *float64 `json:"dailyUploadMb,omitempty"`
|
||||
StorageQuotaMB *float64 `json:"storageQuotaMb,omitempty"`
|
||||
MaxDays *int `json:"maxDays,omitempty"`
|
||||
DailyBoxes *int `json:"dailyBoxes,omitempty"`
|
||||
ActiveBoxes *int `json:"activeBoxes,omitempty"`
|
||||
ShortWindowRequests *int `json:"shortWindowRequests,omitempty"`
|
||||
StorageBackendID *string `json:"storageBackendId,omitempty"`
|
||||
}
|
||||
|
||||
type PublicUser struct {
|
||||
@@ -66,6 +78,7 @@ type PublicUser struct {
|
||||
Role string
|
||||
Status string
|
||||
StorageQuotaMB *float64
|
||||
Policy UserPolicy
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -381,6 +394,97 @@ func (s *AuthService) SetUserStorageQuota(userID string, quotaMB *float64) error
|
||||
return s.saveUser(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) SetUserPolicy(userID string, policy UserPolicy) error {
|
||||
if err := validateUserPolicy(policy); err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := s.UserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Policy = policy
|
||||
user.StorageQuotaMB = policy.StorageQuotaMB
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
return s.saveUser(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) SetUserStorageBackend(userID, backendID string) error {
|
||||
user, err := s.UserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backendID = strings.TrimSpace(backendID)
|
||||
if backendID == "" {
|
||||
user.Policy.StorageBackendID = nil
|
||||
} else {
|
||||
user.Policy.StorageBackendID = &backendID
|
||||
}
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
return s.saveUser(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateUserAdminFields(userID, username, email, role, status string, policy UserPolicy) (User, error) {
|
||||
if err := validateUserPolicy(policy); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return User{}, fmt.Errorf("username is required")
|
||||
}
|
||||
email, err := normalizeEmail(email)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
if role != UserRoleAdmin && role != UserRoleUser {
|
||||
return User{}, fmt.Errorf("invalid role")
|
||||
}
|
||||
if status != UserStatusActive && status != UserStatusDisabled {
|
||||
return User{}, fmt.Errorf("invalid status")
|
||||
}
|
||||
|
||||
var updated User
|
||||
err = s.db.Update(func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket(usersBucket)
|
||||
emails := tx.Bucket(userEmailsBucket)
|
||||
data := users.Get([]byte(userID))
|
||||
if data == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
var user User
|
||||
if err := json.Unmarshal(data, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
if existing := emails.Get([]byte(email)); existing != nil && string(existing) != user.ID {
|
||||
return fmt.Errorf("email is already registered")
|
||||
}
|
||||
if user.Email != email {
|
||||
if err := emails.Delete([]byte(user.Email)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := emails.Put([]byte(email), []byte(user.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user.Username = username
|
||||
user.Email = email
|
||||
user.Role = role
|
||||
user.Status = status
|
||||
user.Policy = policy
|
||||
user.StorageQuotaMB = policy.StorageQuotaMB
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
next, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := users.Put([]byte(user.ID), next); err != nil {
|
||||
return err
|
||||
}
|
||||
updated = user
|
||||
return nil
|
||||
})
|
||||
return updated, err
|
||||
}
|
||||
|
||||
func (s *AuthService) UserByID(id string) (User, error) {
|
||||
var user User
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
@@ -476,6 +580,7 @@ func (s *AuthService) PublicUser(user User) PublicUser {
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
StorageQuotaMB: user.StorageQuotaMB,
|
||||
Policy: user.Policy,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -593,3 +698,28 @@ func VerifyPasswordHash(encoded, password string) bool {
|
||||
actual := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, uint32(len(expected)))
|
||||
return subtle.ConstantTimeCompare(actual, expected) == 1
|
||||
}
|
||||
|
||||
func validateUserPolicy(policy UserPolicy) error {
|
||||
if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 {
|
||||
return fmt.Errorf("max upload override cannot be negative")
|
||||
}
|
||||
if policy.DailyUploadMB != nil && *policy.DailyUploadMB <= 0 {
|
||||
return fmt.Errorf("daily upload override must be positive")
|
||||
}
|
||||
if policy.StorageQuotaMB != nil && *policy.StorageQuotaMB < 0 {
|
||||
return fmt.Errorf("storage quota override cannot be negative")
|
||||
}
|
||||
if policy.MaxDays != nil && *policy.MaxDays <= 0 {
|
||||
return fmt.Errorf("expiration override must be positive")
|
||||
}
|
||||
if policy.DailyBoxes != nil && *policy.DailyBoxes <= 0 {
|
||||
return fmt.Errorf("daily box override must be positive")
|
||||
}
|
||||
if policy.ActiveBoxes != nil && *policy.ActiveBoxes <= 0 {
|
||||
return fmt.Errorf("active box override must be positive")
|
||||
}
|
||||
if policy.ShortWindowRequests != nil && *policy.ShortWindowRequests <= 0 {
|
||||
return fmt.Errorf("short-window request override must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user