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:
Trey t
2026-03-02 09:48:01 -06:00
parent 56d6fa4514
commit 7690f07a2b
123 changed files with 8321 additions and 750 deletions

View File

@@ -1,15 +1,19 @@
package services
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/dto/requests"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-api/internal/testutil"
)
@@ -333,3 +337,122 @@ func TestResidenceService_RemoveUser_CannotRemoveOwner(t *testing.T) {
err := service.RemoveUser(residence.ID, owner.ID, owner.ID)
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner")
}
// setupResidenceServiceWithSubscription creates a ResidenceService wired with a
// SubscriptionService, enabling tier limit enforcement in tests.
func setupResidenceServiceWithSubscription(t *testing.T) (*ResidenceService, *gorm.DB) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
taskRepo := repositories.NewTaskRepository(db)
contractorRepo := repositories.NewContractorRepository(db)
documentRepo := repositories.NewDocumentRepository(db)
subscriptionRepo := repositories.NewSubscriptionRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
subscriptionService := NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
service.SetSubscriptionService(subscriptionService)
return service, db
}
func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) {
service, db := setupResidenceServiceWithSubscription(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Enable global limitations
db.Where("1=1").Delete(&models.SubscriptionSettings{})
err := db.Create(&models.SubscriptionSettings{EnableLimitations: true}).Error
require.NoError(t, err)
// Set free tier limit to 1 property
one := 1
db.Where("tier = ?", models.TierFree).Delete(&models.TierLimits{})
err = db.Create(&models.TierLimits{
Tier: models.TierFree,
PropertiesLimit: &one,
}).Error
require.NoError(t, err)
// Ensure user has a free-tier subscription record
subscriptionRepo := repositories.NewSubscriptionRepository(db)
_, err = subscriptionRepo.GetOrCreate(owner.ID)
require.NoError(t, err)
// First residence should succeed (under the limit)
req := &requests.CreateResidenceRequest{
Name: "First House",
StreetAddress: "1 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
resp, err := service.CreateResidence(req, owner.ID)
require.NoError(t, err)
assert.Equal(t, "First House", resp.Data.Name)
// Second residence should be rejected (at the limit)
req2 := &requests.CreateResidenceRequest{
Name: "Second House",
StreetAddress: "2 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78702",
}
_, err = service.CreateResidence(req2, owner.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.properties_limit_exceeded")
}
func TestCreateResidence_ProTier_AllowsMore(t *testing.T) {
service, db := setupResidenceServiceWithSubscription(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Enable global limitations
db.Where("1=1").Delete(&models.SubscriptionSettings{})
err := db.Create(&models.SubscriptionSettings{EnableLimitations: true}).Error
require.NoError(t, err)
// Set free tier limit to 1 property (pro is unlimited by default: nil limits)
one := 1
db.Where("tier = ?", models.TierFree).Delete(&models.TierLimits{})
err = db.Create(&models.TierLimits{
Tier: models.TierFree,
PropertiesLimit: &one,
}).Error
require.NoError(t, err)
// Create a pro-tier subscription for the user
subscriptionRepo := repositories.NewSubscriptionRepository(db)
sub, err := subscriptionRepo.GetOrCreate(owner.ID)
require.NoError(t, err)
// Upgrade to Pro with a future expiration
future := time.Now().UTC().Add(30 * 24 * time.Hour)
sub.Tier = models.TierPro
sub.ExpiresAt = &future
sub.SubscribedAt = ptrTime(time.Now().UTC())
err = subscriptionRepo.Update(sub)
require.NoError(t, err)
// Create multiple residences — all should succeed for Pro users
for i := 1; i <= 3; i++ {
req := &requests.CreateResidenceRequest{
Name: fmt.Sprintf("House %d", i),
StreetAddress: fmt.Sprintf("%d Main St", i),
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
resp, err := service.CreateResidence(req, owner.ID)
require.NoError(t, err, "Pro user should be able to create residence %d", i)
assert.Equal(t, fmt.Sprintf("House %d", i), resp.Data.Name)
}
}
// ptrTime returns a pointer to the given time.
func ptrTime(t time.Time) *time.Time {
return &t
}