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_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
@@ -52,18 +55,50 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
_usernameController = TextEditingController(text: oauth2?.username ?? '');
_passwordController = TextEditingController(text: oauth2?.password ?? '');
_refreshTokenController =
TextEditingController(text: oauth2?.refreshToken ?? '');
TextEditingController(text: oauth2?.refreshToken ?? 'N/A');
_identityTokenController =
TextEditingController(text: oauth2?.identityToken ?? '');
TextEditingController(text: oauth2?.identityToken ?? 'N/A');
_accessTokenController =
TextEditingController(text: oauth2?.accessToken ?? '');
TextEditingController(text: oauth2?.accessToken ?? 'N/A');
_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
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
return ListView(
shrinkWrap: true,
physics: AlwaysScrollableScrollPhysics(),
children: [
Text(
"Grant Type",
@@ -177,9 +212,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
onChanged: (_) => _updateOAuth2(),
),
),
if (_shouldShowField(
OAuth2Field.scope)) // Based on refined list, Scope is always shown
if (_shouldShowField(OAuth2Field.scope))
..._buildFieldWithSpacing(
AuthTextField(
readOnly: widget.readOnly,
@@ -197,6 +230,12 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
onChanged: (_) => _updateOAuth2(),
),
),
..._buildFieldWithSpacing(
ADTextButton(
label: "Clear OAuth2 Session",
onPressed: clearStoredCredentials,
),
),
Divider(),
kVSpacer16,
..._buildFieldWithSpacing(
@@ -204,6 +243,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
readOnly: widget.readOnly,
controller: _refreshTokenController,
hintText: "Refresh Token",
isObscureText: true,
onChanged: (_) => _updateOAuth2(),
),
),
@@ -212,6 +252,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
readOnly: widget.readOnly,
controller: _identityTokenController,
hintText: "Identity Token",
isObscureText: true,
onChanged: (_) => _updateOAuth2(),
),
),
@@ -219,6 +260,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
AuthTextField(
readOnly: widget.readOnly,
controller: _accessTokenController,
isObscureText: true,
hintText: "Access Token",
onChanged: (_) => _updateOAuth2(),
),
@@ -244,6 +286,7 @@ class _OAuth2FieldsState extends State<OAuth2Fields> {
OAuth2Field.refreshToken,
OAuth2Field.identityToken,
OAuth2Field.accessToken,
OAuth2Field.clearSession,
};
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(
grantType: _grantType,
authorizationUrl: _authorizationUrlController.text.trim(),
clientId: _clientIdController.text.trim(),
accessTokenUrl: _accessTokenUrlController.text.trim(),
clientSecret: _clientSecretController.text.trim(),
credentialsFilePath: credentialsFilePath,
codeChallengeMethod: _codeChallengeMethod,
redirectUrl: _redirectUrlController.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
void dispose() {
_authorizationUrlController.dispose();
@@ -332,4 +389,5 @@ enum OAuth2Field {
refreshToken,
identityToken,
accessToken,
clearSession,
}

View File

@@ -34,8 +34,9 @@ Future<String?> getFileDownloadpath(String? name, String? ext) async {
return null;
}
Future<String?> getDocumentsDirectoryFilePath(String name, String? ext) async {
final Directory tempDir = await getApplicationDocumentsDirectory();
Future<String?> getApplicationSupportDirectoryFilePath(
String name, String? ext) async {
final Directory tempDir = await getApplicationSupportDirectory();
name = name;
ext = (ext != null) ? ".$ext" : "";
String path = '${tempDir.path}/$name$ext';
@@ -73,3 +74,28 @@ Future<XFile?> pickFile() async {
XFile? pickedResult = await openFile();
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 {
const factory AuthOAuth2Model({
@Default(OAuth2GrantType.authorizationCode) OAuth2GrantType grantType,
required String authorizationUrl,
required String accessTokenUrl,
required String clientId,
required String clientSecret,
required String credentialsFilePath,
String? redirectUrl,
String? scope,
String? state,
@Default("sha-256") String codeChallengeMethod,
String? codeVerifier,
String? codeChallenge,
String? username,
String? password,
String? refreshToken,
String? identityToken,
String? accessToken,
}) = _AuthOAuth2Model;

View File

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

View File

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

View File

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