diff --git a/internal/config/config.go b/internal/config/config.go index 23c3832..40c9751 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -53,16 +53,16 @@ type Claims struct { } type OAuthServiceConfig struct { - ClientID string `key:"client-id"` - ClientSecret string `key:"client-secret"` - ClientSecretFile string `key:"client-secret-file"` - Scopes []string `key:"scopes"` - RedirectURL string `key:"redirect-url"` - AuthURL string `key:"auth-url"` - TokenURL string `key:"token-url"` - UserinfoURL string `key:"user-info-url"` - InsecureSkipVerify bool `key:"insecure-skip-verify"` - Name string `key:"name"` + ClientID string `field:"client-id"` + ClientSecret string + ClientSecretFile string + Scopes []string + RedirectURL string `field:"redirect-url"` + AuthURL string `field:"auth-url"` + TokenURL string `field:"token-url"` + UserinfoURL string `field:"user-info-url"` + InsecureSkipVerify bool + Name string } var OverrideProviders = map[string]string{ diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 7d143ac..76044c9 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -147,7 +147,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } } - envProviders, err := decoders.DecodeEnv(envMap) + envProviders, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](envMap, "providers") if err != nil { return nil, err @@ -167,7 +167,7 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st } } - flagProviders, err := decoders.DecodeFlags(flagsMap) + flagProviders, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flagsMap, "providers") if err != nil { return nil, err diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go index 63604b1..a3779c2 100644 --- a/internal/utils/decoders/decoders.go +++ b/internal/utils/decoders/decoders.go @@ -2,30 +2,27 @@ package decoders import ( "reflect" + "regexp" "strings" - "tinyauth/internal/config" + + "golang.org/x/text/cases" + "golang.org/x/text/language" ) -func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string { +func normalizeKeys[T any](input map[string]string, root string, sep string) map[string]string { + knownKeys := getKnownKeys[T]() normalized := make(map[string]string) - knownKeys := getKnownKeys() - for k, v := range keys { - var finalKey []string - var suffix string - var camelClientName string - var camelField string + for k, v := range input { + parts := []string{"tinyauth"} - finalKey = append(finalKey, rootName) - finalKey = append(finalKey, "providers") - lowerKey := strings.ToLower(k) + key := strings.ToLower(k) + key = strings.ReplaceAll(key, sep, "-") - if !strings.HasPrefix(lowerKey, "providers"+sep) { - continue - } + suffix := "" for _, known := range knownKeys { - if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) { + if strings.HasSuffix(key, known) { suffix = known break } @@ -35,55 +32,76 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri continue } - if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" { + parts = append(parts, root) + + id := strings.TrimPrefix(key, root+"-") + id = strings.TrimSuffix(id, "-"+suffix) + + if id == "" { continue } - clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + parts = append(parts, id) + parts = append(parts, suffix) - for i, p := range clientNameParts { + final := "" + + for i, part := range parts { if i == 0 { - camelClientName += p + final += kebabToCamel(part) continue } - if p == "" { - continue - } - camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + final += "." + kebabToCamel(part) } - finalKey = append(finalKey, camelClientName) - - fieldParts := strings.Split(suffix, "-") - - for i, p := range fieldParts { - if i == 0 { - camelField += p - continue - } - if p == "" { - continue - } - camelField += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) - } - - finalKey = append(finalKey, camelField) - normalized[strings.Join(finalKey, ".")] = v + normalized[final] = v } return normalized } -func getKnownKeys() []string { - var known []string +func getKnownKeys[T any]() []string { + var keys []string + var t T - p := config.OAuthServiceConfig{} - v := reflect.ValueOf(p) - typeOfP := v.Type() + v := reflect.ValueOf(t) + typeOfT := v.Type() - for field := range typeOfP.NumField() { - known = append(known, typeOfP.Field(field).Tag.Get("key")) + re := regexp.MustCompile(`[A-Z]`) + + for field := range typeOfT.NumField() { + if typeOfT.Field(field).Tag.Get("field") != "" { + keys = append(keys, typeOfT.Field(field).Tag.Get("field")) + continue + } + + var key string + + for i, char := range typeOfT.Field(field).Name { + if re.MatchString(string(char)) && i != 0 { + key += "-" + strings.ToLower(string(char)) + continue + } + key += strings.ToLower(string(char)) + } + + keys = append(keys, key) } - return known + return keys +} + +func kebabToCamel(input string) string { + res := "" + parts := strings.Split(input, "-") + + for i, part := range parts { + if i == 0 { + res += part + continue + } + res += cases.Title(language.English).String(part) + } + + return res } diff --git a/internal/utils/decoders/decoders_test.go b/internal/utils/decoders/decoders_test.go deleted file mode 100644 index fdec286..0000000 --- a/internal/utils/decoders/decoders_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package decoders_test - -import ( - "testing" - "tinyauth/internal/utils/decoders" - - "gotest.tools/v3/assert" -) - -func TestNormalizeKeys(t *testing.T) { - // Test with env - test := map[string]string{ - "PROVIDERS_CLIENT1_CLIENT_ID": "my-client-id", - "PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret", - "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id", - "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", - "I_LOOK_LIKE_A_KEY_CLIENT_ID": "should-not-appear", - "PROVIDERS_CLIENT_ID": "should-not-appear", - } - expected := map[string]string{ - "tinyauth.providers.client1.clientId": "my-client-id", - "tinyauth.providers.client1.clientSecret": "my-client-secret", - "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", - "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", - } - - normalized := decoders.NormalizeKeys(test, "tinyauth", "_") - assert.DeepEqual(t, normalized, expected) - - // Test with flags (assume -- is already stripped) - test = map[string]string{ - "providers-client1-client-id": "my-client-id", - "providers-client1-client-secret": "my-client-secret", - "providers-my-awesome-client-client-id": "my-awesome-client-id", - "providers-my-awesome-client-client-secret-file": "/path/to/secret", - "providers-should-not-appear-client": "should-not-appear", - "i-look-like-a-key-client-id": "should-not-appear", - "providers-client-id": "should-not-appear", - } - expected = map[string]string{ - "tinyauth.providers.client1.clientId": "my-client-id", - "tinyauth.providers.client1.clientSecret": "my-client-secret", - "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", - "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", - } - - normalized = decoders.NormalizeKeys(test, "tinyauth", "-") - assert.DeepEqual(t, normalized, expected) -} diff --git a/internal/utils/decoders/env_decoder.go b/internal/utils/decoders/env_decoder.go index 4164aa5..532ec64 100644 --- a/internal/utils/decoders/env_decoder.go +++ b/internal/utils/decoders/env_decoder.go @@ -1,20 +1,19 @@ package decoders import ( - "tinyauth/internal/config" - "github.com/traefik/paerser/parser" ) -func DecodeEnv(env map[string]string) (config.Providers, error) { - normalized := NormalizeKeys(env, "tinyauth", "_") - var providers config.Providers +func DecodeEnv[T any, C any](env map[string]string, subName string) (T, error) { + var result T - err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + normalized := normalizeKeys[C](env, subName, "_") + + err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) if err != nil { - return config.Providers{}, err + return result, err } - return providers, nil + return result, nil } diff --git a/internal/utils/decoders/env_decoder_test.go b/internal/utils/decoders/env_decoder_test.go index 2233241..da679f0 100644 --- a/internal/utils/decoders/env_decoder_test.go +++ b/internal/utils/decoders/env_decoder_test.go @@ -9,52 +9,29 @@ import ( ) func TestDecodeEnv(t *testing.T) { - // Variables + // Setup + env := map[string]string{ + "PROVIDERS_GOOGLE_CLIENT_ID": "google-client-id", + "PROVIDERS_GOOGLE_CLIENT_SECRET": "google-client-secret", + "PROVIDERS_MY_GITHUB_CLIENT_ID": "github-client-id", + "PROVIDERS_MY_GITHUB_CLIENT_SECRET": "github-client-secret", + } + expected := config.Providers{ Providers: map[string]config.OAuthServiceConfig{ - "client1": { - ClientID: "client1-id", - ClientSecret: "client1-secret", - Scopes: []string{"client1-scope1", "client1-scope2"}, - RedirectURL: "client1-redirect-url", - AuthURL: "client1-auth-url", - UserinfoURL: "client1-user-info-url", - Name: "Client1", - InsecureSkipVerify: false, + "google": { + ClientID: "google-client-id", + ClientSecret: "google-client-secret", }, - "client2": { - ClientID: "client2-id", - ClientSecret: "client2-secret", - Scopes: []string{"client2-scope1", "client2-scope2"}, - RedirectURL: "client2-redirect-url", - AuthURL: "client2-auth-url", - UserinfoURL: "client2-user-info-url", - Name: "My Awesome Client2", - InsecureSkipVerify: false, + "myGithub": { + ClientID: "github-client-id", + ClientSecret: "github-client-secret", }, }, } - test := map[string]string{ - "PROVIDERS_CLIENT1_CLIENT_ID": "client1-id", - "PROVIDERS_CLIENT1_CLIENT_SECRET": "client1-secret", - "PROVIDERS_CLIENT1_SCOPES": "client1-scope1,client1-scope2", - "PROVIDERS_CLIENT1_REDIRECT_URL": "client1-redirect-url", - "PROVIDERS_CLIENT1_AUTH_URL": "client1-auth-url", - "PROVIDERS_CLIENT1_USER_INFO_URL": "client1-user-info-url", - "PROVIDERS_CLIENT1_NAME": "Client1", - "PROVIDERS_CLIENT1_INSECURE_SKIP_VERIFY": "false", - "PROVIDERS_CLIENT2_CLIENT_ID": "client2-id", - "PROVIDERS_CLIENT2_CLIENT_SECRET": "client2-secret", - "PROVIDERS_CLIENT2_SCOPES": "client2-scope1,client2-scope2", - "PROVIDERS_CLIENT2_REDIRECT_URL": "client2-redirect-url", - "PROVIDERS_CLIENT2_AUTH_URL": "client2-auth-url", - "PROVIDERS_CLIENT2_USER_INFO_URL": "client2-user-info-url", - "PROVIDERS_CLIENT2_NAME": "My Awesome Client2", - "PROVIDERS_CLIENT2_INSECURE_SKIP_VERIFY": "false", - } - // Test - res, err := decoders.DecodeEnv(test) + // Execute + result, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](env, "providers") assert.NilError(t, err) - assert.DeepEqual(t, expected, res) + assert.DeepEqual(t, result, expected) } diff --git a/internal/utils/decoders/flags_decoder.go b/internal/utils/decoders/flags_decoder.go index d973d29..0aae234 100644 --- a/internal/utils/decoders/flags_decoder.go +++ b/internal/utils/decoders/flags_decoder.go @@ -2,23 +2,23 @@ package decoders import ( "strings" - "tinyauth/internal/config" "github.com/traefik/paerser/parser" ) -func DecodeFlags(flags map[string]string) (config.Providers, error) { - filtered := filterFlags(flags) - normalized := NormalizeKeys(filtered, "tinyauth", "-") - var providers config.Providers +func DecodeFlags[T any, C any](flags map[string]string, subName string) (T, error) { + var result T - err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") + filtered := filterFlags(flags) + normalized := normalizeKeys[C](filtered, subName, "_") + + err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) if err != nil { - return config.Providers{}, err + return result, err } - return providers, nil + return result, nil } func filterFlags(flags map[string]string) map[string]string { diff --git a/internal/utils/decoders/flags_decoder_test.go b/internal/utils/decoders/flags_decoder_test.go index 356b4ae..935dea0 100644 --- a/internal/utils/decoders/flags_decoder_test.go +++ b/internal/utils/decoders/flags_decoder_test.go @@ -9,52 +9,29 @@ import ( ) func TestDecodeFlags(t *testing.T) { - // Variables + // Setup + flags := map[string]string{ + "--providers-google-client-id": "google-client-id", + "--providers-google-client-secret": "google-client-secret", + "--providers-my-github-client-id": "github-client-id", + "--providers-my-github-client-secret": "github-client-secret", + } + expected := config.Providers{ Providers: map[string]config.OAuthServiceConfig{ - "client1": { - ClientID: "client1-id", - ClientSecret: "client1-secret", - Scopes: []string{"client1-scope1", "client1-scope2"}, - RedirectURL: "client1-redirect-url", - AuthURL: "client1-auth-url", - UserinfoURL: "client1-user-info-url", - Name: "Client1", - InsecureSkipVerify: false, + "google": { + ClientID: "google-client-id", + ClientSecret: "google-client-secret", }, - "client2": { - ClientID: "client2-id", - ClientSecret: "client2-secret", - Scopes: []string{"client2-scope1", "client2-scope2"}, - RedirectURL: "client2-redirect-url", - AuthURL: "client2-auth-url", - UserinfoURL: "client2-user-info-url", - Name: "My Awesome Client2", - InsecureSkipVerify: false, + "myGithub": { + ClientID: "github-client-id", + ClientSecret: "github-client-secret", }, }, } - test := map[string]string{ - "--providers-client1-client-id": "client1-id", - "--providers-client1-client-secret": "client1-secret", - "--providers-client1-scopes": "client1-scope1,client1-scope2", - "--providers-client1-redirect-url": "client1-redirect-url", - "--providers-client1-auth-url": "client1-auth-url", - "--providers-client1-user-info-url": "client1-user-info-url", - "--providers-client1-name": "Client1", - "--providers-client1-insecure-skip-verify": "false", - "--providers-client2-client-id": "client2-id", - "--providers-client2-client-secret": "client2-secret", - "--providers-client2-scopes": "client2-scope1,client2-scope2", - "--providers-client2-redirect-url": "client2-redirect-url", - "--providers-client2-auth-url": "client2-auth-url", - "--providers-client2-user-info-url": "client2-user-info-url", - "--providers-client2-name": "My Awesome Client2", - "--providers-client2-insecure-skip-verify": "false", - } - // Test - res, err := decoders.DecodeFlags(test) + // Execute + result, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flags, "providers") assert.NilError(t, err) - assert.DeepEqual(t, expected, res) + assert.DeepEqual(t, result, expected) }