This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProfileInput = errors.New("auth: invalid profile change")
|
||||
ErrProfileAccess = errors.New("auth: profile session is unavailable")
|
||||
ErrProfileConflict = errors.New("auth: profile changed; reload before editing")
|
||||
ErrUsernameUnavailable = errors.New("auth: username is unavailable")
|
||||
)
|
||||
|
||||
// OwnProfile contains mutable identity, not credentials or organization roles.
|
||||
// Revision is independent of timestamps and increases for every profile edit.
|
||||
type OwnProfile struct {
|
||||
UserID, Username, Email, DisplayName string
|
||||
Revision int64
|
||||
}
|
||||
|
||||
// ProfileEdit is a trusted repository command, not an HTTP input model. The
|
||||
// application must authenticate the session, validate CSRF/origin and rate-limit
|
||||
// mutations. Username edits additionally require recent reauthentication (and
|
||||
// any account-specific MFA). For password reauthentication, supply the verified
|
||||
// hash so a concurrent password reset invalidates the write. After verified
|
||||
// passkey approval, leave it empty. Do not log or serialize this command.
|
||||
type ProfileEdit struct {
|
||||
UserID string
|
||||
SessionDigest [32]byte
|
||||
ExpectedRevision int64
|
||||
Field, Value string
|
||||
ExpectedPasswordHash string
|
||||
}
|
||||
|
||||
// NormalizeProfileValue validates only supported fields. Email is deliberately
|
||||
// absent: verified mailbox changes need a separate pending/confirmation flow.
|
||||
func NormalizeProfileValue(field, value string) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
switch field {
|
||||
case "username":
|
||||
if !identifierPattern.MatchString(value) {
|
||||
return "", ErrProfileInput
|
||||
}
|
||||
case "display_name":
|
||||
if value == "" || len(value) > 128 || !utf8.ValidString(value) {
|
||||
return "", ErrProfileInput
|
||||
}
|
||||
for _, r := range value {
|
||||
if unicode.IsControl(r) {
|
||||
return "", ErrProfileInput
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "", ErrProfileInput
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// OwnProfileRepository is optional; no change to the authentication Repository
|
||||
// interface is required. It derives access from the current session, never from
|
||||
// a site-wide administrator flag. Implementations atomically recheck identity,
|
||||
// session and revision, mutate one field, and append the audit. Username edits
|
||||
// revoke other sessions but preserve the acting session. They never reassign
|
||||
// stable IDs, memberships, passkeys, billing identities or historical records.
|
||||
type OwnProfileRepository interface {
|
||||
OwnProfile(context.Context, [32]byte, time.Time) (OwnProfile, error)
|
||||
UpdateOwnProfile(context.Context, ProfileEdit, AuditEvent) (OwnProfile, error)
|
||||
}
|
||||
Reference in New Issue
Block a user