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
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
76 changes: 76 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,39 @@ definitions:
- scope_type
- severity
type: object
server.createFeedbackRequest:
properties:
body:
example: Searching for 9780786965601 returns zero results, even though the
volume exists in the catalog.
type: string
rendered_at:
type: string
reporter_email:
example: reporter@example.com
type: string
source:
example: catalog-web:/catalog/search
type: string
title:
example: Catalog search returns no results for exact ISBN match
type: string
type:
example: bug
type: string
website:
type: string
required:
- body
- title
- type
type: object
server.feedbackAcceptedResponse:
properties:
status:
example: accepted
type: string
type: object
server.maintenanceModeRequest:
properties:
description:
Expand Down Expand Up @@ -577,6 +610,49 @@ paths:
summary: Update a banner message
tags:
- banners
/feedback:
post:
consumes:
- application/json
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.
parameters:
- description: Feedback submission
in: body
name: body
required: true
schema:
$ref: '#/definitions/server.createFeedbackRequest'
produces:
- application/json
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'
summary: Submit feedback
tags:
- feedback
/maintenance-modes:
get:
description: Returns every maintenance-mode record (platform and every known
Expand Down
Loading
Loading