[UX-902] fix: create user client-side validation#2233
[UX-902] fix: create user client-side validation#2233eblairmckee wants to merge 2 commits intomasterfrom
Conversation
…ield, added helpers to derive zod schema from proto definitions, and added integration tests
| export function generatePassword(length: number, allowSpecialChars: boolean): string { | ||
| if (length <= 0) { | ||
| return ''; | ||
| } | ||
|
|
||
| const lowercase = 'abcdefghijklmnopqrstuvwxyz'; | ||
| const uppercase = lowercase.toUpperCase(); | ||
| const numbers = '0123456789'; | ||
| const special = '.,&_+|[]/-()'; | ||
|
|
||
| let alphabet = lowercase + uppercase + numbers; | ||
| if (allowSpecialChars) { | ||
| alphabet += special; | ||
| } | ||
|
|
||
| const randomValues = new Uint32Array(length); | ||
| crypto.getRandomValues(randomValues); | ||
|
|
||
| let result = ''; | ||
| for (const n of randomValues) { | ||
| const index = n % alphabet.length; | ||
| const sym = alphabet[index]; | ||
|
|
||
| result += sym; | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
I think we should be using bcrypt or similar to generate the hash, crypto is getting better in native nodejs but nonetheless I would feel safer if we use bcrypt
| mechanism: z.enum(SASL_MECHANISMS), | ||
| // Not in proto — frontend-only field for role assignment | ||
| roles: z.array(z.string()).default([]), | ||
| }); |
There was a problem hiding this comment.
Is there any way we can get it working without casting such as as z.ZodString?
Ideally I would like to rely as much as possible on zod and protovaildate internals.
That being said, we need to discuss whether we go with:
a) merging the zod and protovalidate validation step and present the user with errors accordingly
b) 1st rely on the z.infer() handler where we get all the zod powered schema validation on every form change and then do a 2nd pass on protovalidate on submission? Ideally the 2 schemas should not be very far away from each other
c) have a dedicated package which would have the power to convert all protovalidate schemas to zod, but then we would lose on the protovalidate descriptive errors, so perhaps we may want to consolidate them. Just talking out loud
Personally I would be leaning towards the approach in Gateway where you have protovalidateResolver, but with the additional zod resolver.
| /> | ||
| <FieldDescription> | ||
| Must not contain any whitespace. Dots, hyphens and underscores may be used. Maximum{' '} | ||
| {USERNAME_MAX_LENGTH} characters. |
There was a problem hiding this comment.
did you mean to interpolate the string there
There was a problem hiding this comment.
before we continue with this approach, let's consider creating protovalidate resolver like disccused in this thread https://github.com/redpanda-data/console/pull/2233/changes#r2831228170
malinskibeniamin
left a comment
There was a problem hiding this comment.
left more comments
|
@claude Review the PR |
|
Claude finished @weeco's task in 2m 56s —— View job PR Review: UX-902 fix: create user client-side validationTasks
The overall direction of this PR is solid — deriving validation constraints directly from proto annotations is the right approach to prevent client/server drift. Here are my findings: Critical Issues1. Unsafe
username: (protoSchema.shape.name as z.ZodString)
.regex(USERNAME_REGEX, USERNAME_ERROR_MESSAGE)
...
password: protoSchema.shape.password as z.ZodString,If the proto field ever changes type (or 2.
const isValidPassword = password && password.length >= 4 && password.length <= 64;The change password modal validates 3. Zod schema not memoized — recreated on every render
const form = useForm<UserCreateFormValues>({
resolver: zodResolver(createUserFormSchema(existingUsers)),
...
});
const schema = useMemo(() => createUserFormSchema(existingUsers), [existingUsers]);
const form = useForm({ resolver: zodResolver(schema), ... });Medium Issues4. Modulo bias in
const randomValues = new Uint32Array(length);
crypto.getRandomValues(randomValues);
for (const n of randomValues) {
const index = n % alphabet.length;
5. Missing constraint mappings in
6. Magic numbers for proto scalar types
case 9: // STRING
case 5: // INT32
case 13: // UINT32
case 17: // SINT32
case 8: // BOOLThese should reference the import { ScalarType } from '@bufbuild/protobuf';
case ScalarType.STRING:7. Role assignment failures are silently ignored
await Promise.allSettled(rolePromises);
8. Username uniqueness is case-sensitive
.refine((val) => !existingUsers.includes(val), 'User already exists')If the server treats usernames case-insensitively, a user could type "Admin" when "admin" exists and still get a server error. Consider Minor Issues9.
{Boolean(fieldState.invalid) && <FieldError ... />}
10. Inconsistent
return extractStringConstraints(rules.type.value as StringRules, required);The 11. Fallback with no diagnostic
export const USERNAME_MAX_LENGTH = nameConstraints?.maxLen ?? 128;If Addressing Existing Review Comments
TestsThe integration tests in One gap: there's no test for the Summary: The approach is sound but I'd address the |


UX-902
Problem: Users got an obtuse server error when clicking "Create" with a username exceeding 128 characters — no client-side validation told them what was wrong, so they had to guess the limit by trial and error.
Fix: Added client-side validation with clear error messages for username/password length limits, derived directly from protobuf field constraints so they can't drift from the server.
Changes:
After
Before