1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-28 10:03:42 +02:00
pocketbase/core/settings.go

413 lines
13 KiB
Go

package core
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/security"
)
// Common settings placeholder tokens
const (
EmailPlaceholderAppUrl string = "%APP_URL%"
EmailPlaceholderToken string = "%TOKEN%"
)
// Settings defines common app configuration options.
type Settings struct {
mux sync.RWMutex
Meta MetaConfig `form:"meta" json:"meta"`
Logs LogsConfig `form:"logs" json:"logs"`
Smtp SmtpConfig `form:"smtp" json:"smtp"`
S3 S3Config `form:"s3" json:"s3"`
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"`
UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"`
UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"`
UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"`
EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`
GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"`
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
}
// NewSettings creates and returns a new default Settings instance.
func NewSettings() *Settings {
return &Settings{
Meta: MetaConfig{
AppName: "Acme",
AppUrl: "http://localhost:8090",
SenderName: "Support",
SenderAddress: "support@example.com",
UserVerificationUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-verification/" + EmailPlaceholderToken,
UserResetPasswordUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-password-reset/" + EmailPlaceholderToken,
UserConfirmEmailChangeUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-email-change/" + EmailPlaceholderToken,
},
Logs: LogsConfig{
MaxDays: 7,
},
Smtp: SmtpConfig{
Enabled: false,
Host: "smtp.example.com",
Port: 587,
Username: "",
Password: "",
Tls: false,
},
AdminAuthToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 1209600, // 14 days,
},
AdminPasswordResetToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 1800, // 30 minutes,
},
UserAuthToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 1209600, // 14 days,
},
UserPasswordResetToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 1800, // 30 minutes,
},
UserVerificationToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 604800, // 7 days,
},
UserEmailChangeToken: TokenConfig{
Secret: security.RandomString(50),
Duration: 1800, // 30 minutes,
},
EmailAuth: EmailAuthConfig{
Enabled: true,
MinPasswordLength: 8,
},
GoogleAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
FacebookAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
GithubAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
GitlabAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
}
}
// Validate makes Settings validatable by implementing [validation.Validatable] interface.
func (s *Settings) Validate() error {
s.mux.Lock()
defer s.mux.Unlock()
return validation.ValidateStruct(s,
validation.Field(&s.Meta),
validation.Field(&s.Logs),
validation.Field(&s.AdminAuthToken),
validation.Field(&s.AdminPasswordResetToken),
validation.Field(&s.UserAuthToken),
validation.Field(&s.UserPasswordResetToken),
validation.Field(&s.UserEmailChangeToken),
validation.Field(&s.UserVerificationToken),
validation.Field(&s.Smtp),
validation.Field(&s.S3),
validation.Field(&s.EmailAuth),
validation.Field(&s.GoogleAuth),
validation.Field(&s.FacebookAuth),
validation.Field(&s.GithubAuth),
validation.Field(&s.GitlabAuth),
)
}
// Merge merges `other` settings into the current one.
func (s *Settings) Merge(other *Settings) error {
s.mux.Lock()
defer s.mux.Unlock()
bytes, err := json.Marshal(other)
if err != nil {
return err
}
return json.Unmarshal(bytes, s)
}
// Clone creates a new deep copy of the current settings.
func (c *Settings) Clone() (*Settings, error) {
new := &Settings{}
if err := new.Merge(c); err != nil {
return nil, err
}
return new, nil
}
// RedactClone creates a new deep copy of the current settings,
// while replacing the secret values with `******`.
func (s *Settings) RedactClone() (*Settings, error) {
clone, err := s.Clone()
if err != nil {
return nil, err
}
mask := "******"
sensitiveFields := []*string{
&clone.Smtp.Password,
&clone.S3.Secret,
&clone.AdminAuthToken.Secret,
&clone.AdminPasswordResetToken.Secret,
&clone.UserAuthToken.Secret,
&clone.UserPasswordResetToken.Secret,
&clone.UserEmailChangeToken.Secret,
&clone.UserVerificationToken.Secret,
&clone.GoogleAuth.ClientSecret,
&clone.FacebookAuth.ClientSecret,
&clone.GithubAuth.ClientSecret,
&clone.GitlabAuth.ClientSecret,
}
// mask all sensitive fields
for _, v := range sensitiveFields {
if v != nil && *v != "" {
*v = mask
}
}
return clone, nil
}
// NamedAuthProviderConfigs returns a map with all registered OAuth2
// provider configurations (indexed by their name identifier).
func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
return map[string]AuthProviderConfig{
auth.NameGoogle: s.GoogleAuth,
auth.NameFacebook: s.FacebookAuth,
auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth,
}
}
// -------------------------------------------------------------------
type TokenConfig struct {
Secret string `form:"secret" json:"secret"`
Duration int64 `form:"duration" json:"duration"`
}
// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface.
func (c TokenConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)),
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(31536000)),
)
}
// -------------------------------------------------------------------
type SmtpConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
Host string `form:"host" json:"host"`
Port int `form:"port" json:"port"`
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
// 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"`
}
// Validate makes SmtpConfig validatable by implementing [validation.Validatable] interface.
func (c SmtpConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Host, is.Host, validation.When(c.Enabled, validation.Required)),
validation.Field(&c.Port, validation.When(c.Enabled, validation.Required), validation.Min(0)),
)
}
// -------------------------------------------------------------------
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"`
}
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
func (c S3Config) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Endpoint, is.Host, 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 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"`
UserVerificationUrl string `form:"userVerificationUrl" json:"userVerificationUrl"`
UserResetPasswordUrl string `form:"userResetPasswordUrl" json:"userResetPasswordUrl"`
UserConfirmEmailChangeUrl string `form:"userConfirmEmailChangeUrl" json:"userConfirmEmailChangeUrl"`
}
// 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.Email, validation.Required),
validation.Field(
&c.UserVerificationUrl,
validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
),
validation.Field(
&c.UserResetPasswordUrl,
validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
),
validation.Field(
&c.UserConfirmEmailChangeUrl,
validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
),
)
}
func (c *MetaConfig) checkPlaceholders(params ...string) validation.RuleFunc {
return func(value any) error {
v, _ := value.(string)
if v == "" {
return nil // nothing to check
}
for _, param := range params {
if !strings.Contains(v, param) {
return validation.NewError("validation_missing_required_param", fmt.Sprintf("Missing required parameter %q", param))
}
}
return nil
}
}
// -------------------------------------------------------------------
type LogsConfig struct {
MaxDays int `form:"maxDays" json:"maxDays"`
}
// 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 AuthProviderConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
AllowRegistrations bool `form:"allowRegistrations" json:"allowRegistrations"`
ClientId string `form:"clientId" json:"clientId,omitempty"`
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
AuthUrl string `form:"authUrl" json:"authUrl,omitempty"`
TokenUrl string `form:"tokenUrl" json:"tokenUrl,omitempty"`
UserApiUrl string `form:"userApiUrl" json:"userApiUrl,omitempty"`
}
// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface.
func (c AuthProviderConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.ClientId, validation.When(c.Enabled, validation.Required)),
validation.Field(&c.ClientSecret, validation.When(c.Enabled, validation.Required)),
validation.Field(&c.AuthUrl, is.URL),
validation.Field(&c.TokenUrl, is.URL),
validation.Field(&c.UserApiUrl, is.URL),
)
}
// SetupProvider loads the current AuthProviderConfig into the specified provider.
func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
if !c.Enabled {
return errors.New("The provider is not enabled.")
}
if c.ClientId != "" {
provider.SetClientId(c.ClientId)
}
if c.ClientSecret != "" {
provider.SetClientSecret(c.ClientSecret)
}
if c.AuthUrl != "" {
provider.SetAuthUrl(c.AuthUrl)
}
if c.UserApiUrl != "" {
provider.SetUserApiUrl(c.UserApiUrl)
}
if c.TokenUrl != "" {
provider.SetTokenUrl(c.TokenUrl)
}
return nil
}
// -------------------------------------------------------------------
type EmailAuthConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
}
// Validate makes `EmailAuthConfig` validatable by implementing [validation.Validatable] interface.
func (c EmailAuthConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(
&c.ExceptDomains,
validation.When(len(c.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.OnlyDomains,
validation.When(len(c.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.MinPasswordLength,
validation.When(c.Enabled, validation.Required),
validation.Min(5),
validation.Max(100),
),
)
}