76 lines
2.6 KiB
Go
76 lines
2.6 KiB
Go
// 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)
|
|
}
|