@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
@@ -0,0 +1,37 @@
1
+ // Package passwords holds the bundled common-password blocklist (PRD §6.4,
2
+ // rule COMMON_PASSWORD). Source and licence of the list: NOTICE in this
3
+ // directory. The frontend lazy-loads the same file for its live checklist.
4
+ package passwords
5
+
6
+ import (
7
+ _ "embed"
8
+ "strings"
9
+ )
10
+
11
+ //go:embed common-passwords.txt
12
+ var commonList string
13
+
14
+ // common holds every list entry folded to lower case. The list is printable
15
+ // ASCII, so strings.ToLower is an exact ASCII case fold here.
16
+ var common = func() map[string]struct{} {
17
+ lines := strings.Split(commonList, "\n")
18
+ m := make(map[string]struct{}, len(lines))
19
+ for _, l := range lines {
20
+ l = strings.TrimSuffix(l, "\r")
21
+ if l != "" {
22
+ m[strings.ToLower(l)] = struct{}{}
23
+ }
24
+ }
25
+ return m
26
+ }()
27
+
28
+ // IsCommon reports whether pw equals a list entry, ignoring case. The whole
29
+ // password is compared as submitted: nothing is trimmed, and a list entry
30
+ // inside a longer password does not match.
31
+ func IsCommon(pw string) bool {
32
+ _, ok := common[strings.ToLower(pw)]
33
+ return ok
34
+ }
35
+
36
+ // Len is the number of distinct entries (after case folding).
37
+ func Len() int { return len(common) }
@@ -0,0 +1,35 @@
1
+ package passwords
2
+
3
+ import "testing"
4
+
5
+ func TestIsCommon(t *testing.T) {
6
+ for _, tc := range []struct {
7
+ pw string
8
+ want bool
9
+ }{
10
+ {"password", true},
11
+ {"PASSWORD", true},
12
+ {"PassWord", true},
13
+ {"123456", true},
14
+ {"films+pic+galeries", true},
15
+ {"Films+Pic+Galeries", true},
16
+ {"eyphed", true}, // last line of the file
17
+ {"", false},
18
+ {" password", false}, // nothing is trimmed
19
+ {"password ", false},
20
+ {"password1!", false}, // not on the list
21
+ {"xpasswordx", false}, // an entry inside a longer password does not match
22
+ {"Correct-Horse-1", false},
23
+ } {
24
+ if got := IsCommon(tc.pw); got != tc.want {
25
+ t.Errorf("IsCommon(%q) = %v, want %v", tc.pw, got, tc.want)
26
+ }
27
+ }
28
+ }
29
+
30
+ func TestListIsLoaded(t *testing.T) {
31
+ // 10,001 lines, no case-folded duplicates (NOTICE).
32
+ if got := Len(); got != 10001 {
33
+ t.Errorf("Len() = %d, want 10001", got)
34
+ }
35
+ }
@@ -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,131 @@
1
+ package account
2
+
3
+ import (
4
+ "strings"
5
+ "unicode/utf8"
6
+
7
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/account/internal/passwords"
8
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/web"
9
+ )
10
+
11
+ // Password policy (PRD §6.4). Rule ids are the wire contract (errors[].rule).
12
+ const (
13
+ RuleLength = "LENGTH"
14
+ RuleCharset = "CHARSET"
15
+ RuleComplexity = "COMPLEXITY"
16
+ RuleCommon = "COMMON_PASSWORD"
17
+ RuleKeyboardRun = "KEYBOARD_RUN"
18
+ RuleSequence = "SEQUENCE"
19
+ RuleRepeat = "REPEAT"
20
+ RuleContainsEmpNo = "CONTAINS_EMP_NO"
21
+ )
22
+
23
+ const (
24
+ minPassLen = 12
25
+ maxPassLen = 64 // code points
26
+ minPassClasses = 3
27
+ maxRepeat = 2 // 3+ identical characters in a row is a violation
28
+ )
29
+
30
+ // Rule is one row of the §6.4 table. Violated reports whether password breaks
31
+ // the rule; empNo is "" when the submitted employee ID is malformed.
32
+ type Rule struct {
33
+ ID string
34
+ Violated func(password, empNo string) bool
35
+ }
36
+
37
+ // Policy runs its rules in order and reports every violated one. The slice
38
+ // order is the PRD §6.4 table order, which is part of the contract
39
+ // (testdata/password.json): later PRs add their rule at its table position.
40
+ type Policy struct{ Rules []Rule }
41
+
42
+ // DefaultPolicy returns all eight rules of the PRD §6.4 table, in table order.
43
+ func DefaultPolicy() *Policy {
44
+ return &Policy{Rules: []Rule{
45
+ {RuleLength, violatesLength},
46
+ {RuleCharset, violatesCharset},
47
+ {RuleComplexity, violatesComplexity},
48
+ {RuleCommon, violatesCommon},
49
+ {RuleKeyboardRun, violatesKeyboardRun},
50
+ {RuleSequence, violatesSequence},
51
+ {RuleRepeat, violatesRepeat},
52
+ {RuleContainsEmpNo, violatesEmpNo},
53
+ }}
54
+ }
55
+
56
+ // Check implements PasswordPolicy. Each violation is a PASSWORD_POLICY error
57
+ // on field "password" carrying the rule id.
58
+ func (p *Policy) Check(password, empNo string) []web.FieldError {
59
+ var out []web.FieldError
60
+ for _, r := range p.Rules {
61
+ if r.Violated(password, empNo) {
62
+ out = append(out, web.FieldError{Field: fieldPassword, Code: web.CodePasswordPolicy, Rule: r.ID})
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+ func violatesLength(pw, _ string) bool {
69
+ n := utf8.RuneCountInString(pw)
70
+ return n < minPassLen || n > maxPassLen
71
+ }
72
+
73
+ func violatesCharset(pw, _ string) bool {
74
+ for _, r := range pw {
75
+ if r < 0x20 || r > 0x7E { // also catches U+FFFD from invalid UTF-8
76
+ return true
77
+ }
78
+ }
79
+ return false
80
+ }
81
+
82
+ // violatesComplexity counts upper, lower, digit and symbol; a symbol is any
83
+ // printable non-alphanumeric ASCII character, space included. Characters
84
+ // outside printable ASCII belong to no class (CHARSET reports them).
85
+ func violatesComplexity(pw, _ string) bool {
86
+ var upper, lower, digit, symbol bool
87
+ for _, r := range pw {
88
+ switch {
89
+ case r >= 'A' && r <= 'Z':
90
+ upper = true
91
+ case r >= 'a' && r <= 'z':
92
+ lower = true
93
+ case r >= '0' && r <= '9':
94
+ digit = true
95
+ case r >= 0x20 && r <= 0x7E:
96
+ symbol = true
97
+ }
98
+ }
99
+ n := 0
100
+ for _, c := range [...]bool{upper, lower, digit, symbol} {
101
+ if c {
102
+ n++
103
+ }
104
+ }
105
+ return n < minPassClasses
106
+ }
107
+
108
+ // violatesCommon: the whole password equals an entry of the bundled Top-10,000
109
+ // list, ignoring case (internal/passwords/NOTICE for the source).
110
+ func violatesCommon(pw, _ string) bool { return passwords.IsCommon(pw) }
111
+
112
+ // violatesRepeat: the same character three or more times in a row, compared
113
+ // exactly (case-sensitive: "aAa" is fine).
114
+ func violatesRepeat(pw, _ string) bool {
115
+ run, prev := 0, rune(-1)
116
+ for _, r := range pw {
117
+ if r == prev {
118
+ run++
119
+ } else {
120
+ run, prev = 1, r
121
+ }
122
+ if run > maxRepeat {
123
+ return true
124
+ }
125
+ }
126
+ return false
127
+ }
128
+
129
+ func violatesEmpNo(pw, empNo string) bool {
130
+ return empNo != "" && strings.Contains(pw, empNo)
131
+ }
@@ -0,0 +1,54 @@
1
+ package account
2
+
3
+ const minSequence = 4
4
+
5
+ // seqKey places a character on one of two alphabets: letters a–z (case
6
+ // folded) and digits 0–9. ok is false for anything else, which breaks a
7
+ // sequence. Letters and digits never join each other ("89ab" is two runs of 2),
8
+ // and neighbours in ASCII outside an alphabet do not count ("xyz{" is 3).
9
+ func seqKey(r rune) (alpha int8, pos int8, ok bool) {
10
+ switch {
11
+ case r >= 'a' && r <= 'z':
12
+ return 0, int8(r - 'a'), true
13
+ case r >= 'A' && r <= 'Z':
14
+ return 0, int8(r - 'A'), true
15
+ case r >= '0' && r <= '9':
16
+ return 1, int8(r - '0'), true
17
+ }
18
+ return 0, 0, false
19
+ }
20
+
21
+ // violatesSequence: four or more consecutive letters or digits, ascending or
22
+ // descending, case-insensitive ("abcd", "DcBa", "4321"). Sequences do not wrap
23
+ // ("xyza", "9012" are fine), the same character repeated is not a sequence
24
+ // (that is REPEAT), and a run must keep one direction ("abcba" is fine).
25
+ // Mirrors violatesKeyboardRun.
26
+ func violatesSequence(pw, _ string) bool {
27
+ var prevAlpha, prevPos int8
28
+ have, run, dir := false, 0, int8(0)
29
+ for _, r := range pw {
30
+ a, p, ok := seqKey(r)
31
+ if !ok {
32
+ have, run, dir = false, 0, 0
33
+ continue
34
+ }
35
+ step := int8(0)
36
+ if have && a == prevAlpha {
37
+ step = p - prevPos
38
+ }
39
+ if step == 1 || step == -1 {
40
+ if run >= 2 && step == dir {
41
+ run++
42
+ } else {
43
+ run, dir = 2, step
44
+ }
45
+ } else {
46
+ run, dir = 1, 0
47
+ }
48
+ prevAlpha, prevPos, have = a, p, true
49
+ if run >= minSequence {
50
+ return true
51
+ }
52
+ }
53
+ return false
54
+ }
@@ -0,0 +1,38 @@
1
+ package account
2
+
3
+ import "testing"
4
+
5
+ func TestViolatesSequence(t *testing.T) {
6
+ for _, tc := range []struct {
7
+ pw string
8
+ want bool
9
+ }{
10
+ {"", false},
11
+ {"abc", false},
12
+ {"abcd", true},
13
+ {"dcba", true},
14
+ {"aBcD", true},
15
+ {"WXYZ", true},
16
+ {"0123", true},
17
+ {"9876", true},
18
+ {"3210", true},
19
+ {"x-abcdefg-y", true}, // longer run, inside other text
20
+ {"xyza", false}, // no wrap
21
+ {"9012", false}, // no wrap
22
+ {"zyxw", true},
23
+ {"abcba", false}, // direction change restarts the count
24
+ {"abbc", false}, // a repeat breaks the run
25
+ {"aaaa", false}, // repeat is not a sequence
26
+ {"89ab", false}, // letters and digits do not join
27
+ {"xyz{", false}, // ASCII neighbour outside the alphabet
28
+ {"/012", false}, // ASCII neighbour outside the alphabet
29
+ {"ab-cd", false}, // a separator breaks the run
30
+ {"abécd", false}, // non-ASCII breaks the run
31
+ {"ab12cd", false}, // alphabets switching break the run
32
+ {"abc0123", true},
33
+ } {
34
+ if got := violatesSequence(tc.pw, ""); got != tc.want {
35
+ t.Errorf("violatesSequence(%q) = %v, want %v", tc.pw, got, tc.want)
36
+ }
37
+ }
38
+ }
@@ -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 {