mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 02:07:00 +08:00
feat: add oauth2 credential file handling
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user