From cc8abee296e27f65c08d274d3f612ab88b11970d Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Sat, 19 Jul 2025 01:20:57 +0530 Subject: [PATCH] feat: add environment variable substitution support for auth models --- lib/providers/collection_providers.dart | 2 +- .../auth/api_key_auth_fields.dart | 31 +++--- .../auth/basic_auth_fields.dart | 63 +++++++------ .../auth/bearer_auth_fields.dart | 16 ++-- .../auth/digest_auth_fields.dart | 90 +++++++++++------- .../common_widgets/auth/jwt_auth_fields.dart | 15 +-- lib/utils/envvar_utils.dart | 81 ++++++++++++++++ lib/widgets/field_auth.dart | 76 ++++----------- test/utils/envvar_utils_test.dart | 94 +++++++++++++++++++ 9 files changed, 318 insertions(+), 150 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 26357574..4f0efc08 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -317,7 +317,7 @@ class CollectionStateNotifier var responseRec = await sendHttpRequest( requestId, apiType, - requestModel.httpRequestModel?.authModel, + substitutedHttpRequestModel.authModel, substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, noSSL: noSSL, diff --git a/lib/screens/common_widgets/auth/api_key_auth_fields.dart b/lib/screens/common_widgets/auth/api_key_auth_fields.dart index edf13de9..895735e7 100644 --- a/lib/screens/common_widgets/auth/api_key_auth_fields.dart +++ b/lib/screens/common_widgets/auth/api_key_auth_fields.dart @@ -20,17 +20,16 @@ class ApiKeyAuthFields extends StatefulWidget { } class _ApiKeyAuthFieldsState extends State { - late TextEditingController _keyController; - late TextEditingController _nameController; + late String _key; + late String _name; late String _addKeyTo; @override void initState() { super.initState(); final apiAuth = widget.authData?.apikey; - _keyController = TextEditingController(text: apiAuth?.key ?? ''); - _nameController = - TextEditingController(text: apiAuth?.name ?? kApiKeyHeaderName); + _key = apiAuth?.key ?? ''; + _name = apiAuth?.name ?? kApiKeyHeaderName; _addKeyTo = apiAuth?.location ?? kAddToDefaultLocation; } @@ -66,20 +65,26 @@ class _ApiKeyAuthFieldsState extends State { }, ), const SizedBox(height: 16), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _nameController, hintText: kHintTextFieldName, - onChanged: (value) => _updateApiKeyAuth(), + initialValue: widget.authData?.apikey?.name, + onChanged: (value) { + _name = value; + _updateApiKeyAuth(); + }, ), const SizedBox(height: 16), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _keyController, title: kLabelApiKey, hintText: kHintTextKey, isObscureText: true, - onChanged: (value) => _updateApiKeyAuth(), + initialValue: widget.authData?.apikey?.key, + onChanged: (value) { + _key = value; + _updateApiKeyAuth(); + }, ), ], ); @@ -87,8 +92,8 @@ class _ApiKeyAuthFieldsState extends State { void _updateApiKeyAuth() { final apiKey = AuthApiKeyModel( - key: _keyController.text.trim(), - name: _nameController.text.trim(), + key: _key.trim(), + name: _name.trim(), location: _addKeyTo, ); widget.updateAuth?.call(widget.authData?.copyWith( diff --git a/lib/screens/common_widgets/auth/basic_auth_fields.dart b/lib/screens/common_widgets/auth/basic_auth_fields.dart index 5be5f1e4..bffdc322 100644 --- a/lib/screens/common_widgets/auth/basic_auth_fields.dart +++ b/lib/screens/common_widgets/auth/basic_auth_fields.dart @@ -3,7 +3,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/widgets/widgets.dart'; import 'consts.dart'; -class BasicAuthFields extends StatelessWidget { +class BasicAuthFields extends StatefulWidget { final AuthModel? authData; final Function(AuthModel?)? updateAuth; final bool readOnly; @@ -16,50 +16,55 @@ class BasicAuthFields extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final usernameController = TextEditingController( - text: authData?.basic?.username ?? '', - ); - final passwordController = TextEditingController( - text: authData?.basic?.password ?? '', - ); + State createState() => _BasicAuthFieldsState(); +} +class _BasicAuthFieldsState extends State { + late String _username; + late String _password; + + @override + void initState() { + super.initState(); + _username = widget.authData?.basic?.username ?? ''; + _password = widget.authData?.basic?.password ?? ''; + } + + @override + Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AuthTextField( - readOnly: readOnly, + EnvAuthField( + readOnly: widget.readOnly, hintText: kHintUsername, - controller: usernameController, - onChanged: (_) => _updateBasicAuth( - usernameController, - passwordController, - ), + initialValue: widget.authData?.basic?.username, + onChanged: (value) { + _username = value; + _updateBasicAuth(); + }, ), const SizedBox(height: 16), - AuthTextField( - readOnly: readOnly, + EnvAuthField( + readOnly: widget.readOnly, hintText: kHintPassword, isObscureText: true, - controller: passwordController, - onChanged: (_) => _updateBasicAuth( - usernameController, - passwordController, - ), + initialValue: widget.authData?.basic?.password, + onChanged: (value) { + _password = value; + _updateBasicAuth(); + }, ), ], ); } - void _updateBasicAuth( - TextEditingController usernameController, - TextEditingController passwordController, - ) { + void _updateBasicAuth() { final basicAuth = AuthBasicAuthModel( - username: usernameController.text.trim(), - password: passwordController.text.trim(), + username: _username.trim(), + password: _password.trim(), ); - updateAuth?.call(authData?.copyWith( + widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.basic, basic: basicAuth, ) ?? diff --git a/lib/screens/common_widgets/auth/bearer_auth_fields.dart b/lib/screens/common_widgets/auth/bearer_auth_fields.dart index 2ab8cdb1..97a4d5ac 100644 --- a/lib/screens/common_widgets/auth/bearer_auth_fields.dart +++ b/lib/screens/common_widgets/auth/bearer_auth_fields.dart @@ -20,29 +20,31 @@ class BearerAuthFields extends StatefulWidget { } class _BearerAuthFieldsState extends State { - late TextEditingController _tokenController; + late String _token; @override void initState() { super.initState(); - final bearerAuth = widget.authData?.bearer; - _tokenController = TextEditingController(text: bearerAuth?.token ?? ''); + _token = widget.authData?.bearer?.token ?? ''; } @override Widget build(BuildContext context) { - return AuthTextField( + return EnvAuthField( readOnly: widget.readOnly, - controller: _tokenController, hintText: kHintToken, isObscureText: true, - onChanged: (value) => _updateBearerAuth(), + initialValue: widget.authData?.bearer?.token, + onChanged: (value) { + _token = value; + _updateBearerAuth(); + }, ); } void _updateBearerAuth() { final bearer = AuthBearerModel( - token: _tokenController.text.trim(), + token: _token.trim(), ); widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.bearer, diff --git a/lib/screens/common_widgets/auth/digest_auth_fields.dart b/lib/screens/common_widgets/auth/digest_auth_fields.dart index 8b060ccc..f3d9479a 100644 --- a/lib/screens/common_widgets/auth/digest_auth_fields.dart +++ b/lib/screens/common_widgets/auth/digest_auth_fields.dart @@ -21,25 +21,25 @@ class DigestAuthFields extends StatefulWidget { } class _DigestAuthFieldsState extends State { - late TextEditingController _usernameController; - late TextEditingController _passwordController; - late TextEditingController _realmController; - late TextEditingController _nonceController; + late String _username; + late String _password; + late String _realm; + late String _nonce; late String _algorithmController; - late TextEditingController _qopController; - late TextEditingController _opaqueController; + late String _qop; + late String _opaque; @override void initState() { super.initState(); final digest = widget.authData?.digest; - _usernameController = TextEditingController(text: digest?.username ?? ''); - _passwordController = TextEditingController(text: digest?.password ?? ''); - _realmController = TextEditingController(text: digest?.realm ?? ''); - _nonceController = TextEditingController(text: digest?.nonce ?? ''); + _username = digest?.username ?? ''; + _password = digest?.password ?? ''; + _realm = digest?.realm ?? ''; + _nonce = digest?.nonce ?? ''; _algorithmController = digest?.algorithm ?? kDigestAlgos[0]; - _qopController = TextEditingController(text: digest?.qop ?? kQop[0]); - _opaqueController = TextEditingController(text: digest?.opaque ?? ''); + _qop = digest?.qop ?? kQop[0]; + _opaque = digest?.opaque ?? ''; } @override @@ -48,37 +48,49 @@ class _DigestAuthFieldsState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _usernameController, hintText: kHintUsername, infoText: kInfoDigestUsername, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.username, + onChanged: (value) { + _username = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _passwordController, hintText: kHintPassword, isObscureText: true, infoText: kInfoDigestPassword, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.password, + onChanged: (value) { + _password = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _realmController, hintText: kHintRealm, infoText: kInfoDigestRealm, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.realm, + onChanged: (value) { + _realm = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _nonceController, hintText: kHintNonce, infoText: kInfoDigestNonce, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.nonce, + onChanged: (value) { + _nonce = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), Text( @@ -106,20 +118,26 @@ class _DigestAuthFieldsState extends State { }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _qopController, hintText: kHintQop, infoText: kInfoDigestQop, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.qop, + onChanged: (value) { + _qop = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _opaqueController, hintText: kHintDataString, infoText: kInfoDigestDataString, - onChanged: (_) => _updateDigestAuth(), + initialValue: widget.authData?.digest?.opaque, + onChanged: (value) { + _opaque = value; + _updateDigestAuth(); + }, ), ], ), @@ -128,13 +146,13 @@ class _DigestAuthFieldsState extends State { void _updateDigestAuth() { final digest = AuthDigestModel( - username: _usernameController.text.trim(), - password: _passwordController.text.trim(), - realm: _realmController.text.trim(), - nonce: _nonceController.text.trim(), + username: _username.trim(), + password: _password.trim(), + realm: _realm.trim(), + nonce: _nonce.trim(), algorithm: _algorithmController.trim(), - qop: _qopController.text.trim(), - opaque: _opaqueController.text.trim(), + qop: _qop.trim(), + opaque: _opaque.trim(), ); widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.digest, diff --git a/lib/screens/common_widgets/auth/jwt_auth_fields.dart b/lib/screens/common_widgets/auth/jwt_auth_fields.dart index bff16157..89312312 100644 --- a/lib/screens/common_widgets/auth/jwt_auth_fields.dart +++ b/lib/screens/common_widgets/auth/jwt_auth_fields.dart @@ -21,7 +21,7 @@ class JwtAuthFields extends StatefulWidget { } class _JwtAuthFieldsState extends State { - late TextEditingController _secretController; + late String _secret; late TextEditingController _privateKeyController; late TextEditingController _payloadController; late String _addTokenTo; @@ -32,7 +32,7 @@ class _JwtAuthFieldsState extends State { void initState() { super.initState(); final jwt = widget.authData?.jwt; - _secretController = TextEditingController(text: jwt?.secret ?? ''); + _secret = jwt?.secret ?? ''; _privateKeyController = TextEditingController(text: jwt?.privateKey ?? ''); _payloadController = TextEditingController(text: jwt?.payload ?? ''); _addTokenTo = jwt?.addTokenTo ?? kAddToDefaultLocation; @@ -96,13 +96,16 @@ class _JwtAuthFieldsState extends State { ), const SizedBox(height: 16), if (_algorithm.startsWith(kStartAlgo)) ...[ - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _secretController, isObscureText: true, hintText: kHintSecret, infoText: kInfoSecret, - onChanged: (value) => _updateJwtAuth(), + initialValue: widget.authData?.jwt?.secret, + onChanged: (value) { + _secret = value; + _updateJwtAuth(); + }, ), const SizedBox(height: 16), CheckboxListTile( @@ -207,7 +210,7 @@ class _JwtAuthFieldsState extends State { void _updateJwtAuth() { final jwt = AuthJwtModel( - secret: _secretController.text.trim(), + secret: _secret.trim(), privateKey: _privateKeyController.text.trim(), payload: _payloadController.text.trim(), addTokenTo: _addTokenTo, diff --git a/lib/utils/envvar_utils.dart b/lib/utils/envvar_utils.dart index c94f6eeb..cb528812 100644 --- a/lib/utils/envvar_utils.dart +++ b/lib/utils/envvar_utils.dart @@ -91,10 +91,91 @@ HttpRequestModel substituteHttpRequestModel( ); }).toList(), body: substituteVariables(httpRequestModel.body, combinedEnvVarMap), + authModel: + _substituteAuthModel(httpRequestModel.authModel, combinedEnvVarMap), ); return newRequestModel; } +AuthModel? _substituteAuthModel( + AuthModel? authModel, Map envVarMap) { + if (authModel == null) return null; + + switch (authModel.type) { + case APIAuthType.basic: + if (authModel.basic != null) { + final basic = authModel.basic!; + return authModel.copyWith( + basic: basic.copyWith( + username: substituteVariables(basic.username, envVarMap) ?? + basic.username, + password: substituteVariables(basic.password, envVarMap) ?? + basic.password, + ), + ); + } + break; + case APIAuthType.bearer: + if (authModel.bearer != null) { + final bearer = authModel.bearer!; + return authModel.copyWith( + bearer: bearer.copyWith( + token: substituteVariables(bearer.token, envVarMap) ?? bearer.token, + ), + ); + } + break; + case APIAuthType.apiKey: + if (authModel.apikey != null) { + final apiKey = authModel.apikey!; + return authModel.copyWith( + apikey: apiKey.copyWith( + key: substituteVariables(apiKey.key, envVarMap) ?? apiKey.key, + name: substituteVariables(apiKey.name, envVarMap) ?? apiKey.name, + ), + ); + } + break; + case APIAuthType.jwt: + if (authModel.jwt != null) { + final jwt = authModel.jwt!; + return authModel.copyWith( + jwt: jwt.copyWith( + secret: substituteVariables(jwt.secret, envVarMap) ?? jwt.secret, + privateKey: substituteVariables(jwt.privateKey, envVarMap) ?? + jwt.privateKey, + payload: substituteVariables(jwt.payload, envVarMap) ?? jwt.payload, + ), + ); + } + break; + case APIAuthType.digest: + if (authModel.digest != null) { + final digest = authModel.digest!; + return authModel.copyWith( + digest: digest.copyWith( + username: substituteVariables(digest.username, envVarMap) ?? + digest.username, + password: substituteVariables(digest.password, envVarMap) ?? + digest.password, + realm: substituteVariables(digest.realm, envVarMap) ?? digest.realm, + nonce: substituteVariables(digest.nonce, envVarMap) ?? digest.nonce, + qop: substituteVariables(digest.qop, envVarMap) ?? digest.qop, + opaque: + substituteVariables(digest.opaque, envVarMap) ?? digest.opaque, + ), + ); + } + break; + case APIAuthType.oauth1: + case APIAuthType.oauth2: + case APIAuthType.none: + break; + } + + return authModel; +} + List? getEnvironmentTriggerSuggestions( String query, Map> envMap, diff --git a/lib/widgets/field_auth.dart b/lib/widgets/field_auth.dart index ef60bec0..39d484fa 100644 --- a/lib/widgets/field_auth.dart +++ b/lib/widgets/field_auth.dart @@ -1,44 +1,33 @@ +import 'dart:math'; +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import '../screens/common_widgets/env_trigger_field.dart'; -class AuthTextField extends StatefulWidget { +class EnvAuthField extends StatefulWidget { final String hintText; final String? title; - final TextEditingController controller; final bool isObscureText; final Function(String)? onChanged; final bool readOnly; final String? infoText; + final String? initialValue; - const AuthTextField( + const EnvAuthField( {super.key, this.title, required this.hintText, - required this.controller, required this.onChanged, this.readOnly = false, this.isObscureText = false, - this.infoText}); + this.infoText, + this.initialValue}); @override - State createState() => _AuthFieldState(); + State createState() => _AuthFieldState(); } -class _AuthFieldState extends State { - late bool _obscureText; - - @override - void initState() { - super.initState(); - _obscureText = widget.isObscureText; - } - - void _toggleVisibility() { - setState(() { - _obscureText = !_obscureText; - }); - } - +class _AuthFieldState extends State { @override Widget build(BuildContext context) { return AutofillGroup( @@ -67,49 +56,20 @@ class _AuthFieldState extends State { ], ), const SizedBox(height: 6), - TextFormField( - readOnly: widget.readOnly, - controller: widget.controller, + EnvironmentTriggerField( + keyId: "auth-${widget.title ?? widget.hintText}-${Random.secure()}", + onChanged: widget.readOnly ? null : widget.onChanged, + initialValue: widget.initialValue, style: kCodeStyle.copyWith( color: Theme.of(context).colorScheme.onSurface, fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width - 80), - contentPadding: kP10, + decoration: getTextFieldInputDecoration( + Theme.of(context).colorScheme, hintText: widget.hintText, - hintStyle: Theme.of(context).textTheme.bodySmall, - suffixIcon: widget.isObscureText - ? IconButton( - icon: Icon( - _obscureText ? Icons.visibility_off : Icons.visibility, - size: 20, - ), - onPressed: _toggleVisibility, - ) - : null, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - ), + isDense: true, + contentPadding: kIsMobile ? kPh6b12 : null, ), - validator: (value) { - if (value!.isEmpty) { - return "${widget.hintText} cannot be empty!"; - } - return null; - }, - obscureText: _obscureText, - onChanged: widget.onChanged, ), ], ), diff --git a/test/utils/envvar_utils_test.dart b/test/utils/envvar_utils_test.dart index 48fc7735..409d2aa1 100644 --- a/test/utils/envvar_utils_test.dart +++ b/test/utils/envvar_utils_test.dart @@ -357,4 +357,98 @@ void main() { expect(getVariableStatus(query, envMap, activeEnvironmentId), expected); }); }); + + group("Testing auth model environment variable substitution", () { + test("Testing basic auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: "{{basic_username}}admin", + password: "{{token}}pass", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "basic_username", value: "testuser"), + EnvironmentVariableModel(key: "token", value: "secret"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.basic?.username, "testuseradmin"); + expect(result.authModel?.basic?.password, "secretpass"); + expect(result.url, "api.apidash.dev/test"); + }); + + test("Testing bearer auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: "{{bearer_token}}", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "bearer_token", value: "secret123"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.bearer?.token, "secret123"); + }); + + test("Testing API key auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: "{{api_key}}", + name: "{{header_name}}", + location: "header", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "api_key", value: "key123"), + EnvironmentVariableModel(key: "header_name", value: "X-API-Key"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.apikey?.key, "key123"); + expect(result.authModel?.apikey?.name, "X-API-Key"); + }); + }); }