@johnnyboy11234/esg-idea-contest 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/backend/api/openapi.yaml +83 -0
  2. package/backend/cmd/server/main.go +35 -2
  3. package/backend/db/migration/V005__idea.sql +49 -0
  4. package/backend/db/migration/V006__login_lock.sql +15 -0
  5. package/backend/db/migration/V007__login_attempt.sql +15 -0
  6. package/backend/db/migration/V008__pending_file_deletion.sql +27 -0
  7. package/backend/db/migration/V009__login_throttle_audit.sql +13 -0
  8. package/backend/go.mod +17 -3
  9. package/backend/go.sum +48 -7
  10. package/backend/internal/account/account_test.go +6 -6
  11. package/backend/internal/account/integration_test.go +45 -9
  12. package/backend/internal/account/keyboard.go +75 -0
  13. package/backend/internal/account/keyboard_test.go +43 -0
  14. package/backend/internal/account/logging_integration_test.go +1 -1
  15. package/backend/internal/account/password.go +125 -0
  16. package/backend/internal/account/validate.go +4 -21
  17. package/backend/internal/api/api.gen.go +224 -0
  18. package/backend/internal/auth/internal/store/login_attempt.go +53 -0
  19. package/backend/internal/auth/internal/store/login_lock.go +77 -0
  20. package/backend/internal/auth/internal/store/login_throttle_audit.go +42 -0
  21. package/backend/internal/auth/internal/store/session.go +15 -7
  22. package/backend/internal/auth/login.go +16 -4
  23. package/backend/internal/auth/login_integration_test.go +14 -1
  24. package/backend/internal/auth/login_ip_integration_test.go +153 -0
  25. package/backend/internal/auth/login_lock_integration_test.go +205 -0
  26. package/backend/internal/auth/login_test.go +15 -0
  27. package/backend/internal/auth/login_throttle_audit_integration_test.go +88 -0
  28. package/backend/internal/auth/purge.go +93 -0
  29. package/backend/internal/auth/purge_integration_test.go +220 -0
  30. package/backend/internal/auth/purge_test.go +51 -0
  31. package/backend/internal/auth/session.go +0 -6
  32. package/backend/internal/auth/session_integration_test.go +3 -3
  33. package/backend/internal/auth/throttle.go +122 -0
  34. package/backend/internal/deadline/gate.go +80 -0
  35. package/backend/internal/deadline/gate_integration_test.go +67 -0
  36. package/backend/internal/deadline/gate_test.go +61 -0
  37. package/backend/internal/idea/create.go +352 -0
  38. package/backend/internal/idea/create_integration_test.go +650 -0
  39. package/backend/internal/idea/handler.go +148 -0
  40. package/backend/internal/idea/handler_integration_test.go +107 -0
  41. package/backend/internal/idea/integration_test.go +149 -0
  42. package/backend/internal/idea/internal/store/list.go +146 -0
  43. package/backend/internal/idea/internal/store/list_test.go +33 -0
  44. package/backend/internal/idea/internal/store/store.go +166 -0
  45. package/backend/internal/idea/list.go +166 -0
  46. package/backend/internal/idea/list_integration_test.go +252 -0
  47. package/backend/internal/idea/list_test.go +96 -0
  48. package/backend/internal/idea/service.go +53 -0
  49. package/backend/internal/server/api.go +17 -0
  50. package/backend/internal/server/public.go +1 -0
  51. package/backend/internal/server/slides.go +21 -0
  52. package/backend/internal/shared/config/config.go +44 -1
  53. package/backend/internal/shared/config/config_test.go +21 -13
  54. package/backend/internal/shared/db/intx_integration_test.go +179 -0
  55. package/backend/internal/shared/dbtest/dbtest.go +21 -3
  56. package/backend/internal/shared/dbtest/faults.go +162 -0
  57. package/backend/internal/shared/job/job.go +54 -0
  58. package/backend/internal/shared/job/job_test.go +105 -0
  59. package/backend/internal/shared/web/multipart.go +175 -0
  60. package/backend/internal/shared/web/multipart_test.go +169 -0
  61. package/backend/internal/shared/web/problem.go +1 -0
  62. package/backend/internal/shared/web/request.go +42 -1
  63. package/backend/internal/shared/web/web_test.go +36 -0
  64. package/backend/internal/storage/expectedfilename.go +43 -0
  65. package/backend/internal/storage/expectedfilename_test.go +16 -0
  66. package/backend/internal/storage/internal/store/pending.go +41 -0
  67. package/backend/internal/storage/minio.go +139 -0
  68. package/backend/internal/storage/minio_integration_test.go +87 -0
  69. package/backend/internal/storage/minio_test.go +84 -0
  70. package/backend/internal/storage/pending.go +144 -0
  71. package/backend/internal/storage/pending_integration_test.go +324 -0
  72. package/backend/internal/storage/pending_test.go +23 -0
  73. package/backend/internal/storage/validator.go +100 -0
  74. package/backend/internal/storage/validator_test.go +95 -0
  75. package/backend/test/contract/expected_filename_test.go +60 -0
  76. package/backend/test/contract/password_test.go +46 -0
  77. package/backend/test/matrix/load_test.go +12 -0
  78. package/backend/test/matrix/matrix_integration_test.go +86 -5
  79. package/backend/test/migration/pending_file_deletion_test.go +46 -0
  80. package/docs/API/api-endpoints.md +1 -1
  81. package/docs/API/error-codes.md +1 -1
  82. package/frontend/src/app/App.tsx +11 -2
  83. package/frontend/src/features/auth/IdleWarning.test.tsx +82 -0
  84. package/frontend/src/features/auth/IdleWarning.tsx +66 -0
  85. package/frontend/src/shared/api/activity.ts +20 -0
  86. package/frontend/src/shared/api/http.ts +2 -0
  87. package/frontend/src/shared/api/index.ts +1 -0
  88. package/frontend/src/shared/i18n/locales/en.ts +1 -0
  89. package/frontend/src/shared/i18n/locales/zh-TW.ts +1 -0
  90. package/package.json +1 -1
  91. package/testdata/expected-filename.json +93 -0
  92. package/testdata/password.json +425 -0
  93. package/testdata/permission-matrix.json +48 -0
  94. package/testdata/schema/expected-filename.schema.json +32 -0
  95. package/testdata/schema/password.schema.json +59 -0
  96. package/testdata/schema/permission-matrix.schema.json +21 -0
@@ -152,6 +152,39 @@ paths:
152
152
  - name: size
153
153
  in: query
154
154
  schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
155
+ - name: org
156
+ in: query
157
+ description: Closed set; an invalid value is 422 INVALID_ENUM (never ignored). Validated in the handler.
158
+ schema: { type: string }
159
+ - name: category
160
+ in: query
161
+ schema: { type: string }
162
+ - name: direction
163
+ in: query
164
+ schema: { type: string }
165
+ - name: leaderEmpNo
166
+ in: query
167
+ schema: { type: string }
168
+ - name: createdFrom
169
+ in: query
170
+ description: ISO 8601 instant with offset or Z; inclusive. Validated in the handler.
171
+ schema: { type: string }
172
+ - name: createdTo
173
+ in: query
174
+ description: ISO 8601 instant with offset or Z; exclusive. Validated in the handler.
175
+ schema: { type: string }
176
+ - name: q
177
+ in: query
178
+ description: Case-insensitive "name contains"; `%` and `_` are literal.
179
+ schema: { type: string }
180
+ - name: sort
181
+ in: query
182
+ description: createdAt (default) or name; anything else is 422 INVALID_ENUM. Validated in the handler.
183
+ schema: { type: string }
184
+ - name: order
185
+ in: query
186
+ description: asc or desc (default desc for createdAt, asc for name); anything else is 422 INVALID_ENUM. Validated in the handler.
187
+ schema: { type: string }
155
188
  responses:
156
189
  '200':
157
190
  description: One page of IdeaSummary
@@ -161,6 +194,42 @@ paths:
161
194
  '401': { $ref: '#/components/responses/Problem' }
162
195
  '422': { $ref: '#/components/responses/Problem' }
163
196
  '500': { $ref: '#/components/responses/Problem' }
197
+ post:
198
+ operationId: createIdea
199
+ tags: [ideas]
200
+ summary: Create an Idea with its slides (User only)
201
+ description: >
202
+ One atomic multipart request (PRD 6.6). Order: role 403, state 403, 400 (missing or repeated
203
+ part), 422 text fields (incl. FILENAME_TOO_LONG), file 413 / 422 EMPTY_FILE / 415 /
204
+ 422 FILENAME_MISMATCH, 409 IDEA_DUPLICATE, then upload (503 STORAGE_UNAVAILABLE). A transaction
205
+ whose outcome is unknown answers 503 SERVICE_UNAVAILABLE (the Idea may exist). The Leader is
206
+ the caller; any leader value in the form is ignored. The body is read by the handler, so the
207
+ generated wrapper does not bind it.
208
+ parameters:
209
+ - $ref: '#/components/parameters/CsrfToken'
210
+ requestBody:
211
+ required: true
212
+ content:
213
+ multipart/form-data:
214
+ schema: { $ref: '#/components/schemas/CreateIdeaForm' }
215
+ responses:
216
+ '201':
217
+ description: Created; Location is /api/v1/ideas/{id}
218
+ headers:
219
+ Location:
220
+ schema: { type: string }
221
+ content:
222
+ application/json:
223
+ schema: { $ref: '#/components/schemas/IdeaDetail' }
224
+ '400': { $ref: '#/components/responses/Problem' }
225
+ '401': { $ref: '#/components/responses/Problem' }
226
+ '403': { $ref: '#/components/responses/Problem' }
227
+ '409': { $ref: '#/components/responses/Problem' }
228
+ '413': { $ref: '#/components/responses/Problem' }
229
+ '415': { $ref: '#/components/responses/Problem' }
230
+ '422': { $ref: '#/components/responses/Problem' }
231
+ '500': { $ref: '#/components/responses/Problem' }
232
+ '503': { $ref: '#/components/responses/Problem' }
164
233
  /ideas/{id}:
165
234
  get:
166
235
  operationId: getIdea
@@ -259,6 +328,7 @@ components:
259
328
  field: { type: string }
260
329
  code: { type: string }
261
330
  expected: { type: string }
331
+ rule: { type: string, description: 'PASSWORD_POLICY only, the violated rule id (PRD 6.4)' }
262
332
  Deadline:
263
333
  type: object
264
334
  required: [deadline, state, serverTime]
@@ -272,6 +342,19 @@ components:
272
342
  properties:
273
343
  empNo: { type: string }
274
344
  name: { type: string }
345
+ CreateIdeaForm:
346
+ type: object
347
+ required: [category, org, direction, name, summary, slides]
348
+ properties:
349
+ category: { type: string, description: 'COOL_IDEA or OUTSTANDING_IMPACT' }
350
+ org: { type: string, description: 'AAID, BSID, ICSD, TSID, PLED or DEID' }
351
+ direction: { type: string, description: One of the five ESG directions (PRD 6.6) }
352
+ name: { type: string, description: '1-75 code points after clean-up; no / \ : * ? " < > |' }
353
+ summary: { type: string, description: 1-300 code points after clean-up }
354
+ slides:
355
+ type: string
356
+ format: binary
357
+ description: 'One PDF or PPTX, at most 5 MiB, named {I|O}_{org}_{leaderName}_{leaderEmpNo}_{name}.{pdf|pptx}'
275
358
  IdeaSummary:
276
359
  type: object
277
360
  required: [id, name, category, org, direction, leader, memberCount, myRelation, createdAt, slidesUpdatedAt]
@@ -8,6 +8,7 @@ import (
8
8
  "fmt"
9
9
  "os"
10
10
  "os/signal"
11
+ "sync"
11
12
  "sync/atomic"
12
13
  "syscall"
13
14
 
@@ -17,9 +18,12 @@ import (
17
18
  "github.com/brian-huanggg/AwardRegistration/backend/internal/audit"
18
19
  "github.com/brian-huanggg/AwardRegistration/backend/internal/auth"
19
20
  "github.com/brian-huanggg/AwardRegistration/backend/internal/deadline"
21
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/idea"
20
22
  "github.com/brian-huanggg/AwardRegistration/backend/internal/server"
21
23
  "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/config"
24
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/job"
22
25
  "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/web"
26
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/storage"
23
27
  )
24
28
 
25
29
  func main() {
@@ -52,7 +56,14 @@ func run() error {
52
56
  return err
53
57
  }
54
58
  sessions := auth.New(db)
55
- logins, err := auth.NewLogins(sessions, auditSvc, auth.NoThrottle{}) // B1b-04 wires the real throttle
59
+ logins, err := auth.NewLogins(sessions, auditSvc, auth.DBThrottle{DB: db})
60
+ if err != nil {
61
+ return err
62
+ }
63
+ slides, err := storage.NewMinIO(storage.MinIOConfig{
64
+ Endpoint: cfg.MinIO.Endpoint, AccessKey: cfg.MinIO.AccessKey, SecretKey: cfg.MinIO.SecretKey,
65
+ Bucket: cfg.MinIO.Bucket, Secure: cfg.MinIO.Secure,
66
+ })
56
67
  if err != nil {
57
68
  return err
58
69
  }
@@ -62,14 +73,36 @@ func run() error {
62
73
  Logouts: auth.LogoutHandler{Logouts: &auth.Logouts{Sessions: sessions, Audit: auditSvc}, Fail: errs.Write},
63
74
  Me: auth.MeHandler{Sessions: sessions, Fail: errs.Write},
64
75
  Account: account.Handler{
65
- Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.StubPolicy{}},
76
+ Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.DefaultPolicy()},
66
77
  Fail: errs.Write,
67
78
  },
68
79
  Deadline: deadline.Handler{Store: deadline.SQLStore{DB: db}},
80
+ Ideas: idea.Handler{
81
+ Service: &idea.Service{
82
+ DB: db, Audit: auditSvc,
83
+ Gate: deadline.Gate{Store: deadline.SQLStore{DB: db}},
84
+ Slides: server.IdeaSlideRules(),
85
+ Objects: slides,
86
+ Aborter: storage.NewPendingDeletion(db, slides, log),
87
+ },
88
+ ExpectedFilename: storage.ExpectedFilenameBase,
89
+ Fail: errs.Write,
90
+ },
69
91
  }
70
92
 
93
+ // Background jobs stop when the root context is cancelled, or when Run
94
+ // returns for another reason, and are awaited before the pool closes.
95
+ jobCtx, stopJobs := context.WithCancel(ctx)
96
+ var jobs sync.WaitGroup
97
+ defer func() { stopJobs(); jobs.Wait() }()
98
+ jobs.Go(func() {
99
+ job.Run(jobCtx, log, job.Spec{Name: "auth-purge", Interval: auth.PurgeInterval, Jitter: auth.PurgeJitter},
100
+ auth.Purger{DB: db, Log: log}.Purge)
101
+ })
102
+
71
103
  var ready atomic.Bool
72
104
  return server.Run(ctx, cfg, log, server.NewPublic(log, server.Mount(si),
105
+ web.TrustedProxies(cfg.ProxyHops),
73
106
  auth.Middleware(sessions, server.IsPublic, log),
74
107
  csrf.Middleware(errs.Write, server.ActionFor)), &ready, server.DBCheck(db))
75
108
  }
@@ -0,0 +1,49 @@
1
+ -- V005 (B3-01): idea, one row per team (PRD 6.6). There is no Idea without slides, so the slide metadata
2
+ -- lives on this row as NOT NULL columns (D22). Members are added by V006 (B5-01, idea_member).
3
+ -- category / org / direction closed value sets (VARCHAR + CHECK, never ENUM)
4
+ -- name 1-75 code points, utf8mb4_bin: case-sensitive, unique per leader (D20), not globally
5
+ -- summary 1-300 code points
6
+ -- slides_object_key slides/{ideaId}/{uuid}.{ext} (D50); the clean-up job asks "does any idea reference this key?"
7
+ -- slides_size 1..5 MiB (5,242,880 bytes, PRD 6.8)
8
+ -- version optimistic-locking counter (PRD 9 #3); every successful UPDATE of the row adds 1
9
+ -- The text clean-up (NFC, whitespace) is done by shared/text before the INSERT; the CHECKs are the second line.
10
+
11
+ CREATE TABLE idea (
12
+ id UUID NOT NULL DEFAULT UUID(),
13
+ leader_id UUID NOT NULL,
14
+ category VARCHAR(24) NOT NULL,
15
+ org VARCHAR(4) NOT NULL,
16
+ direction VARCHAR(24) NOT NULL,
17
+ name VARCHAR(75) NOT NULL,
18
+ summary VARCHAR(300) NOT NULL,
19
+ slides_object_key VARCHAR(128) NOT NULL,
20
+ slides_ext VARCHAR(4) NOT NULL,
21
+ slides_size INT UNSIGNED NOT NULL,
22
+ slides_sha256 CHAR(64) NOT NULL,
23
+ slides_updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
24
+ version INT UNSIGNED NOT NULL DEFAULT 0,
25
+ created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
26
+ updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
27
+ PRIMARY KEY (id),
28
+ -- A join request addresses an Idea by (leader employee ID, name) (PRD 6.6); this is also the IDEA_DUPLICATE guard.
29
+ UNIQUE KEY uq_idea_leader_name (leader_id, name),
30
+ -- Deleting a User is not a supported operation while they lead an Idea.
31
+ CONSTRAINT fk_idea_leader FOREIGN KEY (leader_id) REFERENCES app_user (id),
32
+ CONSTRAINT ck_idea_category CHECK (category IN ('COOL_IDEA', 'OUTSTANDING_IMPACT')),
33
+ CONSTRAINT ck_idea_org CHECK (org IN ('AAID', 'BSID', 'ICSD', 'TSID', 'PLED', 'DEID')),
34
+ CONSTRAINT ck_idea_direction CHECK (direction IN ('GREEN_MANUFACTURING', 'RESPONSIBLE_SUPPLY_CHAIN',
35
+ 'INCLUSIVE_WORKPLACE', 'TALENT_DEVELOPMENT', 'CARING_FOR_DISADVANTAGED')),
36
+ CONSTRAINT ck_idea_name CHECK (CHAR_LENGTH(name) BETWEEN 1 AND 75
37
+ AND name NOT REGEXP '[[:cntrl:]]' AND name NOT REGEXP '[/\\\\:*?"<>|]'),
38
+ CONSTRAINT ck_idea_summary CHECK (CHAR_LENGTH(summary) BETWEEN 1 AND 300),
39
+ CONSTRAINT ck_idea_slides_ext CHECK (slides_ext IN ('pdf', 'pptx')),
40
+ CONSTRAINT ck_idea_slides_size CHECK (slides_size BETWEEN 1 AND 5242880),
41
+ CONSTRAINT ck_idea_slides_sha256 CHECK (slides_sha256 REGEXP '^[0-9a-f]{64}$'),
42
+ -- "My Ideas" and the Leader filter; the visibility query starts from here for Users.
43
+ KEY ix_idea_leader (leader_id, created_at, id),
44
+ -- Default list order (createdAt, id as final tie-breaker) and the name order.
45
+ KEY ix_idea_created (created_at, id),
46
+ KEY ix_idea_name (name, id),
47
+ -- Orphan-file check: "no idea row has slides_object_key = ?"
48
+ KEY ix_idea_slides_key (slides_object_key)
49
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
@@ -0,0 +1,15 @@
1
+ -- V006 (B1b-04): per-key login lock (PRD 6.5, OI-12). One row per submitted employee-ID string (control
2
+ -- characters stripped, cut to 32 chars), also for accounts that do not exist. All times are database time.
3
+ -- fail_count consecutive failures, each within 15 min of the previous one
4
+ -- locked_until set by the 5th failure to NOW + 15 min (fixed; later failures never extend it)
5
+ -- The IP sliding window (login_attempt) arrives with B1b-05.
6
+
7
+ CREATE TABLE login_lock (
8
+ lock_key VARCHAR(32) NOT NULL,
9
+ fail_count INT NOT NULL,
10
+ last_fail_at DATETIME(6) NOT NULL,
11
+ locked_until DATETIME(6) NULL,
12
+ PRIMARY KEY (lock_key),
13
+ -- Purge scans by age.
14
+ KEY ix_login_lock_last_fail (last_fail_at)
15
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
@@ -0,0 +1,15 @@
1
+ -- V007 (B1b-05): per-IP login failure events (PRD 6.5). One row per failed login attempt; the IP limit counts
2
+ -- the rows of one address inside a sliding 15-minute window (30 failures -> 429). Throttled attempts are not
3
+ -- recorded. All times are database time. Rows older than the window are dead weight; the purge job (B1b-07)
4
+ -- deletes them by attempted_at.
5
+
6
+ CREATE TABLE login_attempt (
7
+ id BIGINT NOT NULL AUTO_INCREMENT,
8
+ ip INET6 NOT NULL,
9
+ attempted_at DATETIME(6) NOT NULL,
10
+ PRIMARY KEY (id),
11
+ -- Window count and "the 30th newest" lookup per address.
12
+ KEY ix_login_attempt_ip_time (ip, attempted_at),
13
+ -- Purge scans by age.
14
+ KEY ix_login_attempt_time (attempted_at)
15
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,27 @@
1
+ -- V008 (B3-06): pending_file_deletion, MinIO object keys that should be deleted (PRD 6.8, Sequence.md 2, D25, D36).
2
+ -- Written in v1; the clean-up job that drains it is the next phase (PRD 9 #7). All times are database time.
3
+ -- object_key slides/{ideaId}/{uuid}.{ext}; no FK to idea: the row must outlive the idea it came from
4
+ -- reason REPLACED old key, inserted in the transaction that switched the pointer
5
+ -- DELETED key of a deleted Idea, inserted in the delete transaction
6
+ -- UPLOAD_ABORTED new key whose transaction rolled back (delete failed) or ended unknown;
7
+ -- the job deletes it only after next_attempt_at AND if no idea row references it
8
+ -- next_attempt_at earliest time the job may act: NOW for REPLACED / DELETED / known rollback,
9
+ -- NOW + grace period for an unknown outcome (OI-19, proposed 10 min)
10
+ -- attempts failed job attempts so far (the job backs off with next_attempt_at)
11
+ -- A key may appear more than once (UPLOAD_ABORTED after a lost COMMIT ack, later REPLACED), so it is not unique.
12
+ -- A row is removed when the object was deleted, or when the job finds an UPLOAD_ABORTED key still referenced.
13
+
14
+ CREATE TABLE pending_file_deletion (
15
+ id BIGINT NOT NULL AUTO_INCREMENT,
16
+ object_key VARCHAR(128) NOT NULL,
17
+ reason VARCHAR(16) NOT NULL,
18
+ created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
19
+ next_attempt_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
20
+ attempts INT UNSIGNED NOT NULL DEFAULT 0,
21
+ PRIMARY KEY (id),
22
+ CONSTRAINT ck_pending_file_deletion_reason CHECK (reason IN ('REPLACED', 'DELETED', 'UPLOAD_ABORTED')),
23
+ CONSTRAINT ck_pending_file_deletion_key CHECK (CHAR_LENGTH(object_key) BETWEEN 1 AND 128),
24
+ -- The job claims due rows: WHERE next_attempt_at <= NOW(6) ORDER BY next_attempt_at, id FOR UPDATE SKIP LOCKED.
25
+ KEY ix_pending_file_deletion_due (next_attempt_at, id),
26
+ KEY ix_pending_file_deletion_key (object_key)
27
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
@@ -0,0 +1,13 @@
1
+ -- V009 (B1b-06): "a throttled key is audited once per window" (PRD 6.11). One row per throttled subject records
2
+ -- until when its THROTTLED audit row has been written; a throttled attempt writes a new audit row only after it
3
+ -- wins the claim (expired row updated, or row inserted), so replicas agree. All times are database time.
4
+ -- scope 'key' (login_lock key) or 'ip' (source address)
5
+ -- Rows past audited_until are dead weight; the purge job (B1b-07) deletes them.
6
+
7
+ CREATE TABLE login_throttle_audit (
8
+ scope VARCHAR(8) NOT NULL,
9
+ subject VARCHAR(45) NOT NULL,
10
+ audited_until DATETIME(6) NOT NULL,
11
+ PRIMARY KEY (scope, subject),
12
+ KEY ix_login_throttle_audit_until (audited_until)
13
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
package/backend/go.mod CHANGED
@@ -9,9 +9,13 @@ require (
9
9
  github.com/go-chi/chi/v5 v5.2.3
10
10
  github.com/go-sql-driver/mysql v1.9.3
11
11
  github.com/google/uuid v1.6.0
12
+ github.com/minio/minio-go/v7 v7.4.1
12
13
  github.com/oapi-codegen/runtime v1.8.0
13
14
  github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
15
+ github.com/testcontainers/testcontainers-go v0.45.0
14
16
  github.com/testcontainers/testcontainers-go/modules/mariadb v0.45.0
17
+ github.com/testcontainers/testcontainers-go/modules/minio v0.45.0
18
+ github.com/testcontainers/testcontainers-go/modules/toxiproxy v0.45.0
15
19
  golang.org/x/crypto v0.58.0
16
20
  golang.org/x/text v0.43.0
17
21
  )
@@ -33,6 +37,7 @@ require (
33
37
  github.com/docker/go-connections v0.8.2 // indirect
34
38
  github.com/docker/go-units v0.5.0 // indirect
35
39
  github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
40
+ github.com/dustin/go-humanize v1.0.1 // indirect
36
41
  github.com/ebitengine/purego v0.10.1 // indirect
37
42
  github.com/felixge/httpsnoop v1.1.0 // indirect
38
43
  github.com/go-logr/logr v1.4.3 // indirect
@@ -40,9 +45,13 @@ require (
40
45
  github.com/go-ole/go-ole v1.3.0 // indirect
41
46
  github.com/go-openapi/jsonpointer v0.23.1 // indirect
42
47
  github.com/go-openapi/swag/jsonname v0.26.0 // indirect
43
- github.com/klauspost/compress v1.18.7 // indirect
48
+ github.com/klauspost/compress v1.19.2 // indirect
49
+ github.com/klauspost/cpuid/v2 v2.4.0 // indirect
50
+ github.com/klauspost/crc32 v1.3.0 // indirect
44
51
  github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
45
52
  github.com/magiconair/properties v1.18.12 // indirect
53
+ github.com/minio/crc64nvme v1.1.1 // indirect
54
+ github.com/minio/md5-simd v1.1.2 // indirect
46
55
  github.com/moby/docker-image-spec v1.3.1 // indirect
47
56
  github.com/moby/go-archive v0.3.3 // indirect
48
57
  github.com/moby/moby/api v1.56.1 // indirect
@@ -57,17 +66,20 @@ require (
57
66
  github.com/oasdiff/yaml3 v0.0.14 // indirect
58
67
  github.com/opencontainers/go-digest v1.0.0 // indirect
59
68
  github.com/opencontainers/image-spec v1.1.1 // indirect
69
+ github.com/philhofer/fwd v1.2.0 // indirect
60
70
  github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
71
+ github.com/rs/xid v1.6.0 // indirect
61
72
  github.com/shirou/gopsutil/v4 v4.26.6 // indirect
62
- github.com/sirupsen/logrus v1.9.4 // indirect
73
+ github.com/sirupsen/logrus v1.10.2 // indirect
63
74
  github.com/speakeasy-api/jsonpath v0.6.3 // indirect
64
75
  github.com/speakeasy-api/openapi v1.24.0 // indirect
65
76
  github.com/stretchr/testify v1.12.1 // indirect
66
- github.com/testcontainers/testcontainers-go v0.45.0 // indirect
77
+ github.com/tinylib/msgp v1.6.4 // indirect
67
78
  github.com/tklauser/go-sysconf v0.4.0 // indirect
68
79
  github.com/tklauser/numcpus v0.12.0 // indirect
69
80
  github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
70
81
  github.com/yusufpapurcu/wmi v1.2.4 // indirect
82
+ github.com/zeebo/xxh3 v1.1.0 // indirect
71
83
  go.opentelemetry.io/auto/sdk v1.2.1 // indirect
72
84
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
73
85
  go.opentelemetry.io/otel v1.44.0 // indirect
@@ -75,9 +87,11 @@ require (
75
87
  go.opentelemetry.io/otel/trace v1.44.0 // indirect
76
88
  go.yaml.in/yaml/v3 v3.0.5 // indirect
77
89
  golang.org/x/mod v0.41.0 // indirect
90
+ golang.org/x/net v0.60.0 // indirect
78
91
  golang.org/x/sync v0.24.0 // indirect
79
92
  golang.org/x/sys v0.49.0 // indirect
80
93
  golang.org/x/tools v0.51.0 // indirect
94
+ gopkg.in/ini.v1 v1.67.3 // indirect
81
95
  gopkg.in/yaml.v3 v3.0.1 // indirect
82
96
  )
83
97
 
package/backend/go.sum CHANGED
@@ -44,13 +44,16 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
44
44
  github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
45
45
  github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
46
46
  github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
47
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
48
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
47
49
  github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
48
50
  github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
49
51
  github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
50
52
  github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
51
53
  github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
52
- github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
53
54
  github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
55
+ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
56
+ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
54
57
  github.com/getkin/kin-openapi v0.142.0 h1:izj0vBdFprMhitfzaX8sTqztsEQyvwhssBoB6n8NO7w=
55
58
  github.com/getkin/kin-openapi v0.142.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
56
59
  github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
@@ -95,8 +98,13 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
95
98
  github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
96
99
  github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
97
100
  github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
98
- github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
99
- github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
101
+ github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
102
+ github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
103
+ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
104
+ github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
105
+ github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
106
+ github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
107
+ github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
100
108
  github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
101
109
  github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
102
110
  github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -108,6 +116,14 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt
108
116
  github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
109
117
  github.com/magiconair/properties v1.18.12 h1:sT9zQpvTB3B4gzrX0tmZNTEaGyg8Zw55MFYRE32Mr9I=
110
118
  github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
119
+ github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
120
+ github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
121
+ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
122
+ github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
123
+ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
124
+ github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
125
+ github.com/minio/minio-go/v7 v7.4.1 h1:PtU/zIOfQRWZ+QydXAZysc+xZYHdG+BBR4Z6TUvy73w=
126
+ github.com/minio/minio-go/v7 v7.4.1/go.mod h1:nQnQi7zTfXR4lREtYXZQWqo5aw0Rqv+ZqCGrTORygRk=
111
127
  github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
112
128
  github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
113
129
  github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
@@ -159,36 +175,55 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
159
175
  github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
160
176
  github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
161
177
  github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
178
+ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
179
+ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
162
180
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
163
181
  github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
164
182
  github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
165
- github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
166
- github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
183
+ github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
184
+ github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
185
+ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
186
+ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
167
187
  github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
168
188
  github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
169
189
  github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
170
190
  github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
171
191
  github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
172
192
  github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
173
- github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
174
- github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
193
+ github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
194
+ github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
175
195
  github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU=
176
196
  github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI=
177
197
  github.com/speakeasy-api/openapi v1.24.0 h1:opoD27rupX7zBVPq1HkIGLeMOzNNA7JalhYP8q34i04=
178
198
  github.com/speakeasy-api/openapi v1.24.0/go.mod h1:g3+dIMe0AYgbbGvnlQZqesmjAVWSm9BmsjLevnefQrg=
179
199
  github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
180
200
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
201
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
202
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
203
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
181
204
  github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
182
205
  github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
183
206
  github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
184
207
  github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
185
208
  github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
209
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
210
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
211
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
212
+ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
186
213
  github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
187
214
  github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
188
215
  github.com/testcontainers/testcontainers-go v0.45.0 h1:ss9Stm9KDX6JLgGL4jr2fC5dwpabTbCBsRcGadBPR9k=
189
216
  github.com/testcontainers/testcontainers-go v0.45.0/go.mod h1:VMJZ7M+qhgfEHWNbtjm596OyEOraULSTDaus/h0hmJI=
190
217
  github.com/testcontainers/testcontainers-go/modules/mariadb v0.45.0 h1:oN0eXjn9Ae43kJThdWosEiq5mbU7RvkuRIfOZnO8Awg=
191
218
  github.com/testcontainers/testcontainers-go/modules/mariadb v0.45.0/go.mod h1:z5rjEcunHetNxfFAIwKp2uO9RA91aWIC5wvMZyPKZG0=
219
+ github.com/testcontainers/testcontainers-go/modules/minio v0.45.0 h1:y/Nj/4ZkI220EJM6L45dntmNh73qcH7wLEuAgc9pyrE=
220
+ github.com/testcontainers/testcontainers-go/modules/minio v0.45.0/go.mod h1:C07UqpIojab780lr5P9tKaYIIz2sPZWOUjp2E12pmmQ=
221
+ github.com/testcontainers/testcontainers-go/modules/redis v0.45.0 h1:U7GClvzAxLc7pjDfFxw3K6+odTa8NbxEa31svO5gl/Q=
222
+ github.com/testcontainers/testcontainers-go/modules/redis v0.45.0/go.mod h1:06G3YVRf2RHAFNKwtFYhVd4CYgnXLWXmckaJNWI4fGY=
223
+ github.com/testcontainers/testcontainers-go/modules/toxiproxy v0.45.0 h1:bzp1LLWbgE5cWpfuR5lW6PuVddRoz7yzgK2zkwXYMQg=
224
+ github.com/testcontainers/testcontainers-go/modules/toxiproxy v0.45.0/go.mod h1:CWGzEsj3+TX1D21EcpY1SLSuNroXiXfnZUlUY9w71Hw=
225
+ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
226
+ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
192
227
  github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
193
228
  github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
194
229
  github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
@@ -198,6 +233,10 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN
198
233
  github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
199
234
  github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
200
235
  github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
236
+ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
237
+ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
238
+ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
239
+ github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
201
240
  go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
202
241
  go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
203
242
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
@@ -287,6 +326,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
287
326
  gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
288
327
  gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
289
328
  gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
329
+ gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
330
+ gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
290
331
  gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
291
332
  gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
292
333
  gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -15,7 +15,7 @@ import (
15
15
  // A Service with no database: every case here must be rejected before any I/O.
16
16
  func validate(t *testing.T, cmd account.RegisterCommand) *web.Error {
17
17
  t.Helper()
18
- svc := &account.Service{Policy: account.StubPolicy{}}
18
+ svc := &account.Service{Policy: account.DefaultPolicy()}
19
19
  _, err := svc.Register(context.Background(), cmd, account.Request{})
20
20
  var e *web.Error
21
21
  if !errors.As(err, &e) {
@@ -25,7 +25,7 @@ func validate(t *testing.T, cmd account.RegisterCommand) *web.Error {
25
25
  }
26
26
 
27
27
  func good() account.RegisterCommand {
28
- return account.RegisterCommand{EmpNo: "089290", Name: "王小明", Email: "a@b.c", Password: "whatever-goes"}
28
+ return account.RegisterCommand{EmpNo: "089290", Name: "王小明", Email: "a@b.c", Password: "Correct-Horse-1"}
29
29
  }
30
30
 
31
31
  func codes(e *web.Error) []string {
@@ -61,11 +61,11 @@ func TestRegisterValidation(t *testing.T) {
61
61
  {"email non-ASCII", func(c *account.RegisterCommand) { c.Email = "é@x.com" }, web.CodeValidationFailed, "email:INVALID_FORMAT"},
62
62
  {"email empty", func(c *account.RegisterCommand) { c.Email = "" }, web.CodeValidationFailed, "email:REQUIRED"},
63
63
  {"email 255 chars", func(c *account.RegisterCommand) { c.Email = strings.Repeat("a", 253) + "@b" }, web.CodeValidationFailed, "email:OUT_OF_RANGE"},
64
- {"password empty", func(c *account.RegisterCommand) { c.Password = "" }, web.CodeValidationFailed, "password:REQUIRED"},
65
- {"password beyond bcrypt", func(c *account.RegisterCommand) { c.Password = strings.Repeat("p", 73) }, web.CodeValidationFailed, "password:OUT_OF_RANGE"},
64
+ {"password empty", func(c *account.RegisterCommand) { c.Password = "" }, web.CodePasswordPolicy, "password:PASSWORD_POLICY password:PASSWORD_POLICY"},
65
+ {"password beyond bcrypt", func(c *account.RegisterCommand) { c.Password = strings.Repeat("Pp1", 25) }, web.CodePasswordPolicy, "password:PASSWORD_POLICY"},
66
66
  {"all fields in form order", func(c *account.RegisterCommand) {
67
67
  c.EmpNo, c.Name, c.Email, c.Password = "x", "a:b", "nope", ""
68
- }, web.CodeValidationFailed, "empNo:INVALID_FORMAT name:INVALID_NAME_CHARS email:INVALID_FORMAT password:REQUIRED"},
68
+ }, web.CodeValidationFailed, "empNo:INVALID_FORMAT name:INVALID_NAME_CHARS email:INVALID_FORMAT password:PASSWORD_POLICY password:PASSWORD_POLICY"},
69
69
  }
70
70
  for _, tc := range cases {
71
71
  t.Run(tc.name, func(t *testing.T) {
@@ -143,7 +143,7 @@ func TestTopCodeIsPasswordPolicyWhenFirst(t *testing.T) {
143
143
 
144
144
  func handlerFor(got *error) http.Handler {
145
145
  h := account.Handler{
146
- Service: &account.Service{Policy: account.StubPolicy{}},
146
+ Service: &account.Service{Policy: account.DefaultPolicy()},
147
147
  Fail: func(_ http.ResponseWriter, _ *http.Request, err error) { *got = err },
148
148
  }
149
149
  return http.HandlerFunc(h.Register)