cart and checkout
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s

This commit is contained in:
2026-06-27 19:49:00 +02:00
parent 492f54ff45
commit 528c59bfd3
67 changed files with 3618 additions and 1031 deletions
+79 -7
View File
@@ -1,6 +1,7 @@
package customerauth
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -14,14 +15,36 @@ import (
// associated with a credential record.
var ErrEmailExists = errors.New("customerauth: email already registered")
// Credentials is the email→credential index the auth server depends on. It is
// satisfied by the file-backed CredentialStore (single-instance / dev) and by
// RedisCredentialStore (shared, horizontally scalable). All methods normalize
// the email key internally.
type Credentials interface {
// Get returns the record for email and whether it exists. A backing-store
// failure is reported as "not found" so callers fail closed.
Get(ctx context.Context, email string) (Record, bool)
// Register adds a new credential, returning ErrEmailExists if taken.
Register(ctx context.Context, email string, profileID uint64, hash, createdAt string) error
// MarkVerified records email verification. The bool reports whether the
// email was known; an unknown email is not an error (avoids enumeration).
MarkVerified(ctx context.Context, email, verifiedAt string) (bool, error)
// UpdateHash replaces the password hash. The bool reports whether the email
// was known; an unknown email is not an error.
UpdateHash(ctx context.Context, email, hash string) (bool, error)
// Delete removes a credential record by email. The bool reports whether the email
// was known.
Delete(ctx context.Context, email string) (bool, error)
}
// Record is a single stored credential: which profile an email belongs to and
// its password hash. It deliberately does not embed any profile data — the
// profile grain remains the source of truth for that.
type Record struct {
Email string `json:"email"`
ProfileID uint64 `json:"profileId"`
Hash string `json:"hash"`
CreatedAt string `json:"createdAt,omitempty"`
Email string `json:"email"`
ProfileID uint64 `json:"profileId"`
Hash string `json:"hash"`
CreatedAt string `json:"createdAt,omitempty"`
VerifiedAt string `json:"verifiedAt,omitempty"`
}
// CredentialStore is a small email→credential index persisted to a JSON file.
@@ -61,8 +84,9 @@ func NormalizeEmail(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
// Get returns the record for email and whether it exists.
func (s *CredentialStore) Get(email string) (Record, bool) {
// Get returns the record for email and whether it exists. The ctx is accepted
// for interface parity with the Redis store; the file store ignores it.
func (s *CredentialStore) Get(_ context.Context, email string) (Record, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
r, ok := s.byMail[NormalizeEmail(email)]
@@ -71,7 +95,7 @@ func (s *CredentialStore) Get(email string) (Record, bool) {
// Register adds a new credential record and persists the store. It returns
// ErrEmailExists if the email is already taken.
func (s *CredentialStore) Register(email string, profileID uint64, hash, createdAt string) error {
func (s *CredentialStore) Register(_ context.Context, email string, profileID uint64, hash, createdAt string) error {
key := NormalizeEmail(email)
s.mu.Lock()
defer s.mu.Unlock()
@@ -82,6 +106,54 @@ func (s *CredentialStore) Register(email string, profileID uint64, hash, created
return s.persistLocked()
}
// MarkVerified records that email completed email verification and persists the
// store. It is a no-op if already verified. The bool reports whether the email
// was known; an unknown email is not an error (callers avoid leaking which
// emails exist).
func (s *CredentialStore) MarkVerified(_ context.Context, email, verifiedAt string) (bool, error) {
key := NormalizeEmail(email)
s.mu.Lock()
defer s.mu.Unlock()
rec, ok := s.byMail[key]
if !ok {
return false, nil
}
if rec.VerifiedAt != "" {
return true, nil
}
rec.VerifiedAt = verifiedAt
s.byMail[key] = rec
return true, s.persistLocked()
}
// UpdateHash replaces the stored password hash for email and persists the store.
// The bool reports whether the email was known; an unknown email is not an error.
func (s *CredentialStore) UpdateHash(_ context.Context, email, hash string) (bool, error) {
key := NormalizeEmail(email)
s.mu.Lock()
defer s.mu.Unlock()
rec, ok := s.byMail[key]
if !ok {
return false, nil
}
rec.Hash = hash
s.byMail[key] = rec
return true, s.persistLocked()
}
// Delete removes the credential record for email and persists the store.
// The bool reports whether the email was known.
func (s *CredentialStore) Delete(_ context.Context, email string) (bool, error) {
key := NormalizeEmail(email)
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.byMail[key]; !ok {
return false, nil
}
delete(s.byMail, key)
return true, s.persistLocked()
}
// persistLocked writes the whole store to disk atomically. Caller holds s.mu.
func (s *CredentialStore) persistLocked() error {
records := make([]Record, 0, len(s.byMail))