feat: complete shared database tenant sharding
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 ©, 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, ©); err != nil {
|
||||
_, _ = DeleteShortState(kafkaConsumeNamespace, kafkaConsumeKey(tenantID, record.EventID, record.ConsumerGroup))
|
||||
return nil, false, err
|
||||
}
|
||||
return ©, 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
@@ -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) {
|
||||
|
||||
@@ -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,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"`
|
||||
|
||||
Reference in New Issue
Block a user