fix: port mailbox validation from im server
This commit is contained in:
+23
-1
@@ -25,6 +25,11 @@ type API struct {
|
||||
|
||||
const workbenchEventPublishTimeout = 15 * time.Second
|
||||
|
||||
const (
|
||||
maxMailboxSubjectLength = 200
|
||||
maxMailboxContentLength = 5000
|
||||
)
|
||||
|
||||
func New(st *store.Store, authenticator *auth.Authenticator, publisher events.Publisher) *API {
|
||||
if publisher == nil {
|
||||
publisher = events.NoopPublisher{}
|
||||
@@ -879,9 +884,14 @@ func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth
|
||||
if !decodeBody(w, r, &body) {
|
||||
return
|
||||
}
|
||||
subject := strings.TrimSpace(body.Subject)
|
||||
content := strings.TrimSpace(body.Content)
|
||||
if ok := validateMailboxSubmission(w, r, subject, content); !ok {
|
||||
return
|
||||
}
|
||||
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
||||
submission, err := a.store.CreateMailboxSubmission(r.Context(), p.UserID, store.MailboxSubmission{
|
||||
SubmissionID: "mb_" + randomToken(12), Subject: strings.TrimSpace(body.Subject), Content: strings.TrimSpace(body.Content),
|
||||
SubmissionID: "mb_" + randomToken(12), Subject: subject, Content: content,
|
||||
Anonymous: body.Anonymous, AttachmentFileIDs: string(attachments), NoticeVersion: strings.TrimSpace(body.NoticeVersion),
|
||||
}, idempotencyKey)
|
||||
if err != nil {
|
||||
@@ -896,6 +906,18 @@ func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth
|
||||
writeSuccess(w, r, mailboxSubmissionPayload(*submission))
|
||||
}
|
||||
|
||||
func validateMailboxSubmission(w http.ResponseWriter, r *http.Request, subject, content string) bool {
|
||||
if len([]rune(subject)) > maxMailboxSubjectLength {
|
||||
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", map[string]any{"field": "subject", "max_length": maxMailboxSubjectLength})
|
||||
return false
|
||||
}
|
||||
if len([]rune(content)) > maxMailboxContentLength {
|
||||
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", map[string]any{"field": "content", "max_length": maxMailboxContentLength})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *API) publishWorkbenchEvent(w http.ResponseWriter, r *http.Request, p *auth.Principal, module, action, resourceID string, targets []int64, title, summary string, data map[string]any) bool {
|
||||
if a.events == nil {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.118im.com/im_team/im_workbench/internal/store"
|
||||
)
|
||||
|
||||
func TestValidateMailboxSubmissionRejectsTooLongContent(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/workbench/executive-mailbox/submissions", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
if validateMailboxSubmission(rec, req, "建议", strings.Repeat("a", maxMailboxContentLength+1)) {
|
||||
t.Fatal("validateMailboxSubmission returned true for too-long content")
|
||||
}
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body apiError
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if body.Reason != reasonValidationFailed {
|
||||
t.Fatalf("reason=%q body=%s", body.Reason, rec.Body.String())
|
||||
}
|
||||
if body.Details["field"] != "content" {
|
||||
t.Fatalf("details=%v", body.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteStoreErrorMapsPolicyToValidationFailed(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/workbench/executive-mailbox/submissions", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
writeStoreError(rec, req, store.ErrPolicy)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body apiError
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if body.Reason != reasonValidationFailed {
|
||||
t.Fatalf("reason=%q body=%s", body.Reason, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
reasonForbidden = "FORBIDDEN"
|
||||
reasonNotFound = "RESOURCE_NOT_FOUND"
|
||||
reasonConflict = "CONFLICT"
|
||||
reasonValidationFailed = "VALIDATION_FAILED"
|
||||
reasonInternal = "INTERNAL"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,8 @@ func writeStoreError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
writeError(w, r, http.StatusForbidden, 30041, reasonForbidden, "forbidden", nil)
|
||||
case errors.Is(err, ErrConflict), errors.Is(err, store.ErrConflict):
|
||||
writeError(w, r, http.StatusConflict, 30040, reasonConflict, "conflict", nil)
|
||||
case errors.Is(err, store.ErrPolicy):
|
||||
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", nil)
|
||||
default:
|
||||
writeError(w, r, http.StatusInternalServerError, 90000, reasonInternal, "internal error", nil)
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrConflict = errors.New("conflict")
|
||||
ErrPolicy = errors.New("policy violation")
|
||||
)
|
||||
|
||||
+17
-1
@@ -719,8 +719,24 @@ func (s *Store) MailboxConfig(ctx context.Context) (*MailboxConfig, error) {
|
||||
}
|
||||
|
||||
func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submission MailboxSubmission, idempotencyKey string) (*MailboxSubmission, error) {
|
||||
cfg, err := s.MailboxConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submission.Subject = strings.TrimSpace(submission.Subject)
|
||||
submission.Content = strings.TrimSpace(submission.Content)
|
||||
if !cfg.Enabled || (submission.Anonymous && !cfg.AllowAnonymous) {
|
||||
return nil, ErrPolicy
|
||||
}
|
||||
maxLength := cfg.MaxLength
|
||||
if maxLength <= 0 {
|
||||
maxLength = 5000
|
||||
}
|
||||
if len([]rune(submission.Subject)) > 200 || len([]rune(submission.Content)) > maxLength {
|
||||
return nil, ErrPolicy
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
_, err := s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at)
|
||||
_, err = s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`, submission.SubmissionID, userID, submission.Subject, submission.Content, submission.Anonymous, submission.AttachmentFileIDs, submission.NoticeVersion, idempotencyKey, "received", now)
|
||||
if isDuplicate(err) && idempotencyKey != "" {
|
||||
var existing MailboxSubmission
|
||||
|
||||
Reference in New Issue
Block a user