1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -2,9 +2,12 @@ package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/pagination"
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
@@ -21,10 +24,28 @@ type identityMirrorStateStore struct {
Status string `json:"status"`
LastRefreshedAt *time.Time `json:"lastRefreshedAt,omitempty"`
LastError string `json:"lastError,omitempty"`
MirrorVersion string `json:"mirrorVersion,omitempty"`
ObservedCount int64 `json:"observedCount,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
type IdentityMirrorPageQuery struct {
Limit int
Offset int
Cursor string
Search string
TenantID string
TenantSlug string
AllowedTenantKeys map[string]bool
}
type IdentityMirrorPageResult struct {
Items []KratosIdentity
Total int64
Cursor string
NextCursor string
}
// NewRedisService creates and returns a new RedisService
func NewRedisService() (*RedisService, error) {
redisAddr := os.Getenv("REDIS_ADDR")
@@ -199,6 +220,7 @@ func (s *RedisService) GetIdentityCacheStatus(ctx context.Context) (domain.Ident
return domain.IdentityCacheStatus{
Status: status,
RedisReady: true,
MirrorVersion: stored.MirrorVersion,
ObservedCount: stored.ObservedCount,
KeyCount: keyCount,
LastRefreshedAt: stored.LastRefreshedAt,
@@ -271,6 +293,269 @@ func (s *RedisService) ListIdentityMirrors(ctx context.Context) ([]KratosIdentit
return identities, nil
}
func (s *RedisService) StoreIdentityMirror(ctx context.Context, identity KratosIdentity) error {
if s == nil || s.Client == nil {
return os.ErrInvalid
}
identityID := strings.TrimSpace(identity.ID)
if identityID == "" {
return os.ErrInvalid
}
raw, err := json.Marshal(identity)
if err != nil {
return err
}
if err := s.Client.Set(ctx, "identity:mirror:"+identityID, string(raw), 0).Err(); err != nil {
return err
}
score := float64(identityMirrorScoreTime(identity).UnixMilli())
if err := s.Client.ZAdd(ctx, "identity:index:created_at", &redis.Z{
Score: score,
Member: identityID,
}).Err(); err != nil {
return err
}
for _, tenantKey := range identityMirrorTenantKeys(identity.Traits) {
if err := s.Client.SAdd(ctx, "identity:index:tenant:"+tenantKey, identityID).Err(); err != nil {
return err
}
}
return nil
}
func (s *RedisService) ListIdentityMirrorPage(ctx context.Context, query IdentityMirrorPageQuery) (IdentityMirrorPageResult, error) {
if s == nil || s.Client == nil {
return IdentityMirrorPageResult{}, os.ErrInvalid
}
if query.Limit <= 0 {
query.Limit = 50
}
if query.Offset < 0 {
query.Offset = 0
}
cursor, err := pagination.Decode(query.Cursor)
if err != nil {
return IdentityMirrorPageResult{}, err
}
search := strings.ToLower(strings.TrimSpace(query.Search))
targetTenantKeys := identityMirrorTargetTenantKeys(query)
maxScore := "+inf"
if cursor != nil {
maxScore = strconv.FormatInt(cursor.Timestamp.UnixMilli(), 10)
}
const batchSize int64 = 250
var offset int64
var total int64
matched := make([]KratosIdentity, 0, query.Limit+1)
pageStart := query.Offset
if cursor != nil {
pageStart = 0
}
for {
zItems, err := s.Client.ZRevRangeByScoreWithScores(ctx, "identity:index:created_at", &redis.ZRangeBy{
Max: maxScore,
Min: "-inf",
Offset: offset,
Count: batchSize,
}).Result()
if err != nil {
return IdentityMirrorPageResult{}, err
}
if len(zItems) == 0 {
break
}
keys := make([]string, 0, len(zItems))
for _, item := range zItems {
id, ok := item.Member.(string)
if !ok || strings.TrimSpace(id) == "" {
continue
}
keys = append(keys, "identity:mirror:"+id)
}
rawItems, err := s.Client.MGet(ctx, keys...).Result()
if err != nil {
return IdentityMirrorPageResult{}, err
}
for _, raw := range rawItems {
rawString, ok := raw.(string)
if !ok || strings.TrimSpace(rawString) == "" {
continue
}
var identity KratosIdentity
if err := json.Unmarshal([]byte(rawString), &identity); err != nil {
continue
}
if strings.TrimSpace(identity.ID) == "" {
continue
}
if cursor != nil {
timestamp, id := identityMirrorCursorKey(identity)
if !pagination.ComesAfter(timestamp, id, cursor) {
continue
}
}
if !identityMirrorMatchesTenantScope(identity, targetTenantKeys, query.AllowedTenantKeys) {
continue
}
if !identityMirrorMatchesSearch(identity, search) {
continue
}
if total >= int64(pageStart) && len(matched) < query.Limit+1 {
matched = append(matched, identity)
}
total++
}
if len(zItems) < int(batchSize) {
break
}
offset += int64(len(zItems))
}
nextCursor := ""
items := matched
if len(matched) > query.Limit {
items = matched[:query.Limit]
lastTimestamp, lastID := identityMirrorCursorKey(items[len(items)-1])
nextCursor = pagination.Encode(lastTimestamp, lastID)
}
return IdentityMirrorPageResult{
Items: items,
Total: total,
Cursor: query.Cursor,
NextCursor: nextCursor,
}, nil
}
func identityMirrorScoreTime(identity KratosIdentity) time.Time {
if identity.CreatedAt.IsZero() {
return time.Unix(0, 0).UTC()
}
return identity.CreatedAt.UTC()
}
func identityMirrorCursorKey(identity KratosIdentity) (time.Time, string) {
return identityMirrorScoreTime(identity), identity.ID
}
func identityMirrorTenantKeys(traits map[string]any) []string {
keys := make([]string, 0, 4)
seen := make(map[string]bool)
appendKey := func(value string) {
key := strings.ToLower(strings.TrimSpace(value))
if key == "" || seen[key] {
return
}
seen[key] = true
keys = append(keys, key)
}
appendKey(identityMirrorTraitString(traits, "tenant_id"))
appendKey(identityMirrorTraitString(traits, "tenantSlug"))
appointments := identityMirrorAppointments(traits["additionalAppointments"])
if len(appointments) == 0 {
if metadata, ok := traits["metadata"].(map[string]any); ok {
appointments = identityMirrorAppointments(metadata["additionalAppointments"])
}
}
for _, appointment := range appointments {
appendKey(identityMirrorAnyString(appointment["tenantId"]))
appendKey(identityMirrorAnyString(appointment["tenantSlug"]))
appendKey(identityMirrorAnyString(appointment["slug"]))
}
return keys
}
func identityMirrorTargetTenantKeys(query IdentityMirrorPageQuery) map[string]bool {
targets := make(map[string]bool)
for _, value := range []string{query.TenantID, query.TenantSlug} {
key := strings.ToLower(strings.TrimSpace(value))
if key != "" {
targets[key] = true
}
}
return targets
}
func identityMirrorMatchesTenantScope(identity KratosIdentity, targetTenantKeys map[string]bool, allowedTenantKeys map[string]bool) bool {
identityKeys := identityMirrorTenantKeys(identity.Traits)
if len(allowedTenantKeys) > 0 && !identityMirrorAnyKeyAllowed(identityKeys, allowedTenantKeys) {
return false
}
if len(targetTenantKeys) > 0 && !identityMirrorAnyKeyAllowed(identityKeys, targetTenantKeys) {
return false
}
return true
}
func identityMirrorAnyKeyAllowed(keys []string, allowed map[string]bool) bool {
for _, key := range keys {
if allowed[key] {
return true
}
}
return false
}
func identityMirrorMatchesSearch(identity KratosIdentity, search string) bool {
search = strings.TrimSpace(search)
if search == "" {
return true
}
values := []string{
identity.ID,
identityMirrorTraitString(identity.Traits, "email"),
identityMirrorTraitString(identity.Traits, "name"),
identityMirrorTraitString(identity.Traits, "phone_number"),
identityMirrorTraitString(identity.Traits, "loginId"),
}
for _, value := range values {
if strings.Contains(strings.ToLower(value), search) {
return true
}
}
rawTraits, err := json.Marshal(identity.Traits)
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(rawTraits)), search)
}
func identityMirrorTraitString(traits map[string]any, key string) string {
if traits == nil {
return ""
}
return identityMirrorAnyString(traits[key])
}
func identityMirrorAnyString(value any) string {
switch typed := value.(type) {
case string:
return typed
case fmt.Stringer:
return typed.String()
default:
return ""
}
}
func identityMirrorAppointments(value any) []map[string]any {
switch typed := value.(type) {
case []map[string]any:
return typed
case []any:
result := make([]map[string]any, 0, len(typed))
for _, item := range typed {
if appointment, ok := item.(map[string]any); ok {
result = append(result, appointment)
}
}
return result
default:
return nil
}
}
func (s *RedisService) countIdentityCacheKeys(ctx context.Context) (int64, error) {
keys, err := s.identityCacheKeys(ctx)
if err != nil {