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
+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) {