mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="social.myportal.relatica">
|
package="social.myportal.relatica">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@mipmap/ic_launcher_round"
|
||||||
android:label="Relatica">
|
android:label="Relatica">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme"/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2"/>
|
||||||
</application>
|
</application>
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:relatica/models/exec_error.dart';
|
||||||
import 'package:result_monad/src/result_monad_base.dart';
|
import 'package:result_monad/src/result_monad_base.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class BasicCredentials extends ICredentials {
|
class BasicCredentials implements ICredentials {
|
||||||
late final String id;
|
late final String id;
|
||||||
final String username;
|
final String username;
|
||||||
final String password;
|
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:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:relatica/models/auth/basic_credentials.dart';
|
import 'package:relatica/models/auth/basic_credentials.dart';
|
||||||
import 'package:relatica/routes.dart';
|
import 'package:relatica/routes.dart';
|
||||||
import 'package:string_validator/string_validator.dart';
|
import 'package:string_validator/string_validator.dart';
|
||||||
|
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
import '../models/auth/credentials_intf.dart';
|
||||||
|
import '../models/auth/oauth_credentials.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../utils/snackbar_builder.dart';
|
import '../utils/snackbar_builder.dart';
|
||||||
|
|
||||||
|
@ -15,21 +18,57 @@ class SignInScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SignInScreenState extends State<SignInScreen> {
|
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 formKey = GlobalKey<FormState>();
|
||||||
final usernameController = TextEditingController();
|
final usernameController = TextEditingController();
|
||||||
final serverNameController = TextEditingController();
|
final serverNameController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
|
var authType = oauthType;
|
||||||
var hidePassword = true;
|
var hidePassword = true;
|
||||||
|
var showUsernameAndPasswordFields = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final service = getIt<AccountsService>();
|
final service = getIt<AccountsService>();
|
||||||
if (service.loggedIn) {
|
if (service.loggedIn) {
|
||||||
usernameController.text = service.currentProfile.username;
|
setCredentials(null, service.currentProfile.credentials);
|
||||||
passwordController.text =
|
}
|
||||||
(service.currentProfile.credentials as BasicCredentials).password;
|
}
|
||||||
serverNameController.text = service.currentProfile.serverName;
|
|
||||||
|
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,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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(
|
TextFormField(
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
controller: serverNameController,
|
controller: serverNameController,
|
||||||
|
@ -69,64 +129,66 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
TextFormField(
|
if (showUsernameAndPasswordFields) ...[
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
TextFormField(
|
||||||
controller: usernameController,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
keyboardType: TextInputType.emailAddress,
|
controller: usernameController,
|
||||||
validator: (value) {
|
keyboardType: TextInputType.emailAddress,
|
||||||
if (value == null) {
|
validator: (value) {
|
||||||
return null;
|
if (value == null) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (value.contains('@')) {
|
if (value.contains('@')) {
|
||||||
return isEmail(value ?? '')
|
return isEmail(value)
|
||||||
|
? null
|
||||||
|
: 'Not a valid Friendica Account Address';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAlphanumeric(value.replaceAll('-', ''))
|
||||||
? null
|
? null
|
||||||
: 'Not a valid Friendica Account Address';
|
: 'Username should be alpha-numeric';
|
||||||
}
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
return isAlphanumeric(value.replaceAll('-', ''))
|
prefixIcon: const Icon(Icons.alternate_email),
|
||||||
? null
|
hintText: 'Username (user@example.com)',
|
||||||
: 'Username should be alpha-numeric';
|
border: OutlineInputBorder(
|
||||||
},
|
borderSide: BorderSide(
|
||||||
decoration: InputDecoration(
|
color: Theme.of(context).backgroundColor,
|
||||||
prefixIcon: const Icon(Icons.alternate_email),
|
),
|
||||||
hintText: 'Username (user@example.com)',
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).backgroundColor,
|
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
labelText: 'Username',
|
||||||
),
|
),
|
||||||
labelText: 'Username',
|
|
||||||
),
|
),
|
||||||
),
|
const VerticalPadding(),
|
||||||
const VerticalPadding(),
|
TextFormField(
|
||||||
TextFormField(
|
obscureText: hidePassword,
|
||||||
obscureText: hidePassword,
|
controller: passwordController,
|
||||||
controller: passwordController,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
prefixIcon: const Icon(Icons.password),
|
||||||
prefixIcon: const Icon(Icons.password),
|
suffixIcon: IconButton(
|
||||||
suffixIcon: IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
setState(() {
|
||||||
setState(() {
|
hidePassword = !hidePassword;
|
||||||
hidePassword = !hidePassword;
|
});
|
||||||
});
|
},
|
||||||
},
|
icon: hidePassword
|
||||||
icon: hidePassword
|
? const Icon(Icons.remove_red_eye_outlined)
|
||||||
? const Icon(Icons.remove_red_eye_outlined)
|
: const Icon(Icons.remove_red_eye),
|
||||||
: const Icon(Icons.remove_red_eye),
|
|
||||||
),
|
|
||||||
hintText: 'Password',
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).backgroundColor,
|
|
||||||
),
|
),
|
||||||
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(
|
ElevatedButton(
|
||||||
onPressed: () => _signIn(context),
|
onPressed: () => _signIn(context),
|
||||||
child: const Text('Signin'),
|
child: const Text('Signin'),
|
||||||
|
@ -140,12 +202,8 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
final p = loggedOutProfiles[index];
|
final p = loggedOutProfiles[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setCredentials(context, p.credentials);
|
||||||
serverNameController.text = p.serverName;
|
setState(() {});
|
||||||
usernameController.text = p.username;
|
|
||||||
passwordController.text =
|
|
||||||
(p.credentials as BasicCredentials).password;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
title: Text(p.handle),
|
title: Text(p.handle),
|
||||||
subtitle: Text(p.id),
|
subtitle: Text(p.id),
|
||||||
|
@ -167,7 +225,10 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
: false;
|
: false;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
setCredentials(context, p.credentials);
|
||||||
|
setState(() {});
|
||||||
await service.setActiveProfile(p);
|
await service.setActiveProfile(p);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
clearCaches();
|
clearCaches();
|
||||||
context.goNamed(ScreenPaths.timelines);
|
context.goNamed(ScreenPaths.timelines);
|
||||||
|
@ -196,10 +257,25 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
|
|
||||||
void _signIn(BuildContext context) async {
|
void _signIn(BuildContext context) async {
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
if (formKey.currentState?.validate() ?? false) {
|
||||||
final creds = BasicCredentials(
|
ICredentials? creds;
|
||||||
username: usernameController.text,
|
switch (authType) {
|
||||||
password: passwordController.text,
|
case usernamePasswordType:
|
||||||
serverName: serverNameController.text);
|
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);
|
final result = await getIt<AccountsService>().signIn(creds);
|
||||||
if (!mounted) {
|
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 {
|
Future clearActiveProfile({bool withNotification = true}) async {
|
||||||
_currentProfile = null;
|
_currentProfile = null;
|
||||||
if (withNotification) {
|
if (withNotification) {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.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 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../models/auth/basic_credentials.dart';
|
import '../models/auth/basic_credentials.dart';
|
||||||
|
import '../models/auth/oauth_credentials.dart';
|
||||||
import '../models/auth/profile.dart';
|
import '../models/auth/profile.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
|
|
||||||
|
@ -33,10 +34,10 @@ class SecretsService {
|
||||||
await _secureStorage.delete(key: _basicProfilesKey);
|
await _secureStorage.delete(key: _basicProfilesKey);
|
||||||
await _secureStorage.delete(key: _oauthProfilesKey);
|
await _secureStorage.delete(key: _oauthProfilesKey);
|
||||||
return Result.ok(profiles);
|
return Result.ok(profiles);
|
||||||
} on PlatformException catch (e) {
|
} catch (e) {
|
||||||
return Result.error(ExecError(
|
return Result.error(ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
message: e.message ?? '',
|
message: e.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,10 +48,10 @@ class SecretsService {
|
||||||
_cachedProfiles.remove(profile);
|
_cachedProfiles.remove(profile);
|
||||||
_cachedProfiles.add(profile);
|
_cachedProfiles.add(profile);
|
||||||
return await saveCredentials();
|
return await saveCredentials();
|
||||||
} on PlatformException catch (e) {
|
} catch (e) {
|
||||||
return Result.error(ExecError(
|
return Result.error(ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
message: e.message ?? '',
|
message: e.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,48 +60,62 @@ class SecretsService {
|
||||||
try {
|
try {
|
||||||
_cachedProfiles.remove(profile);
|
_cachedProfiles.remove(profile);
|
||||||
return await saveCredentials();
|
return await saveCredentials();
|
||||||
} on PlatformException catch (e) {
|
} catch (e) {
|
||||||
return Result.error(ExecError(
|
return Result.error(ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
message: e.message ?? '',
|
message: e.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<Profile>, ExecError> loadProfiles() async {
|
FutureResult<List<Profile>, ExecError> loadProfiles() async {
|
||||||
try {
|
try {
|
||||||
final basicJson = await _secureStorage.read(key: _basicProfilesKey);
|
await _loadJson(_basicProfilesKey, BasicCredentials.fromJson);
|
||||||
if (basicJson == null) {
|
await _loadJson(_oauthProfilesKey, OAuthCredentials.fromJson);
|
||||||
return Result.ok(profiles);
|
|
||||||
}
|
|
||||||
final basicCreds = (jsonDecode(basicJson) as List<dynamic>)
|
|
||||||
.map((json) => Profile.fromJson(json, BasicCredentials.fromJson))
|
|
||||||
.toList();
|
|
||||||
_cachedProfiles.addAll(basicCreds);
|
|
||||||
return Result.ok(profiles);
|
return Result.ok(profiles);
|
||||||
} on PlatformException catch (e) {
|
} catch (e) {
|
||||||
return Result.error(ExecError(
|
return Result.error(ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
message: e.message ?? '',
|
message: e.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<Profile>, ExecError> saveCredentials() async {
|
FutureResult<List<Profile>, ExecError> saveCredentials() async {
|
||||||
try {
|
try {
|
||||||
final basicCredsJson = _cachedProfiles
|
await _saveJson<BasicCredentials>(_basicProfilesKey);
|
||||||
.where((p) => p.credentials is BasicCredentials)
|
await _saveJson<OAuthCredentials>(_oauthProfilesKey);
|
||||||
.map((p) => p.toJson())
|
|
||||||
.toList();
|
|
||||||
final basicCredsString = jsonEncode(basicCredsJson);
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: _basicProfilesKey, value: basicCredsString);
|
|
||||||
return Result.ok(profiles);
|
return Result.ok(profiles);
|
||||||
} on PlatformException catch (e) {
|
} catch (e) {
|
||||||
return Result.error(ExecError(
|
return Result.error(ExecError(
|
||||||
type: ErrorType.localError,
|
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 desktop_window
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
|
import flutter_web_auth_2
|
||||||
import objectbox_flutter_libs
|
import objectbox_flutter_libs
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
|
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
|
||||||
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|
17
pubspec.lock
17
pubspec.lock
|
@ -427,6 +427,23 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
|
@ -17,6 +17,11 @@ dependencies:
|
||||||
flutter_dotenv: ^5.0.2
|
flutter_dotenv: ^5.0.2
|
||||||
flutter_file_dialog: ^2.3.2
|
flutter_file_dialog: ^2.3.2
|
||||||
flutter_secure_storage: ^7.0.1
|
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
|
flutter_widget_from_html_core: ^0.9.0
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
get_it_mixin: ^3.1.4
|
get_it_mixin: ^3.1.4
|
||||||
|
|
Loading…
Reference in a new issue