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

@@ -12,7 +12,6 @@ import (
"github.com/treytartt/casera-api/internal/apperrors"
"github.com/treytartt/casera-api/internal/dto/requests"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/services"
)
@@ -32,13 +31,16 @@ func NewTaskHandler(taskService *services.TaskService, storageService *services.
// ListTasks handles GET /api/tasks/
func (h *TaskHandler) ListTasks(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
// Auto-capture timezone from header for background job calculations (e.g., daily digest)
// This runs in a goroutine to avoid blocking the response
// Runs synchronously — this is a lightweight DB upsert that should complete quickly
if tzHeader := c.Request().Header.Get("X-Timezone"); tzHeader != "" {
go h.taskService.UpdateUserTimezone(user.ID, tzHeader)
h.taskService.UpdateUserTimezone(user.ID, tzHeader)
}
daysThreshold := 30
@@ -62,7 +64,10 @@ func (h *TaskHandler) ListTasks(c echo.Context) error {
// GetTask handles GET /api/tasks/:id/
func (h *TaskHandler) GetTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_task_id")
@@ -77,7 +82,10 @@ func (h *TaskHandler) GetTask(c echo.Context) error {
// GetTasksByResidence handles GET /api/tasks/by-residence/:residence_id/
func (h *TaskHandler) GetTasksByResidence(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
@@ -106,13 +114,19 @@ func (h *TaskHandler) GetTasksByResidence(c echo.Context) error {
// CreateTask handles POST /api/tasks/
func (h *TaskHandler) CreateTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
var req requests.CreateTaskRequest
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return err
}
response, err := h.taskService.CreateTask(&req, user.ID, userNow)
if err != nil {
@@ -123,7 +137,10 @@ func (h *TaskHandler) CreateTask(c echo.Context) error {
// UpdateTask handles PUT/PATCH /api/tasks/:id/
func (h *TaskHandler) UpdateTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -135,6 +152,9 @@ func (h *TaskHandler) UpdateTask(c echo.Context) error {
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return err
}
response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req, userNow)
if err != nil {
@@ -145,7 +165,10 @@ func (h *TaskHandler) UpdateTask(c echo.Context) error {
// DeleteTask handles DELETE /api/tasks/:id/
func (h *TaskHandler) DeleteTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_task_id")
@@ -160,7 +183,10 @@ func (h *TaskHandler) DeleteTask(c echo.Context) error {
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
func (h *TaskHandler) MarkInProgress(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -177,7 +203,10 @@ func (h *TaskHandler) MarkInProgress(c echo.Context) error {
// CancelTask handles POST /api/tasks/:id/cancel/
func (h *TaskHandler) CancelTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -194,7 +223,10 @@ func (h *TaskHandler) CancelTask(c echo.Context) error {
// UncancelTask handles POST /api/tasks/:id/uncancel/
func (h *TaskHandler) UncancelTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -211,7 +243,10 @@ func (h *TaskHandler) UncancelTask(c echo.Context) error {
// ArchiveTask handles POST /api/tasks/:id/archive/
func (h *TaskHandler) ArchiveTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -228,7 +263,10 @@ func (h *TaskHandler) ArchiveTask(c echo.Context) error {
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
func (h *TaskHandler) UnarchiveTask(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -246,7 +284,10 @@ func (h *TaskHandler) UnarchiveTask(c echo.Context) error {
// QuickComplete handles POST /api/tasks/:id/quick-complete/
// Lightweight endpoint for widget - just returns 200 OK on success
func (h *TaskHandler) QuickComplete(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_task_id")
@@ -263,7 +304,10 @@ func (h *TaskHandler) QuickComplete(c echo.Context) error {
// GetTaskCompletions handles GET /api/tasks/:id/completions/
func (h *TaskHandler) GetTaskCompletions(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_task_id")
@@ -278,7 +322,10 @@ func (h *TaskHandler) GetTaskCompletions(c echo.Context) error {
// ListCompletions handles GET /api/task-completions/
func (h *TaskHandler) ListCompletions(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
response, err := h.taskService.ListCompletions(user.ID)
if err != nil {
return err
@@ -288,7 +335,10 @@ func (h *TaskHandler) ListCompletions(c echo.Context) error {
// GetCompletion handles GET /api/task-completions/:id/
func (h *TaskHandler) GetCompletion(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_completion_id")
@@ -304,7 +354,10 @@ func (h *TaskHandler) GetCompletion(c echo.Context) error {
// CreateCompletion handles POST /api/task-completions/
// Supports both JSON and multipart form data (for image uploads)
func (h *TaskHandler) CreateCompletion(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
userNow := middleware.GetUserNow(c)
var req requests.CreateTaskCompletionRequest
@@ -367,6 +420,10 @@ func (h *TaskHandler) CreateCompletion(c echo.Context) error {
}
}
if err := c.Validate(&req); err != nil {
return err
}
response, err := h.taskService.CreateCompletion(&req, user.ID, userNow)
if err != nil {
return err
@@ -376,7 +433,10 @@ func (h *TaskHandler) CreateCompletion(c echo.Context) error {
// UpdateCompletion handles PUT /api/task-completions/:id/
func (h *TaskHandler) UpdateCompletion(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_completion_id")
@@ -386,6 +446,9 @@ func (h *TaskHandler) UpdateCompletion(c echo.Context) error {
if err := c.Bind(&req); err != nil {
return apperrors.BadRequest("error.invalid_request")
}
if err := c.Validate(&req); err != nil {
return err
}
response, err := h.taskService.UpdateCompletion(uint(completionID), user.ID, &req)
if err != nil {
@@ -396,7 +459,10 @@ func (h *TaskHandler) UpdateCompletion(c echo.Context) error {
// DeleteCompletion handles DELETE /api/task-completions/:id/
func (h *TaskHandler) DeleteCompletion(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return apperrors.BadRequest("error.invalid_completion_id")