check / check (push) Successful in 4m8s
Usernames are limited to 1024 bytes, so no account can exist that is unable to log in. The username rides in the session cookie, which browsers and securecookie refuse past about 4 KB, leaving room for roughly 2000 bytes of username; the limit is about half that. User.BeforeSave returns ErrUsernameTooLong when a whole User is created or saved. A byte-counting check constraint on users.username catches every other write, including a column update. The limit appears in the constant and in the struct tag; a test fails if they disagree. Model: opus-5-5
59 lines
2.0 KiB
Go
59 lines
2.0 KiB
Go
package database
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// MaxUsernameBytes is the longest username, in bytes, that a user may
|
||
// have. The same number appears in the check constraint on
|
||
// User.Username, because a struct tag cannot reference a constant.
|
||
//
|
||
// A login stores the username in the session cookie, and both
|
||
// securecookie and browsers refuse a cookie value past about 4096
|
||
// bytes. That value is the session base64-encoded twice, so it holds
|
||
// 4096 × 3/4 × 3/4 = 2304 bytes of session, and the signature,
|
||
// timestamp and the session's other values take about 270 of those: a
|
||
// username longer than about 2030 bytes can never log in. The limit is
|
||
// about half that, so the session can carry more values later without
|
||
// locking out an account whose username is already at the limit.
|
||
const MaxUsernameBytes = 1024
|
||
|
||
// ErrUsernameTooLong is returned when a user is saved with a username
|
||
// longer than MaxUsernameBytes.
|
||
var ErrUsernameTooLong = errors.New("username is too long")
|
||
|
||
// User represents a user of the webhooker service
|
||
//
|
||
//nolint:lll // a struct tag cannot wrap
|
||
type User struct {
|
||
BaseModel
|
||
|
||
Username string `gorm:"uniqueIndex;not null;check:length(CAST(username AS BLOB)) <= 1024" json:"username"`
|
||
Password string `gorm:"not null" json:"-"` // Argon2 hashed
|
||
|
||
// Relations
|
||
Webhooks []Webhook `json:"webhooks,omitempty"`
|
||
APIKeys []APIKey `json:"apiKeys,omitempty"`
|
||
}
|
||
|
||
// BeforeSave rejects a username longer than MaxUsernameBytes when a whole
|
||
// User is created or saved, so those calls get ErrUsernameTooLong rather
|
||
// than the database's constraint error. A column update such as
|
||
// Update("username", ...) is caught only by the check constraint, as is
|
||
// any path that writes the table without this model.
|
||
func (u *User) BeforeSave(_ *gorm.DB) error {
|
||
if len(u.Username) > MaxUsernameBytes {
|
||
return fmt.Errorf(
|
||
"%w: %d bytes, limit is %d",
|
||
ErrUsernameTooLong,
|
||
len(u.Username),
|
||
MaxUsernameBytes,
|
||
)
|
||
}
|
||
|
||
return nil
|
||
}
|