Files
web/auth/profile.go
T
gamertan a16283efd7
verify / verify (push) Successful in 4m35s
Add session-bound personal profile editing
2026-09-05 02:37:38 -04:00

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)
}