Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/domainprovider"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
LifecycleContext() context.Context
|
||||
Logger() mlogger.Logger
|
||||
DomainProvider() domainprovider.DomainProvider
|
||||
Config() *Config
|
||||
ProfileConfig() ConfigProvider
|
||||
DBFactory() db.Factory
|
||||
Permissions() auth.Provider
|
||||
RuntimeComponent(name string) any
|
||||
Register() Register
|
||||
}
|
||||
|
||||
type MicroServiceFactoryT = func(API) (mservice.MicroService, error)
|
||||
@@ -0,0 +1,41 @@
|
||||
package api
|
||||
|
||||
type AuthStorageConfig struct {
|
||||
AccountsCollection string `yaml:"accounts_collection"`
|
||||
RefreshTokensCollection string `yaml:"refresh_tokens_collection"`
|
||||
ClientsCollection string `yaml:"clients_collection"`
|
||||
VerificationTokensCollection string `yaml:"verification_tokens_collection"`
|
||||
LoginAttemptsCollection string `yaml:"login_attempts_collection"`
|
||||
VerificationRateLimitsCollection string `yaml:"verification_rate_limits_collection"`
|
||||
}
|
||||
|
||||
func (c *AuthStorageConfig) WithDefaults() AuthStorageConfig {
|
||||
if c == nil {
|
||||
c = &AuthStorageConfig{}
|
||||
}
|
||||
res := *c
|
||||
if res.AccountsCollection == "" {
|
||||
res.AccountsCollection = "accounts"
|
||||
}
|
||||
if res.RefreshTokensCollection == "" {
|
||||
res.RefreshTokensCollection = "refresh_tokens"
|
||||
}
|
||||
if res.ClientsCollection == "" {
|
||||
res.ClientsCollection = "clients"
|
||||
}
|
||||
if res.VerificationTokensCollection == "" {
|
||||
res.VerificationTokensCollection = "verification_tokens"
|
||||
}
|
||||
if res.LoginAttemptsCollection == "" {
|
||||
res.LoginAttemptsCollection = "login_attempts"
|
||||
}
|
||||
if res.VerificationRateLimitsCollection == "" {
|
||||
res.VerificationRateLimitsCollection = "verification_rate_limits"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type VerificationConfig struct {
|
||||
AllowedPurposes []string `yaml:"allowed_purposes"`
|
||||
RateLimits *VerificationRateLimitsConfig `yaml:"rate_limits"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
mwa "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/middleware"
|
||||
storageconfig "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/storage"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mw *mwa.Config `yaml:"middleware"`
|
||||
AuthStorage *AuthStorageConfig `yaml:"auth_storage"`
|
||||
Verification *VerificationConfig `yaml:"verification"`
|
||||
Storage *storageconfig.Config `yaml:"storage"`
|
||||
}
|
||||
|
||||
func (c *Config) SharedConfig() *Config {
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api/sresponse"
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api/ws"
|
||||
api "git.sendico.io/tech/sxchnge/bff/pkg/api/http"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
)
|
||||
|
||||
type Register interface {
|
||||
Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
|
||||
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
|
||||
PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc)
|
||||
WSHandler(messageType string, handler ws.HandlerFunc)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
|
||||
type ConfigProvider interface {
|
||||
SharedConfig() *Config
|
||||
}
|
||||
|
||||
type ConfigFactory func() ConfigProvider
|
||||
type RuntimeComponents map[string]any
|
||||
type RuntimeBootstrapFunc func(API) (RuntimeComponents, error)
|
||||
type RuntimeFinishFunc func(API) error
|
||||
|
||||
type RuntimeConfig struct {
|
||||
Name string
|
||||
Services []ServiceFactory
|
||||
EnableWebSocket bool
|
||||
ConfigFactory ConfigFactory
|
||||
Bootstrap RuntimeBootstrapFunc
|
||||
Finish RuntimeFinishFunc
|
||||
}
|
||||
|
||||
type ServiceFactory struct {
|
||||
Name mservice.Type
|
||||
Factory MicroServiceFactoryT
|
||||
}
|
||||
|
||||
func (cfg RuntimeConfig) RuntimeName() string {
|
||||
if cfg.Name == "" {
|
||||
return "edge"
|
||||
}
|
||||
return cfg.Name
|
||||
}
|
||||
|
||||
func (cfg RuntimeConfig) NewConfig() ConfigProvider {
|
||||
if cfg.ConfigFactory != nil {
|
||||
return cfg.ConfigFactory()
|
||||
}
|
||||
return &Config{}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type ChangePolicies struct {
|
||||
RoleRef bson.ObjectID `json:"roleRef"`
|
||||
Add *[]model.RolePolicy `json:"add,omitempty"`
|
||||
Remove *[]model.RolePolicy `json:"remove,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
type ChangeRole struct {
|
||||
AccountRef bson.ObjectID `json:"accountRef"`
|
||||
NewRoleDescriptionRef bson.ObjectID `json:"newRoleDescriptionRef"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package srequest
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
|
||||
type Login struct {
|
||||
model.SessionIdentifier `json:",inline"`
|
||||
model.LoginData `json:"login"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
|
||||
type AccessTokenRefresh = model.ClientRefreshToken
|
||||
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
|
||||
type TokenRefreshRotate = model.ClientRefreshToken
|
||||
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package srequest
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
|
||||
type VerificationCodeRequest struct {
|
||||
Purpose string `json:"purpose"`
|
||||
Target string `json:"target,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type CodeVerificationRequest struct {
|
||||
VerificationCodeRequest `json:",inline"`
|
||||
Code string `json:"code"`
|
||||
SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"`
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type AccountData struct {
|
||||
model.AccountPublic `json:",inline"`
|
||||
IsAnonymous bool `json:"isAnonymous"`
|
||||
}
|
||||
|
||||
type accountResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Account AccountData `json:"account"`
|
||||
}
|
||||
|
||||
type accountsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Accounts []AccountData `json:"accounts"`
|
||||
}
|
||||
|
||||
func NewAccountData(account *model.Account, isAnonymous bool) *AccountData {
|
||||
return &AccountData{
|
||||
AccountPublic: account.AccountPublic,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
func Account(logger mlogger.Logger, account *model.Account, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountResponse{
|
||||
Account: *NewAccountData(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func Accounts(logger mlogger.Logger, accounts []model.Account, orgRef bson.ObjectID, accessToken *TokenData) http.HandlerFunc {
|
||||
publicAccounts := make([]AccountData, len(accounts))
|
||||
for i := range accounts {
|
||||
publicAccounts[i] = *NewAccountData(
|
||||
&accounts[i],
|
||||
model.AccountIsAnonymous(&accounts[i].UserDataBase, orgRef),
|
||||
)
|
||||
}
|
||||
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountsResponse{
|
||||
Accounts: publicAccounts,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package sresponse
|
||||
|
||||
type AuthResponse struct {
|
||||
AccessToken TokenData `json:"accessToken"`
|
||||
}
|
||||
|
||||
type authResponse = AuthResponse
|
||||
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
)
|
||||
|
||||
type fileUploaded struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func FileUploaded(logger mlogger.Logger, url string) http.HandlerFunc {
|
||||
return response.Ok(logger, &fileUploaded{URL: url})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/auth/browser"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
type loginResponse struct {
|
||||
accountResponse
|
||||
RefreshToken TokenData `json:"refreshToken"`
|
||||
}
|
||||
|
||||
func Login(logger mlogger.Logger, account *model.Account, accessToken, refreshToken *TokenData) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
responseRefreshToken := *refreshToken
|
||||
if browser.UsesCookie(r) {
|
||||
browser.SetRefreshCookie(w, r, refreshToken.Token, refreshToken.Expiration)
|
||||
responseRefreshToken.Token = ""
|
||||
}
|
||||
response.Ok(
|
||||
logger,
|
||||
&loginResponse{
|
||||
accountResponse: accountResponse{
|
||||
Account: *NewAccountData(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
RefreshToken: responseRefreshToken,
|
||||
},
|
||||
).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
type pendingLoginResponse struct {
|
||||
Account accountResponse `json:"account"`
|
||||
PendingToken TokenData `json:"pendingToken"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, target string) http.HandlerFunc {
|
||||
return response.Accepted(
|
||||
logger,
|
||||
&pendingLoginResponse{
|
||||
Account: accountResponse{
|
||||
Account: *NewAccountData(account, false),
|
||||
authResponse: authResponse{},
|
||||
},
|
||||
PendingToken: *pendingToken,
|
||||
Target: target,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/auth/browser"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestBrowserLoginMovesRefreshCredentialToHttpOnlyCookie(t *testing.T) {
|
||||
refresh := &TokenData{Token: "refresh-secret", Expiration: time.Now().Add(time.Hour)}
|
||||
access := &TokenData{Token: "access-secret", Expiration: time.Now().Add(time.Minute)}
|
||||
req := httptest.NewRequest(http.MethodPost, "http://app.sendico.io/api/v1/accounts/login", nil)
|
||||
req.Header.Set(browser.TransportHeader, browser.TransportCookie)
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
Login(zap.NewNop(), &model.Account{}, access, refresh).ServeHTTP(recorder, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.NotContains(t, recorder.Body.String(), refresh.Token)
|
||||
require.Contains(t, recorder.Body.String(), access.Token)
|
||||
setCookie := recorder.Header().Get("Set-Cookie")
|
||||
require.True(t, strings.HasPrefix(setCookie, browser.RefreshCookie+"="))
|
||||
require.Contains(t, setCookie, "HttpOnly")
|
||||
require.Contains(t, setCookie, "Secure")
|
||||
require.Contains(t, setCookie, "SameSite=Strict")
|
||||
}
|
||||
|
||||
func TestNativeLoginKeepsRefreshCredentialInResponse(t *testing.T) {
|
||||
refresh := &TokenData{Token: "native-refresh", Expiration: time.Now().Add(time.Hour)}
|
||||
access := &TokenData{Token: "native-access", Expiration: time.Now().Add(time.Minute)}
|
||||
req := httptest.NewRequest(http.MethodPost, "http://native.test/api/v1/accounts/login", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
Login(zap.NewNop(), &model.Account{}, access, refresh).ServeHTTP(recorder, req)
|
||||
|
||||
require.Contains(t, recorder.Body.String(), refresh.Token)
|
||||
require.Empty(t, recorder.Header().Get("Set-Cookie"))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
type organizationsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Organizations []model.Organization `json:"organizations"`
|
||||
}
|
||||
|
||||
func Organization(logger mlogger.Logger, organization *model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return Organizations(logger, []model.Organization{*organization}, accessToken)
|
||||
}
|
||||
|
||||
func Organizations(logger mlogger.Logger, organizations []model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationsResponse{
|
||||
Organizations: organizations,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
type organizationPublicResponse struct {
|
||||
Organizations []model.OrganizationBase `json:"organizations"`
|
||||
}
|
||||
|
||||
func OrganizationPublic(logger mlogger.Logger, organization *model.OrganizationBase) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationPublicResponse{
|
||||
Organizations: []model.OrganizationBase{*organization},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
type permissionsDescription struct {
|
||||
Roles []model.RoleDescription `json:"roles"`
|
||||
Policies []model.PolicyDescription `json:"policies"`
|
||||
}
|
||||
|
||||
type permissionsData struct {
|
||||
Roles []model.Role `json:"roles"`
|
||||
Policies []model.RolePolicy `json:"policies"`
|
||||
Permissions []model.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
type permissionsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Descriptions permissionsDescription `json:"descriptions"`
|
||||
Permissions permissionsData `json:"permissions"`
|
||||
}
|
||||
|
||||
func Permissions(logger mlogger.Logger,
|
||||
rolesDescs []model.RoleDescription, policiesDescs []model.PolicyDescription,
|
||||
roles []model.Role, policies []model.RolePolicy, permissions []model.Permission,
|
||||
accessToken *TokenData,
|
||||
) http.HandlerFunc {
|
||||
return response.Ok(logger, permissionsResponse{
|
||||
Descriptions: permissionsDescription{
|
||||
Roles: rolesDescs,
|
||||
Policies: policiesDescs,
|
||||
},
|
||||
Permissions: permissionsData{
|
||||
Roles: roles,
|
||||
Policies: policies,
|
||||
Permissions: permissions,
|
||||
},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
emodel "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
type (
|
||||
HandlerFunc = func(r *http.Request) http.HandlerFunc
|
||||
AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc
|
||||
PendingAccountHandlerFunc = func(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
)
|
||||
|
||||
type resultAuth struct {
|
||||
authResponse `json:",inline"`
|
||||
response.Result `json:",inline"`
|
||||
}
|
||||
|
||||
func Success(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, &resultAuth{
|
||||
Result: response.Result{Result: true},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func Failed(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Accepted(logger, &resultAuth{
|
||||
Result: response.Result{Result: false},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package sresponse
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenData struct {
|
||||
Token string `json:"token"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package sresponse
|
||||
|
||||
type VerificationResponse struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
TTLSeconds int `json:"ttl_seconds"`
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sharedlimit "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/ratelimit"
|
||||
)
|
||||
|
||||
type VerificationRateLimitsConfig struct {
|
||||
PerDestination sharedlimit.WindowConfig `yaml:"per_destination"`
|
||||
PerIP sharedlimit.WindowConfig `yaml:"per_ip"`
|
||||
}
|
||||
|
||||
type VerificationRateLimits struct {
|
||||
PerDestination sharedlimit.Window
|
||||
PerIP sharedlimit.Window
|
||||
}
|
||||
|
||||
func DefaultVerificationRateLimits() VerificationRateLimits {
|
||||
return VerificationRateLimits{
|
||||
PerDestination: sharedlimit.Window{Limit: 5, Window: time.Hour},
|
||||
PerIP: sharedlimit.Window{Limit: 20, Window: time.Hour},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *VerificationConfig) ResolveRateLimits() (VerificationRateLimits, error) {
|
||||
if c == nil || c.RateLimits == nil {
|
||||
return DefaultVerificationRateLimits(), nil
|
||||
}
|
||||
destination, err := sharedlimit.ResolveWindow(c.RateLimits.PerDestination, "verification.rate_limits.per_destination")
|
||||
if err != nil {
|
||||
return VerificationRateLimits{}, err
|
||||
}
|
||||
ip, err := sharedlimit.ResolveWindow(c.RateLimits.PerIP, "verification.rate_limits.per_ip")
|
||||
if err != nil {
|
||||
return VerificationRateLimits{}, err
|
||||
}
|
||||
return VerificationRateLimits{PerDestination: destination, PerIP: ip}, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sharedlimit "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/ratelimit"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
func TestVerificationRateLimitsDefaults(t *testing.T) {
|
||||
got, err := (*VerificationConfig)(nil).ResolveRateLimits()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve defaults: %v", err)
|
||||
}
|
||||
if got.PerDestination.Limit != 5 || got.PerDestination.Window != time.Hour ||
|
||||
got.PerIP.Limit != 20 || got.PerIP.Window != time.Hour {
|
||||
t.Fatalf("unexpected defaults: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerificationRateLimitsRejectInvalidPolicy(t *testing.T) {
|
||||
cfg := &VerificationConfig{RateLimits: &VerificationRateLimitsConfig{
|
||||
PerDestination: sharedlimit.WindowConfig{Limit: 5, Window: "1h"},
|
||||
PerIP: sharedlimit.WindowConfig{Limit: 0, Window: "1h"},
|
||||
}}
|
||||
_, err := cfg.ResolveRateLimits()
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/middleware"
|
||||
)
|
||||
|
||||
type (
|
||||
Config = middleware.WebSocketConfig
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
ResponseHandler func(messageType string, conn *websocket.Conn)
|
||||
HandlerFunc func(ctx context.Context, msg Message) ResponseHandler
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/api/http/response"
|
||||
|
||||
type Message struct {
|
||||
response.BaseResponse
|
||||
ID string `json:"id"`
|
||||
MessageType string `json:"messageType"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package middleware
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCredentialedCORSRequiresExplicitOrigins(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config CORSSettings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "explicit origin",
|
||||
config: CORSSettings{
|
||||
AllowCredentials: true,
|
||||
AllowedOrigins: []string{"https://app.sendico.io"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard",
|
||||
config: CORSSettings{
|
||||
AllowCredentials: true,
|
||||
AllowedOrigins: []string{"*"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing origin",
|
||||
config: CORSSettings{
|
||||
AllowCredentials: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "non-credentialed compatibility",
|
||||
config: CORSSettings{
|
||||
AllowedOrigins: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.config.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Validate() error = %v, wantErr %t", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sharedlimit "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/ratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLoginAccountLimit = 5
|
||||
defaultLoginAccountWindow = 15 * time.Minute
|
||||
defaultLoginIPLimit = 50
|
||||
defaultLoginIPWindow = 15 * time.Minute
|
||||
)
|
||||
|
||||
type LoginRateLimitWindowConfig = sharedlimit.WindowConfig
|
||||
|
||||
type LoginRateLimitsConfig struct {
|
||||
PerAccount LoginRateLimitWindowConfig `yaml:"per_account"`
|
||||
PerIP LoginRateLimitWindowConfig `yaml:"per_ip"`
|
||||
}
|
||||
|
||||
type LoginRateLimitWindow = sharedlimit.Window
|
||||
|
||||
type LoginRateLimits struct {
|
||||
PerAccount LoginRateLimitWindow
|
||||
PerIP LoginRateLimitWindow
|
||||
}
|
||||
|
||||
func (c *Config) ResolveLoginRateLimits() (LoginRateLimits, error) {
|
||||
if c == nil || c.LoginRateLimits == nil {
|
||||
return LoginRateLimits{
|
||||
PerAccount: LoginRateLimitWindow{Limit: defaultLoginAccountLimit, Window: defaultLoginAccountWindow},
|
||||
PerIP: LoginRateLimitWindow{Limit: defaultLoginIPLimit, Window: defaultLoginIPWindow},
|
||||
}, nil
|
||||
}
|
||||
|
||||
account, err := sharedlimit.ResolveWindow(c.LoginRateLimits.PerAccount, "middleware.login_rate_limits.per_account")
|
||||
if err != nil {
|
||||
return LoginRateLimits{}, err
|
||||
}
|
||||
ip, err := sharedlimit.ResolveWindow(c.LoginRateLimits.PerIP, "middleware.login_rate_limits.per_ip")
|
||||
if err != nil {
|
||||
return LoginRateLimits{}, err
|
||||
}
|
||||
return LoginRateLimits{PerAccount: account, PerIP: ip}, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
func TestResolveLoginRateLimitsUsesSecureDefaults(t *testing.T) {
|
||||
got, err := (&Config{}).ResolveLoginRateLimits()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve defaults: %v", err)
|
||||
}
|
||||
if got.PerAccount.Limit != 5 || got.PerAccount.Window != 15*time.Minute {
|
||||
t.Fatalf("unexpected account defaults: %+v", got.PerAccount)
|
||||
}
|
||||
if got.PerIP.Limit != 50 || got.PerIP.Window != 15*time.Minute {
|
||||
t.Fatalf("unexpected IP defaults: %+v", got.PerIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLoginRateLimitsRejectsUnsafeConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *LoginRateLimitsConfig
|
||||
}{
|
||||
{
|
||||
name: "zero account threshold",
|
||||
cfg: &LoginRateLimitsConfig{
|
||||
PerAccount: LoginRateLimitWindowConfig{Window: "15m"},
|
||||
PerIP: LoginRateLimitWindowConfig{Limit: 50, Window: "15m"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid IP window",
|
||||
cfg: &LoginRateLimitsConfig{
|
||||
PerAccount: LoginRateLimitWindowConfig{Limit: 5, Window: "15m"},
|
||||
PerIP: LoginRateLimitWindowConfig{Limit: 50, Window: "forever"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "excessive account window",
|
||||
cfg: &LoginRateLimitsConfig{
|
||||
PerAccount: LoginRateLimitWindowConfig{Limit: 5, Window: "25h"},
|
||||
PerIP: LoginRateLimitWindowConfig{Limit: 50, Window: "15m"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := (&Config{LoginRateLimits: test.cfg}).ResolveLoginRateLimits()
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
type CORSSettings struct {
|
||||
MaxAge int `yaml:"max_age"`
|
||||
AllowedOrigins []string `yaml:"allowed_origins"`
|
||||
AllowedMethods []string `yaml:"allowed_methods"`
|
||||
AllowedHeaders []string `yaml:"allowed_headers"`
|
||||
ExposedHeaders []string `yaml:"exposed_headers"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
}
|
||||
|
||||
func (c CORSSettings) Validate() error {
|
||||
if !c.AllowCredentials {
|
||||
return nil
|
||||
}
|
||||
if len(c.AllowedOrigins) == 0 {
|
||||
return merrors.InvalidArgument("credentialed CORS requires explicit allowed origins", "middleware.CORS.allowed_origins")
|
||||
}
|
||||
for _, origin := range c.AllowedOrigins {
|
||||
if strings.TrimSpace(origin) == "*" {
|
||||
return merrors.InvalidArgument("credentialed CORS does not allow wildcard origins", "middleware.CORS.allowed_origins")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
PublicKey any
|
||||
PrivateKey []byte
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
type SignatureConfig struct {
|
||||
PublicKeyEnv string `yaml:"public_key_env,omitempty"`
|
||||
PrivateKeyEnv string `yaml:"secret_key_env"`
|
||||
Algorithm string `yaml:"algorithm"`
|
||||
}
|
||||
|
||||
type TokenExpiration struct {
|
||||
Account int `yaml:"account"`
|
||||
Refresh int `yaml:"refresh"`
|
||||
}
|
||||
|
||||
type TokenConfig struct {
|
||||
Expiration TokenExpiration `yaml:"expiration_hours"`
|
||||
Length int `yaml:"length"`
|
||||
}
|
||||
|
||||
const MaxAccessTokenTTLHours = 1
|
||||
|
||||
func (c TokenConfig) Validate() error {
|
||||
if c.Expiration.Account <= 0 || c.Expiration.Account > MaxAccessTokenTTLHours {
|
||||
return merrors.InvalidArgument("access token expiration must be between 1 and 1 hour", "middleware.token.expiration_hours.account")
|
||||
}
|
||||
if c.Expiration.Refresh <= c.Expiration.Account {
|
||||
return merrors.InvalidArgument("refresh token expiration must exceed access token expiration", "middleware.token.expiration_hours.refresh")
|
||||
}
|
||||
if c.Length < 32 {
|
||||
return merrors.InvalidArgument("refresh token length must be at least 32 bytes", "middleware.token.length")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WebSocketConfig struct {
|
||||
EndpointEnv string `yaml:"endpoint_env"`
|
||||
Timeout int `yaml:"timeout"`
|
||||
}
|
||||
|
||||
type PasswordChecks struct {
|
||||
Digit bool `yaml:"digit"`
|
||||
Upper bool `yaml:"upper"`
|
||||
Lower bool `yaml:"lower"`
|
||||
Special bool `yaml:"special"`
|
||||
MinLength int `yaml:"min_length"`
|
||||
}
|
||||
|
||||
type PasswordConfig struct {
|
||||
TokenLength int `yaml:"token_length"`
|
||||
Check PasswordChecks `yaml:"check"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DomainEnv string `yaml:"domain_env"`
|
||||
EndPointEnv string `yaml:"api_endpoint_env"`
|
||||
APIProtocolEnv string `yaml:"api_protocol_env"`
|
||||
Signature SignatureConfig `yaml:"signature"`
|
||||
CORS CORSSettings `yaml:"CORS"`
|
||||
WebSocket WebSocketConfig `yaml:"websocket"`
|
||||
Token TokenConfig `yaml:"token"`
|
||||
Password PasswordConfig `yaml:"password"`
|
||||
LoginRateLimits *LoginRateLimitsConfig `yaml:"login_rate_limits"`
|
||||
}
|
||||
|
||||
type MapClaims = map[string]any
|
||||
|
||||
func getKey(osEnv string) any {
|
||||
if len(osEnv) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []byte(os.Getenv(osEnv))
|
||||
}
|
||||
|
||||
func SignatureConf(conf *Config) Signature {
|
||||
return Signature{
|
||||
PrivateKey: []byte(os.Getenv(conf.Signature.PrivateKeyEnv)),
|
||||
PublicKey: getKey(conf.Signature.PublicKeyEnv),
|
||||
Algorithm: conf.Signature.Algorithm,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package middleware
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTokenConfigValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config TokenConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "secure explicit TTLs",
|
||||
config: TokenConfig{
|
||||
Expiration: TokenExpiration{Account: 1, Refresh: 720},
|
||||
Length: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rejects legacy 24 hour access token",
|
||||
config: TokenConfig{
|
||||
Expiration: TokenExpiration{Account: 24, Refresh: 720},
|
||||
Length: 32,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects missing access TTL",
|
||||
config: TokenConfig{
|
||||
Expiration: TokenExpiration{Refresh: 720},
|
||||
Length: 32,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects weak refresh token",
|
||||
config: TokenConfig{
|
||||
Expiration: TokenExpiration{Account: 1, Refresh: 720},
|
||||
Length: 16,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.config.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Validate() error = %v, wantErr %t", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/middleware"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mduration "git.sendico.io/tech/sxchnge/bff/pkg/mutil/duration"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type AccountToken struct {
|
||||
AccountRef bson.ObjectID
|
||||
Login string
|
||||
Name string
|
||||
Locale string
|
||||
model.SessionIdentifier
|
||||
Expiration time.Time
|
||||
Pending bool
|
||||
}
|
||||
|
||||
func createAccountToken(a *model.Account, expiration int, session model.SessionIdentifier) AccountToken {
|
||||
return AccountToken{
|
||||
AccountRef: *a.GetID(),
|
||||
Login: a.Login,
|
||||
Name: a.Name,
|
||||
Locale: a.Locale,
|
||||
SessionIdentifier: session,
|
||||
Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
|
||||
Pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
func getTokenParam(claims middleware.MapClaims, param string) (string, error) {
|
||||
id, ok := claims[param].(string)
|
||||
if !ok {
|
||||
return "", merrors.NoData(fmt.Sprintf("param '%s' not found", param))
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const (
|
||||
paramNameID = "id"
|
||||
paramNameName = "name"
|
||||
paramNameLocale = "locale"
|
||||
paramNameLogin = "login"
|
||||
paramNameClientID = "clientId"
|
||||
paramNameDeviceID = "deviceId"
|
||||
paramNameSessionVersion = "sessionVersion"
|
||||
paramNameExpiration = "exp"
|
||||
paramNamePending = "pending"
|
||||
)
|
||||
|
||||
func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
|
||||
var at AccountToken
|
||||
var err error
|
||||
var account string
|
||||
if account, err = getTokenParam(claims, paramNameID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.AccountRef, err = bson.ObjectIDFromHex(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Login, err = getTokenParam(claims, paramNameLogin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Name, err = getTokenParam(claims, paramNameName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientID, ok := claims[paramNameClientID]; ok {
|
||||
if clientIDText, ok := clientID.(string); ok {
|
||||
at.ClientID = clientIDText
|
||||
}
|
||||
}
|
||||
if deviceID, ok := claims[paramNameDeviceID]; ok {
|
||||
if deviceIDText, ok := deviceID.(string); ok {
|
||||
at.DeviceID = deviceIDText
|
||||
}
|
||||
}
|
||||
if version, ok := claims[paramNameSessionVersion]; ok {
|
||||
if versionText, ok := version.(string); ok {
|
||||
at.SessionVersion = versionText
|
||||
}
|
||||
}
|
||||
if pending, ok := claims[paramNamePending]; ok {
|
||||
if pbool, ok := pending.(bool); ok {
|
||||
at.Pending = pbool
|
||||
}
|
||||
}
|
||||
if expValue, ok := claims[paramNameExpiration]; ok {
|
||||
switch exp := expValue.(type) {
|
||||
case time.Time:
|
||||
at.Expiration = exp
|
||||
case float64:
|
||||
at.Expiration = time.Unix(int64(exp), 0)
|
||||
case int64:
|
||||
at.Expiration = time.Unix(exp, 0)
|
||||
default:
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
} else {
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
return &at, nil
|
||||
}
|
||||
|
||||
func Account2ClaimsForSession(a *model.Account, expiration int, session model.SessionIdentifier) middleware.MapClaims {
|
||||
t := createAccountToken(a, expiration, session)
|
||||
return middleware.MapClaims{
|
||||
paramNameID: t.AccountRef.Hex(),
|
||||
paramNameLogin: t.Login,
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameClientID: t.ClientID,
|
||||
paramNameDeviceID: t.DeviceID,
|
||||
paramNameSessionVersion: t.SessionVersion,
|
||||
paramNameExpiration: t.Expiration.Unix(),
|
||||
paramNamePending: t.Pending,
|
||||
}
|
||||
}
|
||||
|
||||
func PendingAccount2Claims(a *model.Account, expirationMinutes int) middleware.MapClaims {
|
||||
t := createAccountToken(a, expirationMinutes/60, model.SessionIdentifier{})
|
||||
t.Expiration = time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
|
||||
t.Pending = true
|
||||
return middleware.MapClaims{
|
||||
paramNameID: t.AccountRef.Hex(),
|
||||
paramNameLogin: t.Login,
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameExpiration: t.Expiration.Unix(),
|
||||
paramNamePending: t.Pending,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
basemodel "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestAccountTokenPreservesSessionIdentity(t *testing.T) {
|
||||
account := &basemodel.Account{}
|
||||
account.SetID(bson.NewObjectID())
|
||||
account.Login = "security@example.com"
|
||||
account.Name = "Security"
|
||||
account.Locale = "en"
|
||||
session := basemodel.SessionIdentifier{
|
||||
ClientID: "web",
|
||||
DeviceID: "device-a",
|
||||
SessionVersion: "session-version-a",
|
||||
}
|
||||
|
||||
token, err := Claims2Token(Account2ClaimsForSession(account, 1, session))
|
||||
if err != nil {
|
||||
t.Fatalf("decode account claims: %v", err)
|
||||
}
|
||||
if token.SessionIdentifier != session {
|
||||
t.Fatalf("session identity = %#v, want %#v", token.SessionIdentifier, session)
|
||||
}
|
||||
if token.Expiration.IsZero() {
|
||||
t.Fatal("access token expiration is missing")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
const (
|
||||
MinWindow = time.Second
|
||||
MaxWindow = 24 * time.Hour
|
||||
MaxLimit = 10000
|
||||
)
|
||||
|
||||
type WindowConfig struct {
|
||||
Limit int `yaml:"limit"`
|
||||
Window string `yaml:"window"`
|
||||
}
|
||||
|
||||
type Window struct {
|
||||
Limit int
|
||||
Window time.Duration
|
||||
}
|
||||
|
||||
func ResolveWindow(raw WindowConfig, field string) (Window, error) {
|
||||
if raw.Limit < 1 || raw.Limit > MaxLimit {
|
||||
return Window{}, merrors.InvalidArgument("rate limit must be between 1 and 10000", field+".limit")
|
||||
}
|
||||
window, err := time.ParseDuration(strings.TrimSpace(raw.Window))
|
||||
if err != nil {
|
||||
return Window{}, merrors.InvalidArgument("rate limit window must be a valid duration", field+".window")
|
||||
}
|
||||
if window < MinWindow || window > MaxWindow {
|
||||
return Window{}, merrors.InvalidArgument("rate limit window must be between 1s and 24h", field+".window")
|
||||
}
|
||||
return Window{Limit: raw.Limit, Window: window}, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package storage
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
|
||||
type Driver string
|
||||
|
||||
const (
|
||||
LocalFS Driver = "local_fs"
|
||||
AWSS3 Driver = "aws_s3"
|
||||
)
|
||||
|
||||
type Config = model.DriverConfig[Driver]
|
||||
|
||||
type LocalFSConfig struct {
|
||||
RootPath string `mapstructure:"root_path" yaml:"root_path"`
|
||||
}
|
||||
|
||||
type AWSS3Config struct {
|
||||
AccessKeyIDEnv string `mapstructure:"access_key_id_env" yaml:"access_key_id_env"`
|
||||
SecretAccessKeyEnv string `mapstructure:"secret_access_key_env" yaml:"secret_access_key_env"`
|
||||
RegionEnv string `mapstructure:"region_env" yaml:"region_env"`
|
||||
BucketNameEnv string `mapstructure:"bucket_name_env" yaml:"bucket_name_env"`
|
||||
EndpointEnv string `mapstructure:"endpoint_env" yaml:"endpoint_env"`
|
||||
UsePathStyle bool `mapstructure:"use_path_style" yaml:"use_path_style"`
|
||||
}
|
||||
Reference in new issue
Block a user