@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
@@ -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,75 @@
1
+ package account
2
+
3
+ import "strings"
4
+
5
+ const minKeyboardRun = 4
6
+
7
+ // keyboardRows are the four rows of US QWERTY (PRD §6.4). Rows do not join
8
+ // and do not wrap: the end of one row is not adjacent to the start of the next.
9
+ var keyboardRows = [...]string{
10
+ "1234567890-=",
11
+ "qwertyuiop[]",
12
+ "asdfghjkl;'",
13
+ "zxcvbnm,./",
14
+ }
15
+
16
+ // shiftedRows holds the shifted glyph of each key, position for position with
17
+ // keyboardRows (letters are folded by case instead).
18
+ var shiftedRows = [...]string{
19
+ "!@#$%^&*()_+",
20
+ "QWERTYUIOP{}",
21
+ "ASDFGHJKL:\"",
22
+ "ZXCVBNM<>?",
23
+ }
24
+
25
+ // keyPos is a key's row and column on the keyboard.
26
+ type keyPos struct{ row, col int8 }
27
+
28
+ // keyOf maps every unshifted and shifted glyph to its key. Glyphs on no row
29
+ // (` ~ \ | and anything non-ASCII) are absent and break a run.
30
+ var keyOf = func() map[rune]keyPos {
31
+ m := make(map[rune]keyPos, 2*len(keyboardRows[0])*4)
32
+ for r := range keyboardRows {
33
+ for c, g := range keyboardRows[r] {
34
+ m[g] = keyPos{int8(r), int8(c)}
35
+ }
36
+ for c, g := range shiftedRows[r] {
37
+ m[g] = keyPos{int8(r), int8(c)}
38
+ }
39
+ }
40
+ return m
41
+ }()
42
+
43
+ // violatesKeyboardRun: four or more adjacent keys on one row, read left to
44
+ // right or right to left, case-insensitive, a shifted glyph counting as its
45
+ // key (`!@#$` is `1234`). The same key repeated is not a run (that is REPEAT),
46
+ // and a run must keep one direction ("qwer" yes, "qwqw" no).
47
+ func violatesKeyboardRun(pw, _ string) bool {
48
+ var prev keyPos
49
+ have, run, dir := false, 0, int8(0)
50
+ for _, r := range strings.ToLower(pw) {
51
+ k, ok := keyOf[r]
52
+ if !ok {
53
+ have, run, dir = false, 0, 0
54
+ continue
55
+ }
56
+ step := int8(0)
57
+ if have && k.row == prev.row {
58
+ step = k.col - prev.col
59
+ }
60
+ if step == 1 || step == -1 {
61
+ if run >= 2 && step == dir {
62
+ run++
63
+ } else {
64
+ run, dir = 2, step
65
+ }
66
+ } else {
67
+ run, dir = 1, 0
68
+ }
69
+ prev, have = k, true
70
+ if run >= minKeyboardRun {
71
+ return true
72
+ }
73
+ }
74
+ return false
75
+ }
@@ -0,0 +1,43 @@
1
+ package account
2
+
3
+ import "testing"
4
+
5
+ func TestViolatesKeyboardRun(t *testing.T) {
6
+ for _, tc := range []struct {
7
+ pw string
8
+ want bool
9
+ }{
10
+ {"", false},
11
+ {"qwe", false},
12
+ {"qwer", true},
13
+ {"rewq", true},
14
+ {"QwEr", true},
15
+ {"1234", true},
16
+ {"4321", true},
17
+ {"!@#$", true},
18
+ {"1@3$", true},
19
+ {"=-09", true},
20
+ {"[poi", true},
21
+ {"zxcv", true},
22
+ {"/.,m", true},
23
+ {"asdf", true},
24
+ {"';lk", true},
25
+ {"{POI", true},
26
+ {"xqwertyx", true},
27
+ {"p-asdf", true}, // rows do not join: only asdf counts
28
+ {"pasd", false}, // no wrap row to row
29
+ {"0-=q", false}, // "0-=" is 3 keys, then a new row
30
+ {"qwewq", false}, // direction change restarts the count
31
+ {"qw~er", false},
32
+ {"qwdf", false},
33
+ {"qqqq", false},
34
+ {"1111", false},
35
+ {"`123", false},
36
+ {"éqwe", false},
37
+ {"aaa-sdf", false},
38
+ } {
39
+ if got := violatesKeyboardRun(tc.pw, ""); got != tc.want {
40
+ t.Errorf("violatesKeyboardRun(%q) = %v, want %v", tc.pw, got, tc.want)
41
+ }
42
+ }
43
+ }
@@ -43,7 +43,7 @@ func TestRegisterLogsNeverContainPassword(t *testing.T) {
43
43
  auditSvc := audit.New(db, log)
44
44
  errs := server.ErrorAdapter{Audit: auditSvc, Log: log}
45
45
  api := server.API{Account: account.Handler{
46
- Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.StubPolicy{}},
46
+ Service: &account.Service{DB: db, Audit: auditSvc, Policy: account.DefaultPolicy()},
47
47
  Fail: errs.Write,
48
48
  }}
49
49
  e := env{db: db, h: apispec.Load(t).Validate(t, server.NewPublic(log, server.Mount(api)))}
@@ -0,0 +1,125 @@
1
+ package account
2
+
3
+ import (
4
+ "strings"
5
+ "unicode/utf8"
6
+
7
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/web"
8
+ )
9
+
10
+ // Password policy (PRD §6.4). Rule ids are the wire contract (errors[].rule).
11
+ const (
12
+ RuleLength = "LENGTH"
13
+ RuleCharset = "CHARSET"
14
+ RuleComplexity = "COMPLEXITY"
15
+ RuleCommon = "COMMON_PASSWORD"
16
+ RuleKeyboardRun = "KEYBOARD_RUN"
17
+ RuleSequence = "SEQUENCE"
18
+ RuleRepeat = "REPEAT"
19
+ RuleContainsEmpNo = "CONTAINS_EMP_NO"
20
+ )
21
+
22
+ const (
23
+ minPassLen = 12
24
+ maxPassLen = 64 // code points
25
+ minPassClasses = 3
26
+ maxRepeat = 2 // 3+ identical characters in a row is a violation
27
+ )
28
+
29
+ // Rule is one row of the §6.4 table. Violated reports whether password breaks
30
+ // the rule; empNo is "" when the submitted employee ID is malformed.
31
+ type Rule struct {
32
+ ID string
33
+ Violated func(password, empNo string) bool
34
+ }
35
+
36
+ // Policy runs its rules in order and reports every violated one. The slice
37
+ // order is the PRD §6.4 table order, which is part of the contract
38
+ // (testdata/password.json): later PRs add their rule at its table position.
39
+ type Policy struct{ Rules []Rule }
40
+
41
+ // DefaultPolicy returns the rules implemented so far, in table order.
42
+ // SEQUENCE and COMMON_PASSWORD arrive with B1b-03 and later.
43
+ func DefaultPolicy() *Policy {
44
+ return &Policy{Rules: []Rule{
45
+ {RuleLength, violatesLength},
46
+ {RuleCharset, violatesCharset},
47
+ {RuleComplexity, violatesComplexity},
48
+ {RuleKeyboardRun, violatesKeyboardRun},
49
+ {RuleRepeat, violatesRepeat},
50
+ {RuleContainsEmpNo, violatesEmpNo},
51
+ }}
52
+ }
53
+
54
+ // Check implements PasswordPolicy. Each violation is a PASSWORD_POLICY error
55
+ // on field "password" carrying the rule id.
56
+ func (p *Policy) Check(password, empNo string) []web.FieldError {
57
+ var out []web.FieldError
58
+ for _, r := range p.Rules {
59
+ if r.Violated(password, empNo) {
60
+ out = append(out, web.FieldError{Field: fieldPassword, Code: web.CodePasswordPolicy, Rule: r.ID})
61
+ }
62
+ }
63
+ return out
64
+ }
65
+
66
+ func violatesLength(pw, _ string) bool {
67
+ n := utf8.RuneCountInString(pw)
68
+ return n < minPassLen || n > maxPassLen
69
+ }
70
+
71
+ func violatesCharset(pw, _ string) bool {
72
+ for _, r := range pw {
73
+ if r < 0x20 || r > 0x7E { // also catches U+FFFD from invalid UTF-8
74
+ return true
75
+ }
76
+ }
77
+ return false
78
+ }
79
+
80
+ // violatesComplexity counts upper, lower, digit and symbol; a symbol is any
81
+ // printable non-alphanumeric ASCII character, space included. Characters
82
+ // outside printable ASCII belong to no class (CHARSET reports them).
83
+ func violatesComplexity(pw, _ string) bool {
84
+ var upper, lower, digit, symbol bool
85
+ for _, r := range pw {
86
+ switch {
87
+ case r >= 'A' && r <= 'Z':
88
+ upper = true
89
+ case r >= 'a' && r <= 'z':
90
+ lower = true
91
+ case r >= '0' && r <= '9':
92
+ digit = true
93
+ case r >= 0x20 && r <= 0x7E:
94
+ symbol = true
95
+ }
96
+ }
97
+ n := 0
98
+ for _, c := range [...]bool{upper, lower, digit, symbol} {
99
+ if c {
100
+ n++
101
+ }
102
+ }
103
+ return n < minPassClasses
104
+ }
105
+
106
+ // violatesRepeat: the same character three or more times in a row, compared
107
+ // exactly (case-sensitive: "aAa" is fine).
108
+ func violatesRepeat(pw, _ string) bool {
109
+ run, prev := 0, rune(-1)
110
+ for _, r := range pw {
111
+ if r == prev {
112
+ run++
113
+ } else {
114
+ run, prev = 1, r
115
+ }
116
+ if run > maxRepeat {
117
+ return true
118
+ }
119
+ }
120
+ return false
121
+ }
122
+
123
+ func violatesEmpNo(pw, empNo string) bool {
124
+ return empNo != "" && strings.Contains(pw, empNo)
125
+ }
@@ -9,10 +9,9 @@ import (
9
9
 
10
10
  // Field limits (PRD §6.3).
11
11
  const (
12
- empNoLen = 6
13
- maxNameLen = 50
14
- maxEmailLen = 254
15
- maxPassBytes = 72 // bcrypt silently ignores (x/crypto: rejects) anything longer
12
+ empNoLen = 6
13
+ maxNameLen = 50
14
+ maxEmailLen = 254
16
15
  )
17
16
 
18
17
  // Field names as they appear in errors[]; the order is the form order.
@@ -65,28 +64,12 @@ func checkEmail(s string) (string, text.Problem) {
65
64
  }
66
65
 
67
66
  // PasswordPolicy checks a candidate password and lists every violation, in
68
- // PRD §6.4 table order. empNo is the submitted employee ID when it is
67
+ // PRD §6.4 table order (see Policy). empNo is the submitted employee ID when it is
69
68
  // well-formed and "" otherwise (the CONTAINS_EMP_NO rule is then skipped).
70
69
  type PasswordPolicy interface {
71
70
  Check(password, empNo string) []web.FieldError
72
71
  }
73
72
 
74
- // StubPolicy is the placeholder until B1b-01 delivers the §6.4 rules. It only
75
- // refuses what the account cannot store: an empty password and one bcrypt
76
- // cannot hash (longer than 72 bytes). Everything else is accepted.
77
- type StubPolicy struct{}
78
-
79
- // Check implements PasswordPolicy.
80
- func (StubPolicy) Check(password, _ string) []web.FieldError {
81
- switch {
82
- case password == "":
83
- return []web.FieldError{{Field: fieldPassword, Code: web.CodeRequired}}
84
- case len(password) > maxPassBytes:
85
- return []web.FieldError{{Field: fieldPassword, Code: web.CodeOutOfRange}}
86
- }
87
- return nil
88
- }
89
-
90
73
  // topCode is the response code of a 422: the first field error's code when it
91
74
  // is a specific registered code, else VALIDATION_FAILED (error-codes.md §2).
92
75
  func topCode(fields []web.FieldError) web.ErrorCode {
@@ -71,6 +71,27 @@ func (e LoginResponseRole) Valid() bool {
71
71
  }
72
72
  }
73
73
 
74
+ // CreateIdeaForm defines model for CreateIdeaForm.
75
+ type CreateIdeaForm struct {
76
+ // Category COOL_IDEA or OUTSTANDING_IMPACT
77
+ Category string `json:"category"`
78
+
79
+ // Direction One of the five ESG directions (PRD 6.6)
80
+ Direction string `json:"direction"`
81
+
82
+ // Name 1-75 code points after clean-up; no / \ : * ? " < > |
83
+ Name string `json:"name"`
84
+
85
+ // Org AAID, BSID, ICSD, TSID, PLED or DEID
86
+ Org string `json:"org"`
87
+
88
+ // Slides One PDF or PPTX, at most 5 MiB, named {I|O}_{org}_{leaderName}_{leaderEmpNo}_{name}.{pdf|pptx}
89
+ Slides openapi_types.File `json:"slides"`
90
+
91
+ // Summary 1-300 code points after clean-up
92
+ Summary string `json:"summary"`
93
+ }
94
+
74
95
  // CsrfTokenResponse defines model for CsrfTokenResponse.
75
96
  type CsrfTokenResponse struct {
76
97
  Token string `json:"token"`
@@ -93,6 +114,9 @@ type FieldError struct {
93
114
  Code string `json:"code"`
94
115
  Expected *string `json:"expected,omitempty"`
95
116
  Field string `json:"field"`
117
+
118
+ // Rule PASSWORD_POLICY only, the violated rule id (PRD 6.4)
119
+ Rule *string `json:"rule,omitempty"`
96
120
  }
97
121
 
98
122
  // IdeaDetail defines model for IdeaDetail.
@@ -245,6 +269,33 @@ type RegisterParams struct {
245
269
  type ListIdeasParams struct {
246
270
  Page *int `form:"page,omitempty" json:"page,omitempty"`
247
271
  Size *int `form:"size,omitempty" json:"size,omitempty"`
272
+
273
+ // Org Closed set; an invalid value is 422 INVALID_ENUM (never ignored). Validated in the handler.
274
+ Org *string `form:"org,omitempty" json:"org,omitempty"`
275
+ Category *string `form:"category,omitempty" json:"category,omitempty"`
276
+ Direction *string `form:"direction,omitempty" json:"direction,omitempty"`
277
+ LeaderEmpNo *string `form:"leaderEmpNo,omitempty" json:"leaderEmpNo,omitempty"`
278
+
279
+ // CreatedFrom ISO 8601 instant with offset or Z; inclusive. Validated in the handler.
280
+ CreatedFrom *string `form:"createdFrom,omitempty" json:"createdFrom,omitempty"`
281
+
282
+ // CreatedTo ISO 8601 instant with offset or Z; exclusive. Validated in the handler.
283
+ CreatedTo *string `form:"createdTo,omitempty" json:"createdTo,omitempty"`
284
+
285
+ // Q Case-insensitive "name contains"; `%` and `_` are literal.
286
+ Q *string `form:"q,omitempty" json:"q,omitempty"`
287
+
288
+ // Sort createdAt (default) or name; anything else is 422 INVALID_ENUM. Validated in the handler.
289
+ Sort *string `form:"sort,omitempty" json:"sort,omitempty"`
290
+
291
+ // Order asc or desc (default desc for createdAt, asc for name); anything else is 422 INVALID_ENUM. Validated in the handler.
292
+ Order *string `form:"order,omitempty" json:"order,omitempty"`
293
+ }
294
+
295
+ // CreateIdeaParams defines parameters for CreateIdea.
296
+ type CreateIdeaParams struct {
297
+ // XCSRFToken Session-bound token from GET /auth/csrf. Mandatory on every write, but enforced by the CSRF middleware (403 CSRF_INVALID), so the generated wrapper must not reject its absence first (400).
298
+ XCSRFToken *CsrfToken `json:"X-CSRF-Token,omitempty"`
248
299
  }
249
300
 
250
301
  // LoginJSONRequestBody defines body for Login for application/json ContentType.
@@ -253,6 +304,9 @@ type LoginJSONRequestBody = LoginRequest
253
304
  // RegisterJSONRequestBody defines body for Register for application/json ContentType.
254
305
  type RegisterJSONRequestBody = RegisterRequest
255
306
 
307
+ // CreateIdeaMultipartRequestBody defines body for CreateIdea for multipart/form-data ContentType.
308
+ type CreateIdeaMultipartRequestBody = CreateIdeaForm
309
+
256
310
  // ServerInterface represents all server handlers.
257
311
  type ServerInterface interface {
258
312
  // GetCsrfToken Session-bound CSRF token (anonymous before login)
@@ -276,6 +330,9 @@ type ServerInterface interface {
276
330
  // ListIdeas List Ideas visible to the caller
277
331
  // (GET /ideas)
278
332
  ListIdeas(w http.ResponseWriter, r *http.Request, params ListIdeasParams)
333
+ // CreateIdea Create an Idea with its slides (User only)
334
+ // (POST /ideas)
335
+ CreateIdea(w http.ResponseWriter, r *http.Request, params CreateIdeaParams)
279
336
  // GetIdea Idea detail
280
337
  // (GET /ideas/{id})
281
338
  GetIdea(w http.ResponseWriter, r *http.Request, id IdeaId)
@@ -327,6 +384,12 @@ func (_ Unimplemented) ListIdeas(w http.ResponseWriter, r *http.Request, params
327
384
  w.WriteHeader(http.StatusNotImplemented)
328
385
  }
329
386
 
387
+ // CreateIdea Create an Idea with its slides (User only)
388
+ // (POST /ideas)
389
+ func (_ Unimplemented) CreateIdea(w http.ResponseWriter, r *http.Request, params CreateIdeaParams) {
390
+ w.WriteHeader(http.StatusNotImplemented)
391
+ }
392
+
330
393
  // GetIdea Idea detail
331
394
  // (GET /ideas/{id})
332
395
  func (_ Unimplemented) GetIdea(w http.ResponseWriter, r *http.Request, id IdeaId) {
@@ -542,6 +605,123 @@ func (siw *ServerInterfaceWrapper) ListIdeas(w http.ResponseWriter, r *http.Requ
542
605
  return
543
606
  }
544
607
 
608
+ // ------------- Optional query parameter "org" -------------
609
+
610
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "org", r.URL.Query(), &params.Org, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
611
+ if err != nil {
612
+ var requiredError *runtime.RequiredParameterError
613
+ if errors.As(err, &requiredError) {
614
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "org"})
615
+ } else {
616
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "org", Err: err})
617
+ }
618
+ return
619
+ }
620
+
621
+ // ------------- Optional query parameter "category" -------------
622
+
623
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "category", r.URL.Query(), &params.Category, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
624
+ if err != nil {
625
+ var requiredError *runtime.RequiredParameterError
626
+ if errors.As(err, &requiredError) {
627
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "category"})
628
+ } else {
629
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "category", Err: err})
630
+ }
631
+ return
632
+ }
633
+
634
+ // ------------- Optional query parameter "direction" -------------
635
+
636
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "direction", r.URL.Query(), &params.Direction, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
637
+ if err != nil {
638
+ var requiredError *runtime.RequiredParameterError
639
+ if errors.As(err, &requiredError) {
640
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "direction"})
641
+ } else {
642
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "direction", Err: err})
643
+ }
644
+ return
645
+ }
646
+
647
+ // ------------- Optional query parameter "leaderEmpNo" -------------
648
+
649
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "leaderEmpNo", r.URL.Query(), &params.LeaderEmpNo, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
650
+ if err != nil {
651
+ var requiredError *runtime.RequiredParameterError
652
+ if errors.As(err, &requiredError) {
653
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "leaderEmpNo"})
654
+ } else {
655
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "leaderEmpNo", Err: err})
656
+ }
657
+ return
658
+ }
659
+
660
+ // ------------- Optional query parameter "createdFrom" -------------
661
+
662
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "createdFrom", r.URL.Query(), &params.CreatedFrom, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
663
+ if err != nil {
664
+ var requiredError *runtime.RequiredParameterError
665
+ if errors.As(err, &requiredError) {
666
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "createdFrom"})
667
+ } else {
668
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "createdFrom", Err: err})
669
+ }
670
+ return
671
+ }
672
+
673
+ // ------------- Optional query parameter "createdTo" -------------
674
+
675
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "createdTo", r.URL.Query(), &params.CreatedTo, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
676
+ if err != nil {
677
+ var requiredError *runtime.RequiredParameterError
678
+ if errors.As(err, &requiredError) {
679
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "createdTo"})
680
+ } else {
681
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "createdTo", Err: err})
682
+ }
683
+ return
684
+ }
685
+
686
+ // ------------- Optional query parameter "q" -------------
687
+
688
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "q", r.URL.Query(), &params.Q, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
689
+ if err != nil {
690
+ var requiredError *runtime.RequiredParameterError
691
+ if errors.As(err, &requiredError) {
692
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "q"})
693
+ } else {
694
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "q", Err: err})
695
+ }
696
+ return
697
+ }
698
+
699
+ // ------------- Optional query parameter "sort" -------------
700
+
701
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "sort", r.URL.Query(), &params.Sort, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
702
+ if err != nil {
703
+ var requiredError *runtime.RequiredParameterError
704
+ if errors.As(err, &requiredError) {
705
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "sort"})
706
+ } else {
707
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err})
708
+ }
709
+ return
710
+ }
711
+
712
+ // ------------- Optional query parameter "order" -------------
713
+
714
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "order", r.URL.Query(), &params.Order, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
715
+ if err != nil {
716
+ var requiredError *runtime.RequiredParameterError
717
+ if errors.As(err, &requiredError) {
718
+ siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "order"})
719
+ } else {
720
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "order", Err: err})
721
+ }
722
+ return
723
+ }
724
+
545
725
  handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
546
726
  siw.Handler.ListIdeas(w, r, params)
547
727
  }))
@@ -553,6 +733,47 @@ func (siw *ServerInterfaceWrapper) ListIdeas(w http.ResponseWriter, r *http.Requ
553
733
  handler.ServeHTTP(w, r)
554
734
  }
555
735
 
736
+ // CreateIdea operation middleware
737
+ func (siw *ServerInterfaceWrapper) CreateIdea(w http.ResponseWriter, r *http.Request) {
738
+
739
+ var err error
740
+ _ = err
741
+
742
+ // Parameter object where we will unmarshal all parameters from the context
743
+ var params CreateIdeaParams
744
+
745
+ headers := r.Header
746
+
747
+ // ------------- Optional header parameter "X-CSRF-Token" -------------
748
+ if valueList, found := headers[http.CanonicalHeaderKey("X-CSRF-Token")]; found {
749
+ var XCSRFToken CsrfToken
750
+ n := len(valueList)
751
+ if n != 1 {
752
+ siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "X-CSRF-Token", Count: n})
753
+ return
754
+ }
755
+
756
+ err = runtime.BindStyledParameterWithOptions("simple", "X-CSRF-Token", valueList[0], &XCSRFToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""})
757
+ if err != nil {
758
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "X-CSRF-Token", Err: err})
759
+ return
760
+ }
761
+
762
+ params.XCSRFToken = &XCSRFToken
763
+
764
+ }
765
+
766
+ handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
767
+ siw.Handler.CreateIdea(w, r, params)
768
+ }))
769
+
770
+ for _, middleware := range siw.HandlerMiddlewares {
771
+ handler = middleware(handler)
772
+ }
773
+
774
+ handler.ServeHTTP(w, r)
775
+ }
776
+
556
777
  // GetIdea operation middleware
557
778
  func (siw *ServerInterfaceWrapper) GetIdea(w http.ResponseWriter, r *http.Request) {
558
779
 
@@ -713,6 +934,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
713
934
  r.Group(func(r chi.Router) {
714
935
  r.Get(options.BaseURL+"/ideas", wrapper.ListIdeas)
715
936
  })
937
+ r.Group(func(r chi.Router) {
938
+ r.Post(options.BaseURL+"/ideas", wrapper.CreateIdea)
939
+ })
716
940
  r.Group(func(r chi.Router) {
717
941
  r.Get(options.BaseURL+"/ideas/{id}", wrapper.GetIdea)
718
942
  })