gohorsejobs/backend/internal/services/activity_log_service.go
Tiago Yamamoto 9ee9f6855c feat: implementar múltiplas features
Backend:
- Password reset flow (forgot/reset endpoints, tokens table)
- Profile management (PUT /users/me, skills, experience, education)
- Tickets system (CRUD, messages, stats)
- Activity logs (list, stats)
- Document validator (CNPJ, CPF, EIN support)
- Input sanitizer (XSS prevention)
- Full-text search em vagas (plainto_tsquery)
- Filtros avançados (location, salary, workMode)
- Ordenação (date, salary, relevance)

Frontend:
- Forgot/Reset password pages
- Candidate profile edit page
- Sanitize utilities (sanitize.ts)

Backoffice:
- TicketsModule proxy
- ActivityLogsModule proxy
- Dockerfile otimizado (multi-stage, non-root, healthcheck)

Migrations:
- 013: Profile fields to users
- 014: Password reset tokens
- 015: Tickets table
- 016: Activity logs table
2025-12-27 11:19:47 -03:00

134 lines
3.7 KiB
Go

package services
import (
"database/sql"
"encoding/json"
"time"
"github.com/rede5/gohorsejobs/backend/internal/models"
)
type ActivityLogService struct {
db *sql.DB
}
func NewActivityLogService(db *sql.DB) *ActivityLogService {
return &ActivityLogService{db: db}
}
// Log creates a new activity log entry
func (s *ActivityLogService) Log(userID *int, tenantID *string, action string, resourceType, resourceID *string, description *string, metadata map[string]interface{}, ipAddress, userAgent *string) error {
var metadataJSON []byte
if metadata != nil {
metadataJSON, _ = json.Marshal(metadata)
}
query := `
INSERT INTO activity_logs (user_id, tenant_id, action, resource_type, resource_id, description, metadata, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
_, err := s.db.Exec(query, userID, tenantID, action, resourceType, resourceID, description, metadataJSON, ipAddress, userAgent)
return err
}
// List lists activity logs with filters
func (s *ActivityLogService) List(filter models.ActivityLogFilter) ([]models.ActivityLog, error) {
query := `
SELECT al.id, al.user_id, al.tenant_id, al.action, al.resource_type, al.resource_id,
al.description, al.metadata, al.ip_address, al.user_agent, al.created_at,
u.full_name as user_name
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE ($1::int IS NULL OR al.user_id = $1)
AND ($2::varchar IS NULL OR al.tenant_id = $2)
AND ($3::varchar IS NULL OR al.action = $3)
AND ($4::varchar IS NULL OR al.resource_type = $4)
AND ($5::timestamp IS NULL OR al.created_at >= $5)
AND ($6::timestamp IS NULL OR al.created_at <= $6)
ORDER BY al.created_at DESC
LIMIT $7 OFFSET $8
`
limit := filter.Limit
if limit == 0 {
limit = 50
}
rows, err := s.db.Query(query,
filter.UserID, filter.TenantID, filter.Action, filter.ResourceType,
filter.StartDate, filter.EndDate, limit, filter.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []models.ActivityLog
for rows.Next() {
var log models.ActivityLog
err := rows.Scan(
&log.ID, &log.UserID, &log.TenantID, &log.Action, &log.ResourceType, &log.ResourceID,
&log.Description, &log.Metadata, &log.IPAddress, &log.UserAgent, &log.CreatedAt,
&log.UserName,
)
if err != nil {
return nil, err
}
logs = append(logs, log)
}
return logs, nil
}
// GetStats gets activity log statistics
func (s *ActivityLogService) GetStats() (*models.ActivityLogStats, error) {
stats := &models.ActivityLogStats{}
now := time.Now()
// Counts
countQuery := `
SELECT
COUNT(*) FILTER (WHERE created_at >= $1) as today,
COUNT(*) FILTER (WHERE created_at >= $2) as this_week,
COUNT(*) FILTER (WHERE created_at >= $3) as this_month
FROM activity_logs
`
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
startOfWeek := startOfDay.AddDate(0, 0, -int(now.Weekday()))
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
err := s.db.QueryRow(countQuery, startOfDay, startOfWeek, startOfMonth).
Scan(&stats.TotalToday, &stats.TotalThisWeek, &stats.TotalThisMonth)
if err != nil {
return nil, err
}
// Top actions
topActionsQuery := `
SELECT action, COUNT(*) as count
FROM activity_logs
WHERE created_at >= $1
GROUP BY action
ORDER BY count DESC
LIMIT 10
`
rows, err := s.db.Query(topActionsQuery, startOfWeek)
if err == nil {
defer rows.Close()
for rows.Next() {
var ac models.ActionCount
if err := rows.Scan(&ac.Action, &ac.Count); err == nil {
stats.TopActions = append(stats.TopActions, ac)
}
}
}
// Recent activity (last 20)
recentLogs, _ := s.List(models.ActivityLogFilter{Limit: 20})
stats.RecentActivity = recentLogs
return stats, nil
}