Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@

## 0.13.0 - 2026-10-07

### Added
- Add POST /feedback anonymous GitHub issue proxy
- Wire feedback rate-limiter to Redis cache



## 0.12.0 - 2026-09-09

### Added
Expand Down
4 changes: 3 additions & 1 deletion cmd/admin-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/sweetrpg/admin-api/constants"
"github.com/sweetrpg/admin-api/docs"
"github.com/sweetrpg/admin-api/feedback"
"github.com/sweetrpg/admin-api/models"
"github.com/sweetrpg/admin-api/server"
apiconstants "github.com/sweetrpg/api-core.go/constants"
Expand Down Expand Up @@ -111,7 +112,8 @@ func main() {
r.Use(ratelimit.Middleware(redisPool, ratelimit.DefaultOptions()))

authzClient := authz.NewClient(util.GetEnv(constants.AUTH_API_URL, ""), util.GetEnv(constants.USERS_API_URL, ""))
server.SetupHandlers(r, authzClient)
issueCreator := feedback.NewGitHubIssueClient(util.GetEnv(constants.GITHUB_FEEDBACK_TOKEN, ""))
server.SetupHandlers(r, authzClient, issueCreator, feedback.RateLimitMiddleware(redisPool))

_ = r.Run(util.GetEnv(apiconstants.BIND_ADDRESS, ":8000"))
}
Expand Down
34 changes: 34 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package constants

import "time"

// Environment variable names
const (
HEALTH_TOKEN = "HEALTH_TOKEN"
Expand All @@ -16,6 +18,16 @@ const (
// subject to its canonical users._id for write-path created_by/updated_by stamps. See
// canonical-user-ids-across-services in sweetrpg/platform.
USERS_API_URL = "USERS_API_URL"

// GITHUB_FEEDBACK_TOKEN authenticates the feedback package's GitHub Issues API calls.
// Scoped to Issues:write on sweetrpg/platform only - see design.md.
GITHUB_FEEDBACK_TOKEN = "GITHUB_FEEDBACK_TOKEN"

// RATE_LIMIT_FEEDBACK and RATE_LIMIT_FEEDBACK_WINDOW_SECONDS tune the feedback-specific
// rate-limit tier, stricter than api-core.go/ratelimit's standard tier. See
// feedback.RateLimitMiddleware.
RATE_LIMIT_FEEDBACK = "RATE_LIMIT_FEEDBACK"
RATE_LIMIT_FEEDBACK_WINDOW_SECONDS = "RATE_LIMIT_FEEDBACK_WINDOW_SECONDS"
)

// Value constants
Expand All @@ -42,4 +54,26 @@ const (
// AdminActionAuditLogCollection is the MongoDB collection name for write-route
// audit records.
AdminActionAuditLogCollection = "admin_action_audit_logs"

// RateLimitFeedbackDefault and RateLimitFeedbackWindowDefault are the feedback tier's
// fallback budget when RATE_LIMIT_FEEDBACK[_WINDOW_SECONDS] aren't set: 5 submissions
// per 5 minutes per client, well under the platform-default standard tier (see
// design.md's "dedicated feedback-specific rate-limit tier" decision).
RateLimitFeedbackDefault = 5
RateLimitFeedbackWindowDefault = 300

// FeedbackTitleMaxLength and FeedbackBodyMaxLength cap submitted text, rejected with a
// 400 when exceeded (see the anonymous-feedback-submission spec's "Oversized submission
// rejected" scenario).
FeedbackTitleMaxLength = 200
FeedbackBodyMaxLength = 10000

// FeedbackMinFillTime is the minimum time between the form rendering (client-reported
// rendered_at) and submission for a human to plausibly have filled it out. Submissions
// faster than this are silently discarded - see design.md's honeypot/fill-time decision.
FeedbackMinFillTime = 3 * time.Second

// FeedbackSourceLabel is applied to every issue created from a feedback submission, so a
// spam wave can be bulk-filtered/closed without affecting other issues.
FeedbackSourceLabel = "source:in-app-feedback"
)
103 changes: 103 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,64 @@ const docTemplate = `{
}
}
},
"/feedback": {
"post": {
"description": "Accept an anonymous bug report or feature request and file it as a GitHub Issue on sweetrpg/platform, labeled source:in-app-feedback plus bug or feature. No GitHub credentials are ever requested from or exposed to the caller. Subject to a feedback-specific rate limit stricter than this service's default.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feedback"
],
"summary": "Submit feedback",
"parameters": [
{
"description": "Feedback submission",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.createFeedbackRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.feedbackAcceptedResponse"
}
},
"400": {
"description": "missing/invalid type, title, or body, or title/body exceeds the length cap",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"429": {
"description": "feedback-specific rate limit exceeded",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"502": {
"description": "GitHub API call failed or timed out",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"503": {
"description": "rate-limit backend unavailable",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
}
}
}
},
"/maintenance-modes": {
"get": {
"description": "Returns every maintenance-mode record (platform and every known service scope) regardless of enabled state, for admin-web's management view.",
Expand Down Expand Up @@ -1031,6 +1089,51 @@ const docTemplate = `{
}
}
},
"server.createFeedbackRequest": {
"type": "object",
"required": [
"body",
"title",
"type"
],
"properties": {
"body": {
"type": "string",
"example": "Searching for 9780786965601 returns zero results, even though the volume exists in the catalog."
},
"rendered_at": {
"type": "string"
},
"reporter_email": {
"type": "string",
"example": "reporter@example.com"
},
"source": {
"type": "string",
"example": "catalog-web:/catalog/search"
},
"title": {
"type": "string",
"example": "Catalog search returns no results for exact ISBN match"
},
"type": {
"type": "string",
"example": "bug"
},
"website": {
"type": "string"
}
}
},
"server.feedbackAcceptedResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "accepted"
}
}
},
"server.maintenanceModeRequest": {
"type": "object",
"required": [
Expand Down
103 changes: 103 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,64 @@
}
}
},
"/feedback": {
"post": {
"description": "Accept an anonymous bug report or feature request and file it as a GitHub Issue on sweetrpg/platform, labeled source:in-app-feedback plus bug or feature. No GitHub credentials are ever requested from or exposed to the caller. Subject to a feedback-specific rate limit stricter than this service's default.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feedback"
],
"summary": "Submit feedback",
"parameters": [
{
"description": "Feedback submission",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.createFeedbackRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.feedbackAcceptedResponse"
}
},
"400": {
"description": "missing/invalid type, title, or body, or title/body exceeds the length cap",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"429": {
"description": "feedback-specific rate limit exceeded",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"502": {
"description": "GitHub API call failed or timed out",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
},
"503": {
"description": "rate-limit backend unavailable",
"schema": {
"$ref": "#/definitions/vo.ErrorVO"
}
}
}
}
},
"/maintenance-modes": {
"get": {
"description": "Returns every maintenance-mode record (platform and every known service scope) regardless of enabled state, for admin-web's management view.",
Expand Down Expand Up @@ -1023,6 +1081,51 @@
}
}
},
"server.createFeedbackRequest": {
"type": "object",
"required": [
"body",
"title",
"type"
],
"properties": {
"body": {
"type": "string",
"example": "Searching for 9780786965601 returns zero results, even though the volume exists in the catalog."
},
"rendered_at": {
"type": "string"
},
"reporter_email": {
"type": "string",
"example": "reporter@example.com"
},
"source": {
"type": "string",
"example": "catalog-web:/catalog/search"
},
"title": {
"type": "string",
"example": "Catalog search returns no results for exact ISBN match"
},
"type": {
"type": "string",
"example": "bug"
},
"website": {
"type": "string"
}
}
},
"server.feedbackAcceptedResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "accepted"
}
}
},
"server.maintenanceModeRequest": {
"type": "object",
"required": [
Expand Down
Loading
Loading