mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
63007fc8c7
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":92,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
280 lines
8.4 KiB
Go
280 lines
8.4 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
|
)
|
|
|
|
const (
|
|
authSourceActiveRedisKey = "oauth:auth_sources:active"
|
|
authSourceByNameRedisKeyFmt = "oauth:auth_sources:by_name:%s"
|
|
authSourceByNameRedisPattern = "oauth:auth_sources:by_name:*"
|
|
authSourceActiveRAMKey = "active"
|
|
authSourceCacheTTL = time.Hour
|
|
authSourceRAMMaximumSize = 64
|
|
authSourceInvalidationChannel = "oauth:auth_source_invalidation"
|
|
)
|
|
|
|
// authSourceRedisRecord persists full auth source credentials in Redis.
|
|
type authSourceRedisRecord struct {
|
|
ID uint64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
DisplayName string `json:"display_name"`
|
|
IsActive bool `json:"is_active"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
OpenIDDiscoveryURL string `json:"openid_discovery_url"`
|
|
Scopes string `json:"scopes"`
|
|
IconURL string `json:"icon_url"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
ClientSecretConfigured bool `json:"client_secret_configured"`
|
|
}
|
|
|
|
var (
|
|
authSourceActiveRAM = ram.MustNew[string, []model.AuthSource](ram.Options{MaximumSize: authSourceRAMMaximumSize})
|
|
authSourceByNameRAM = ram.MustNew[string, model.AuthSource](ram.Options{MaximumSize: authSourceRAMMaximumSize})
|
|
authSourceListenerOnce sync.Once
|
|
authSourceListenerCtx context.Context
|
|
authSourceListenerCancel context.CancelFunc
|
|
authSourceListenerDone chan struct{}
|
|
)
|
|
|
|
func cloneAuthSources(sources []model.AuthSource) []model.AuthSource {
|
|
if len(sources) == 0 {
|
|
return nil
|
|
}
|
|
cloned := make([]model.AuthSource, len(sources))
|
|
copy(cloned, sources)
|
|
return cloned
|
|
}
|
|
|
|
func cloneAuthSource(source model.AuthSource) model.AuthSource {
|
|
return source
|
|
}
|
|
|
|
func normalizeAuthSourceName(name string) string {
|
|
return strings.TrimSpace(strings.ToLower(name))
|
|
}
|
|
|
|
func authSourceByNameRedisKey(name string) string {
|
|
return fmt.Sprintf(authSourceByNameRedisKeyFmt, normalizeAuthSourceName(name))
|
|
}
|
|
|
|
func authSourceToRedisRecord(source model.AuthSource) authSourceRedisRecord {
|
|
return authSourceRedisRecord{
|
|
ID: source.ID,
|
|
Name: source.Name,
|
|
Type: source.Type,
|
|
DisplayName: source.DisplayName,
|
|
IsActive: source.IsActive,
|
|
ClientID: source.ClientID,
|
|
ClientSecret: source.ClientSecret,
|
|
OpenIDDiscoveryURL: source.OpenIDDiscoveryURL,
|
|
Scopes: source.Scopes,
|
|
IconURL: source.IconURL,
|
|
CreatedAt: source.CreatedAt,
|
|
UpdatedAt: source.UpdatedAt,
|
|
ClientSecretConfigured: source.ClientSecretConfigured,
|
|
}
|
|
}
|
|
|
|
func redisRecordToAuthSource(record authSourceRedisRecord) model.AuthSource {
|
|
return model.AuthSource{
|
|
ID: record.ID,
|
|
Name: record.Name,
|
|
Type: record.Type,
|
|
DisplayName: record.DisplayName,
|
|
IsActive: record.IsActive,
|
|
ClientID: record.ClientID,
|
|
ClientSecret: record.ClientSecret,
|
|
OpenIDDiscoveryURL: record.OpenIDDiscoveryURL,
|
|
Scopes: record.Scopes,
|
|
IconURL: record.IconURL,
|
|
CreatedAt: record.CreatedAt,
|
|
UpdatedAt: record.UpdatedAt,
|
|
ClientSecretConfigured: record.ClientSecretConfigured,
|
|
}
|
|
}
|
|
|
|
func ensureAuthSourceCacheListener() {
|
|
if db.Redis == nil {
|
|
return
|
|
}
|
|
authSourceListenerOnce.Do(startAuthSourceCacheInvalidationListener)
|
|
}
|
|
|
|
func startAuthSourceCacheInvalidationListener() {
|
|
authSourceListenerCtx, authSourceListenerCancel = context.WithCancel(context.Background())
|
|
authSourceListenerDone = make(chan struct{})
|
|
|
|
redisClient := db.Redis // 捕获当前客户端:goroutine 不读可变全局,避免与测试置空 db.Redis 竞争
|
|
go func() {
|
|
listenerCtx := authSourceListenerCtx
|
|
defer close(authSourceListenerDone)
|
|
|
|
pubsub := redisClient.Subscribe(listenerCtx, authSourceInvalidationChannel)
|
|
defer func() {
|
|
_ = pubsub.Close()
|
|
}()
|
|
|
|
go func() {
|
|
<-listenerCtx.Done()
|
|
_ = pubsub.Close()
|
|
}()
|
|
|
|
for range pubsub.Channel() {
|
|
authSourceActiveRAM.InvalidateAll()
|
|
authSourceByNameRAM.InvalidateAll()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func publishAuthSourceRAMInvalidation(ctx context.Context) {
|
|
if db.Redis == nil {
|
|
return
|
|
}
|
|
_ = db.Redis.Publish(ctx, authSourceInvalidationChannel, "reset").Err()
|
|
}
|
|
|
|
func populateActiveAuthSourceCache(ctx context.Context, sources []model.AuthSource) {
|
|
cloned := cloneAuthSources(sources)
|
|
authSourceActiveRAM.Set(authSourceActiveRAMKey, cloned)
|
|
if db.Redis != nil {
|
|
_ = db.SetJSON(ctx, authSourceActiveRedisKey, cloned, authSourceCacheTTL)
|
|
}
|
|
}
|
|
|
|
func populateAuthSourceByNameCache(ctx context.Context, name string, source *model.AuthSource) {
|
|
if source == nil {
|
|
return
|
|
}
|
|
cloned := cloneAuthSource(*source)
|
|
authSourceByNameRAM.Set(normalizeAuthSourceName(name), cloned)
|
|
if db.Redis != nil {
|
|
record := authSourceToRedisRecord(cloned)
|
|
_ = db.SetJSON(ctx, authSourceByNameRedisKey(name), record, authSourceCacheTTL)
|
|
}
|
|
}
|
|
|
|
// GetActiveAuthSourcesCached returns active auth sources from RAM, Redis, or the database.
|
|
func GetActiveAuthSourcesCached(ctx context.Context) ([]model.AuthSource, error) {
|
|
ensureAuthSourceCacheListener()
|
|
|
|
if sources, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); ok {
|
|
return cloneAuthSources(sources), nil
|
|
}
|
|
|
|
if db.Redis != nil {
|
|
var sources []model.AuthSource
|
|
if err := db.GetJSON(ctx, authSourceActiveRedisKey, &sources); err == nil {
|
|
populateActiveAuthSourceCache(ctx, sources)
|
|
return cloneAuthSources(sources), nil
|
|
}
|
|
}
|
|
|
|
sources, err := GetActiveAuthSources(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
populateActiveAuthSourceCache(ctx, sources)
|
|
return cloneAuthSources(sources), nil
|
|
}
|
|
|
|
// GetAuthSourceByNameCached returns an auth source by name from RAM, Redis, or the database.
|
|
func GetAuthSourceByNameCached(ctx context.Context, name string) (*model.AuthSource, error) {
|
|
ensureAuthSourceCacheListener()
|
|
|
|
normalized := normalizeAuthSourceName(name)
|
|
if normalized == "" {
|
|
return GetAuthSourceByName(ctx, name)
|
|
}
|
|
|
|
if source, ok := authSourceByNameRAM.GetIfPresent(normalized); ok {
|
|
cloned := cloneAuthSource(source)
|
|
return &cloned, nil
|
|
}
|
|
|
|
if db.Redis != nil {
|
|
var record authSourceRedisRecord
|
|
if err := db.GetJSON(ctx, authSourceByNameRedisKey(name), &record); err == nil {
|
|
source := redisRecordToAuthSource(record)
|
|
populateAuthSourceByNameCache(ctx, name, &source)
|
|
cloned := cloneAuthSource(source)
|
|
return &cloned, nil
|
|
}
|
|
}
|
|
|
|
source, err := GetAuthSourceByName(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
populateAuthSourceByNameCache(ctx, name, source)
|
|
cloned := cloneAuthSource(*source)
|
|
return &cloned, nil
|
|
}
|
|
|
|
// InvalidateAuthSourceCache clears active and per-name auth source caches from RAM and Redis.
|
|
func InvalidateAuthSourceCache(ctx context.Context) error {
|
|
ensureAuthSourceCacheListener()
|
|
|
|
authSourceActiveRAM.InvalidateAll()
|
|
authSourceByNameRAM.InvalidateAll()
|
|
|
|
if db.Redis == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := db.Redis.Del(ctx, db.PrefixedKey(authSourceActiveRedisKey)).Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
pattern := db.PrefixedKey(authSourceByNameRedisPattern)
|
|
iter := db.Redis.Scan(ctx, 0, pattern, 0).Iterator()
|
|
var keys []string
|
|
for iter.Next(ctx) {
|
|
keys = append(keys, iter.Val())
|
|
}
|
|
if err := iter.Err(); err != nil {
|
|
return err
|
|
}
|
|
if len(keys) > 0 {
|
|
if err := db.Redis.Del(ctx, keys...).Err(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
publishAuthSourceRAMInvalidation(ctx)
|
|
return nil
|
|
}
|
|
|
|
// StopAuthSourceCacheListener stops the Redis Pub/Sub subscription listener and resets the sync.Once guard.
|
|
func StopAuthSourceCacheListener() {
|
|
if authSourceListenerCancel != nil {
|
|
authSourceListenerCancel()
|
|
if authSourceListenerDone != nil {
|
|
<-authSourceListenerDone
|
|
}
|
|
authSourceListenerCancel = nil
|
|
authSourceListenerDone = nil
|
|
}
|
|
authSourceListenerOnce = sync.Once{}
|
|
}
|
|
|
|
// ResetAuthSourceRAMCacheForTest clears only the process-local RAM cache.
|
|
func ResetAuthSourceRAMCacheForTest() {
|
|
authSourceActiveRAM.InvalidateAll()
|
|
authSourceByNameRAM.InvalidateAll()
|
|
}
|