refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -94,8 +94,11 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
services = append(services, cs)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if services == nil {
|
||||
services = []CustomService{}
|
||||
}
|
||||
@@ -153,6 +156,10 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
services = append(services, cs)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Run count query ONLY after consuming the data query result set,
|
||||
// so pgx does not return "conn busy" on the same transaction.
|
||||
@@ -175,7 +182,6 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
nextCursor = &cursor
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if services == nil {
|
||||
services = []CustomService{}
|
||||
}
|
||||
@@ -251,7 +257,6 @@ func CreateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
cs.LastUsedAt = &lastUsedAt.Time
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(cs)
|
||||
}
|
||||
@@ -293,7 +298,6 @@ func GetCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
cs.LastUsedAt = &lastUsedAt.Time
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cs)
|
||||
}
|
||||
|
||||
@@ -339,6 +343,23 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Whitelist validation: only allow known column names to prevent SQL injection
|
||||
// via dynamic map keys used as column identifiers.
|
||||
var allowedCustomServiceFields = map[string]bool{
|
||||
"name": true,
|
||||
"description": true,
|
||||
"price": true,
|
||||
"duration_minutes": true,
|
||||
"minimum_age_required": true,
|
||||
"notes": true,
|
||||
}
|
||||
for field := range updates {
|
||||
if !allowedCustomServiceFields[field] {
|
||||
http.Error(w, "Invalid field: "+field, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setClauses := make([]string, 0, len(updates))
|
||||
args := make([]interface{}, 0, len(updates)+1)
|
||||
argIdx := 1
|
||||
@@ -351,7 +372,14 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
query := "UPDATE custom_services SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + strconv.Itoa(argIdx)
|
||||
|
||||
result, err := db.Conn.Exec(r.Context(), query, args...)
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
result, err := tx.Exec(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update custom service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -361,7 +389,11 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"})
|
||||
}
|
||||
|
||||
@@ -440,7 +472,6 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Custom service promoted to regular service",
|
||||
"new_service_id": newServiceID,
|
||||
@@ -470,7 +501,14 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := db.Conn.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
result, err := tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete custom service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -480,7 +518,11 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user