1. chat.EnsureChat: ON CONFLICT DO UPDATE (race-safe) 2. chat.Send: tx.Begin/Commit (atomic INSERT message + UPDATE last_msg_at) 3. chat.MarkRead: добавил member-check (NOT a member -> 403) 4. chat.Message: +ReadAt field 5. migrations/0006: read_at column on messages 6. locations.Nearby: bounding-box prefilter + CTE (haversine только для отфильтрованных) 7. audit.Log: real metadata JSON passthrough (no more 'null' TODO) 8. main.go: slog JSON logger (был stdlib log) 9. WS: 'message_read' event от MarkRead 10. WS push: добавлены read/read_at/edited в message payload 11. Bumped v0.6.0
49 lines
1.5 KiB
Go
49 lines
1.5 KiB
Go
package audit
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
type Event struct {
|
||
ID int64 `json:"id"`
|
||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||
Action string `json:"action"`
|
||
TargetType string `json:"target_type,omitempty"`
|
||
TargetID string `json:"target_id,omitempty"`
|
||
Metadata []byte `json:"metadata,omitempty"`
|
||
IP string `json:"ip,omitempty"`
|
||
UserAgent string `json:"user_agent,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
type Repo struct {
|
||
pool *pgxpool.Pool
|
||
}
|
||
|
||
func NewRepo(pool *pgxpool.Pool) *Repo {
|
||
return &Repo{pool: pool}
|
||
}
|
||
|
||
func (r *Repo) Log(ctx context.Context, e *Event) error {
|
||
meta := []byte("null")
|
||
if len(e.Metadata) > 0 {
|
||
// Если Metadata уже валидный JSON — пишем как есть
|
||
// Иначе (хэштег, текст) — обернём в JSON-объект
|
||
meta = e.Metadata
|
||
// если начинается не с { или [, оборачиваем
|
||
if len(meta) == 0 || (meta[0] != '{' && meta[0] != '[' && meta[0] != '"') {
|
||
meta = []byte(`{"data":` + string(meta) + `}`)
|
||
}
|
||
}
|
||
return r.pool.QueryRow(ctx, `
|
||
INSERT INTO audit_log (user_id, action, target_type, target_id, metadata, ip, user_agent)
|
||
VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5::jsonb, NULLIF($6, '')::inet, $7)
|
||
RETURNING id, created_at`,
|
||
e.UserID, e.Action, e.TargetType, e.TargetID, string(meta), e.IP, e.UserAgent,
|
||
).Scan(&e.ID, &e.CreatedAt)
|
||
}
|