feat: complete shared database tenant sharding

This commit is contained in:
richjohnrambo
2026-09-23 02:19:07 +08:00
parent 1ec2feee47
commit e37b52c3c0
37 changed files with 946 additions and 1420 deletions
+48 -1
View File
@@ -128,6 +128,9 @@ type groupMessageRuntime interface {
AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error)
ClaimShortState(namespace, id string, ttl time.Duration) (bool, error)
ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool, bool, error)
SetShortStateValue(namespace, id, value string, ttl time.Duration) error
GetShortStateValue(namespace, id string) (string, bool, error)
DeleteShortState(namespace, id string) error
ShortStateExists(namespace, id string) (bool, error)
StoreLoginSession(token []byte, session LoginSession, ttl time.Duration) error
}
@@ -266,6 +269,28 @@ func ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool,
return claimed, sameValue, true, err
}
func SetShortStateValue(namespace, id, value string, ttl time.Duration) (bool, error) {
if groupMsgRuntime == nil || namespace == "" || id == "" || value == "" {
return false, nil
}
return true, groupMsgRuntime.SetShortStateValue(namespace, id, value, ttl)
}
func GetShortStateValue(namespace, id string) (string, bool, bool, error) {
if groupMsgRuntime == nil || namespace == "" || id == "" {
return "", false, false, nil
}
value, found, err := groupMsgRuntime.GetShortStateValue(namespace, id)
return value, found, true, err
}
func DeleteShortState(namespace, id string) (bool, error) {
if groupMsgRuntime == nil || namespace == "" || id == "" {
return false, nil
}
return true, groupMsgRuntime.DeleteShortState(namespace, id)
}
func RevokeLoginToken(tenantID types.TenantID, token []byte, ttl time.Duration) (bool, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || len(token) == 0 || ttl <= 0 {
return false, nil
@@ -1272,6 +1297,28 @@ func (r *redisGroupMessageRuntime) ClaimShortStateValue(namespace, id, value str
return false, existing == value, nil
}
func (r *redisGroupMessageRuntime) SetShortStateValue(namespace, id, value string, ttl time.Duration) error {
if ttl <= 0 {
ttl = time.Duration(r.cfg.ShortTTL) * time.Second
}
return r.client.Set(context.Background(), r.shortStateKey(namespace, id), value, ttl).Err()
}
func (r *redisGroupMessageRuntime) GetShortStateValue(namespace, id string) (string, bool, error) {
value, err := r.client.Get(context.Background(), r.shortStateKey(namespace, id)).Result()
if err == redis.Nil {
return "", false, nil
}
if err != nil {
return "", false, err
}
return value, true, nil
}
func (r *redisGroupMessageRuntime) DeleteShortState(namespace, id string) error {
return r.client.Del(context.Background(), r.shortStateKey(namespace, id)).Err()
}
func (r *redisGroupMessageRuntime) ShortStateExists(namespace, id string) (bool, error) {
count, err := r.client.Exists(context.Background(), r.shortStateKey(namespace, id)).Result()
return count > 0, err
@@ -1450,7 +1497,7 @@ func (r *redisGroupMessageRuntime) loginSessionKey(tokenHash string) string {
func (r *redisGroupMessageRuntime) cacheTTL(namespace string) time.Duration {
ttl := r.cfg.CacheTTL
if namespace == "verification" || namespace == "action_token" {
if namespace == "verification" || namespace == "action_token" || namespace == "login_lockout" {
ttl = r.cfg.ShortTTL
}
return time.Duration(ttl) * time.Second
+25 -2
View File
@@ -15,6 +15,7 @@ type testGroupMessageRuntime struct {
claims map[string]*groupMessageUniqueRecord
seqs map[string]int64
shortStates map[string]bool
shortValues map[string]string
objects map[string][]byte
deletedPrefixes []string
deletedObjects []string
@@ -25,6 +26,7 @@ func newTestGroupMessageRuntime() *testGroupMessageRuntime {
claims: make(map[string]*groupMessageUniqueRecord),
seqs: make(map[string]int64),
shortStates: make(map[string]bool),
shortValues: make(map[string]string),
objects: make(map[string][]byte),
}
}
@@ -209,8 +211,29 @@ func (r *testGroupMessageRuntime) ClaimShortState(namespace, id string, _ time.D
return true, nil
}
func (r *testGroupMessageRuntime) ClaimShortStateValue(string, string, string, time.Duration) (bool, bool, error) {
return true, true, nil
func (r *testGroupMessageRuntime) ClaimShortStateValue(namespace, id, value string, _ time.Duration) (bool, bool, error) {
key := namespace + ":" + id
if existing, ok := r.shortValues[key]; ok {
return false, existing == value, nil
}
r.shortValues[key] = value
return true, false, nil
}
func (r *testGroupMessageRuntime) SetShortStateValue(namespace, id, value string, _ time.Duration) error {
r.shortValues[namespace+":"+id] = value
return nil
}
func (r *testGroupMessageRuntime) GetShortStateValue(namespace, id string) (string, bool, error) {
value, ok := r.shortValues[namespace+":"+id]
return value, ok, nil
}
func (r *testGroupMessageRuntime) DeleteShortState(namespace, id string) error {
delete(r.shortStates, namespace+":"+id)
delete(r.shortValues, namespace+":"+id)
return nil
}
func (r *testGroupMessageRuntime) ShortStateExists(namespace, id string) (bool, error) {
+156 -38
View File
@@ -1,6 +1,10 @@
package store
import (
"encoding/json"
"errors"
"strconv"
"strings"
"time"
adapter "github.com/tinode/chat/server/db"
@@ -8,9 +12,6 @@ import (
)
type HTTPInfrastructurePersistenceInterface interface {
BeginIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) (*types.HTTPIdempotencyRecord, bool, error)
CompleteIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) error
AbortIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) error
BeginKafkaConsume(types.TenantID, *types.KafkaConsumeRecord, time.Time) (*types.KafkaConsumeRecord, bool, error)
MarkKafkaConsumeSucceeded(types.TenantID, string, string, time.Time) error
MarkKafkaConsumeFailed(types.TenantID, string, string, string, string, time.Time) (*types.KafkaConsumeRecord, error)
@@ -25,6 +26,49 @@ type httpInfrastructureMapper struct{}
var HTTPInfrastructure HTTPInfrastructurePersistenceInterface = httpInfrastructureMapper{}
const (
kafkaConsumeNamespace = "kafka_consume"
kafkaConsumeRecordTTL = 7 * 24 * time.Hour
)
var errKafkaConsumeRedisUnavailable = errors.New("kafka consume redis unavailable")
func kafkaConsumeKey(tenantID types.TenantID, eventID, consumerGroup string) string {
return strconv.FormatInt(int64(tenantID), 10) + ":" + strings.TrimSpace(eventID) + ":" + strings.TrimSpace(consumerGroup)
}
func kafkaConsumeRecordKey(tenantID types.TenantID, eventID, consumerGroup string) string {
return kafkaConsumeKey(tenantID, eventID, consumerGroup) + ":record"
}
func kafkaConsumeRead(tenantID types.TenantID, eventID, consumerGroup string) (*types.KafkaConsumeRecord, bool, bool, error) {
raw, found, enabled, err := GetShortStateValue(kafkaConsumeNamespace, kafkaConsumeRecordKey(tenantID, eventID, consumerGroup))
if err != nil || !enabled || !found {
return nil, found, enabled, err
}
var record types.KafkaConsumeRecord
if err = json.Unmarshal([]byte(raw), &record); err != nil {
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeRecordKey(tenantID, eventID, consumerGroup))
return nil, false, true, err
}
return &record, true, true, nil
}
func kafkaConsumeWrite(tenantID types.TenantID, record *types.KafkaConsumeRecord) error {
if record == nil {
return types.ErrMalformed
}
payload, err := json.Marshal(record)
if err != nil {
return err
}
enabled, err := SetShortStateValue(kafkaConsumeNamespace, kafkaConsumeRecordKey(tenantID, record.EventID, record.ConsumerGroup), string(payload), kafkaConsumeRecordTTL)
if !enabled {
return errKafkaConsumeRedisUnavailable
}
return err
}
func tenantHTTPInfrastructureAdapter(tenantID types.TenantID) (adapter.TenantHTTPInfrastructureAdapter, error) {
business, err := tenantBusinessAdapter(tenantID)
if err != nil {
@@ -37,52 +81,109 @@ func tenantHTTPInfrastructureAdapter(tenantID types.TenantID) (adapter.TenantHTT
return infrastructure, nil
}
func (httpInfrastructureMapper) BeginIdempotency(tenantID types.TenantID, record *types.HTTPIdempotencyRecord) (*types.HTTPIdempotencyRecord, bool, error) {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
return nil, false, err
}
return infrastructure.TenantHTTPIdempotencyBegin(tenantID, record)
}
func (httpInfrastructureMapper) CompleteIdempotency(tenantID types.TenantID, record *types.HTTPIdempotencyRecord) error {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
return err
}
return infrastructure.TenantHTTPIdempotencyComplete(tenantID, record)
}
func (httpInfrastructureMapper) AbortIdempotency(tenantID types.TenantID, record *types.HTTPIdempotencyRecord) error {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
return err
}
return infrastructure.TenantHTTPIdempotencyAbort(tenantID, record)
}
func (httpInfrastructureMapper) BeginKafkaConsume(tenantID types.TenantID, record *types.KafkaConsumeRecord, deadline time.Time) (*types.KafkaConsumeRecord, bool, error) {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
if record == nil || strings.TrimSpace(record.EventID) == "" || strings.TrimSpace(record.ConsumerGroup) == "" || strings.TrimSpace(record.SourceTopic) == "" {
return nil, false, types.ErrMalformed
}
if existing, found, enabled, err := kafkaConsumeRead(tenantID, record.EventID, record.ConsumerGroup); err != nil || !enabled {
if err == nil {
err = errKafkaConsumeRedisUnavailable
}
return nil, false, err
} else if found && (existing.Status == types.KafkaConsumeSucceeded || existing.Status == types.KafkaConsumeDead) {
return existing, false, nil
} else if found {
record.RetryCount = existing.RetryCount
record.LastReason = existing.LastReason
record.LastError = existing.LastError
record.CreatedAt = existing.CreatedAt
}
ttl := time.Until(deadline)
if ttl <= 0 {
ttl = 5 * time.Minute
}
claimed, _, enabled, err := ClaimShortStateValue(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, record.EventID, record.ConsumerGroup), types.KafkaConsumeProcessing, ttl)
if err != nil || !enabled {
if err == nil {
err = errKafkaConsumeRedisUnavailable
}
return nil, false, err
}
return infrastructure.TenantKafkaConsumeBegin(tenantID, record, deadline)
if !claimed {
existing, found, enabled, err := kafkaConsumeRead(tenantID, record.EventID, record.ConsumerGroup)
if err != nil || !enabled {
if err == nil {
err = errKafkaConsumeRedisUnavailable
}
return nil, false, err
}
if found {
return existing, false, nil
}
copy := *record
copy.Status = types.KafkaConsumeProcessing
copy.ProcessingDeadline = &deadline
return &copy, false, nil
}
now := time.Now().UTC().Round(time.Millisecond)
copy := *record
copy.Status = types.KafkaConsumeProcessing
copy.ProcessingDeadline = &deadline
if copy.CreatedAt.IsZero() {
copy.CreatedAt = now
}
copy.UpdatedAt = now
if err = kafkaConsumeWrite(tenantID, &copy); err != nil {
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, record.EventID, record.ConsumerGroup))
return nil, false, err
}
return &copy, true, nil
}
func (httpInfrastructureMapper) MarkKafkaConsumeSucceeded(tenantID types.TenantID, eventID, consumerGroup string, now time.Time) error {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
record, found, enabled, err := kafkaConsumeRead(tenantID, eventID, consumerGroup)
if err != nil || !enabled {
if err == nil {
err = errKafkaConsumeRedisUnavailable
}
return err
}
return infrastructure.TenantKafkaConsumeSucceed(tenantID, eventID, consumerGroup, now)
if !found {
return types.ErrNotFound
}
record.Status = types.KafkaConsumeSucceeded
record.SucceededAt = &now
record.ProcessingDeadline = nil
record.UpdatedAt = now
if err = kafkaConsumeWrite(tenantID, record); err != nil {
return err
}
_, err = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, eventID, consumerGroup))
return err
}
func (httpInfrastructureMapper) MarkKafkaConsumeFailed(tenantID types.TenantID, eventID, consumerGroup, reason, message string, now time.Time) (*types.KafkaConsumeRecord, error) {
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
if err != nil {
record, found, enabled, err := kafkaConsumeRead(tenantID, eventID, consumerGroup)
if err != nil || !enabled {
if err == nil {
err = errKafkaConsumeRedisUnavailable
}
return nil, err
}
return infrastructure.TenantKafkaConsumeFail(tenantID, eventID, consumerGroup, reason, message, now)
if !found {
return nil, types.ErrNotFound
}
record.Status = types.KafkaConsumeFailed
record.RetryCount++
record.LastReason = reason
record.LastError = message
record.ProcessingDeadline = nil
record.UpdatedAt = now
if err = kafkaConsumeWrite(tenantID, record); err != nil {
return nil, err
}
_, err = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, eventID, consumerGroup))
return record, err
}
func (httpInfrastructureMapper) MarkKafkaConsumeDead(tenantID types.TenantID, dead *types.KafkaDeadLetter, now time.Time) error {
@@ -90,7 +191,18 @@ func (httpInfrastructureMapper) MarkKafkaConsumeDead(tenantID types.TenantID, de
if err != nil {
return err
}
return infrastructure.TenantKafkaConsumeDead(tenantID, dead, now)
if err = infrastructure.TenantKafkaConsumeDead(tenantID, dead, now); err != nil {
return err
}
record := &types.KafkaConsumeRecord{
EventID: dead.EventID, ConsumerGroup: dead.ConsumerGroup, SourceTopic: dead.SourceTopic,
EventType: dead.EventType, BizType: dead.BizType, BizID: dead.BizID,
Status: types.KafkaConsumeDead, RetryCount: dead.RetryCount, LastReason: dead.LastReason,
LastError: dead.LastError, DeadAt: &now, CreatedAt: now, UpdatedAt: now,
}
_ = kafkaConsumeWrite(tenantID, record)
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, dead.EventID, dead.ConsumerGroup))
return nil
}
func (httpInfrastructureMapper) ListKafkaDeadLetters(tenantID types.TenantID, opts types.KafkaDeadLetterListOptions) (*types.KafkaDeadLetterPage, error) {
@@ -122,5 +234,11 @@ func (httpInfrastructureMapper) MarkKafkaDeadLetterReplay(tenantID types.TenantI
if err != nil {
return nil, err
}
return infrastructure.TenantKafkaDeadLetterMarkReplay(tenantID, deadID, resolvedBy, note, now)
dead, err := infrastructure.TenantKafkaDeadLetterMarkReplay(tenantID, deadID, resolvedBy, note, now)
if err != nil {
return nil, err
}
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, dead.EventID, dead.ConsumerGroup))
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeRecordKey(tenantID, dead.EventID, dead.ConsumerGroup))
return dead, nil
}
+128 -85
View File
@@ -3,6 +3,7 @@ package store
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -4326,7 +4327,7 @@ type UserSecurityPersistenceInterface interface {
ListSystemNotifications(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.SystemNotification, string, error)
CountUnreadSystemNotifications(tenantID types.TenantID, uid types.Uid) (int, error)
CountUnreadSystemNotificationsByEventPrefix(tenantID types.TenantID, uid types.Uid, eventPrefix string) (int, error)
MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error
MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) (*types.SystemNotification, error)
}
type userSecurityMapper struct{}
@@ -4384,30 +4385,39 @@ func (userSecurityMapper) CreateActionToken(tenantID types.TenantID, token *type
if token == nil || token.TenantID != tenantID {
return types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
if err = tenantAdp.TenantActionTokenCreate(tenantID, token); err != nil {
return err
}
if cacheErr := CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token); cacheErr != nil {
logs.Warn.Printf("cache action token[%s]: write failed: %v", token.TokenID, cacheErr)
}
return nil
now := types.TimeNow()
token.CreatedAt = now
token.UpdatedAt = now
return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token)
}
func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if !tenantID.IsValid() || uid.IsZero() || token == "" || action == "" {
return nil, types.ErrMalformed
}
if now.IsZero() {
now = types.TimeNow()
}
tokenID, secret, ok := strings.Cut(token, ".")
if !ok || tokenID == "" || secret == "" {
return nil, types.ErrMalformed
}
cacheID := actionTokenCacheID(uid, tokenID, action, target)
var cached types.ActionToken
ok, err := GetCachedObject(tenantID, cacheNamespaceActionToken, cacheID, &cached)
if err != nil {
return nil, err
}
consumed, err := tenantAdp.TenantActionTokenConsume(tenantID, uid, token, action, target, now)
if err == nil {
tokenID, _, _ := strings.Cut(token, ".")
_ = DeleteCachedObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(uid, tokenID, action, target))
if !ok || cached.State != 0 || cached.TokenHash != actionTokenHash(tenantID, uid, action, target, tokenID, secret) || !now.Before(cached.ExpiresAt) {
return nil, types.ErrNotFound
}
return consumed, err
cached.State = 1
cached.ConsumedAt = &now
cached.UpdatedAt = now
if err = DeleteCachedObject(tenantID, cacheNamespaceActionToken, cacheID); err != nil {
return nil, err
}
return &cached, nil
}
func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) {
@@ -4500,10 +4510,10 @@ func (userSecurityMapper) CountUnreadSystemNotificationsByEventPrefix(tenantID t
return tenantAdp.TenantSystemNotificationUnreadCountByEventPrefix(tenantID, uid, eventPrefix)
}
func (userSecurityMapper) MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error {
func (userSecurityMapper) MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) (*types.SystemNotification, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
return nil, err
}
return tenantAdp.TenantSystemNotificationMarkRead(tenantID, uid, eventID, readAt)
}
@@ -4512,26 +4522,30 @@ func (verificationMapper) Create(tenantID types.TenantID, vc *types.Verification
if vc == nil || vc.TenantID != tenantID {
return types.ErrMalformed
}
if cacheErr := cacheVerificationCode(tenantID, vc); cacheErr != nil {
return cacheErr
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
logs.Warn.Printf("verification[%s]: audit adapter unavailable: %v", vc.ChallengeID, err)
return nil
}
if err = tenantAdp.TenantVerificationCreate(tenantID, vc); err != nil {
return err
}
if cacheErr := cacheVerificationCode(tenantID, vc); cacheErr != nil {
logs.Warn.Printf("cache verification[%s]: write failed: %v", vc.ChallengeID, cacheErr)
logs.Warn.Printf("verification[%s]: audit insert failed: %v", vc.ChallengeID, err)
}
return nil
}
func (verificationMapper) Get(tenantID types.TenantID, challengeID string) (*types.VerificationCode, error) {
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
tenantAdp, err := tenantBusinessAdapter(tenantID)
var vc types.VerificationCode
ok, err := GetCachedObject(tenantID, cacheNamespaceVerification, challengeID, &vc)
if err != nil {
return nil, err
}
return tenantAdp.TenantVerificationGet(tenantID, challengeID)
if !ok {
return nil, types.ErrNotFound
}
return &vc, nil
}
func (verificationMapper) GetActive(tenantID types.TenantID, scene, phoneHash string, now time.Time) (*types.VerificationCode, error) {
@@ -4547,74 +4561,59 @@ func (verificationMapper) GetActive(tenantID types.TenantID, scene, phoneHash st
}
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, activeCacheID)
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
vc, err := tenantAdp.TenantVerificationGetActive(tenantID, scene, phoneHash, now)
if err == nil && vc != nil {
if cacheErr := cacheVerificationCode(tenantID, vc); cacheErr != nil {
logs.Warn.Printf("cache verification active[%s]: write failed: %v", activeCacheID, cacheErr)
}
}
return vc, err
return nil, types.ErrNotFound
}
func (verificationMapper) Fail(tenantID types.TenantID, challengeID string, state types.VerificationState) error {
tenantAdp, err := tenantBusinessAdapter(tenantID)
vc, err := Verifications.Get(tenantID, challengeID)
if err != nil {
return err
}
if err = tenantAdp.TenantVerificationFail(tenantID, challengeID, state); err != nil {
return err
}
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
_ = cacheVerificationCode(tenantID, vc)
} else {
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
}
return nil
vc.Attempts++
vc.State = state
vc.UpdatedAt = types.TimeNow()
return cacheVerificationCode(tenantID, vc)
}
func (verificationMapper) MarkVerified(tenantID types.TenantID, challengeID, tokenHash string, verifiedAt time.Time) error {
tenantAdp, err := tenantBusinessAdapter(tenantID)
vc, err := Verifications.Get(tenantID, challengeID)
if err != nil {
return err
}
if err = tenantAdp.TenantVerificationMarkVerified(tenantID, challengeID, tokenHash, verifiedAt); err != nil {
return err
if vc.State != types.VerificationStatePending || !verifiedAt.Before(vc.ExpiresAt) {
return types.ErrNotFound
}
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
_ = cacheVerificationCode(tenantID, vc)
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
} else {
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
}
return nil
vc.State = types.VerificationStateVerified
vc.VerificationTokenHash = &tokenHash
vc.VerifiedAt = &verifiedAt
vc.UpdatedAt = verifiedAt
return cacheVerificationCode(tenantID, vc)
}
func (verificationMapper) Consume(tenantID types.TenantID, challengeID, tokenHash string, consumedAt time.Time) error {
tenantAdp, err := tenantBusinessAdapter(tenantID)
vc, err := Verifications.Get(tenantID, challengeID)
if err != nil {
return err
}
if err = tenantAdp.TenantVerificationConsume(tenantID, challengeID, tokenHash, consumedAt); err != nil {
if vc.State != types.VerificationStateVerified || vc.VerificationTokenHash == nil || *vc.VerificationTokenHash != tokenHash || !consumedAt.Before(vc.ExpiresAt) {
return types.ErrNotFound
}
vc.State = types.VerificationStateConsumed
vc.ConsumedAt = &consumedAt
vc.UpdatedAt = consumedAt
if err = cacheVerificationCode(tenantID, vc); err != nil {
return err
}
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
_ = cacheVerificationCode(tenantID, vc)
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
} else {
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
}
return nil
return DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
}
func cacheVerificationCode(tenantID types.TenantID, vc *types.VerificationCode) error {
if vc == nil {
return nil
}
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, vc.ChallengeID)
if err := CacheObject(tenantID, cacheNamespaceVerification, vc.ChallengeID, vc); err != nil {
return err
}
if vc.State == types.VerificationStatePending {
return CacheObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash), vc.ChallengeID)
}
@@ -4630,6 +4629,22 @@ func actionTokenCacheID(uid types.Uid, tokenID, action, target string) string {
return uid.String() + ":" + strings.TrimSpace(tokenID) + ":" + strings.TrimSpace(action) + ":" + strings.TrimSpace(target)
}
func actionTokenHash(tenantID types.TenantID, uid types.Uid, action, target, tokenID, secret string) string {
h := sha256.New()
h.Write([]byte(strconv.FormatInt(int64(tenantID), 10)))
h.Write([]byte{0})
h.Write([]byte(strconv.FormatInt(DecodeUid(uid), 10)))
h.Write([]byte{0})
h.Write([]byte(action))
h.Write([]byte{0})
h.Write([]byte(target))
h.Write([]byte{0})
h.Write([]byte(tokenID))
h.Write([]byte{0})
h.Write([]byte(secret))
return fmt.Sprintf("%x", h.Sum(nil))
}
// LoginLockoutPersistenceInterface stores per-account login failure counters
// and temporary lock state in each tenant business database.
type LoginLockoutPersistenceInterface interface {
@@ -4665,43 +4680,71 @@ func tenantLoginLockoutAdapter(tenantID types.TenantID) (adapter.TenantLoginLock
}
func (loginLockoutMapper) Get(tenantID types.TenantID, accountHash string) (*types.LoginLockout, error) {
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
var lockout types.LoginLockout
ok, err := GetCachedObject(tenantID, "login_lockout", accountHash, &lockout)
if err != nil {
return nil, err
}
return lockAdp.TenantLoginLockoutGet(tenantID, accountHash)
if !ok {
return nil, types.ErrNotFound
}
return &lockout, nil
}
func (loginLockoutMapper) RecordFailure(tenantID types.TenantID, accountHash string, userID *int64, windowCutoff, now time.Time) (*types.LoginLockout, error) {
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
if err != nil {
current, err := LoginLockouts.Get(tenantID, accountHash)
if err != nil && !errors.Is(err, types.ErrNotFound) {
return nil, err
}
return lockAdp.TenantLoginLockoutRecordFailure(tenantID, accountHash, userID, windowCutoff, now)
if current == nil {
current = &types.LoginLockout{TenantID: tenantID, AccountHash: accountHash}
}
if current.WindowStartAt == nil || current.WindowStartAt.Before(windowCutoff) {
current.FailedAttempts = 0
start := now
current.WindowStartAt = &start
}
current.FailedAttempts++
if userID != nil {
current.UserID = *userID
}
current.LastAttemptAt = &now
current.UpdatedAt = &now
return current, CacheObject(tenantID, "login_lockout", accountHash, current)
}
func (loginLockoutMapper) Lock(tenantID types.TenantID, accountHash string, lockedUntil, now time.Time) error {
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
if err != nil {
current, err := LoginLockouts.Get(tenantID, accountHash)
if err != nil && !errors.Is(err, types.ErrNotFound) {
return err
}
return lockAdp.TenantLoginLockoutLock(tenantID, accountHash, lockedUntil, now)
if current == nil {
current = &types.LoginLockout{TenantID: tenantID, AccountHash: accountHash}
}
if current.LockedUntil == nil || current.LockedUntil.Before(lockedUntil) {
current.LockedUntil = &lockedUntil
}
current.UpdatedAt = &now
return CacheObject(tenantID, "login_lockout", accountHash, current)
}
func (loginLockoutMapper) Reset(tenantID types.TenantID, accountHash string) error {
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
if err != nil {
return err
}
return lockAdp.TenantLoginLockoutReset(tenantID, accountHash)
return DeleteCachedObject(tenantID, "login_lockout", accountHash)
}
func (loginLockoutMapper) Release(tenantID types.TenantID, accountHash string, now time.Time) error {
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
if err != nil {
current, err := LoginLockouts.Get(tenantID, accountHash)
if err != nil && !errors.Is(err, types.ErrNotFound) {
return err
}
return lockAdp.TenantLoginLockoutRelease(tenantID, accountHash, now)
if current == nil {
return nil
}
current.FailedAttempts = 0
current.WindowStartAt = nil
current.LockedUntil = nil
current.UpdatedAt = &now
return CacheObject(tenantID, "login_lockout", accountHash, current)
}
func tenantRegistrationInviteAdapter(tenantID types.TenantID) (adapter.TenantRegistrationInviteAdapter, error) {
-18
View File
@@ -7,9 +7,6 @@ import (
)
const (
HTTPIdempotencyProcessing = "processing"
HTTPIdempotencyCompleted = "completed"
KafkaConsumeProcessing = "processing"
KafkaConsumeSucceeded = "succeeded"
KafkaConsumeFailed = "failed"
@@ -22,21 +19,6 @@ const (
KafkaDeadStatusResolved = "resolved"
)
// HTTPIdempotencyRecord stores the first response for one authenticated write.
type HTTPIdempotencyRecord struct {
UserID Uid
Method string
Route string
Key string
BodyHash string
State string
ResponseStatus int
ResponseBody []byte
CreatedAt time.Time
UpdatedAt time.Time
ExpiresAt time.Time
}
// KafkaConsumeRecord stores idempotent consumer state for one event and group.
type KafkaConsumeRecord struct {
EventID string
+2 -2
View File
@@ -2,8 +2,8 @@ package types
import "time"
// LoginLockout is the per-account login failure counter and lock state
// stored in each tenant business database (table im_login_lockouts).
// LoginLockout is the per-account login failure counter and lock state stored
// in Redis.
type LoginLockout struct {
TenantID TenantID `json:"tenant_id"`
ID int64 `json:"id"`