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, so every // path that saves a user through GORM gets ErrUsernameTooLong rather // than the database's constraint error. The check constraint behind it // holds for 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 }