mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-20 11:56:23 +00:00
318 lines
14 KiB
Go
318 lines
14 KiB
Go
package appconfig
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/italypaleale/go-kit/utils"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
)
|
|
|
|
type AppConfigModel struct {
|
|
// General
|
|
AppName AppConfigValue `json:"appName" env:"APP_NAME" public:"true"`
|
|
SessionDuration AppConfigValue `json:"sessionDuration" env:"SESSION_DURATION" type:"int"` // In minutes
|
|
HomePageURL AppConfigValue `json:"homePageUrl" env:"HOME_PAGE_URL" public:"true"`
|
|
EmailsVerified AppConfigValue `json:"emailsVerified" env:"EMAILS_VERIFIED" type:"bool"`
|
|
AccentColor AppConfigValue `json:"accentColor" env:"ACCENT_COLOR" public:"true"`
|
|
DisableAnimations AppConfigValue `json:"disableAnimations" env:"DISABLE_ANIMATIONS" type:"bool" public:"true"`
|
|
AllowOwnAccountEdit AppConfigValue `json:"allowOwnAccountEdit" env:"ALLOW_OWN_ACCOUNT_EDIT" type:"bool" public:"true"`
|
|
AllowUserSignups AppConfigValue `json:"allowUserSignups" env:"ALLOW_USER_SIGNUPS" public:"true"`
|
|
|
|
SignupDefaultUserGroupIDs AppConfigValue `json:"signupDefaultUserGroupIDs" env:"SIGNUP_DEFAULT_USER_GROUP_IDS"` // JSON-encoded array of strings
|
|
SignupDefaultCustomClaims AppConfigValue `json:"signupDefaultCustomClaims" env:"SIGNUP_DEFAULT_CUSTOM_CLAIMS"` // JSON-encoded array of {key:string,value:string}
|
|
// Email
|
|
RequireUserEmail AppConfigValue `json:"requireUserEmail" env:"REQUIRE_USER_EMAIL" type:"bool" public:"true"`
|
|
SmtpHost AppConfigValue `json:"smtpHost" env:"SMTP_HOST"`
|
|
SmtpPort AppConfigValue `json:"smtpPort" env:"SMTP_PORT"`
|
|
SmtpFrom AppConfigValue `json:"smtpFrom" env:"SMTP_FROM"`
|
|
SmtpUser AppConfigValue `json:"smtpUser" env:"SMTP_USER"`
|
|
SmtpPassword AppConfigValue `json:"smtpPassword" env:"SMTP_PASSWORD" sensitive:"true"`
|
|
SmtpTls AppConfigValue `json:"smtpTls" env:"SMTP_TLS"`
|
|
SmtpSkipCertVerify AppConfigValue `json:"smtpSkipCertVerify" env:"SMTP_SKIP_CERT_VERIFY" type:"bool"`
|
|
EmailLoginNotificationEnabled AppConfigValue `json:"emailLoginNotificationEnabled" env:"EMAIL_LOGIN_NOTIFICATION_ENABLED" type:"bool"`
|
|
EmailOneTimeAccessAsUnauthenticatedEnabled AppConfigValue `json:"emailOneTimeAccessAsUnauthenticatedEnabled" env:"EMAIL_ONE_TIME_ACCESS_AS_UNAUTHENTICATED_ENABLED" type:"bool" public:"true"`
|
|
EmailOneTimeAccessAsAdminEnabled AppConfigValue `json:"emailOneTimeAccessAsAdminEnabled" env:"EMAIL_ONE_TIME_ACCESS_AS_ADMIN_ENABLED" type:"bool" public:"true"`
|
|
EmailApiKeyExpirationEnabled AppConfigValue `json:"emailApiKeyExpirationEnabled" env:"EMAIL_API_KEY_EXPIRATION_ENABLED" type:"bool"`
|
|
EmailVerificationEnabled AppConfigValue `json:"emailVerificationEnabled" env:"EMAIL_VERIFICATION_ENABLED" type:"bool" public:"true"`
|
|
// LDAP
|
|
LdapEnabled AppConfigValue `json:"ldapEnabled" env:"LDAP_ENABLED" type:"bool" public:"true"`
|
|
LdapUrl AppConfigValue `json:"ldapUrl" env:"LDAP_URL"`
|
|
LdapBindDn AppConfigValue `json:"ldapBindDn" env:"LDAP_BIND_DN"`
|
|
LdapBindPassword AppConfigValue `json:"ldapBindPassword" env:"LDAP_BIND_PASSWORD" sensitive:"true"`
|
|
LdapBase AppConfigValue `json:"ldapBase" env:"LDAP_BASE"`
|
|
LdapUserSearchFilter AppConfigValue `json:"ldapUserSearchFilter" env:"LDAP_USER_SEARCH_FILTER"`
|
|
LdapUserGroupSearchFilter AppConfigValue `json:"ldapUserGroupSearchFilter" env:"LDAP_USER_GROUP_SEARCH_FILTER"`
|
|
LdapSkipCertVerify AppConfigValue `json:"ldapSkipCertVerify" env:"LDAP_SKIP_CERT_VERIFY" type:"bool"`
|
|
LdapAttributeUserUniqueIdentifier AppConfigValue `json:"ldapAttributeUserUniqueIdentifier" env:"LDAP_ATTRIBUTE_USER_UNIQUE_IDENTIFIER"`
|
|
LdapAttributeUserUsername AppConfigValue `json:"ldapAttributeUserUsername" env:"LDAP_ATTRIBUTE_USER_USERNAME"`
|
|
LdapAttributeUserEmail AppConfigValue `json:"ldapAttributeUserEmail" env:"LDAP_ATTRIBUTE_USER_EMAIL"`
|
|
LdapAttributeUserFirstName AppConfigValue `json:"ldapAttributeUserFirstName" env:"LDAP_ATTRIBUTE_USER_FIRST_NAME"`
|
|
LdapAttributeUserLastName AppConfigValue `json:"ldapAttributeUserLastName" env:"LDAP_ATTRIBUTE_USER_LAST_NAME"`
|
|
LdapAttributeUserDisplayName AppConfigValue `json:"ldapAttributeUserDisplayName" env:"LDAP_ATTRIBUTE_USER_DISPLAY_NAME"`
|
|
LdapAttributeUserProfilePicture AppConfigValue `json:"ldapAttributeUserProfilePicture" env:"LDAP_ATTRIBUTE_USER_PROFILE_PICTURE"`
|
|
LdapAttributeGroupMember AppConfigValue `json:"ldapAttributeGroupMember" env:"LDAP_ATTRIBUTE_GROUP_MEMBER"`
|
|
LdapAttributeGroupUniqueIdentifier AppConfigValue `json:"ldapAttributeGroupUniqueIdentifier" env:"LDAP_ATTRIBUTE_GROUP_UNIQUE_IDENTIFIER"`
|
|
LdapAttributeGroupName AppConfigValue `json:"ldapAttributeGroupName" env:"LDAP_ATTRIBUTE_GROUP_NAME"`
|
|
LdapAdminGroupName AppConfigValue `json:"ldapAdminGroupName" env:"LDAP_ADMIN_GROUP_NAME"`
|
|
LdapSoftDeleteUsers AppConfigValue `json:"ldapSoftDeleteUsers" env:"LDAP_SOFT_DELETE_USERS" type:"bool"`
|
|
// WebAuthn
|
|
WebauthnUserVerification AppConfigValue `json:"webauthnUserVerification" env:"WEBAUTHN_USER_VERIFICATION"`
|
|
WebauthnAllowSyncedPasskeys AppConfigValue `json:"webauthnAllowSyncedPasskeys" env:"WEBAUTHN_ALLOW_SYNCED_PASSKEYS" type:"bool"`
|
|
WebauthnAuthenticatorAttachment AppConfigValue `json:"webauthnAuthenticatorAttachment" env:"WEBAUTHN_AUTHENTICATOR_ATTACHMENT"`
|
|
// OIDC
|
|
CIMDURLAllowlist AppConfigValue `json:"cimdUrlAllowlist" env:"CIMD_URL_ALLOWLIST"` // JSON-encoded array of strings
|
|
}
|
|
|
|
// appConfigEnvName returns the explicit environment variable name for a JSON configuration field
|
|
func appConfigEnvName(jsonName string) (string, bool) {
|
|
modelType := reflect.TypeFor[AppConfigModel]()
|
|
for i := range modelType.NumField() {
|
|
field := modelType.Field(i)
|
|
fieldJSONName, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
|
if fieldJSONName == jsonName {
|
|
envName := field.Tag.Get("env")
|
|
return envName, envName != ""
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// Clone returns a deep copy of the AppConfigModel.
|
|
func (m *AppConfigModel) Clone() *AppConfigModel {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
// All fields are value types (AppConfigValue is a string), so copying the struct is sufficient for a deep copy.
|
|
clone := *m
|
|
return &clone
|
|
}
|
|
|
|
// AppConfigValue holds a value
|
|
type AppConfigValue string
|
|
|
|
// IsTrue returns true if the value is a truthy string, such as "true", "t", "yes", "1", etc.
|
|
func (a AppConfigValue) IsTrue() bool {
|
|
return utils.IsTruthy(string(a))
|
|
}
|
|
|
|
// AsDurationMinutes returns the value as a time.Duration, interpreting the string as a whole number of minutes.
|
|
func (a AppConfigValue) AsDurationMinutes() time.Duration {
|
|
val, err := strconv.Atoi(string(a))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return time.Duration(val) * time.Minute
|
|
}
|
|
|
|
// String implements fmt.Stringer
|
|
func (a AppConfigValue) String() string {
|
|
return string(a)
|
|
}
|
|
|
|
func getDefaultConfig() *AppConfigModel {
|
|
// Values are the default ones
|
|
return &AppConfigModel{
|
|
// General
|
|
AppName: "Pocket ID",
|
|
SessionDuration: "60",
|
|
HomePageURL: "/settings/account",
|
|
EmailsVerified: "false",
|
|
DisableAnimations: "false",
|
|
AllowOwnAccountEdit: "true",
|
|
AllowUserSignups: "disabled",
|
|
SignupDefaultUserGroupIDs: "[]",
|
|
SignupDefaultCustomClaims: "[]",
|
|
AccentColor: "default",
|
|
// Email
|
|
RequireUserEmail: "true",
|
|
SmtpHost: "",
|
|
SmtpPort: "",
|
|
SmtpFrom: "",
|
|
SmtpUser: "",
|
|
SmtpPassword: "",
|
|
SmtpTls: "none",
|
|
SmtpSkipCertVerify: "false",
|
|
EmailLoginNotificationEnabled: "false",
|
|
EmailOneTimeAccessAsUnauthenticatedEnabled: "false",
|
|
EmailOneTimeAccessAsAdminEnabled: "false",
|
|
EmailApiKeyExpirationEnabled: "false",
|
|
EmailVerificationEnabled: "false",
|
|
// LDAP
|
|
LdapEnabled: "false",
|
|
LdapUrl: "",
|
|
LdapBindDn: "",
|
|
LdapBindPassword: "",
|
|
LdapBase: "",
|
|
LdapUserSearchFilter: "(objectClass=person)",
|
|
LdapUserGroupSearchFilter: "(objectClass=groupOfNames)",
|
|
LdapSkipCertVerify: "false",
|
|
LdapAttributeUserUniqueIdentifier: "",
|
|
LdapAttributeUserUsername: "",
|
|
LdapAttributeUserEmail: "",
|
|
LdapAttributeUserFirstName: "",
|
|
LdapAttributeUserLastName: "",
|
|
LdapAttributeUserDisplayName: "cn",
|
|
LdapAttributeUserProfilePicture: "",
|
|
LdapAttributeGroupMember: "member",
|
|
LdapAttributeGroupUniqueIdentifier: "",
|
|
LdapAttributeGroupName: "",
|
|
LdapAdminGroupName: "",
|
|
LdapSoftDeleteUsers: "true",
|
|
// WebAuthn
|
|
WebauthnUserVerification: "required",
|
|
WebauthnAllowSyncedPasskeys: "true",
|
|
WebauthnAuthenticatorAttachment: "any",
|
|
// OIDC
|
|
CIMDURLAllowlist: "[]",
|
|
}
|
|
}
|
|
|
|
// applyDefaults fills empty properties from the default configuration and reports whether the model changed
|
|
func (m *AppConfigModel) applyDefaults() bool {
|
|
defaults := reflect.ValueOf(getDefaultConfig()).Elem()
|
|
values := reflect.ValueOf(m).Elem()
|
|
changed := false
|
|
|
|
for i := range values.NumField() {
|
|
if values.Field(i).String() != "" || defaults.Field(i).String() == "" {
|
|
continue
|
|
}
|
|
|
|
values.Field(i).Set(defaults.Field(i))
|
|
changed = true
|
|
}
|
|
|
|
return changed
|
|
}
|
|
|
|
// Replace updates every configuration property with the values from the input DTO
|
|
// An empty string value resets the corresponding property to its default value
|
|
func (m *AppConfigModel) Replace(input dto.AppConfigUpdateDto) {
|
|
// Collect the values from the input DTO into a map, keyed by the "json" tag
|
|
inRv := reflect.ValueOf(input)
|
|
inRt := inRv.Type()
|
|
values := make(map[string]string, inRt.NumField())
|
|
for i := range inRt.NumField() {
|
|
// Get the value of the json tag, taking only what's before the comma
|
|
key, _, _ := strings.Cut(inRt.Field(i).Tag.Get("json"), ",")
|
|
values[key] = inRv.Field(i).String()
|
|
}
|
|
|
|
// Iterate through all the properties, setting each one from the input
|
|
// Properties that are missing from the input or have an empty value are reset to their default
|
|
defaults := reflect.ValueOf(getDefaultConfig()).Elem()
|
|
rv := reflect.ValueOf(m).Elem()
|
|
rt := rv.Type()
|
|
for i := range rt.NumField() {
|
|
key, _, _ := strings.Cut(rt.Field(i).Tag.Get("json"), ",")
|
|
|
|
value, ok := values[key]
|
|
if !ok || value == "" {
|
|
value = defaults.Field(i).String()
|
|
}
|
|
|
|
rv.Field(i).SetString(value)
|
|
}
|
|
}
|
|
|
|
// Update sets configuration properties from the provided key-value pairs
|
|
// Keys correspond to the "json" tags on the model
|
|
// An empty string value resets the property to its default value
|
|
func (m *AppConfigModel) Update(values map[string]string) error {
|
|
rv := reflect.ValueOf(m).Elem()
|
|
rt := rv.Type()
|
|
defaults := reflect.ValueOf(getDefaultConfig()).Elem()
|
|
|
|
// Iterate through the key-value pairs
|
|
for key, value := range values {
|
|
// Find the field in the struct whose "json" tag matches
|
|
fieldIdx := -1
|
|
for j := range rt.NumField() {
|
|
// Separate the key (before the comma) from any optional attributes after
|
|
tagValue, _, _ := strings.Cut(rt.Field(j).Tag.Get("json"), ",")
|
|
if tagValue == key {
|
|
fieldIdx = j
|
|
break
|
|
}
|
|
}
|
|
if fieldIdx < 0 {
|
|
return AppConfigKeyNotFoundError{field: key}
|
|
}
|
|
|
|
// An empty string means we use the default value for the property
|
|
if value == "" {
|
|
value = defaults.Field(fieldIdx).String()
|
|
}
|
|
|
|
rv.Field(fieldIdx).SetString(value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AppConfigVariable is a single application configuration property, as a key/value pair
|
|
type AppConfigVariable struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// ToAppConfigVariableSlice returns the configuration as a slice of key/value pairs
|
|
// If showAll is false, only properties marked as public are included
|
|
// If redactSensitiveValues is true, sensitive values are redacted when the UI config is disabled
|
|
func (m *AppConfigModel) ToAppConfigVariableSlice(showAll bool, redactSensitiveValues bool) []AppConfigVariable {
|
|
// Iterate through all fields
|
|
cfgValue := reflect.ValueOf(m).Elem()
|
|
cfgType := cfgValue.Type()
|
|
|
|
res := make([]AppConfigVariable, 0, cfgType.NumField())
|
|
for i := range cfgType.NumField() {
|
|
field := cfgType.Field(i)
|
|
|
|
key, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
|
if key == "" {
|
|
continue
|
|
}
|
|
|
|
// If we're only showing public variables and this is not public, skip it
|
|
if !showAll && field.Tag.Get("public") != "true" {
|
|
continue
|
|
}
|
|
|
|
value := cfgValue.Field(i).String()
|
|
|
|
// Redact sensitive values if the value isn't empty, the UI config is disabled, and redactSensitiveValues is true
|
|
if value != "" && common.EnvConfig.UiConfigDisabled && redactSensitiveValues && field.Tag.Get("sensitive") == "true" {
|
|
value = "XXXXXXXXXX"
|
|
}
|
|
|
|
res = append(res, AppConfigVariable{
|
|
Key: key,
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
type AppConfigKeyNotFoundError struct {
|
|
field string
|
|
}
|
|
|
|
func (e AppConfigKeyNotFoundError) Error() string {
|
|
return "cannot find config key '" + e.field + "'"
|
|
}
|
|
|
|
func (e AppConfigKeyNotFoundError) Is(target error) bool {
|
|
// Ignore the field property when checking if an error is of the type AppConfigKeyNotFoundError
|
|
_, ok := errors.AsType[*AppConfigKeyNotFoundError](target)
|
|
return ok
|
|
}
|