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
134 lines
3.7 KiB
Go
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
|
|
}
|