Simplify admin publishing pipeline
This commit is contained in:
parent
13e7e4026d
commit
9186801c7f
37 changed files with 750 additions and 3367 deletions
|
|
@ -3,6 +3,7 @@ package admin
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
|
@ -25,6 +26,25 @@ type PostListOptions struct {
|
|||
Offset int
|
||||
}
|
||||
|
||||
type AuditLogInput struct {
|
||||
ActorID *string
|
||||
ActorUsername string
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
IPAddress string
|
||||
UserAgent string
|
||||
Details map[string]any
|
||||
}
|
||||
|
||||
type AuditLogListOptions struct {
|
||||
Action string
|
||||
ResourceType string
|
||||
Query string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type PostInput struct {
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
|
|
@ -389,28 +409,6 @@ WHERE id = $1`, id))
|
|||
return job, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateAsset(ctx context.Context, asset Asset) (Asset, error) {
|
||||
created, err := scanAsset(s.db.QueryRow(ctx, `
|
||||
INSERT INTO assets (path, original_name, mime_type, size_bytes, sha256)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (path)
|
||||
DO UPDATE SET original_name = excluded.original_name,
|
||||
mime_type = excluded.mime_type,
|
||||
size_bytes = excluded.size_bytes,
|
||||
sha256 = excluded.sha256
|
||||
RETURNING id, path, original_name, mime_type, size_bytes, sha256, created_at, created_by`,
|
||||
asset.Path,
|
||||
asset.OriginalName,
|
||||
asset.MimeType,
|
||||
asset.SizeBytes,
|
||||
asset.SHA256,
|
||||
))
|
||||
if err != nil {
|
||||
return Asset{}, fmt.Errorf("create asset: %w", err)
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *Store) PublishedPostsForExport(ctx context.Context) ([]Post, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, slug, title, summary, body_markdown, status, cover, version,
|
||||
|
|
@ -473,6 +471,114 @@ WHERE id = $1`, id, log, message)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateAuditLog(ctx context.Context, input AuditLogInput) error {
|
||||
action := strings.TrimSpace(input.Action)
|
||||
if action == "" {
|
||||
return errors.New("audit action is required")
|
||||
}
|
||||
details := input.Details
|
||||
if details == nil {
|
||||
details = map[string]any{}
|
||||
}
|
||||
detailsJSON, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode audit details: %w", err)
|
||||
}
|
||||
_, err = s.db.Exec(ctx, `
|
||||
INSERT INTO audit_logs (actor_id, actor_username, action, resource_type, resource_id, ip_address, user_agent, details)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
|
||||
input.ActorID,
|
||||
input.ActorUsername,
|
||||
action,
|
||||
strings.TrimSpace(input.ResourceType),
|
||||
strings.TrimSpace(input.ResourceID),
|
||||
strings.TrimSpace(input.IPAddress),
|
||||
strings.TrimSpace(input.UserAgent),
|
||||
string(detailsJSON),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create audit log: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListAuditLogs(ctx context.Context, opts AuditLogListOptions) ([]AuditLog, error) {
|
||||
limit := opts.Limit
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
offset := opts.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
args := []any{limit, offset}
|
||||
where := "true"
|
||||
if strings.TrimSpace(opts.Action) != "" {
|
||||
args = append(args, strings.TrimSpace(opts.Action))
|
||||
where += fmt.Sprintf(" AND action = $%d", len(args))
|
||||
}
|
||||
if strings.TrimSpace(opts.ResourceType) != "" {
|
||||
args = append(args, strings.TrimSpace(opts.ResourceType))
|
||||
where += fmt.Sprintf(" AND resource_type = $%d", len(args))
|
||||
}
|
||||
if strings.TrimSpace(opts.Query) != "" {
|
||||
args = append(args, "%"+strings.TrimSpace(opts.Query)+"%")
|
||||
where += fmt.Sprintf(` AND (
|
||||
actor_username ILIKE $%d OR action ILIKE $%d OR resource_type ILIKE $%d OR resource_id ILIKE $%d OR details::text ILIKE $%d
|
||||
)`, len(args), len(args), len(args), len(args), len(args))
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, actor_id, actor_username, action, resource_type, resource_id, ip_address, user_agent, details, created_at
|
||||
FROM audit_logs
|
||||
WHERE `+where+`
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AuditLog
|
||||
for rows.Next() {
|
||||
log, err := scanAuditLog(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("audit log rows: %w", err)
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (s *Store) CountAuditLogs(ctx context.Context, opts AuditLogListOptions) (int, error) {
|
||||
args := []any{}
|
||||
where := "true"
|
||||
if strings.TrimSpace(opts.Action) != "" {
|
||||
args = append(args, strings.TrimSpace(opts.Action))
|
||||
where += fmt.Sprintf(" AND action = $%d", len(args))
|
||||
}
|
||||
if strings.TrimSpace(opts.ResourceType) != "" {
|
||||
args = append(args, strings.TrimSpace(opts.ResourceType))
|
||||
where += fmt.Sprintf(" AND resource_type = $%d", len(args))
|
||||
}
|
||||
if strings.TrimSpace(opts.Query) != "" {
|
||||
args = append(args, "%"+strings.TrimSpace(opts.Query)+"%")
|
||||
where += fmt.Sprintf(` AND (
|
||||
actor_username ILIKE $%d OR action ILIKE $%d OR resource_type ILIKE $%d OR resource_id ILIKE $%d OR details::text ILIKE $%d
|
||||
)`, len(args), len(args), len(args), len(args), len(args))
|
||||
}
|
||||
|
||||
var total int
|
||||
if err := s.db.QueryRow(ctx, `SELECT count(*) FROM audit_logs WHERE `+where, args...).Scan(&total); err != nil {
|
||||
return 0, fmt.Errorf("count audit logs: %w", err)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func validatePostInput(input PostInput, creating bool) error {
|
||||
if strings.TrimSpace(input.Title) == "" {
|
||||
return errors.New("title is required")
|
||||
|
|
@ -780,24 +886,35 @@ func scanBuildJob(row postScanner) (BuildJob, error) {
|
|||
return job, nil
|
||||
}
|
||||
|
||||
func scanAsset(row postScanner) (Asset, error) {
|
||||
var asset Asset
|
||||
var createdBy sql.NullString
|
||||
func scanAuditLog(row postScanner) (AuditLog, error) {
|
||||
var log AuditLog
|
||||
var actorID sql.NullString
|
||||
var details []byte
|
||||
err := row.Scan(
|
||||
&asset.ID,
|
||||
&asset.Path,
|
||||
&asset.OriginalName,
|
||||
&asset.MimeType,
|
||||
&asset.SizeBytes,
|
||||
&asset.SHA256,
|
||||
&asset.CreatedAt,
|
||||
&createdBy,
|
||||
&log.ID,
|
||||
&actorID,
|
||||
&log.ActorUsername,
|
||||
&log.Action,
|
||||
&log.ResourceType,
|
||||
&log.ResourceID,
|
||||
&log.IPAddress,
|
||||
&log.UserAgent,
|
||||
&details,
|
||||
&log.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Asset{}, err
|
||||
return AuditLog{}, err
|
||||
}
|
||||
asset.CreatedBy = nullStringPtr(createdBy)
|
||||
return asset, nil
|
||||
log.ActorID = nullStringPtr(actorID)
|
||||
if len(details) > 0 {
|
||||
if err := json.Unmarshal(details, &log.Details); err != nil {
|
||||
return AuditLog{}, fmt.Errorf("decode audit details: %w", err)
|
||||
}
|
||||
}
|
||||
if log.Details == nil {
|
||||
log.Details = map[string]any{}
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func nullTimePtr(value sql.NullTime) *time.Time {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue