Skip to content

Commit 94d166a

Browse files
fix: increase max password length to 64 and improve validation error message (#3713)
1 parent c9156e5 commit 94d166a

5 files changed

Lines changed: 136 additions & 47 deletions

File tree

api/v1/server/handlers/users/update_login.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ func (u *UserService) UserUpdateLogin(ctx echo.Context, request gen.UserUpdateLo
2727
if apiErrors, err := u.config.Validator.ValidateAPI(request.Body); err != nil {
2828
return nil, err
2929
} else if apiErrors != nil {
30-
return gen.UserUpdateLogin400JSONResponse(*apiErrors), nil
30+
return gen.UserUpdateLogin400JSONResponse(
31+
apierrors.NewAPIErrors(ErrInvalidCredentials),
32+
), nil
3133
}
3234

3335
if err := u.checkUserRestrictionsForEmail(u.config, string(request.Body.Email)); err != nil {

frontend/app/src/pages/auth/register/components/user-register-form.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { z } from 'zod';
1111
const schema = z.object({
1212
name: z.string().min(3, 'Name must be at least 3 characters long'),
1313
email: z.string().email('Invalid email address'),
14-
password: z.string().min(8, 'Password must be at least 8 characters long'),
14+
password: z
15+
.string()
16+
.min(8, 'Password must be at least 8 characters long')
17+
.max(64, 'Password must be at most 64 characters long')
18+
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
19+
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
20+
.regex(/[0-9]/, 'Password must contain at least one number'),
1521
});
1622

1723
type SubmitType = z.infer<typeof schema>;

pkg/validator/default.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
const (
1717
EmailErr = "Invalid email address"
18-
PasswordErr = "Invalid password. Passwords must be at least 8 characters in length, contain an upper and lowercase letter, and contain at least one number."
18+
PasswordErr = "Invalid password. Passwords must be between 8 and 64 characters in length, contain an upper and lowercase letter, and contain at least one number."
1919
UUIDErr = "Invalid UUID reference"
2020
HatchetNameErr = "Hatchet names must match the regex ^[a-zA-Z0-9\\.\\-_]+$"
2121
ActionIDErr = "Invalid action ID. Action IDs must be in the format <integrationId>:<verb>"

pkg/validator/validator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func passwordValidation(pw string) bool {
104104
}
105105
}
106106

107-
return hasNumber && hasUpper && hasLower && pwLen >= 8 && pwLen <= 32
107+
return hasNumber && hasUpper && hasLower && pwLen >= 8 && pwLen <= 64
108108
}
109109

110110
func IsValidUUID(u string) bool {

pkg/validator/validator_test.go

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,70 +12,151 @@ type nameResource struct {
1212
DisplayName string `validate:"hatchetName"`
1313
}
1414

15-
func TestValidatorInvalidName(t *testing.T) {
16-
v := newValidator()
17-
18-
err := v.Struct(&nameResource{
19-
DisplayName: "&&!!",
20-
})
21-
22-
assert.ErrorContains(t, err, "validation for 'DisplayName' failed on the 'hatchetName' tag", "should throw error on invalid name")
23-
}
15+
func TestValidatorName(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
input string
19+
wantErrTag string
20+
}{
21+
{
22+
name: "valid name",
23+
input: "test-name",
24+
},
25+
{
26+
name: "invalid name with special characters",
27+
input: "&&!!",
28+
wantErrTag: "hatchetName",
29+
},
30+
}
2431

25-
func TestValidatorValidName(t *testing.T) {
2632
v := newValidator()
2733

28-
err := v.Struct(&nameResource{
29-
DisplayName: "test-name",
30-
})
31-
32-
assert.NoError(t, err, "no error")
34+
for _, tt := range tests {
35+
t.Run(tt.name, func(t *testing.T) {
36+
err := v.Struct(&nameResource{DisplayName: tt.input})
37+
if tt.wantErrTag != "" {
38+
assert.ErrorContains(t, err, "validation for 'DisplayName' failed on the '"+tt.wantErrTag+"' tag")
39+
} else {
40+
assert.NoError(t, err)
41+
}
42+
})
43+
}
3344
}
3445

3546
type cronResource struct {
3647
Cron string `validate:"cron"`
3748
}
3849

39-
func TestValidatorValidCron(t *testing.T) {
50+
func TestValidatorCron(t *testing.T) {
51+
tests := []struct {
52+
name string
53+
input string
54+
wantErrTag string
55+
}{
56+
{
57+
name: "valid cron expression",
58+
input: "*/5 * * * *",
59+
},
60+
{
61+
name: "invalid cron expression (missing field)",
62+
input: "*/5 * * *",
63+
wantErrTag: "cron",
64+
},
65+
}
66+
4067
v := newValidator()
4168

42-
err := v.Struct(&cronResource{
43-
Cron: "*/5 * * * *",
44-
})
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
err := v.Struct(&cronResource{Cron: tt.input})
72+
if tt.wantErrTag != "" {
73+
assert.ErrorContains(t, err, "validation for 'Cron' failed on the '"+tt.wantErrTag+"' tag")
74+
} else {
75+
assert.NoError(t, err)
76+
}
77+
})
78+
}
79+
}
4580

46-
assert.NoError(t, err, "no error")
81+
type durationResource struct {
82+
Duration string `validate:"duration"`
4783
}
4884

49-
func TestValidatorInvalidCron(t *testing.T) {
50-
v := newValidator()
85+
func TestValidatorDuration(t *testing.T) {
86+
tests := []struct {
87+
name string
88+
input string
89+
wantErrTag string
90+
}{
91+
{
92+
name: "valid duration",
93+
input: "5s",
94+
},
95+
{
96+
name: "invalid duration (missing unit)",
97+
input: "5",
98+
wantErrTag: "duration",
99+
},
100+
}
51101

52-
err := v.Struct(&cronResource{
53-
Cron: "*/5 * * *",
54-
})
102+
v := newValidator()
55103

56-
assert.ErrorContains(t, err, "validation for 'Cron' failed on the 'cron' tag", "should throw error on invalid cron")
104+
for _, tt := range tests {
105+
t.Run(tt.name, func(t *testing.T) {
106+
err := v.Struct(&durationResource{Duration: tt.input})
107+
if tt.wantErrTag != "" {
108+
assert.ErrorContains(t, err, "validation for 'Duration' failed on the '"+tt.wantErrTag+"' tag")
109+
} else {
110+
assert.NoError(t, err)
111+
}
112+
})
113+
}
57114
}
58115

59-
func TestValidatorValidDuration(t *testing.T) {
60-
v := newValidator()
116+
type passwordResource struct {
117+
Password string `validate:"password"`
118+
}
61119

62-
err := v.Struct(&struct {
63-
Duration string `validate:"duration"`
120+
func TestValidatorPassword(t *testing.T) {
121+
tests := []struct {
122+
name string
123+
input string
124+
wantErrTag string
64125
}{
65-
Duration: "5s",
66-
})
126+
{
127+
name: "valid password",
128+
input: "ValidPass1",
129+
},
130+
{
131+
name: "too short (under 8 characters)",
132+
input: "Short1",
133+
wantErrTag: "password",
134+
},
135+
{
136+
name: "valid password between 32 and 64 characters",
137+
input: "ThisPasswordIsLongerThan32CharsButValid1A123456789012",
138+
},
139+
{
140+
name: "valid password at exactly 64 characters",
141+
input: "ValidPassword1AValidPassword1AValidPassword1AValidPassword1A1234",
142+
},
143+
{
144+
name: "too long (over 64 characters)",
145+
input: "ValidPassword1AValidPassword1AValidPassword1AValidPassword1A12345",
146+
wantErrTag: "password",
147+
},
148+
}
67149

68-
assert.NoError(t, err, "no error")
69-
}
70-
71-
func TestValidatorInvalidDuration(t *testing.T) {
72150
v := newValidator()
73151

74-
err := v.Struct(&struct {
75-
Duration string `validate:"duration"`
76-
}{
77-
Duration: "5",
78-
})
79-
80-
assert.ErrorContains(t, err, "validation for 'Duration' failed on the 'duration' tag", "should throw error on invalid duration")
152+
for _, tt := range tests {
153+
t.Run(tt.name, func(t *testing.T) {
154+
err := v.Struct(&passwordResource{Password: tt.input})
155+
if tt.wantErrTag != "" {
156+
assert.ErrorContains(t, err, "validation for 'Password' failed on the '"+tt.wantErrTag+"' tag")
157+
} else {
158+
assert.NoError(t, err)
159+
}
160+
})
161+
}
81162
}

0 commit comments

Comments
 (0)