mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 12:23:31 +00:00
Initial wiring in of OAuth
This commit is contained in:
parent
af5d0728ce
commit
04ed08bebb
9 changed files with 425 additions and 112 deletions
|
@ -1,43 +1,53 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="social.myportal.relatica">
|
||||
package="social.myportal.relatica">
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="Relatica">
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="Relatica">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="flutter_web_auth_2">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="relatica"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2"/>
|
||||
</application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
</manifest>
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:relatica/models/exec_error.dart';
|
|||
import 'package:result_monad/src/result_monad_base.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class BasicCredentials extends ICredentials {
|
||||
class BasicCredentials implements ICredentials {
|
||||
late final String id;
|
||||
final String username;
|
||||
final String password;
|
||||
|
|
168
lib/models/auth/oauth_credentials.dart
Normal file
168
lib/models/auth/oauth_credentials.dart
Normal file
|
@ -0,0 +1,168 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:relatica/models/auth/credentials_intf.dart';
|
||||
import 'package:relatica/models/exec_error.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class OAuthCredentials implements ICredentials {
|
||||
static final _logger = Logger('$OAuthCredentials');
|
||||
String clientId;
|
||||
String clientSecret;
|
||||
final String redirectUrl;
|
||||
final String redirectScheme;
|
||||
String accessToken;
|
||||
|
||||
@override
|
||||
final String serverName;
|
||||
|
||||
@override
|
||||
final String id;
|
||||
|
||||
OAuthCredentials({
|
||||
required this.clientId,
|
||||
required this.clientSecret,
|
||||
required this.redirectUrl,
|
||||
required this.redirectScheme,
|
||||
required this.accessToken,
|
||||
required this.serverName,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
factory OAuthCredentials.bootstrap(String serverName) {
|
||||
final redirectScheme = Platform.isWindows || Platform.isLinux
|
||||
? 'http://localhost:43824'
|
||||
: 'relatica';
|
||||
final redirectUrl = Platform.isWindows || Platform.isLinux
|
||||
? redirectScheme
|
||||
: '$redirectScheme:/';
|
||||
return OAuthCredentials(
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
redirectUrl: redirectUrl,
|
||||
redirectScheme: redirectScheme,
|
||||
accessToken: '',
|
||||
serverName: serverName,
|
||||
id: const Uuid().v4(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get authHeaderValue => 'Bearer $accessToken';
|
||||
|
||||
@override
|
||||
FutureResult<ICredentials, ExecError> signIn() async {
|
||||
final result = await _getIds()
|
||||
.andThenAsync((_) async => await _login())
|
||||
.andThenSuccessAsync((_) async => this);
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'clientId': clientId,
|
||||
'clientSecret': clientSecret,
|
||||
'redirectUrl': redirectUrl,
|
||||
'redirectScheme': redirectScheme,
|
||||
'accessToken': accessToken,
|
||||
'serverName': serverName,
|
||||
'id': id,
|
||||
};
|
||||
|
||||
static OAuthCredentials fromJson(Map<String, dynamic> json) =>
|
||||
OAuthCredentials(
|
||||
clientId: json['clientId'],
|
||||
clientSecret: json['clientSecret'],
|
||||
redirectUrl: json['redirectUrl'],
|
||||
redirectScheme: json['redirectScheme'],
|
||||
accessToken: json['accessToken'],
|
||||
serverName: json['serverName'],
|
||||
id: json['id'],
|
||||
);
|
||||
|
||||
FutureResult<bool, ExecError> _getIds() async {
|
||||
if (clientId.isNotEmpty || clientSecret.isNotEmpty) {
|
||||
_logger.info(
|
||||
'Client ID and Client Secret are already set, skipping ID fetching');
|
||||
return Result.ok(true);
|
||||
}
|
||||
final idEndpoint =
|
||||
Uri.parse('https://friendicadevtest1.myportal.social/api/v1/apps');
|
||||
final response = await http.post(idEndpoint, body: {
|
||||
'client_name': 'Relatica',
|
||||
'redirect_uris': '$redirectUrl',
|
||||
'scopes': 'read write push',
|
||||
'website': 'https://myportal.social',
|
||||
});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_logger.severe('Error: ${response.statusCode}: ${response.body}');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.serverError,
|
||||
message: 'Error: ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
}
|
||||
|
||||
final json = jsonDecode(response.body);
|
||||
clientId = json['client_id'];
|
||||
clientSecret = json['client_secret'];
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> _login() async {
|
||||
if (accessToken.isNotEmpty) {
|
||||
_logger.info('Already have access token, skipping');
|
||||
return Result.ok(true);
|
||||
}
|
||||
// Construct the url
|
||||
final url = Uri.https(serverName, '/oauth/authorize', {
|
||||
'response_type': 'code',
|
||||
'client_id': clientId,
|
||||
'redirect_uri': redirectUrl,
|
||||
'scope': 'read write push',
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await FlutterWebAuth2.authenticate(
|
||||
url: url.toString(), callbackUrlScheme: redirectScheme);
|
||||
final code = Uri.parse(result).queryParameters['code'];
|
||||
if (code == null) {
|
||||
_logger.severe(
|
||||
'Error code was not returned with the query parameters: $result');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.serverError,
|
||||
message: 'Error getting the response code during authentication',
|
||||
);
|
||||
}
|
||||
final url2 = Uri.parse('https://$serverName/oauth/token');
|
||||
final body = {
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
'redirect_uri': redirectUrl,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
};
|
||||
final response = await http.post(url2, body: body);
|
||||
if (response.statusCode != 200) {
|
||||
_logger.severe('Error: ${response.statusCode}: ${response.body}');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.serverError,
|
||||
message: 'Error: ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
}
|
||||
accessToken = jsonDecode(response.body)['access_token'];
|
||||
} catch (e) {
|
||||
_logger.severe('Exception while Doing OAuth Process: $e');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.serverError,
|
||||
message: 'Exception while Doing OAuth Process: $e',
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:relatica/models/auth/basic_credentials.dart';
|
||||
import 'package:relatica/routes.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
import '../controls/padding.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/auth/credentials_intf.dart';
|
||||
import '../models/auth/oauth_credentials.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
|
@ -15,21 +18,57 @@ class SignInScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
static final _logger = Logger('$SignInScreen');
|
||||
static const usernamePasswordType = 'Username/Password';
|
||||
static const oauthType = 'OAuth';
|
||||
static final authTypes = [usernamePasswordType, oauthType];
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final usernameController = TextEditingController();
|
||||
final serverNameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
var authType = oauthType;
|
||||
var hidePassword = true;
|
||||
var showUsernameAndPasswordFields = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final service = getIt<AccountsService>();
|
||||
if (service.loggedIn) {
|
||||
usernameController.text = service.currentProfile.username;
|
||||
passwordController.text =
|
||||
(service.currentProfile.credentials as BasicCredentials).password;
|
||||
serverNameController.text = service.currentProfile.serverName;
|
||||
setCredentials(null, service.currentProfile.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
void setBasicCredentials(BasicCredentials credentials) {
|
||||
usernameController.text = credentials.username;
|
||||
passwordController.text = credentials.password;
|
||||
serverNameController.text = credentials.serverName;
|
||||
showUsernameAndPasswordFields = true;
|
||||
authType = usernamePasswordType;
|
||||
}
|
||||
|
||||
void setOauthCredentials(OAuthCredentials credentials) {
|
||||
serverNameController.text = credentials.serverName;
|
||||
showUsernameAndPasswordFields = false;
|
||||
authType = oauthType;
|
||||
}
|
||||
|
||||
void setCredentials(BuildContext? context, ICredentials credentials) {
|
||||
if (credentials is BasicCredentials) {
|
||||
setBasicCredentials(credentials);
|
||||
return;
|
||||
}
|
||||
|
||||
if (credentials is OAuthCredentials) {
|
||||
setOauthCredentials(credentials);
|
||||
return;
|
||||
}
|
||||
|
||||
final msg = 'Unknown credentials type: ${credentials.runtimeType}';
|
||||
_logger.severe(msg);
|
||||
|
||||
if (context?.mounted ?? false) {
|
||||
buildSnackbar(context!, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +90,27 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
DropdownButton<String>(
|
||||
value: authType,
|
||||
items: authTypes
|
||||
.map((a) => DropdownMenuItem(value: a, child: Text(a)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
authType = value ?? '';
|
||||
switch (value) {
|
||||
case usernamePasswordType:
|
||||
showUsernameAndPasswordFields = true;
|
||||
break;
|
||||
case oauthType:
|
||||
showUsernameAndPasswordFields = false;
|
||||
break;
|
||||
default:
|
||||
print("Don't know this");
|
||||
}
|
||||
});
|
||||
}),
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
controller: serverNameController,
|
||||
|
@ -69,64 +129,66 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
controller: usernameController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (showUsernameAndPasswordFields) ...[
|
||||
TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
controller: usernameController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.contains('@')) {
|
||||
return isEmail(value ?? '')
|
||||
if (value.contains('@')) {
|
||||
return isEmail(value)
|
||||
? null
|
||||
: 'Not a valid Friendica Account Address';
|
||||
}
|
||||
|
||||
return isAlphanumeric(value.replaceAll('-', ''))
|
||||
? null
|
||||
: 'Not a valid Friendica Account Address';
|
||||
}
|
||||
|
||||
return isAlphanumeric(value.replaceAll('-', ''))
|
||||
? null
|
||||
: 'Username should be alpha-numeric';
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.alternate_email),
|
||||
hintText: 'Username (user@example.com)',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
: 'Username should be alpha-numeric';
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.alternate_email),
|
||||
hintText: 'Username (user@example.com)',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
labelText: 'Username',
|
||||
),
|
||||
labelText: 'Username',
|
||||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
obscureText: hidePassword,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.password),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
hidePassword = !hidePassword;
|
||||
});
|
||||
},
|
||||
icon: hidePassword
|
||||
? const Icon(Icons.remove_red_eye_outlined)
|
||||
: const Icon(Icons.remove_red_eye),
|
||||
),
|
||||
hintText: 'Password',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
obscureText: hidePassword,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.password),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
hidePassword = !hidePassword;
|
||||
});
|
||||
},
|
||||
icon: hidePassword
|
||||
? const Icon(Icons.remove_red_eye_outlined)
|
||||
: const Icon(Icons.remove_red_eye),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
hintText: 'Password',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
labelText: 'Password',
|
||||
),
|
||||
labelText: 'Password',
|
||||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
const VerticalPadding(),
|
||||
],
|
||||
ElevatedButton(
|
||||
onPressed: () => _signIn(context),
|
||||
child: const Text('Signin'),
|
||||
|
@ -140,12 +202,8 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
final p = loggedOutProfiles[index];
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
serverNameController.text = p.serverName;
|
||||
usernameController.text = p.username;
|
||||
passwordController.text =
|
||||
(p.credentials as BasicCredentials).password;
|
||||
});
|
||||
setCredentials(context, p.credentials);
|
||||
setState(() {});
|
||||
},
|
||||
title: Text(p.handle),
|
||||
subtitle: Text(p.id),
|
||||
|
@ -167,7 +225,10 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
: false;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
setCredentials(context, p.credentials);
|
||||
setState(() {});
|
||||
await service.setActiveProfile(p);
|
||||
|
||||
if (mounted) {
|
||||
clearCaches();
|
||||
context.goNamed(ScreenPaths.timelines);
|
||||
|
@ -196,10 +257,25 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
|
||||
void _signIn(BuildContext context) async {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
final creds = BasicCredentials(
|
||||
username: usernameController.text,
|
||||
password: passwordController.text,
|
||||
serverName: serverNameController.text);
|
||||
ICredentials? creds;
|
||||
switch (authType) {
|
||||
case usernamePasswordType:
|
||||
creds = BasicCredentials(
|
||||
username: usernameController.text,
|
||||
password: passwordController.text,
|
||||
serverName: serverNameController.text);
|
||||
break;
|
||||
case oauthType:
|
||||
creds = OAuthCredentials.bootstrap(serverNameController.text);
|
||||
break;
|
||||
default:
|
||||
buildSnackbar(context, 'Unknown authorization type: $authType');
|
||||
break;
|
||||
}
|
||||
|
||||
if (creds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await getIt<AccountsService>().signIn(creds);
|
||||
if (!mounted) {
|
||||
|
|
|
@ -119,6 +119,26 @@ class AccountsService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
Future removeProfile(Profile profile, {bool withNotification = true}) async {
|
||||
if (_currentProfile == profile) {
|
||||
await clearActiveProfile(withNotification: withNotification);
|
||||
}
|
||||
_loggedInProfiles.remove(profile);
|
||||
_loggedOutProfiles.remove(profile);
|
||||
await secretsService.removeProfile(profile);
|
||||
|
||||
if (_loggedInProfiles.isNotEmpty) {
|
||||
setActiveProfile(
|
||||
_loggedInProfiles.first,
|
||||
withNotification: withNotification,
|
||||
);
|
||||
}
|
||||
|
||||
if (withNotification) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future clearActiveProfile({bool withNotification = true}) async {
|
||||
_currentProfile = null;
|
||||
if (withNotification) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:relatica/models/auth/credentials_intf.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../models/auth/basic_credentials.dart';
|
||||
import '../models/auth/oauth_credentials.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/exec_error.dart';
|
||||
|
||||
|
@ -33,10 +34,10 @@ class SecretsService {
|
|||
await _secureStorage.delete(key: _basicProfilesKey);
|
||||
await _secureStorage.delete(key: _oauthProfilesKey);
|
||||
return Result.ok(profiles);
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: e.message ?? '',
|
||||
message: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -47,10 +48,10 @@ class SecretsService {
|
|||
_cachedProfiles.remove(profile);
|
||||
_cachedProfiles.add(profile);
|
||||
return await saveCredentials();
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: e.message ?? '',
|
||||
message: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -59,48 +60,62 @@ class SecretsService {
|
|||
try {
|
||||
_cachedProfiles.remove(profile);
|
||||
return await saveCredentials();
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: e.message ?? '',
|
||||
message: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
FutureResult<List<Profile>, ExecError> loadProfiles() async {
|
||||
try {
|
||||
final basicJson = await _secureStorage.read(key: _basicProfilesKey);
|
||||
if (basicJson == null) {
|
||||
return Result.ok(profiles);
|
||||
}
|
||||
final basicCreds = (jsonDecode(basicJson) as List<dynamic>)
|
||||
.map((json) => Profile.fromJson(json, BasicCredentials.fromJson))
|
||||
.toList();
|
||||
_cachedProfiles.addAll(basicCreds);
|
||||
await _loadJson(_basicProfilesKey, BasicCredentials.fromJson);
|
||||
await _loadJson(_oauthProfilesKey, OAuthCredentials.fromJson);
|
||||
return Result.ok(profiles);
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: e.message ?? '',
|
||||
message: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
FutureResult<List<Profile>, ExecError> saveCredentials() async {
|
||||
try {
|
||||
final basicCredsJson = _cachedProfiles
|
||||
.where((p) => p.credentials is BasicCredentials)
|
||||
.map((p) => p.toJson())
|
||||
.toList();
|
||||
final basicCredsString = jsonEncode(basicCredsJson);
|
||||
await _secureStorage.write(
|
||||
key: _basicProfilesKey, value: basicCredsString);
|
||||
await _saveJson<BasicCredentials>(_basicProfilesKey);
|
||||
await _saveJson<OAuthCredentials>(_oauthProfilesKey);
|
||||
return Result.ok(profiles);
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: e.message ?? '',
|
||||
message: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadJson(
|
||||
String key,
|
||||
ICredentials Function(Map<String, dynamic>) fromJson,
|
||||
) async {
|
||||
final jsonString = await _secureStorage.read(key: key);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final profiles = (jsonDecode(jsonString) as List<dynamic>)
|
||||
.map((json) => Profile.fromJson(json, fromJson))
|
||||
.toList();
|
||||
_cachedProfiles.addAll(profiles);
|
||||
}
|
||||
|
||||
Future<void> _saveJson<T>(
|
||||
String key,
|
||||
) async {
|
||||
final json = _cachedProfiles
|
||||
.where((p) => p.credentials is T)
|
||||
.map((p) => p.toJson())
|
||||
.toList();
|
||||
final jsonString = jsonEncode(json);
|
||||
await _secureStorage.write(key: key, value: jsonString);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import Foundation
|
|||
import desktop_window
|
||||
import device_info_plus
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_web_auth_2
|
||||
import objectbox_flutter_libs
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
|
@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
|
||||
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
|
17
pubspec.lock
17
pubspec.lock
|
@ -427,6 +427,23 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: flutter_web_auth_2
|
||||
ref: add-linux-support
|
||||
resolved-ref: ce43f48b3b6c0b055e3871eedf1feee1cbe1a2d9
|
||||
url: "https://github.com/HankG/flutter_web_auth_2.git"
|
||||
source: git
|
||||
version: "2.0.4"
|
||||
flutter_web_auth_2_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_web_auth_2_platform_interface
|
||||
sha256: "777df76e0a3b3c3ec2c33bda2fd832d58ba68183644ede0738665399c8810048"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
|
@ -17,6 +17,11 @@ dependencies:
|
|||
flutter_dotenv: ^5.0.2
|
||||
flutter_file_dialog: ^2.3.2
|
||||
flutter_secure_storage: ^7.0.1
|
||||
flutter_web_auth_2:
|
||||
git:
|
||||
url: https://github.com/HankG/flutter_web_auth_2.git
|
||||
path: flutter_web_auth_2
|
||||
ref: add-linux-support
|
||||
flutter_widget_from_html_core: ^0.9.0
|
||||
get_it: ^7.2.0
|
||||
get_it_mixin: ^3.1.4
|
||||
|
|
Loading…
Reference in a new issue