mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-27 23:46:18 +02:00
676 lines
18 KiB
Go
676 lines
18 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
|
"github.com/go-ozzo/ozzo-validation/v4/is"
|
|
"github.com/pocketbase/pocketbase/core/validators"
|
|
"github.com/pocketbase/pocketbase/tools/cron"
|
|
"github.com/pocketbase/pocketbase/tools/hook"
|
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
"github.com/pocketbase/pocketbase/tools/security"
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
|
)
|
|
|
|
const (
|
|
paramsTable = "_params"
|
|
|
|
paramsKeySettings = "settings"
|
|
|
|
systemHookIdSettings = "__pbSettingsSystemHook__"
|
|
)
|
|
|
|
func (app *BaseApp) registerSettingsHooks() {
|
|
saveFunc := func(me *ModelEvent) error {
|
|
if err := me.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if me.Model.PK() == paramsKeySettings {
|
|
// auto reload the app settings because we don't know whether
|
|
// the Settings model is the app one or a different one
|
|
return errors.Join(
|
|
me.App.Settings().PostScan(),
|
|
me.App.ReloadSettings(),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
app.OnModelAfterCreateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdSettings,
|
|
Func: saveFunc,
|
|
Priority: -999,
|
|
})
|
|
|
|
app.OnModelAfterUpdateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdSettings,
|
|
Func: saveFunc,
|
|
Priority: -999,
|
|
})
|
|
|
|
app.OnModelDelete(paramsTable).Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdSettings,
|
|
Func: func(me *ModelEvent) error {
|
|
if me.Model.PK() == paramsKeySettings {
|
|
return errors.New("the app params settings cannot be deleted")
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -999,
|
|
})
|
|
|
|
app.OnCollectionUpdate().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdSettings,
|
|
Func: func(e *CollectionEvent) error {
|
|
oldCollection, err := e.App.FindCachedCollectionByNameOrId(e.Collection.Id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve old cached collection: %w", err)
|
|
}
|
|
|
|
err = e.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update existing rate limit rules on collection rename
|
|
if oldCollection.Name != e.Collection.Name {
|
|
var hasChange bool
|
|
|
|
rules := e.App.Settings().RateLimits.Rules
|
|
for i := 0; i < len(rules); i++ {
|
|
if strings.HasPrefix(rules[i].Label, oldCollection.Name+":") {
|
|
rules[i].Label = strings.Replace(rules[i].Label, oldCollection.Name+":", e.Collection.Name+":", 1)
|
|
hasChange = true
|
|
}
|
|
}
|
|
|
|
if hasChange {
|
|
e.App.Settings().RateLimits.Rules = rules
|
|
err = e.App.Save(e.App.Settings())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Priority: 99,
|
|
})
|
|
}
|
|
|
|
var (
|
|
_ Model = (*Settings)(nil)
|
|
_ PostValidator = (*Settings)(nil)
|
|
_ DBExporter = (*Settings)(nil)
|
|
)
|
|
|
|
type settings struct {
|
|
SMTP SMTPConfig `form:"smtp" json:"smtp"`
|
|
Backups BackupsConfig `form:"backups" json:"backups"`
|
|
S3 S3Config `form:"s3" json:"s3"`
|
|
Meta MetaConfig `form:"meta" json:"meta"`
|
|
Logs LogsConfig `form:"logs" json:"logs"`
|
|
Batch BatchConfig `form:"batch" json:"batch"`
|
|
RateLimits RateLimitsConfig `form:"rateLimits" json:"rateLimits"`
|
|
TrustedProxy TrustedProxyConfig `form:"trustedProxy" json:"trustedProxy"`
|
|
}
|
|
|
|
// Settings defines the PocketBase app settings.
|
|
type Settings struct {
|
|
settings
|
|
|
|
mu sync.RWMutex
|
|
isNew bool
|
|
}
|
|
|
|
func newDefaultSettings() *Settings {
|
|
return &Settings{
|
|
isNew: true,
|
|
settings: settings{
|
|
Meta: MetaConfig{
|
|
AppName: "Acme",
|
|
AppURL: "http://localhost:8090",
|
|
HideControls: false,
|
|
SenderName: "Support",
|
|
SenderAddress: "support@example.com",
|
|
},
|
|
Logs: LogsConfig{
|
|
MaxDays: 5,
|
|
LogIP: true,
|
|
},
|
|
SMTP: SMTPConfig{
|
|
Enabled: false,
|
|
Host: "smtp.example.com",
|
|
Port: 587,
|
|
Username: "",
|
|
Password: "",
|
|
TLS: false,
|
|
},
|
|
Backups: BackupsConfig{
|
|
CronMaxKeep: 3,
|
|
},
|
|
Batch: BatchConfig{
|
|
Enabled: false,
|
|
MaxRequests: 50,
|
|
Timeout: 3,
|
|
},
|
|
RateLimits: RateLimitsConfig{
|
|
Enabled: false, // @todo once tested enough enable by default for new installations
|
|
Rules: []RateLimitRule{
|
|
{Label: "*:auth", MaxRequests: 2, Duration: 3},
|
|
{Label: "*:create", MaxRequests: 20, Duration: 5},
|
|
{Label: "/api/batch", MaxRequests: 3, Duration: 1},
|
|
{Label: "/api/", MaxRequests: 300, Duration: 10},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TableName implements [Model.TableName] interface method.
|
|
func (s *Settings) TableName() string {
|
|
return paramsTable
|
|
}
|
|
|
|
// PK implements [Model.LastSavedPK] interface method.
|
|
func (s *Settings) LastSavedPK() any {
|
|
return paramsKeySettings
|
|
}
|
|
|
|
// PK implements [Model.PK] interface method.
|
|
func (s *Settings) PK() any {
|
|
return paramsKeySettings
|
|
}
|
|
|
|
// IsNew implements [Model.IsNew] interface method.
|
|
func (s *Settings) IsNew() bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
return s.isNew
|
|
}
|
|
|
|
// MarkAsNew implements [Model.MarkAsNew] interface method.
|
|
func (s *Settings) MarkAsNew() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.isNew = true
|
|
}
|
|
|
|
// MarkAsNew implements [Model.MarkAsNotNew] interface method.
|
|
func (s *Settings) MarkAsNotNew() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.isNew = false
|
|
}
|
|
|
|
// PostScan implements [Model.PostScan] interface method.
|
|
func (s *Settings) PostScan() error {
|
|
s.MarkAsNotNew()
|
|
return nil
|
|
}
|
|
|
|
// String returns a serialized string representation of the current settings.
|
|
func (s *Settings) String() string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
raw, _ := json.Marshal(s)
|
|
return string(raw)
|
|
}
|
|
|
|
// DBExport prepares and exports the current settings for db persistence.
|
|
func (s *Settings) DBExport(app App) (map[string]any, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
now := types.NowDateTime()
|
|
|
|
result := map[string]any{
|
|
"id": s.PK(),
|
|
}
|
|
|
|
if s.IsNew() {
|
|
result["created"] = now
|
|
}
|
|
result["updated"] = now
|
|
|
|
encoded, err := json.Marshal(s.settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encryptionKey := os.Getenv(app.EncryptionEnv())
|
|
if encryptionKey != "" {
|
|
encryptVal, encryptErr := security.Encrypt(encoded, encryptionKey)
|
|
if encryptErr != nil {
|
|
return nil, encryptErr
|
|
}
|
|
|
|
result["value"] = encryptVal
|
|
} else {
|
|
result["value"] = encoded
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// PostValidate implements the [PostValidator] interface and defines
|
|
// the Settings model validations.
|
|
func (s *Settings) PostValidate(ctx context.Context, app App) error {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
return validation.ValidateStructWithContext(ctx, s,
|
|
validation.Field(&s.Meta),
|
|
validation.Field(&s.Logs),
|
|
validation.Field(&s.SMTP),
|
|
validation.Field(&s.S3),
|
|
validation.Field(&s.Backups),
|
|
validation.Field(&s.Batch),
|
|
validation.Field(&s.RateLimits),
|
|
validation.Field(&s.TrustedProxy),
|
|
)
|
|
}
|
|
|
|
// Merge merges the "other" settings into the current one.
|
|
func (s *Settings) Merge(other *Settings) error {
|
|
other.mu.RLock()
|
|
defer other.mu.RUnlock()
|
|
|
|
raw, err := json.Marshal(other.settings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
return json.Unmarshal(raw, &s)
|
|
}
|
|
|
|
// Clone creates a new deep copy of the current settings.
|
|
func (s *Settings) Clone() (*Settings, error) {
|
|
clone := &Settings{
|
|
isNew: s.isNew,
|
|
}
|
|
|
|
if err := clone.Merge(s); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return clone, nil
|
|
}
|
|
|
|
// MarshalJSON implements the [json.Marshaler] interface.
|
|
//
|
|
// Note that sensitive fields (S3 secret, SMTP password, etc.) are excluded.
|
|
func (s *Settings) MarshalJSON() ([]byte, error) {
|
|
s.mu.RLock()
|
|
copy := s.settings
|
|
s.mu.RUnlock()
|
|
|
|
sensitiveFields := []*string{
|
|
©.SMTP.Password,
|
|
©.S3.Secret,
|
|
©.Backups.S3.Secret,
|
|
}
|
|
|
|
// mask all sensitive fields
|
|
for _, v := range sensitiveFields {
|
|
if v != nil && *v != "" {
|
|
*v = ""
|
|
}
|
|
}
|
|
|
|
return json.Marshal(copy)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type SMTPConfig struct {
|
|
Enabled bool `form:"enabled" json:"enabled"`
|
|
Port int `form:"port" json:"port"`
|
|
Host string `form:"host" json:"host"`
|
|
Username string `form:"username" json:"username"`
|
|
Password string `form:"password" json:"password,omitempty"`
|
|
|
|
// SMTP AUTH - PLAIN (default) or LOGIN
|
|
AuthMethod string `form:"authMethod" json:"authMethod"`
|
|
|
|
// Whether to enforce TLS encryption for the mail server connection.
|
|
//
|
|
// When set to false StartTLS command is send, leaving the server
|
|
// to decide whether to upgrade the connection or not.
|
|
TLS bool `form:"tls" json:"tls"`
|
|
|
|
// LocalName is optional domain name or IP address used for the
|
|
// EHLO/HELO exchange (if not explicitly set, defaults to "localhost").
|
|
//
|
|
// This is required only by some SMTP servers, such as Gmail SMTP-relay.
|
|
LocalName string `form:"localName" json:"localName"`
|
|
}
|
|
|
|
// Validate makes SMTPConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c SMTPConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(
|
|
&c.Host,
|
|
validation.When(c.Enabled, validation.Required),
|
|
is.Host,
|
|
),
|
|
validation.Field(
|
|
&c.Port,
|
|
validation.When(c.Enabled, validation.Required),
|
|
validation.Min(0),
|
|
),
|
|
validation.Field(
|
|
&c.AuthMethod,
|
|
// don't require it for backward compatibility
|
|
// (fallback internally to PLAIN)
|
|
// validation.When(c.Enabled, validation.Required),
|
|
validation.In(mailer.SMTPAuthLogin, mailer.SMTPAuthPlain),
|
|
),
|
|
validation.Field(&c.LocalName, is.Host),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type S3Config struct {
|
|
Enabled bool `form:"enabled" json:"enabled"`
|
|
Bucket string `form:"bucket" json:"bucket"`
|
|
Region string `form:"region" json:"region"`
|
|
Endpoint string `form:"endpoint" json:"endpoint"`
|
|
AccessKey string `form:"accessKey" json:"accessKey"`
|
|
Secret string `form:"secret" json:"secret,omitempty"`
|
|
ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"`
|
|
}
|
|
|
|
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
|
|
func (c S3Config) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)),
|
|
validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)),
|
|
validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)),
|
|
validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)),
|
|
validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type BatchConfig struct {
|
|
Enabled bool `form:"enabled" json:"enabled"`
|
|
|
|
// MaxRequests is the maximum allowed batch request to execute.
|
|
MaxRequests int `form:"maxRequests" json:"maxRequests"`
|
|
|
|
// Timeout is the the max duration in seconds to wait before cancelling the batch transaction.
|
|
Timeout int64 `form:"timeout" json:"timeout"`
|
|
|
|
// MaxBodySize is the maximum allowed batch request body size in bytes.
|
|
//
|
|
// If not set, fallbacks to max ~128MB.
|
|
MaxBodySize int64 `form:"maxBodySize" json:"maxBodySize"`
|
|
}
|
|
|
|
// Validate makes BatchConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c BatchConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.MaxRequests, validation.When(c.Enabled, validation.Required), validation.Min(0)),
|
|
validation.Field(&c.Timeout, validation.When(c.Enabled, validation.Required), validation.Min(0)),
|
|
validation.Field(&c.MaxBodySize, validation.Min(0)),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type BackupsConfig struct {
|
|
// Cron is a cron expression to schedule auto backups, eg. "* * * * *".
|
|
//
|
|
// Leave it empty to disable the auto backups functionality.
|
|
Cron string `form:"cron" json:"cron"`
|
|
|
|
// CronMaxKeep is the the max number of cron generated backups to
|
|
// keep before removing older entries.
|
|
//
|
|
// This field works only when the cron config has valid cron expression.
|
|
CronMaxKeep int `form:"cronMaxKeep" json:"cronMaxKeep"`
|
|
|
|
// S3 is an optional S3 storage config specifying where to store the app backups.
|
|
S3 S3Config `form:"s3" json:"s3"`
|
|
}
|
|
|
|
// Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c BackupsConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.S3),
|
|
validation.Field(&c.Cron, validation.By(checkCronExpression)),
|
|
validation.Field(
|
|
&c.CronMaxKeep,
|
|
validation.When(c.Cron != "", validation.Required),
|
|
validation.Min(1),
|
|
),
|
|
)
|
|
}
|
|
|
|
func checkCronExpression(value any) error {
|
|
v, _ := value.(string)
|
|
if v == "" {
|
|
return nil // nothing to check
|
|
}
|
|
|
|
_, err := cron.NewSchedule(v)
|
|
if err != nil {
|
|
return validation.NewError("validation_invalid_cron", err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type MetaConfig struct {
|
|
AppName string `form:"appName" json:"appName"`
|
|
AppURL string `form:"appURL" json:"appURL"`
|
|
SenderName string `form:"senderName" json:"senderName"`
|
|
SenderAddress string `form:"senderAddress" json:"senderAddress"`
|
|
HideControls bool `form:"hideControls" json:"hideControls"`
|
|
}
|
|
|
|
// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c MetaConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
|
|
validation.Field(&c.AppURL, validation.Required, is.URL),
|
|
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
|
|
validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type LogsConfig struct {
|
|
MaxDays int `form:"maxDays" json:"maxDays"`
|
|
MinLevel int `form:"minLevel" json:"minLevel"`
|
|
LogIP bool `form:"logIP" json:"logIP"`
|
|
LogAuthId bool `form:"logAuthId" json:"logAuthId"`
|
|
}
|
|
|
|
// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c LogsConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.MaxDays, validation.Min(0)),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type TrustedProxyConfig struct {
|
|
// Headers is a list of explicit trusted header(s) to check.
|
|
Headers []string `form:"headers" json:"headers"`
|
|
|
|
// UseLeftmostIP specifies to use the left-mostish IP from the trusted headers.
|
|
//
|
|
// Note that this could be insecure when used with X-Forward-For header
|
|
// because some proxies like AWS ELB allow users to prepend their own header value
|
|
// before appending the trusted ones.
|
|
UseLeftmostIP bool `form:"useLeftmostIP" json:"useLeftmostIP"`
|
|
}
|
|
|
|
// MarshalJSON implements the [json.Marshaler] interface.
|
|
func (c TrustedProxyConfig) MarshalJSON() ([]byte, error) {
|
|
type alias TrustedProxyConfig
|
|
|
|
// serialize as empty array
|
|
if c.Headers == nil {
|
|
c.Headers = []string{}
|
|
}
|
|
|
|
return json.Marshal(alias(c))
|
|
}
|
|
|
|
// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface.
|
|
func (c TrustedProxyConfig) Validate() error {
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
type RateLimitsConfig struct {
|
|
Rules []RateLimitRule `form:"rules" json:"rules"`
|
|
Enabled bool `form:"enabled" json:"enabled"`
|
|
}
|
|
|
|
// FindRateLimitRule returns the first matching rule based on the provided labels.
|
|
func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string) (RateLimitRule, bool) {
|
|
var prefixRules []int
|
|
|
|
for i, label := range searchLabels {
|
|
// check for direct match
|
|
for j := range c.Rules {
|
|
if label == c.Rules[j].Label {
|
|
return c.Rules[j], true
|
|
}
|
|
|
|
if i == 0 && strings.HasSuffix(c.Rules[j].Label, "/") {
|
|
prefixRules = append(prefixRules, j)
|
|
}
|
|
}
|
|
|
|
// check for prefix match
|
|
if len(prefixRules) > 0 {
|
|
for j := range prefixRules {
|
|
if strings.HasPrefix(label+"/", c.Rules[prefixRules[j]].Label) {
|
|
return c.Rules[prefixRules[j]], true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return RateLimitRule{}, false
|
|
}
|
|
|
|
// MarshalJSON implements the [json.Marshaler] interface.
|
|
func (c RateLimitsConfig) MarshalJSON() ([]byte, error) {
|
|
type alias RateLimitsConfig
|
|
|
|
// serialize as empty array
|
|
if c.Rules == nil {
|
|
c.Rules = []RateLimitRule{}
|
|
}
|
|
|
|
return json.Marshal(alias(c))
|
|
}
|
|
|
|
// Validate makes RateLimitsConfig validatable by implementing [validation.Validatable] interface.
|
|
func (c RateLimitsConfig) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(
|
|
&c.Rules,
|
|
validation.When(c.Enabled, validation.Required),
|
|
validation.By(checkUniqueRuleLabel),
|
|
),
|
|
)
|
|
}
|
|
|
|
func checkUniqueRuleLabel(value any) error {
|
|
rules, ok := value.([]RateLimitRule)
|
|
if !ok {
|
|
return validators.ErrUnsupportedValueType
|
|
}
|
|
|
|
labels := make(map[string]struct{}, len(rules))
|
|
|
|
for i, rule := range rules {
|
|
_, ok := labels[rule.Label]
|
|
if ok {
|
|
return validation.Errors{
|
|
strconv.Itoa(i): validation.Errors{
|
|
"label": validation.NewError("validation_duplicated_rate_limit_tag", "Rate limit tag with label "+rule.Label+" already exists.").
|
|
SetParams(map[string]any{"label": rule.Label}),
|
|
},
|
|
}
|
|
} else {
|
|
labels[rule.Label] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var rateLimitRuleLabelRegex = regexp.MustCompile(`^(\w+\ \/[\w\/-]*|\/[\w\/-]*|^\w+\:\w+|\*\:\w+|\w+)$`)
|
|
|
|
type RateLimitRule struct {
|
|
// Label is the identifier of the current rule.
|
|
//
|
|
// It could be a tag, complete path or path prerefix (when ends with `/`).
|
|
//
|
|
// Example supported labels:
|
|
// - test_a (plain text "tag")
|
|
// - users:create
|
|
// - *:create
|
|
// - /
|
|
// - /api
|
|
// - POST /api/collections/
|
|
Label string `form:"label" json:"label"`
|
|
|
|
// MaxRequests is the max allowed number of requests per Duration.
|
|
MaxRequests int `form:"maxRequests" json:"maxRequests"`
|
|
|
|
// Duration specifies the interval (in seconds) per which to reset
|
|
// the counted/accumulated rate limiter tokens.
|
|
Duration int64 `form:"duration" json:"duration"`
|
|
}
|
|
|
|
// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface.
|
|
func (c RateLimitRule) Validate() error {
|
|
return validation.ValidateStruct(&c,
|
|
validation.Field(&c.Label, validation.Required, validation.Match(rateLimitRuleLabelRegex)),
|
|
validation.Field(&c.MaxRequests, validation.Required, validation.Min(1)),
|
|
validation.Field(&c.Duration, validation.Required, validation.Min(1)),
|
|
)
|
|
}
|
|
|
|
// DurationTime returns the tag's Duration as [time.Duration].
|
|
func (c RateLimitRule) DurationTime() time.Duration {
|
|
return time.Duration(c.Duration) * time.Second
|
|
}
|