141 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			141 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package g8
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/TwiN/gocache/v2"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// ErrNoExpiration is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start the
 | |
| 	// janitor despite no expiration being configured.
 | |
| 	// To clarify, this is because the cache janitor is only useful when an expiration is set.
 | |
| 	ErrNoExpiration = errors.New("no point starting the janitor if the TTL is set to not expire")
 | |
| 
 | |
| 	// ErrCacheNotInitialized is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start
 | |
| 	// the janitor despite the cache not having been initialized using ClientProvider.WithCache
 | |
| 	ErrCacheNotInitialized = errors.New("cannot start janitor because cache is not configured")
 | |
| )
 | |
| 
 | |
| // ClientProvider has the task of retrieving a Client from an external source (e.g. a database) when provided with a
 | |
| // token. It should be used when you have a lot of tokens, and it wouldn't make sense to register all of them using
 | |
| // AuthorizationService's WithToken, WithTokens, WithClient or WithClients.
 | |
| //
 | |
| // Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4
 | |
| // aforementioned functions, the client provider will not be used by the AuthorizationService when a request is made
 | |
| // with said token. It will, however, be called upon if a token that is not explicitly registered in
 | |
| // AuthorizationService is sent alongside a request going through the Gate.
 | |
| //
 | |
| //     clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
 | |
| //         // We'll assume that the following function calls your database and returns a struct "User" that
 | |
| //         // has the user's token as well as the permissions granted to said user
 | |
| //         user := database.GetUserByToken(token)
 | |
| //         if user != nil {
 | |
| //             return g8.NewClient(user.Token).WithPermissions(user.Permissions)
 | |
| //         }
 | |
| //         return nil
 | |
| //     })
 | |
| //     gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
 | |
| //
 | |
| type ClientProvider struct {
 | |
| 	getClientByTokenFunc func(token string) *Client
 | |
| 
 | |
| 	cache *gocache.Cache
 | |
| 	ttl   time.Duration
 | |
| }
 | |
| 
 | |
| // NewClientProvider creates a ClientProvider
 | |
| // The parameter that must be passed is a function that the provider will use to retrieve a client by a given token
 | |
| //
 | |
| // Example:
 | |
| //     clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
 | |
| //         // We'll assume that the following function calls your database and returns a struct "User" that
 | |
| //         // has the user's token as well as the permissions granted to said user
 | |
| //         user := database.GetUserByToken(token)
 | |
| //         if user == nil {
 | |
| //             return nil
 | |
| //         }
 | |
| //         return g8.NewClient(user.Token).WithPermissions(user.Permissions)
 | |
| //     })
 | |
| //     gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
 | |
| //
 | |
| func NewClientProvider(getClientByTokenFunc func(token string) *Client) *ClientProvider {
 | |
| 	return &ClientProvider{
 | |
| 		getClientByTokenFunc: getClientByTokenFunc,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithCache adds cache options to the ClientProvider.
 | |
| //
 | |
| // ttl is the time until the cache entry will expire. A TTL of gocache.NoExpiration (-1) means no expiration
 | |
| // maxSize is the maximum amount of entries that can be in the cache at any given time.
 | |
| // If a value of gocache.NoMaxSize (0) or less is provided for maxSize, there will be no maximum size.
 | |
| //
 | |
| // Example:
 | |
| //     clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
 | |
| //         // We'll assume that the following function calls your database and returns a struct "User" that
 | |
| //         // has the user's token as well as the permissions granted to said user
 | |
| //         user := database.GetUserByToken(token)
 | |
| //         if user != nil {
 | |
| //             return g8.NewClient(user.Token).WithPermissions(user.Permissions)
 | |
| //         }
 | |
| //         return nil
 | |
| //     })
 | |
| //     gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider.WithCache(time.Hour, 70000)))
 | |
| //
 | |
| func (provider *ClientProvider) WithCache(ttl time.Duration, maxSize int) *ClientProvider {
 | |
| 	provider.cache = gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(maxSize)
 | |
| 	provider.ttl = ttl
 | |
| 	return provider
 | |
| }
 | |
| 
 | |
| // StartCacheJanitor starts the cache janitor, which passively deletes expired cache entries in the background.
 | |
| //
 | |
| // Not really necessary unless you have a lot of clients (100000+).
 | |
| //
 | |
| // Even without the janitor, active eviction will still happen (i.e. when GetClientByToken is called, but the cache
 | |
| // entry for the given token has expired, the cache entry will be automatically deleted and re-fetched from the
 | |
| // user-defined getClientByTokenFunc)
 | |
| func (provider *ClientProvider) StartCacheJanitor() error {
 | |
| 	if provider.cache == nil {
 | |
| 		// Can't start the cache janitor if there's no cache
 | |
| 		return ErrCacheNotInitialized
 | |
| 	}
 | |
| 	if provider.ttl != gocache.NoExpiration {
 | |
| 		return provider.cache.StartJanitor()
 | |
| 	}
 | |
| 	return ErrNoExpiration
 | |
| }
 | |
| 
 | |
| // StopCacheJanitor stops the cache janitor
 | |
| //
 | |
| // Not required unless your application initializes multiple providers over the course of its lifecycle.
 | |
| // In English, that means if you initialize a ClientProvider only once on application start and it stays up
 | |
| // until your application shuts down, you don't need to call this function.
 | |
| func (provider *ClientProvider) StopCacheJanitor() {
 | |
| 	if provider.cache != nil {
 | |
| 		provider.cache.StopJanitor()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetClientByToken retrieves a client by its token through the provided getClientByTokenFunc.
 | |
| func (provider *ClientProvider) GetClientByToken(token string) *Client {
 | |
| 	if provider.cache == nil {
 | |
| 		return provider.getClientByTokenFunc(token)
 | |
| 	}
 | |
| 	if cachedClient, exists := provider.cache.Get(token); exists {
 | |
| 		if cachedClient == nil {
 | |
| 			return nil
 | |
| 		}
 | |
| 		// Safely typecast the client.
 | |
| 		// Regardless of whether the typecast is successful or not, we return client since it'll be either client or
 | |
| 		// nil. Technically, it should never be nil, but it's better to be safe than sorry.
 | |
| 		client, _ := cachedClient.(*Client)
 | |
| 		return client
 | |
| 	}
 | |
| 	client := provider.getClientByTokenFunc(token)
 | |
| 	provider.cache.SetWithTTL(token, client, provider.ttl)
 | |
| 	return client
 | |
| }
 |