@johnnyboy11234/esg-idea-contest 0.3.0 → 0.3.2

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 (138) hide show
  1. package/backend/api/openapi.yaml +119 -0
  2. package/backend/cmd/server/main.go +36 -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/internal/passwords/NOTICE +36 -0
  13. package/backend/internal/account/internal/passwords/common-passwords.txt +10001 -0
  14. package/backend/internal/account/internal/passwords/passwords.go +37 -0
  15. package/backend/internal/account/internal/passwords/passwords_test.go +35 -0
  16. package/backend/internal/account/keyboard.go +75 -0
  17. package/backend/internal/account/keyboard_test.go +43 -0
  18. package/backend/internal/account/logging_integration_test.go +1 -1
  19. package/backend/internal/account/password.go +131 -0
  20. package/backend/internal/account/sequence.go +54 -0
  21. package/backend/internal/account/sequence_test.go +38 -0
  22. package/backend/internal/account/validate.go +4 -21
  23. package/backend/internal/api/api.gen.go +262 -0
  24. package/backend/internal/auth/internal/store/login_attempt.go +53 -0
  25. package/backend/internal/auth/internal/store/login_lock.go +77 -0
  26. package/backend/internal/auth/internal/store/login_throttle_audit.go +42 -0
  27. package/backend/internal/auth/internal/store/session.go +15 -7
  28. package/backend/internal/auth/login.go +16 -4
  29. package/backend/internal/auth/login_integration_test.go +14 -1
  30. package/backend/internal/auth/login_ip_integration_test.go +153 -0
  31. package/backend/internal/auth/login_lock_integration_test.go +205 -0
  32. package/backend/internal/auth/login_test.go +15 -0
  33. package/backend/internal/auth/login_throttle_audit_integration_test.go +88 -0
  34. package/backend/internal/auth/purge.go +93 -0
  35. package/backend/internal/auth/purge_integration_test.go +220 -0
  36. package/backend/internal/auth/purge_test.go +51 -0
  37. package/backend/internal/auth/session.go +0 -6
  38. package/backend/internal/auth/session_integration_test.go +3 -3
  39. package/backend/internal/auth/throttle.go +122 -0
  40. package/backend/internal/deadline/gate.go +80 -0
  41. package/backend/internal/deadline/gate_integration_test.go +67 -0
  42. package/backend/internal/deadline/gate_test.go +61 -0
  43. package/backend/internal/idea/create.go +356 -0
  44. package/backend/internal/idea/create_integration_test.go +680 -0
  45. package/backend/internal/idea/download.go +193 -0
  46. package/backend/internal/idea/download_integration_test.go +311 -0
  47. package/backend/internal/idea/download_test.go +25 -0
  48. package/backend/internal/idea/handler.go +151 -0
  49. package/backend/internal/idea/handler_integration_test.go +107 -0
  50. package/backend/internal/idea/integration_test.go +149 -0
  51. package/backend/internal/idea/internal/store/list.go +146 -0
  52. package/backend/internal/idea/internal/store/list_test.go +33 -0
  53. package/backend/internal/idea/internal/store/store.go +166 -0
  54. package/backend/internal/idea/list.go +166 -0
  55. package/backend/internal/idea/list_integration_test.go +252 -0
  56. package/backend/internal/idea/list_test.go +96 -0
  57. package/backend/internal/idea/service.go +54 -0
  58. package/backend/internal/server/api.go +22 -0
  59. package/backend/internal/server/binding.go +4 -0
  60. package/backend/internal/server/public.go +1 -0
  61. package/backend/internal/server/server.go +20 -1
  62. package/backend/internal/server/slides.go +21 -0
  63. package/backend/internal/server/slides_test.go +82 -0
  64. package/backend/internal/shared/config/config.go +44 -1
  65. package/backend/internal/shared/config/config_test.go +21 -13
  66. package/backend/internal/shared/db/intx_integration_test.go +179 -0
  67. package/backend/internal/shared/dbtest/dbtest.go +21 -3
  68. package/backend/internal/shared/dbtest/faults.go +162 -0
  69. package/backend/internal/shared/job/job.go +54 -0
  70. package/backend/internal/shared/job/job_test.go +105 -0
  71. package/backend/internal/shared/web/multipart.go +175 -0
  72. package/backend/internal/shared/web/multipart_test.go +169 -0
  73. package/backend/internal/shared/web/problem.go +1 -0
  74. package/backend/internal/shared/web/request.go +42 -1
  75. package/backend/internal/shared/web/transport.go +4 -3
  76. package/backend/internal/shared/web/web_test.go +36 -0
  77. package/backend/internal/storage/expectedfilename.go +43 -0
  78. package/backend/internal/storage/expectedfilename_test.go +16 -0
  79. package/backend/internal/storage/internal/store/pending.go +41 -0
  80. package/backend/internal/storage/minio.go +139 -0
  81. package/backend/internal/storage/minio_integration_test.go +87 -0
  82. package/backend/internal/storage/minio_test.go +84 -0
  83. package/backend/internal/storage/pending.go +144 -0
  84. package/backend/internal/storage/pending_integration_test.go +324 -0
  85. package/backend/internal/storage/pending_test.go +23 -0
  86. package/backend/internal/storage/validator.go +100 -0
  87. package/backend/internal/storage/validator_test.go +95 -0
  88. package/backend/test/arch/arch_test.go +5 -1
  89. package/backend/test/contract/expected_filename_test.go +60 -0
  90. package/backend/test/contract/password_test.go +46 -0
  91. package/backend/test/matrix/load_test.go +12 -0
  92. package/backend/test/matrix/matrix_integration_test.go +92 -5
  93. package/backend/test/migration/pending_file_deletion_test.go +46 -0
  94. package/docs/API/api-endpoints.md +1 -1
  95. package/docs/API/error-codes.md +1 -1
  96. package/docs/Architecture/Architecture.md +1 -1
  97. package/docs/Architecture/Backend.md +2 -1
  98. package/frontend/Dockerfile +2 -0
  99. package/frontend/package-lock.json +1 -0
  100. package/frontend/package.json +1 -0
  101. package/frontend/src/app/App.tsx +11 -2
  102. package/frontend/src/app/pages/ideaCreate.test.tsx +308 -0
  103. package/frontend/src/app/pages/ideas.tsx +23 -9
  104. package/frontend/src/app/pages/login.test.tsx +38 -1
  105. package/frontend/src/app/pages/register.test.tsx +32 -0
  106. package/frontend/src/features/auth/IdleWarning.test.tsx +82 -0
  107. package/frontend/src/features/auth/IdleWarning.tsx +66 -0
  108. package/frontend/src/features/auth/LoginForm.tsx +21 -3
  109. package/frontend/src/features/auth/PasswordChecklist.test.tsx +73 -0
  110. package/frontend/src/features/auth/PasswordChecklist.tsx +96 -0
  111. package/frontend/src/features/auth/RegisterForm.tsx +11 -2
  112. package/frontend/src/features/auth/common-passwords.ts +31 -0
  113. package/frontend/src/features/auth/password-policy.test.ts +64 -0
  114. package/frontend/src/features/auth/password-policy.ts +168 -0
  115. package/frontend/src/features/auth/useCountdown.test.tsx +33 -0
  116. package/frontend/src/features/auth/useCountdown.ts +28 -0
  117. package/frontend/src/features/idea/CreateIdeaForm.tsx +279 -0
  118. package/frontend/src/features/idea/create.ts +123 -0
  119. package/frontend/src/shared/api/activity.ts +20 -0
  120. package/frontend/src/shared/api/http.ts +2 -0
  121. package/frontend/src/shared/api/index.ts +1 -0
  122. package/frontend/src/shared/api/types.generated.ts +319 -1
  123. package/frontend/src/shared/i18n/locales/en.ts +46 -4
  124. package/frontend/src/shared/i18n/locales/zh-TW.ts +46 -4
  125. package/frontend/src/shared/slides/index.ts +74 -0
  126. package/frontend/src/shared/slides/slides.test.ts +51 -0
  127. package/frontend/src/shared/ui/copy-button.tsx +52 -0
  128. package/frontend/src/shared/ui/native-select.tsx +27 -0
  129. package/frontend/src/shared/ui/textarea.tsx +19 -0
  130. package/frontend/src/test-setup.ts +37 -2
  131. package/frontend/vite.config.ts +3 -1
  132. package/package.json +1 -1
  133. package/testdata/expected-filename.json +93 -0
  134. package/testdata/password.json +582 -0
  135. package/testdata/permission-matrix.json +71 -0
  136. package/testdata/schema/expected-filename.schema.json +32 -0
  137. package/testdata/schema/password.schema.json +59 -0
  138. package/testdata/schema/permission-matrix.schema.json +22 -1
@@ -32,7 +32,7 @@ func newEnv(t *testing.T) env {
32
32
  auditSvc := audit.New(db, nil)
33
33
  errs := server.ErrorAdapter{Audit: auditSvc}
34
34
  api := server.API{Account: account.Handler{
35
- Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.StubPolicy{}},
35
+ Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.DefaultPolicy()},
36
36
  Fail: errs.Write,
37
37
  }}
38
38
  h := apispec.Load(t).Validate(t, server.NewPublic(slog.New(slog.DiscardHandler), server.Mount(api)))
@@ -49,6 +49,8 @@ func (e env) register(body string) *httptest.ResponseRecorder {
49
49
  return rec
50
50
  }
51
51
 
52
+ const goodPW = "Correct-Horse-1"
53
+
52
54
  func reg(empNo, name, email, pw string) string {
53
55
  b, _ := json.Marshal(map[string]string{"empNo": empNo, "name": name, "email": email, "password": pw})
54
56
  return string(b)
@@ -150,16 +152,16 @@ func TestRegisterSuccess(t *testing.T) {
150
152
 
151
153
  func TestRegisterConflicts(t *testing.T) {
152
154
  e := newEnv(t)
153
- if rec := e.register(reg("089290", "A", "a@x.com", "pw")); rec.Code != 201 {
155
+ if rec := e.register(reg("089290", "A", "a@x.com", goodPW)); rec.Code != 201 {
154
156
  t.Fatal(rec.Body)
155
157
  }
156
158
 
157
159
  cases := []struct {
158
160
  name, body, code, reason string
159
161
  }{
160
- {"same employee ID", reg("089290", "B", "b@x.com", "pw"), "EMP_NO_TAKEN", "emp_no_taken"},
161
- {"same email, any case", reg("089291", "B", " A@X.COM ", "pw"), "EMAIL_TAKEN", "email_taken"},
162
- {"both taken reports the employee ID", reg("089290", "B", "a@x.com", "pw"), "EMP_NO_TAKEN", "emp_no_taken"},
162
+ {"same employee ID", reg("089290", "B", "b@x.com", goodPW), "EMP_NO_TAKEN", "emp_no_taken"},
163
+ {"same email, any case", reg("089291", "B", " A@X.COM ", goodPW), "EMAIL_TAKEN", "email_taken"},
164
+ {"both taken reports the employee ID", reg("089290", "B", "a@x.com", goodPW), "EMP_NO_TAKEN", "emp_no_taken"},
163
165
  }
164
166
  for _, tc := range cases {
165
167
  rec := e.register(tc.body)
@@ -189,14 +191,14 @@ func TestRegisterConflicts(t *testing.T) {
189
191
 
190
192
  func TestRegisterValidationBeatsConflictAndIsAudited(t *testing.T) {
191
193
  e := newEnv(t)
192
- e.register(reg("089290", "A", "a@x.com", "pw"))
194
+ e.register(reg("089290", "A", "a@x.com", goodPW))
193
195
 
194
196
  // The employee ID is taken, but the email is malformed: 422, never 409.
195
- rec := e.register(reg("089290", "B", "a b@x.com", "pw"))
197
+ rec := e.register(reg("089290", "B", "a b@x.com", goodPW))
196
198
  if rec.Code != 422 || problemCode(t, rec) != "VALIDATION_FAILED" {
197
199
  t.Fatalf("%d %s", rec.Code, rec.Body)
198
200
  }
199
- rec = e.register(reg("12 4", "B/C", "ok@x.com", "pw"))
201
+ rec = e.register(reg("12 4", "B/C", "ok@x.com", goodPW))
200
202
  if rec.Code != 422 {
201
203
  t.Fatalf("%d %s", rec.Code, rec.Body)
202
204
  }
@@ -248,7 +250,7 @@ func TestRegisterRaceHasOneWinner(t *testing.T) {
248
250
  wg.Add(1)
249
251
  go func() {
250
252
  defer wg.Done()
251
- codes[i] = e.register(reg("089290", "A", "race"+string(rune('a'+i))+"@x.com", "pw")).Code
253
+ codes[i] = e.register(reg("089290", "A", "race"+string(rune('a'+i))+"@x.com", goodPW)).Code
252
254
  }()
253
255
  }
254
256
  wg.Wait()
@@ -266,3 +268,37 @@ func TestRegisterRaceHasOneWinner(t *testing.T) {
266
268
  t.Fatalf("codes = %v, users = %d", codes, e.users(t))
267
269
  }
268
270
  }
271
+
272
+ func TestRegisterWeakPasswordListsEveryRuleInTableOrder(t *testing.T) {
273
+ e := newEnv(t)
274
+ // Format and policy errors come back together; the password contains the
275
+ // employee ID, repeats a character and has only two classes.
276
+ rec := e.register(reg("089290", "A", "nope", "aaa089290"))
277
+ if rec.Code != 422 || problemCode(t, rec) != "VALIDATION_FAILED" {
278
+ t.Fatalf("%d %s", rec.Code, rec.Body)
279
+ }
280
+ var body struct {
281
+ Errors []struct{ Field, Code, Rule string }
282
+ }
283
+ if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
284
+ t.Fatal(err)
285
+ }
286
+ var got []string
287
+ for _, f := range body.Errors {
288
+ got = append(got, f.Field+":"+f.Code+":"+f.Rule)
289
+ }
290
+ want := "email:INVALID_FORMAT: password:PASSWORD_POLICY:LENGTH password:PASSWORD_POLICY:COMPLEXITY " +
291
+ "password:PASSWORD_POLICY:REPEAT password:PASSWORD_POLICY:CONTAINS_EMP_NO"
292
+ if strings.Join(got, " ") != want {
293
+ t.Errorf("errors = %v\nwant %s", got, want)
294
+ }
295
+
296
+ // Alone, a weak password makes PASSWORD_POLICY the top-level code.
297
+ rec = e.register(reg("089290", "A", "a@x.com", "short"))
298
+ if rec.Code != 422 || problemCode(t, rec) != "PASSWORD_POLICY" {
299
+ t.Fatalf("%d %s", rec.Code, rec.Body)
300
+ }
301
+ if e.users(t) != 0 {
302
+ t.Error("a rejected registration created a user")
303
+ }
304
+ }
@@ -0,0 +1,36 @@
1
+ common-passwords.txt
2
+ ====================
3
+
4
+ Source: SecLists, Passwords/Common-Credentials/10k-most-common.txt
5
+ https://github.com/danielmiessler/SecLists
6
+ Commit: a1a06ab57c3f4efc450d80738a72c11597aac81a
7
+ SHA-256: 68782d6a4a19a4768d5f15dd66bd534e7a33055cc755411e33f16d18c50fdcce
8
+ Entries: 10,001 lines, printable ASCII, no case-folded duplicates
9
+ Changes: none (byte-for-byte copy)
10
+
11
+ Used by: backend rule COMMON_PASSWORD (PRD §6.4, embedded with go:embed) and
12
+ the frontend live password checklist (lazy-loaded chunk).
13
+
14
+ Licence of the source file:
15
+
16
+ MIT License
17
+
18
+ Copyright (c) 2018 Daniel Miessler
19
+
20
+ Permission is hereby granted, free of charge, to any person obtaining a copy
21
+ of this software and associated documentation files (the "Software"), to deal
22
+ in the Software without restriction, including without limitation the rights
23
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24
+ copies of the Software, and to permit persons to whom the Software is
25
+ furnished to do so, subject to the following conditions:
26
+
27
+ The above copyright notice and this permission notice shall be included in all
28
+ copies or substantial portions of the Software.
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36
+ SOFTWARE.