fix(frontend): cap the inferred username at what the column holds

`usr.username` is VARCHAR(50) while the provider name and email it is
derived from run to 255, and `create_workspace` inserts the value
untruncated — so a long first name failed the same way the invalid
characters did: posted invisibly, refused on insert, with nothing on
screen naming the field. Undefined instead, which the form already
routes to the full one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
This commit is contained in:
Guilhem Lemouel
2026-09-04 17:48:19 +02:00
co-authored by Claude Opus 5
parent 7d0794adfa
commit b5c148dd8d
2 changed files with 19 additions and 5 deletions
@@ -52,4 +52,11 @@ describe('usernameFromName', () => {
expect(usernameFromName('++')).toBeUndefined()
expect(usernameFromName('')).toBeUndefined()
})
it('answers undefined rather than a value the column cannot hold', () => {
// `usr.username` is VARCHAR(50) while the name and email it is derived from run to 255,
// and `create_workspace` inserts it untruncated.
expect(usernameFromName('a'.repeat(50))).toBe('a'.repeat(50))
expect(usernameFromName('a'.repeat(51))).toBeUndefined()
})
})
+12 -5
View File
@@ -45,15 +45,22 @@ export interface UsernamePolicy {
suggested?: string
}
/** What `usr.username` holds, and neither the provider name nor the email is bounded by it. */
const USERNAME_MAX_LENGTH = 50
/**
* A username the `proper_username` constraint accepts: `^[\w-]+$`, so word characters and
* hyphens and nothing else. Everything else is dropped rather than substituted — `O'Connor`
* is `oconnor`, not `o-connor` — and a name with nothing left of it answers undefined, which
* is the caller's cue to ask for one instead of posting a value the database refuses.
* A username the whole `usr.username` contract accepts: the `proper_username` constraint
* (`^[\w-]+$`, so word characters and hyphens and nothing else) and the column's own 50
* characters. Anything outside the class is dropped rather than substituted — `O'Connor` is
* `oconnor`, not `o-connor`.
*
* Undefined where nothing usable is left or where what is left is too long, which is the
* caller's cue to ask for one: `create_workspace` inserts this value with no truncation, so a
* name the column refuses would fail on insert with nothing on screen naming the field.
*/
export function usernameFromName(name: string): string | undefined {
const cleaned = name.toLowerCase().replace(/[^\w-]/g, '')
return cleaned === '' ? undefined : cleaned
return cleaned === '' || cleaned.length > USERNAME_MAX_LENGTH ? undefined : cleaned
}
/**