mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-29 21:25:43 +00:00 
			
		
		
		
	feat: add env decoder
This commit is contained in:
		
							
								
								
									
										136
									
								
								internal/utils/decoders/env_decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								internal/utils/decoders/env_decoder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | |||||||
|  | package decoders | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"slices" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
|  | 	"github.com/traefik/paerser/parser" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func DecodeEnv(env map[string]string) (config.Providers, error) { | ||||||
|  | 	normalized := normalizeEnv(env, "tinyauth") | ||||||
|  |  | ||||||
|  | 	node, err := decodeEnvsToNode(normalized, "tinyauth", "tinyauth_providers") | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.Providers{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var providers config.Providers | ||||||
|  |  | ||||||
|  | 	metaOpts := parser.MetadataOpts{TagName: "env", AllowSliceAsStruct: true} | ||||||
|  |  | ||||||
|  | 	err = parser.AddMetadata(&providers, node, metaOpts) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.Providers{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = parser.Fill(&providers, node, parser.FillerOpts{AllowSliceAsStruct: true}) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.Providers{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return providers, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeEnvsToNode(env map[string]string, rootName string, filters ...string) (*parser.Node, error) { | ||||||
|  | 	sorted := sortEnvKeys(env, filters) | ||||||
|  |  | ||||||
|  | 	var node *parser.Node | ||||||
|  |  | ||||||
|  | 	for i, k := range sorted { | ||||||
|  | 		split := strings.SplitN(k, "_", 4) | ||||||
|  |  | ||||||
|  | 		if split[0] != rootName { | ||||||
|  | 			return nil, fmt.Errorf("invalid env root %s", split[0]) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if slices.Contains(split, "") { | ||||||
|  | 			return nil, fmt.Errorf("invalid element: %s", k) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if i == 0 { | ||||||
|  | 			node = &parser.Node{} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		decodeEnvToNode(node, split, env[k]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return node, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeEnvToNode(root *parser.Node, path []string, value string) { | ||||||
|  | 	if len(root.Name) == 0 { | ||||||
|  | 		root.Name = path[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !(len(path) > 1) { | ||||||
|  | 		root.Value = value | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if n := containsEnvNode(root.Children, path[1]); n != nil { | ||||||
|  | 		decodeEnvToNode(n, path[1:], value) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	child := &parser.Node{Name: path[1]} | ||||||
|  | 	decodeEnvToNode(child, path[1:], value) | ||||||
|  | 	root.Children = append(root.Children, child) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func containsEnvNode(node []*parser.Node, name string) *parser.Node { | ||||||
|  | 	for _, n := range node { | ||||||
|  | 		if strings.EqualFold(n.Name, name) { | ||||||
|  | 			return n | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func sortEnvKeys(env map[string]string, filters []string) []string { | ||||||
|  | 	var sorted []string | ||||||
|  |  | ||||||
|  | 	for k := range env { | ||||||
|  | 		if len(filters) == 0 { | ||||||
|  | 			sorted = append(sorted, k) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, f := range filters { | ||||||
|  | 			if strings.HasPrefix(k, f) { | ||||||
|  | 				sorted = append(sorted, k) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sort.Strings(sorted) | ||||||
|  | 	return sorted | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // normalizeEnv converts env vars from PROVIDERS_CLIENT1_CLIENT_ID to tinyauth_providers_client_clientId | ||||||
|  | func normalizeEnv(env map[string]string, rootName string) map[string]string { | ||||||
|  | 	n := make(map[string]string) | ||||||
|  | 	for k, v := range env { | ||||||
|  | 		fk := strings.ToLower(k) | ||||||
|  | 		fks := strings.SplitN(fk, "_", 3) | ||||||
|  | 		fkb := "" | ||||||
|  | 		for i, s := range strings.Split(fks[len(fks)-1], "_") { | ||||||
|  | 			if i == 0 { | ||||||
|  | 				fkb += s | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			fkb += utils.Capitalize(s) | ||||||
|  | 		} | ||||||
|  | 		fk = rootName + "_" + strings.Join(fks[:len(fks)-1], "_") + "_" + fkb | ||||||
|  | 		n[fk] = v | ||||||
|  | 	} | ||||||
|  | 	return n | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								internal/utils/decoders/env_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/utils/decoders/env_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | package decoders_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  | 	"tinyauth/internal/utils/decoders" | ||||||
|  |  | ||||||
|  | 	"gotest.tools/v3/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestDecodeEnv(t *testing.T) { | ||||||
|  | 	// Variables | ||||||
|  | 	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, | ||||||
|  | 			}, | ||||||
|  | 			"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, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	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) | ||||||
|  | 	assert.NilError(t, err) | ||||||
|  | 	assert.DeepEqual(t, expected, res) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros