@@ -21,6 +21,7 @@ import ( |
| 21 | "github.com/jackc/pgx/v5/pgtype" | 21 | "github.com/jackc/pgx/v5/pgtype" |
| 22 | "github.com/jackc/pgx/v5/pgxpool" | 22 | "github.com/jackc/pgx/v5/pgxpool" |
| 23 | | 23 | |
| | 24 | + "github.com/tenseleyFlow/shithub/internal/auth/audit" |
| 24 | "github.com/tenseleyFlow/shithub/internal/auth/throttle" | 25 | "github.com/tenseleyFlow/shithub/internal/auth/throttle" |
| 25 | issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc" | 26 | issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc" |
| 26 | mdrender "github.com/tenseleyFlow/shithub/internal/markdown" | 27 | mdrender "github.com/tenseleyFlow/shithub/internal/markdown" |
@@ -33,6 +34,12 @@ type Deps struct { |
| 33 | Pool *pgxpool.Pool | 34 | Pool *pgxpool.Pool |
| 34 | Limiter *throttle.Limiter | 35 | Limiter *throttle.Limiter |
| 35 | Logger *slog.Logger | 36 | Logger *slog.Logger |
| | 37 | + // Audit is optional; when non-nil, state-changing orchestrator |
| | 38 | + // calls (SetState, SetLock, AddComment) record an audit row. The |
| | 39 | + // repo lifecycle package writes audit rows directly via deps.Audit; |
| | 40 | + // this field ensures issues/PR mutations are equally traceable |
| | 41 | + // (S00-S25 audit, M). |
| | 42 | + Audit *audit.Recorder |
| 36 | } | 43 | } |
| 37 | | 44 | |
| 38 | // Errors returned by the orchestrator. Handlers map these to status | 45 | // Errors returned by the orchestrator. Handlers map these to status |
@@ -117,7 +124,7 @@ func Create(ctx context.Context, deps Deps, p CreateParams) (issuesdb.Issue, err |
| 117 | } | 124 | } |
| 118 | | 125 | |
| 119 | // Render markdown for the cached body html. | 126 | // Render markdown for the cached body html. |
| 120 | - html, _ := mdrender.RenderHTML([]byte(p.Body)) | 127 | + html := renderBodyHTML(ctx, deps, p.Body) |
| 121 | row.BodyHtmlCached = pgtype.Text{String: html, Valid: html != ""} | 128 | row.BodyHtmlCached = pgtype.Text{String: html, Valid: html != ""} |
| 122 | | 129 | |
| 123 | if err := q.UpdateIssueTitleBody(ctx, tx, issuesdb.UpdateIssueTitleBodyParams{ | 130 | if err := q.UpdateIssueTitleBody(ctx, tx, issuesdb.UpdateIssueTitleBodyParams{ |
@@ -178,7 +185,7 @@ func AddComment(ctx context.Context, deps Deps, p CommentCreateParams) (issuesdb |
| 178 | return issuesdb.IssueComment{}, ErrIssueLocked | 185 | return issuesdb.IssueComment{}, ErrIssueLocked |
| 179 | } | 186 | } |
| 180 | | 187 | |
| 181 | - html, _ := mdrender.RenderHTML([]byte(body)) | 188 | + html := renderBodyHTML(ctx, deps, body) |
| 182 | | 189 | |
| 183 | tx, err := deps.Pool.Begin(ctx) | 190 | tx, err := deps.Pool.Begin(ctx) |
| 184 | if err != nil { | 191 | if err != nil { |
@@ -209,6 +216,11 @@ func AddComment(ctx context.Context, deps Deps, p CommentCreateParams) (issuesdb |
| 209 | return issuesdb.IssueComment{}, err | 216 | return issuesdb.IssueComment{}, err |
| 210 | } | 217 | } |
| 211 | committed = true | 218 | committed = true |
| | 219 | + if deps.Audit != nil { |
| | 220 | + _ = deps.Audit.Record(ctx, deps.Pool, p.AuthorUserID, |
| | 221 | + audit.ActionIssueCommentCreated, audit.TargetIssue, p.IssueID, |
| | 222 | + map[string]any{"comment_id": c.ID}) |
| | 223 | + } |
| 212 | return c, nil | 224 | return c, nil |
| 213 | } | 225 | } |
| 214 | | 226 | |
@@ -262,6 +274,11 @@ func SetState(ctx context.Context, deps Deps, actorUserID, issueID int64, newSta |
| 262 | return err | 274 | return err |
| 263 | } | 275 | } |
| 264 | committed = true | 276 | committed = true |
| | 277 | + if deps.Audit != nil { |
| | 278 | + _ = deps.Audit.Record(ctx, deps.Pool, actorUserID, |
| | 279 | + audit.ActionIssueStateChanged, audit.TargetIssue, issueID, |
| | 280 | + map[string]any{"state": newState, "reason": reason}) |
| | 281 | + } |
| 265 | return nil | 282 | return nil |
| 266 | } | 283 | } |
| 267 | | 284 | |
@@ -308,5 +325,25 @@ func SetLock(ctx context.Context, deps Deps, actorUserID, issueID int64, locked |
| 308 | return err | 325 | return err |
| 309 | } | 326 | } |
| 310 | committed = true | 327 | committed = true |
| | 328 | + if deps.Audit != nil { |
| | 329 | + _ = deps.Audit.Record(ctx, deps.Pool, actorUserID, |
| | 330 | + audit.ActionIssueLockChanged, audit.TargetIssue, issueID, |
| | 331 | + map[string]any{"locked": locked, "reason": reason}) |
| | 332 | + } |
| 311 | return nil | 333 | return nil |
| 312 | } | 334 | } |
| | 335 | + |
| | 336 | +// renderBodyHTML wraps markdown.RenderHTML with a logger-aware error |
| | 337 | +// path. Body length is bounded upstream (orchestrator validation + |
| | 338 | +// DB CHECK at 65535), so ErrInputTooLarge is structurally impossible |
| | 339 | +// here — but if it ever fires, log loudly: it means a precondition |
| | 340 | +// somewhere upstream regressed. The audit (S00-S25, M) flagged the |
| | 341 | +// `_`-discard pattern as the kind of slop where a real bug could hide. |
| | 342 | +func renderBodyHTML(ctx context.Context, deps Deps, body string) string { |
| | 343 | + html, err := mdrender.RenderHTML([]byte(body)) |
| | 344 | + if err != nil && deps.Logger != nil { |
| | 345 | + deps.Logger.WarnContext(ctx, "issues: markdown render failed", |
| | 346 | + "error", err, "body_bytes", len(body)) |
| | 347 | + } |
| | 348 | + return html |
| | 349 | +} |