feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
@@ -0,0 +1,56 @@
// Package antiabuse provides runtime in-process adapters for auth-specific
// public abuse controls.
package antiabuse
import (
"context"
"fmt"
"sync"
"time"
"galaxy/authsession/internal/domain/challenge"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/ports"
)
// SendEmailCodeProtector is a concurrency-safe in-process resend-throttle
// adapter for public send-email-code attempts.
type SendEmailCodeProtector struct {
mu sync.Mutex
reservedUntil map[common.Email]time.Time
}
// CheckAndReserve applies the fixed Stage-17 resend cooldown using input.Now
// as the authoritative decision timestamp.
func (p *SendEmailCodeProtector) CheckAndReserve(ctx context.Context, input ports.SendEmailCodeAbuseInput) (ports.SendEmailCodeAbuseResult, error) {
if ctx == nil {
return ports.SendEmailCodeAbuseResult{}, fmt.Errorf("check and reserve send email code abuse: nil context")
}
if err := ctx.Err(); err != nil {
return ports.SendEmailCodeAbuseResult{}, err
}
if err := input.Validate(); err != nil {
return ports.SendEmailCodeAbuseResult{}, fmt.Errorf("check and reserve send email code abuse: %w", err)
}
p.mu.Lock()
defer p.mu.Unlock()
if p.reservedUntil == nil {
p.reservedUntil = make(map[common.Email]time.Time)
}
reservedUntil, exists := p.reservedUntil[input.Email]
if exists && input.Now.Before(reservedUntil) {
return ports.SendEmailCodeAbuseResult{
Outcome: ports.SendEmailCodeAbuseOutcomeThrottled,
}, nil
}
p.reservedUntil[input.Email] = input.Now.UTC().Add(challenge.ResendThrottleCooldown)
return ports.SendEmailCodeAbuseResult{
Outcome: ports.SendEmailCodeAbuseOutcomeAllowed,
}, nil
}
var _ ports.SendEmailCodeAbuseProtector = (*SendEmailCodeProtector)(nil)
@@ -0,0 +1,64 @@
package antiabuse
import (
"context"
"testing"
"time"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/ports"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSendEmailCodeProtectorCheckAndReserve(t *testing.T) {
t.Parallel()
protector := &SendEmailCodeProtector{}
email := common.Email("pilot@example.com")
now := time.Unix(10, 0).UTC()
result, err := protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{
Email: email,
Now: now,
})
require.NoError(t, err)
assert.Equal(t, ports.SendEmailCodeAbuseOutcomeAllowed, result.Outcome)
result, err = protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{
Email: email,
Now: now.Add(30 * time.Second),
})
require.NoError(t, err)
assert.Equal(t, ports.SendEmailCodeAbuseOutcomeThrottled, result.Outcome)
result, err = protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{
Email: email,
Now: now.Add(time.Minute),
})
require.NoError(t, err)
assert.Equal(t, ports.SendEmailCodeAbuseOutcomeAllowed, result.Outcome)
}
func TestSendEmailCodeProtectorNilOrCanceledContext(t *testing.T) {
t.Parallel()
protector := &SendEmailCodeProtector{}
_, err := protector.CheckAndReserve(nil, ports.SendEmailCodeAbuseInput{
Email: common.Email("pilot@example.com"),
Now: time.Unix(10, 0).UTC(),
})
require.Error(t, err)
assert.ErrorContains(t, err, "nil context")
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = protector.CheckAndReserve(ctx, ports.SendEmailCodeAbuseInput{
Email: common.Email("pilot@example.com"),
Now: time.Unix(10, 0).UTC(),
})
require.Error(t, err)
assert.ErrorIs(t, err, context.Canceled)
}