feat: add oauth2 credential file handling

This commit is contained in:
Udhay-Adithya
2025-07-21 23:07:34 +05:30
parent 4e924fc946
commit f458d00341
6 changed files with 122 additions and 32 deletions

View File

@@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:apidash/utils/file_utils.dart';
import 'package:apidash/widgets/field_auth.dart'; import 'package:apidash/widgets/field_auth.dart';
import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:apidash_design_system/apidash_design_system.dart';
@@ -52,18 +55,50 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
_usernameController = TextEditingController(text: oauth2?.username ?? ''); _usernameController = TextEditingController(text: oauth2?.username ?? '');
_passwordController = TextEditingController(text: oauth2?.password ?? ''); _passwordController = TextEditingController(text: oauth2?.password ?? '');
_refreshTokenController = _refreshTokenController =
TextEditingController(text: oauth2?.refreshToken ?? ''); TextEditingController(text: oauth2?.refreshToken ?? 'N/A');
_identityTokenController = _identityTokenController =
TextEditingController(text: oauth2?.identityToken ?? ''); TextEditingController(text: oauth2?.identityToken ?? 'N/A');
_accessTokenController = _accessTokenController =
TextEditingController(text: oauth2?.accessToken ?? ''); TextEditingController(text: oauth2?.accessToken ?? 'N/A');
_codeChallengeMethod = oauth2?.codeChallengeMethod ?? 'sha-256'; _codeChallengeMethod = oauth2?.codeChallengeMethod ?? 'sha-256';
// Load credentials from file if available
_loadCredentialsFromFile();
}
Future<void> _loadCredentialsFromFile() async {
final credentialsFilePath = widget.authData?.oauth2?.credentialsFilePath;
if (credentialsFilePath != null && credentialsFilePath.isNotEmpty) {
final credentialsFile = await loadFileFromPath(credentialsFilePath);
if (credentialsFile != null) {
final credentials = await credentialsFile.readAsString();
final Map<String, dynamic> decoded = jsonDecode(credentials);
setState(() {
if (decoded['refreshToken'] != null) {
_refreshTokenController.text = decoded['refreshToken']!;
} else {
_refreshTokenController.text = "N/A";
}
if (decoded['id_token'] != null) {
_identityTokenController.text = decoded['identityToken']!;
} else {
_identityTokenController.text = "N/A";
}
if (decoded['accessToken'] != null) {
_accessTokenController.text = decoded['accessToken']!;
} else {
_accessTokenController.text = "N/A";
}
});
}
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return ListView(
crossAxisAlignment: CrossAxisAlignment.start, shrinkWrap: true,
physics: AlwaysScrollableScrollPhysics(),
children: [ children: [
Text( Text(
"Grant Type", "Grant Type",
@@ -177,9 +212,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
onChanged: (_) => _updateOAuth2(), onChanged: (_) => _updateOAuth2(),
), ),
), ),
if (_shouldShowField( if (_shouldShowField(OAuth2Field.scope))
OAuth2Field.scope)) // Based on refined list, Scope is always shown
..._buildFieldWithSpacing( ..._buildFieldWithSpacing(
AuthTextField( AuthTextField(
readOnly: widget.readOnly, readOnly: widget.readOnly,
@@ -197,6 +230,12 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
onChanged: (_) => _updateOAuth2(), onChanged: (_) => _updateOAuth2(),
), ),
), ),
..._buildFieldWithSpacing(
ADTextButton(
label: "Clear OAuth2 Session",
onPressed: clearStoredCredentials,
),
),
Divider(), Divider(),
kVSpacer16, kVSpacer16,
..._buildFieldWithSpacing( ..._buildFieldWithSpacing(
@@ -204,6 +243,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
readOnly: widget.readOnly, readOnly: widget.readOnly,
controller: _refreshTokenController, controller: _refreshTokenController,
hintText: "Refresh Token", hintText: "Refresh Token",
isObscureText: true,
onChanged: (_) => _updateOAuth2(), onChanged: (_) => _updateOAuth2(),
), ),
), ),
@@ -212,6 +252,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
readOnly: widget.readOnly, readOnly: widget.readOnly,
controller: _identityTokenController, controller: _identityTokenController,
hintText: "Identity Token", hintText: "Identity Token",
isObscureText: true,
onChanged: (_) => _updateOAuth2(), onChanged: (_) => _updateOAuth2(),
), ),
), ),
@@ -219,6 +260,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
AuthTextField( AuthTextField(
readOnly: widget.readOnly, readOnly: widget.readOnly,
controller: _accessTokenController, controller: _accessTokenController,
isObscureText: true,
hintText: "Access Token", hintText: "Access Token",
onChanged: (_) => _updateOAuth2(), onChanged: (_) => _updateOAuth2(),
), ),
@@ -244,6 +286,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
OAuth2Field.refreshToken, OAuth2Field.refreshToken,
OAuth2Field.identityToken, OAuth2Field.identityToken,
OAuth2Field.accessToken, OAuth2Field.accessToken,
OAuth2Field.clearSession,
}; };
if (alwaysShownFields.contains(field)) { if (alwaysShownFields.contains(field)) {
@@ -270,13 +313,20 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
} }
} }
void _updateOAuth2() { void _updateOAuth2() async {
final String? credentialsFilePath =
await getApplicationSupportDirectoryFilePath(
"oauth2_credentials", "json");
if (credentialsFilePath == null) {
return;
}
final updatedOAuth2 = AuthOAuth2Model( final updatedOAuth2 = AuthOAuth2Model(
grantType: _grantType, grantType: _grantType,
authorizationUrl: _authorizationUrlController.text.trim(), authorizationUrl: _authorizationUrlController.text.trim(),
clientId: _clientIdController.text.trim(), clientId: _clientIdController.text.trim(),
accessTokenUrl: _accessTokenUrlController.text.trim(), accessTokenUrl: _accessTokenUrlController.text.trim(),
clientSecret: _clientSecretController.text.trim(), clientSecret: _clientSecretController.text.trim(),
credentialsFilePath: credentialsFilePath,
codeChallengeMethod: _codeChallengeMethod, codeChallengeMethod: _codeChallengeMethod,
redirectUrl: _redirectUrlController.text.trim(), redirectUrl: _redirectUrlController.text.trim(),
scope: _scopeController.text.trim(), scope: _scopeController.text.trim(),
@@ -300,6 +350,13 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
); );
} }
Future<void> clearStoredCredentials() async {
final credentialsFilePath = widget.authData?.oauth2?.credentialsFilePath;
if (credentialsFilePath != null && credentialsFilePath.isNotEmpty) {
await deleteFileFromPath(credentialsFilePath);
}
}
@override @override
void dispose() { void dispose() {
_authorizationUrlController.dispose(); _authorizationUrlController.dispose();
@@ -332,4 +389,5 @@ enum OAuth2Field {
refreshToken, refreshToken,
identityToken, identityToken,
accessToken, accessToken,
clearSession,
} }

View File

@@ -34,8 +34,9 @@ Future<String?> getFileDownloadpath(String? name, String? ext) async {
return null; return null;
} }
Future<String?> getDocumentsDirectoryFilePath(String name, String? ext) async { Future<String?> getApplicationSupportDirectoryFilePath(
final Directory tempDir = await getApplicationDocumentsDirectory(); String name, String? ext) async {
final Directory tempDir = await getApplicationSupportDirectory();
name = name; name = name;
ext = (ext != null) ? ".$ext" : ""; ext = (ext != null) ? ".$ext" : "";
String path = '${tempDir.path}/$name$ext'; String path = '${tempDir.path}/$name$ext';
@@ -73,3 +74,28 @@ Future<XFile?> pickFile() async {
XFile? pickedResult = await openFile(); XFile? pickedResult = await openFile();
return pickedResult; return pickedResult;
} }
Future<File?> loadFileFromPath(String filePath) async {
try {
final file = File(filePath);
if (!await file.exists()) {
return null;
}
return file;
} catch (e) {
return null;
}
}
Future<bool> deleteFileFromPath(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
return true;
}
return false;
} catch (e) {
return false;
}
}

View File

@@ -9,35 +9,21 @@ part 'auth_oauth2_model.freezed.dart';
class AuthOAuth2Model with _$AuthOAuth2Model { class AuthOAuth2Model with _$AuthOAuth2Model {
const factory AuthOAuth2Model({ const factory AuthOAuth2Model({
@Default(OAuth2GrantType.authorizationCode) OAuth2GrantType grantType, @Default(OAuth2GrantType.authorizationCode) OAuth2GrantType grantType,
required String authorizationUrl, required String authorizationUrl,
required String accessTokenUrl, required String accessTokenUrl,
required String clientId, required String clientId,
required String clientSecret, required String clientSecret,
required String credentialsFilePath,
String? redirectUrl, String? redirectUrl,
String? scope, String? scope,
String? state, String? state,
@Default("sha-256") String codeChallengeMethod, @Default("sha-256") String codeChallengeMethod,
String? codeVerifier, String? codeVerifier,
String? codeChallenge, String? codeChallenge,
String? username, String? username,
String? password, String? password,
String? refreshToken, String? refreshToken,
String? identityToken, String? identityToken,
String? accessToken, String? accessToken,
}) = _AuthOAuth2Model; }) = _AuthOAuth2Model;

View File

@@ -26,6 +26,7 @@ mixin _$AuthOAuth2Model {
String get accessTokenUrl => throw _privateConstructorUsedError; String get accessTokenUrl => throw _privateConstructorUsedError;
String get clientId => throw _privateConstructorUsedError; String get clientId => throw _privateConstructorUsedError;
String get clientSecret => throw _privateConstructorUsedError; String get clientSecret => throw _privateConstructorUsedError;
String get credentialsFilePath => throw _privateConstructorUsedError;
String? get redirectUrl => throw _privateConstructorUsedError; String? get redirectUrl => throw _privateConstructorUsedError;
String? get scope => throw _privateConstructorUsedError; String? get scope => throw _privateConstructorUsedError;
String? get state => throw _privateConstructorUsedError; String? get state => throw _privateConstructorUsedError;
@@ -61,6 +62,7 @@ abstract class $AuthOAuth2ModelCopyWith<$Res> {
String accessTokenUrl, String accessTokenUrl,
String clientId, String clientId,
String clientSecret, String clientSecret,
String credentialsFilePath,
String? redirectUrl, String? redirectUrl,
String? scope, String? scope,
String? state, String? state,
@@ -95,6 +97,7 @@ class _$AuthOAuth2ModelCopyWithImpl<$Res, $Val extends AuthOAuth2Model>
Object? accessTokenUrl = null, Object? accessTokenUrl = null,
Object? clientId = null, Object? clientId = null,
Object? clientSecret = null, Object? clientSecret = null,
Object? credentialsFilePath = null,
Object? redirectUrl = freezed, Object? redirectUrl = freezed,
Object? scope = freezed, Object? scope = freezed,
Object? state = freezed, Object? state = freezed,
@@ -129,6 +132,10 @@ class _$AuthOAuth2ModelCopyWithImpl<$Res, $Val extends AuthOAuth2Model>
? _value.clientSecret ? _value.clientSecret
: clientSecret // ignore: cast_nullable_to_non_nullable : clientSecret // ignore: cast_nullable_to_non_nullable
as String, as String,
credentialsFilePath: null == credentialsFilePath
? _value.credentialsFilePath
: credentialsFilePath // ignore: cast_nullable_to_non_nullable
as String,
redirectUrl: freezed == redirectUrl redirectUrl: freezed == redirectUrl
? _value.redirectUrl ? _value.redirectUrl
: redirectUrl // ignore: cast_nullable_to_non_nullable : redirectUrl // ignore: cast_nullable_to_non_nullable
@@ -194,6 +201,7 @@ abstract class _$$AuthOAuth2ModelImplCopyWith<$Res>
String accessTokenUrl, String accessTokenUrl,
String clientId, String clientId,
String clientSecret, String clientSecret,
String credentialsFilePath,
String? redirectUrl, String? redirectUrl,
String? scope, String? scope,
String? state, String? state,
@@ -227,6 +235,7 @@ class __$$AuthOAuth2ModelImplCopyWithImpl<$Res>
Object? accessTokenUrl = null, Object? accessTokenUrl = null,
Object? clientId = null, Object? clientId = null,
Object? clientSecret = null, Object? clientSecret = null,
Object? credentialsFilePath = null,
Object? redirectUrl = freezed, Object? redirectUrl = freezed,
Object? scope = freezed, Object? scope = freezed,
Object? state = freezed, Object? state = freezed,
@@ -261,6 +270,10 @@ class __$$AuthOAuth2ModelImplCopyWithImpl<$Res>
? _value.clientSecret ? _value.clientSecret
: clientSecret // ignore: cast_nullable_to_non_nullable : clientSecret // ignore: cast_nullable_to_non_nullable
as String, as String,
credentialsFilePath: null == credentialsFilePath
? _value.credentialsFilePath
: credentialsFilePath // ignore: cast_nullable_to_non_nullable
as String,
redirectUrl: freezed == redirectUrl redirectUrl: freezed == redirectUrl
? _value.redirectUrl ? _value.redirectUrl
: redirectUrl // ignore: cast_nullable_to_non_nullable : redirectUrl // ignore: cast_nullable_to_non_nullable
@@ -319,6 +332,7 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model {
required this.accessTokenUrl, required this.accessTokenUrl,
required this.clientId, required this.clientId,
required this.clientSecret, required this.clientSecret,
required this.credentialsFilePath,
this.redirectUrl, this.redirectUrl,
this.scope, this.scope,
this.state, this.state,
@@ -347,6 +361,8 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model {
@override @override
final String clientSecret; final String clientSecret;
@override @override
final String credentialsFilePath;
@override
final String? redirectUrl; final String? redirectUrl;
@override @override
final String? scope; final String? scope;
@@ -372,7 +388,7 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model {
@override @override
String toString() { String toString() {
return 'AuthOAuth2Model(grantType: $grantType, authorizationUrl: $authorizationUrl, accessTokenUrl: $accessTokenUrl, clientId: $clientId, clientSecret: $clientSecret, redirectUrl: $redirectUrl, scope: $scope, state: $state, codeChallengeMethod: $codeChallengeMethod, codeVerifier: $codeVerifier, codeChallenge: $codeChallenge, username: $username, password: $password, refreshToken: $refreshToken, identityToken: $identityToken, accessToken: $accessToken)'; return 'AuthOAuth2Model(grantType: $grantType, authorizationUrl: $authorizationUrl, accessTokenUrl: $accessTokenUrl, clientId: $clientId, clientSecret: $clientSecret, credentialsFilePath: $credentialsFilePath, redirectUrl: $redirectUrl, scope: $scope, state: $state, codeChallengeMethod: $codeChallengeMethod, codeVerifier: $codeVerifier, codeChallenge: $codeChallenge, username: $username, password: $password, refreshToken: $refreshToken, identityToken: $identityToken, accessToken: $accessToken)';
} }
@override @override
@@ -390,6 +406,8 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model {
other.clientId == clientId) && other.clientId == clientId) &&
(identical(other.clientSecret, clientSecret) || (identical(other.clientSecret, clientSecret) ||
other.clientSecret == clientSecret) && other.clientSecret == clientSecret) &&
(identical(other.credentialsFilePath, credentialsFilePath) ||
other.credentialsFilePath == credentialsFilePath) &&
(identical(other.redirectUrl, redirectUrl) || (identical(other.redirectUrl, redirectUrl) ||
other.redirectUrl == redirectUrl) && other.redirectUrl == redirectUrl) &&
(identical(other.scope, scope) || other.scope == scope) && (identical(other.scope, scope) || other.scope == scope) &&
@@ -421,6 +439,7 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model {
accessTokenUrl, accessTokenUrl,
clientId, clientId,
clientSecret, clientSecret,
credentialsFilePath,
redirectUrl, redirectUrl,
scope, scope,
state, state,
@@ -458,6 +477,7 @@ abstract class _AuthOAuth2Model implements AuthOAuth2Model {
required final String accessTokenUrl, required final String accessTokenUrl,
required final String clientId, required final String clientId,
required final String clientSecret, required final String clientSecret,
required final String credentialsFilePath,
final String? redirectUrl, final String? redirectUrl,
final String? scope, final String? scope,
final String? state, final String? state,
@@ -485,6 +505,8 @@ abstract class _AuthOAuth2Model implements AuthOAuth2Model {
@override @override
String get clientSecret; String get clientSecret;
@override @override
String get credentialsFilePath;
@override
String? get redirectUrl; String? get redirectUrl;
@override @override
String? get scope; String? get scope;

View File

@@ -16,6 +16,7 @@ _$AuthOAuth2ModelImpl _$$AuthOAuth2ModelImplFromJson(
accessTokenUrl: json['accessTokenUrl'] as String, accessTokenUrl: json['accessTokenUrl'] as String,
clientId: json['clientId'] as String, clientId: json['clientId'] as String,
clientSecret: json['clientSecret'] as String, clientSecret: json['clientSecret'] as String,
credentialsFilePath: json['credentialsFilePath'] as String,
redirectUrl: json['redirectUrl'] as String?, redirectUrl: json['redirectUrl'] as String?,
scope: json['scope'] as String?, scope: json['scope'] as String?,
state: json['state'] as String?, state: json['state'] as String?,
@@ -37,6 +38,7 @@ Map<String, dynamic> _$$AuthOAuth2ModelImplToJson(
'accessTokenUrl': instance.accessTokenUrl, 'accessTokenUrl': instance.accessTokenUrl,
'clientId': instance.clientId, 'clientId': instance.clientId,
'clientSecret': instance.clientSecret, 'clientSecret': instance.clientSecret,
'credentialsFilePath': instance.credentialsFilePath,
'redirectUrl': instance.redirectUrl, 'redirectUrl': instance.redirectUrl,
'scope': instance.scope, 'scope': instance.scope,
'state': instance.state, 'state': instance.state,

View File

@@ -170,11 +170,7 @@ Future<HttpRequestModel> handleAuth(
throw Exception("No Redirect URL found!"); throw Exception("No Redirect URL found!");
} }
//TODO: Create a proper credentials file path, use the existing file utils if needed. final credentialsFile = File(oauth2.credentialsFilePath);
final credentialsDir = Directory.systemTemp;
final credentialsFile = File(
'${credentialsDir.path}/oauth2_credentials.json',
);
switch (oauth2.grantType) { switch (oauth2.grantType) {
case OAuth2GrantType.authorizationCode: case OAuth2GrantType.authorizationCode: