Harden API security: input validation, safe auth extraction, new tests, and deploy config
Comprehensive security hardening from audit findings: - Add validation tags to all DTO request structs (max lengths, ranges, enums) - Replace unsafe type assertions with MustGetAuthUser helper across all handlers - Remove query-param token auth from admin middleware (prevents URL token leakage) - Add request validation calls in handlers that were missing c.Validate() - Remove goroutines in handlers (timezone update now synchronous) - Add sanitize middleware and path traversal protection (path_utils) - Stop resetting admin passwords on migration restart - Warn on well-known default SECRET_KEY - Add ~30 new test files covering security regressions, auth safety, repos, and services - Add deploy/ config, audit digests, and AUDIT_FINDINGS documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
@@ -30,31 +32,37 @@ func (r *SubscriptionRepository) FindByUserID(userID uint) (*models.UserSubscrip
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// GetOrCreate gets or creates a subscription for a user (defaults to free tier)
|
||||
// GetOrCreate gets or creates a subscription for a user (defaults to free tier).
|
||||
// Uses a transaction to avoid TOCTOU race conditions on concurrent requests.
|
||||
func (r *SubscriptionRepository) GetOrCreate(userID uint) (*models.UserSubscription, error) {
|
||||
sub, err := r.FindByUserID(userID)
|
||||
if err == nil {
|
||||
return sub, nil
|
||||
}
|
||||
var sub models.UserSubscription
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
sub = &models.UserSubscription{
|
||||
UserID: userID,
|
||||
Tier: models.TierFree,
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Where("user_id = ?", userID).First(&sub).Error
|
||||
if err == nil {
|
||||
return nil // Found existing subscription
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err // Unexpected error
|
||||
}
|
||||
|
||||
// Record not found -- create with free tier defaults
|
||||
sub = models.UserSubscription{
|
||||
UserID: userID,
|
||||
Tier: models.TierFree,
|
||||
AutoRenew: true,
|
||||
}
|
||||
if err := r.db.Create(sub).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sub, nil
|
||||
return tx.Create(&sub).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// Update updates a subscription
|
||||
func (r *SubscriptionRepository) Update(sub *models.UserSubscription) error {
|
||||
return r.db.Save(sub).Error
|
||||
return r.db.Omit("User").Save(sub).Error
|
||||
}
|
||||
|
||||
// UpgradeToPro upgrades a user to Pro tier using a transaction with row locking
|
||||
@@ -63,7 +71,7 @@ func (r *SubscriptionRepository) UpgradeToPro(userID uint, expiresAt time.Time,
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Lock the row for update
|
||||
var sub models.UserSubscription
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", userID).First(&sub).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,7 +94,7 @@ func (r *SubscriptionRepository) DowngradeToFree(userID uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Lock the row for update
|
||||
var sub models.UserSubscription
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", userID).First(&sub).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,7 +173,7 @@ func (r *SubscriptionRepository) GetTierLimits(tier models.SubscriptionTier) (*m
|
||||
var limits models.TierLimits
|
||||
err := r.db.Where("tier = ?", tier).First(&limits).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Return defaults
|
||||
if tier == models.TierFree {
|
||||
defaults := models.GetDefaultFreeLimits()
|
||||
@@ -193,7 +201,7 @@ func (r *SubscriptionRepository) GetSettings() (*models.SubscriptionSettings, er
|
||||
var settings models.SubscriptionSettings
|
||||
err := r.db.First(&settings).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Return default settings (limitations disabled)
|
||||
return &models.SubscriptionSettings{
|
||||
EnableLimitations: false,
|
||||
|
||||
Reference in New Issue
Block a user