Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/providers/collection_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/terminal/terminal.dart';
import 'package:better_networking/better_networking.dart';
import 'providers.dart';
import '../models/models.dart';
import '../services/services.dart';
Expand Down Expand Up @@ -216,6 +217,7 @@ class CollectionStateNotifier
String? id,
HTTPVerb? method,
AuthModel? authModel,
AuthInheritanceType? authInheritanceType,
String? url,
String? name,
String? description,
Expand Down Expand Up @@ -275,6 +277,7 @@ class CollectionStateNotifier
headers: headers ?? currentHttpRequestModel.headers,
params: params ?? currentHttpRequestModel.params,
authModel: authModel ?? currentHttpRequestModel.authModel,
authInheritanceType: authInheritanceType ?? currentHttpRequestModel.authInheritanceType,
isHeaderEnabledList: isHeaderEnabledList ??
currentHttpRequestModel.isHeaderEnabledList,
isParamEnabledList:
Expand Down Expand Up @@ -628,9 +631,25 @@ class CollectionStateNotifier
HttpRequestModel httpRequestModel) {
var envMap = ref.read(availableEnvironmentVariablesStateProvider);
var activeEnvId = ref.read(activeEnvironmentIdStateProvider);
var environments = ref.read(environmentsStateNotifierProvider);

// Handle auth inheritance
HttpRequestModel processedRequestModel = httpRequestModel;
if (httpRequestModel.authInheritanceType == AuthInheritanceType.environment &&
activeEnvId != null &&
environments != null &&
environments[activeEnvId] != null) {

final environment = environments[activeEnvId]!;
if (environment.defaultAuthModel != null) {
processedRequestModel = httpRequestModel.copyWith(
authModel: environment.defaultAuthModel,
);
}
}
Comment on lines +636 to +649
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth inheritance logic in getSubstitutedHttpRequestModel lacks test coverage. Given the complexity of this feature and that it's a core part of the PR, tests should verify:

  1. Auth is inherited when authInheritanceType is environment and environment has defaultAuthModel
  2. Request-specific auth is used when authInheritanceType is none or null
  3. Behavior when environment doesn't have defaultAuthModel
  4. Behavior when no active environment is selected

Copilot uses AI. Check for mistakes.

return substituteHttpRequestModel(
httpRequestModel,
processedRequestModel,
envMap,
activeEnvId,
);
Expand Down
3 changes: 3 additions & 0 deletions lib/providers/environment_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:apidash/consts.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/file_utils.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:better_networking/better_networking.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/services.dart' show hiveHandler, HiveHandler;

Expand Down Expand Up @@ -126,11 +127,13 @@ class EnvironmentsStateNotifier
String id, {
String? name,
List<EnvironmentVariableModel>? values,
AuthModel? defaultAuthModel,
}) {
final environment = state![id]!;
final updatedEnvironment = environment.copyWith(
name: name ?? environment.name,
values: values ?? environment.values,
defaultAuthModel: defaultAuthModel ?? environment.defaultAuthModel,
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defaultAuthModel update logic has a potential issue. When defaultAuthModel is null in the function arguments but exists in the current environment, it will preserve the existing value due to the ?? operator. However, if you want to explicitly remove the defaultAuthModel (set it to null), this logic prevents that.

Consider using a different pattern to allow explicit null values, such as using an Optional<T> wrapper or checking if the parameter was provided.

Copilot uses AI. Check for mistakes.
);
state = {
...state!,
Expand Down
115 changes: 69 additions & 46 deletions lib/screens/common_widgets/auth/auth_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:better_networking/better_networking.dart';
import 'api_key_auth_fields.dart';
import 'basic_auth_fields.dart';
import 'bearer_auth_fields.dart';
Expand All @@ -12,16 +13,20 @@ import 'oauth2_field.dart';

class AuthPage extends StatelessWidget {
final AuthModel? authModel;
final AuthInheritanceType? authInheritanceType;
final bool readOnly;
final Function(APIAuthType? newType)? onChangedAuthType;
final Function(AuthModel? model)? updateAuthData;
final Function(AuthInheritanceType? newType)? onChangedAuthInheritanceType;

const AuthPage({
super.key,
this.authModel,
this.authInheritanceType,
this.readOnly = false,
this.onChangedAuthType,
this.updateAuthData,
this.onChangedAuthInheritanceType,
});

@override
Expand All @@ -41,61 +46,79 @@ class AuthPage extends StatelessWidget {
SizedBox(
height: 8,
),
ADPopupMenu<APIAuthType>(
value: authModel?.type.displayType,
values: APIAuthType.values
.map((type) => (type, type.displayType))
.toList(),
tooltip: kTooltipSelectAuth,
isOutlined: true,
onChanged: readOnly ? null : onChangedAuthType,
),
const SizedBox(height: 48),
switch (authModel?.type) {
APIAuthType.basic => BasicAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.bearer => BearerAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
// Auth inheritance selection
if (!readOnly) ...[
ADPopupMenu<AuthInheritanceType>(
value: (authInheritanceType ?? AuthInheritanceType.none).displayType,
values: AuthInheritanceType.values
.map((type) => (type, type.displayType))
.toList(),
tooltip: "Select auth inheritance type",
isOutlined: true,
onChanged: onChangedAuthInheritanceType,
),
const SizedBox(height: 16),
Comment on lines 46 to +60
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] These SizedBox widgets should use const for better performance:

const SizedBox(height: 8),

and

const SizedBox(height: 16),

Copilot uses AI. Check for mistakes.
],
Comment on lines +49 to +61
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth inheritance dropdown is shown for all non-readonly scenarios, but it should only be shown in the request auth editor, not in the environment auth editor. Environment-level auth is the source of inherited auth, so showing an inheritance dropdown there doesn't make logical sense.

Consider adding a parameter to conditionally show the inheritance dropdown, or pass a flag to disable it when used in environment context.

Copilot uses AI. Check for mistakes.
// Show auth type selection only when not inheriting
if (authInheritanceType != AuthInheritanceType.environment) ...[
ADPopupMenu<APIAuthType>(
value: authModel?.type.displayType,
values: APIAuthType.values
.map((type) => (type, type.displayType))
.toList(),
tooltip: kTooltipSelectAuth,
isOutlined: true,
onChanged: readOnly ? null : onChangedAuthType,
),
const SizedBox(height: 48),
switch (authModel?.type) {
APIAuthType.basic => BasicAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.bearer => BearerAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.apiKey => ApiKeyAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
APIAuthType.apiKey => ApiKeyAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.jwt => JwtAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
APIAuthType.jwt => JwtAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.digest => DigestAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
APIAuthType.digest => DigestAuthFields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.oauth1 => OAuth1Fields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.oauth2 => OAuth2Fields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
APIAuthType.oauth2 => OAuth2Fields(
readOnly: readOnly,
authData: authModel,
updateAuth: updateAuthData,
),
APIAuthType.none =>
Text(readOnly ? kMsgNoAuth : kMsgNoAuthSelected),
_ => Text(readOnly
? "${authModel?.type.name} $kMsgAuthNotSupported"
: kMsgNotImplemented),
}
APIAuthType.none =>
Text(readOnly ? kMsgNoAuth : kMsgNoAuthSelected),
_ => Text(readOnly
? "${authModel?.type.name} $kMsgAuthNotSupported"
: kMsgNotImplemented),
}
] else ...[
const Text("Using environment inherited authentication"),
]
],
),
),
);
}
}
}
73 changes: 73 additions & 0 deletions lib/screens/envvar/environment_auth_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'package:better_networking/better_networking.dart';
import '../common_widgets/common_widgets.dart';

class EnvironmentAuthEditor extends ConsumerWidget {
const EnvironmentAuthEditor({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final environment = ref.watch(selectedEnvironmentModelProvider);

if (environment == null) {
return const SizedBox.shrink();
}

final defaultAuthModel = environment.defaultAuthModel;

return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Default Authentication",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 16),
const Text(
"Set default authentication for all requests using this environment",
style: TextStyle(
fontSize: 14,
),
),
const SizedBox(height: 24),
AuthPage(
authModel: defaultAuthModel,
readOnly: false,
onChangedAuthType: (newType) {
if (newType != null) {
final updatedAuthModel = defaultAuthModel?.copyWith(type: newType) ??
AuthModel(type: newType);
ref
.read(environmentsStateNotifierProvider.notifier)
.updateEnvironment(
environment.id,
values: environment.values,
defaultAuthModel: updatedAuthModel,
);
}
},
updateAuthData: (model) {
ref
.read(environmentsStateNotifierProvider.notifier)
.updateEnvironment(
environment.id,
values: environment.values,
defaultAuthModel: model,
);
},
),
],
),
);
}
}
52 changes: 37 additions & 15 deletions lib/screens/envvar/environment_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import '../common_widgets/common_widgets.dart';
import './editor_pane/variables_pane.dart';
import 'environment_auth_editor.dart';

class EnvironmentEditor extends ConsumerWidget {
const EnvironmentEditor({super.key});
Expand All @@ -15,6 +16,7 @@ class EnvironmentEditor extends ConsumerWidget {
final id = ref.watch(selectedEnvironmentIdStateProvider);
final name = ref
.watch(selectedEnvironmentModelProvider.select((value) => value?.name));

return Padding(
padding: context.isMediumWindow
? kPb10
Expand Down Expand Up @@ -84,24 +86,44 @@ class EnvironmentEditor extends ConsumerWidget {
borderRadius: kBorderRadius12,
),
elevation: 0,
child: const Padding(
padding: kPv6,
child: DefaultTabController(
length: 2,
child: Column(
children: [
kHSpacer40,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(width: 30),
Text("Variable"),
SizedBox(width: 30),
Text("Value"),
SizedBox(width: 40),
TabBar(
tabs: const [
Tab(text: "Variables"),
Tab(text: "Auth"),
],
),
kHSpacer40,
Divider(),
Expanded(child: EditEnvironmentVariables())
const Expanded(
child: TabBarView(
children: [
Padding(
padding: kPv6,
child: Column(
children: [
kHSpacer40,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(width: 30),
Text("Variable"),
SizedBox(width: 30),
Text("Value"),
SizedBox(width: 40),
Comment on lines +110 to +114
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] These SizedBox widgets should use const for better performance since they have constant dimensions:

const SizedBox(width: 30),
Text("Variable"),
const SizedBox(width: 30),
Text("Value"),
const SizedBox(width: 40),

Note: The Text widgets cannot be const as they are not literal strings in this context.

Copilot uses AI. Check for mistakes.
],
),
kHSpacer40,
Divider(),
Expanded(child: EditEnvironmentVariables())
],
),
),
EnvironmentAuthEditor(),
],
),
),
],
),
),
Expand All @@ -112,4 +134,4 @@ class EnvironmentEditor extends ConsumerWidget {
),
);
}
}
}
Loading