mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-12-26 04:48:51 +00:00
Can sign in user
This commit is contained in:
parent
b315b5ad77
commit
23f25e7704
@ -1,3 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:comunic/models/login_tokens.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Accounts credentials helper
|
||||
///
|
||||
/// Stores current account tokens
|
||||
@ -8,7 +13,27 @@ class AccountCredentialsHelper {
|
||||
|
||||
/// Checkout whether current user is signed in or not
|
||||
Future<bool> signedIn() async {
|
||||
return false; // TODO : implement
|
||||
return await get() != null;
|
||||
}
|
||||
|
||||
/// Set new login tokens
|
||||
Future<void> set(LoginTokens tokens) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString("login_tokens", tokens.toString());
|
||||
}
|
||||
|
||||
/// Get current [LoginTokens]. Returns null if none or in case of failure
|
||||
Future<LoginTokens> get() async {
|
||||
try {
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final string = prefs.getString("login_tokens");
|
||||
if(string == null) return null;
|
||||
return LoginTokens.fromJSON(jsonDecode(string));
|
||||
|
||||
} on Exception catch(e){
|
||||
print(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
41
lib/helpers/account_helper.dart
Normal file
41
lib/helpers/account_helper.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:comunic/helpers/account_credentials_helper.dart';
|
||||
import 'package:comunic/helpers/api_helper.dart';
|
||||
import 'package:comunic/models/api_request.dart';
|
||||
import 'package:comunic/models/authentication_details.dart';
|
||||
import 'package:comunic/models/login_tokens.dart';
|
||||
|
||||
/// Account helper
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
enum AuthResult {
|
||||
SUCCESS,
|
||||
TOO_MANY_ATTEMPTS,
|
||||
NETWORK_ERROR,
|
||||
INVALID_CREDENTIALS
|
||||
}
|
||||
|
||||
class AccountHelper {
|
||||
/// Sign in user
|
||||
Future<AuthResult> signIn(AuthenticationDetails auth) async {
|
||||
final request = APIRequest(uri: "account/login");
|
||||
request.addString("userMail", auth.email);
|
||||
request.addString("userPassword", auth.password);
|
||||
|
||||
final response = await APIHelper().exec(request);
|
||||
|
||||
// Handle errors
|
||||
if (response.code == 401)
|
||||
return AuthResult.INVALID_CREDENTIALS;
|
||||
else if (response.code == 429)
|
||||
return AuthResult.TOO_MANY_ATTEMPTS;
|
||||
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
|
||||
|
||||
//Save login tokens
|
||||
final tokensObj = response.getObject()["tokens"];
|
||||
await AccountCredentialsHelper()
|
||||
.set(LoginTokens(tokensObj["token1"], tokensObj["token2"]));
|
||||
|
||||
return AuthResult.SUCCESS;
|
||||
}
|
||||
}
|
68
lib/helpers/api_helper.dart
Normal file
68
lib/helpers/api_helper.dart
Normal file
@ -0,0 +1,68 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:comunic/models/api_request.dart';
|
||||
import 'package:comunic/models/api_response.dart';
|
||||
import 'package:comunic/models/config.dart';
|
||||
|
||||
/// API Helper
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class APIHelper {
|
||||
final _httpClient = HttpClient();
|
||||
|
||||
/// Execute a [request] on the server and returns a [APIResponse]
|
||||
///
|
||||
/// This method should never throw but the response code of the [APIResponse]
|
||||
/// should be verified before accessing response content
|
||||
Future<APIResponse> exec(APIRequest request) async {
|
||||
try {
|
||||
//Add API tokens
|
||||
request.addString("serviceName", config().serviceName);
|
||||
request.addString("serviceToken", config().serviceToken);
|
||||
|
||||
//Add user tokens (if required)
|
||||
if (request.needLogin) throw "Can add user tokens right now !";
|
||||
|
||||
// Prepare request body
|
||||
String requestBody = "";
|
||||
request.args.forEach((key, value) => requestBody +=
|
||||
Uri.encodeQueryComponent(key) +
|
||||
"=" +
|
||||
Uri.encodeQueryComponent(value) +
|
||||
"&");
|
||||
|
||||
List<int> bodyBytes = utf8.encode(requestBody);
|
||||
|
||||
// Determine server URL
|
||||
final path = config().apiServerUri + request.uri;
|
||||
Uri url;
|
||||
if (!config().apiServerSecure)
|
||||
url = Uri.http(config().apiServerName, path);
|
||||
else
|
||||
url = Uri.https(config().apiServerName, path);
|
||||
|
||||
//Connect to server
|
||||
final connection = await _httpClient.postUrl(url);
|
||||
connection.headers.set("Content-Length", bodyBytes.length.toString());
|
||||
connection.headers
|
||||
.set("Content-Type", "application/x-www-form-urlencoded");
|
||||
connection.add(bodyBytes);
|
||||
|
||||
final response = await connection.close();
|
||||
|
||||
if (response.statusCode != HttpStatus.ok)
|
||||
return APIResponse(response.statusCode, null);
|
||||
|
||||
//Save & return response
|
||||
final responseBody = await response.transform(utf8.decoder).join();
|
||||
|
||||
return APIResponse(response.statusCode, responseBody);
|
||||
} on Exception catch (e) {
|
||||
print(e.toString());
|
||||
print("Could not execute a request!");
|
||||
return APIResponse(-1, null);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import 'package:comunic/helpers/account_credentials_helper.dart';
|
||||
import 'package:comunic/ui/login_route.dart';
|
||||
import 'package:comunic/models/config.dart';
|
||||
import 'package:comunic/ui/routes/home_route.dart';
|
||||
import 'package:comunic/ui/routes/login_route.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Main file of the application
|
||||
@ -7,6 +10,16 @@ import 'package:flutter/material.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
void main() {
|
||||
/// Initialize application configuration
|
||||
///
|
||||
// TODO : Adapt to production
|
||||
Config.set(Config(
|
||||
apiServerName: "devweb.local",
|
||||
apiServerUri: "/comunic/api/",
|
||||
apiServerSecure: false,
|
||||
serviceName: "ComunicFlutter",
|
||||
serviceToken: "G9sZCBmb3IgVWJ1bnR1CkNvbW1lbnRbbmVdPeCkieCkrOCkq"));
|
||||
|
||||
runApp(ComunicApplication());
|
||||
}
|
||||
|
||||
@ -15,20 +28,37 @@ class ComunicApplication extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
// We need to know whether the user is signed in or not to continue
|
||||
home: FutureBuilder(
|
||||
future: AccountCredentialsHelper().signedIn(),
|
||||
builder: (b, AsyncSnapshot<bool> c){
|
||||
if(!c.hasData)
|
||||
return CircularProgressIndicator();
|
||||
|
||||
else
|
||||
if(c.data)
|
||||
throw "Not supported yet !";
|
||||
else
|
||||
return LoginRoute();
|
||||
},
|
||||
),
|
||||
home: ComunicApplicationHome(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ComunicApplicationHome extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ComunicApplicationHomeState();
|
||||
}
|
||||
|
||||
class _ComunicApplicationHomeState extends State<ComunicApplicationHome> {
|
||||
bool _signedIn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
AccountCredentialsHelper().signedIn().then((v) {
|
||||
setState(() {
|
||||
_signedIn = v;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_signedIn == null) return buildLoadingPage();
|
||||
|
||||
if (_signedIn)
|
||||
return HomeRoute();
|
||||
else
|
||||
return LoginRoute();
|
||||
}
|
||||
}
|
||||
|
27
lib/models/api_request.dart
Normal file
27
lib/models/api_request.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// API Request model
|
||||
///
|
||||
/// Contains all the information associated to an API request
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class APIRequest {
|
||||
final String uri;
|
||||
final bool needLogin;
|
||||
Map<String, String> args;
|
||||
|
||||
APIRequest({
|
||||
@required this.uri,
|
||||
this.needLogin = false,
|
||||
}) : assert(uri != null),
|
||||
assert(needLogin != null),
|
||||
args = Map();
|
||||
|
||||
void addString(String name, String value) => args[name] = value;
|
||||
|
||||
void addInt(String name, int value) => args[name] = value.toString();
|
||||
|
||||
void addBool(String name, bool value) =>
|
||||
args[name] = value ? "true" : "false";
|
||||
}
|
16
lib/models/api_response.dart
Normal file
16
lib/models/api_response.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// API response
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class APIResponse {
|
||||
final int code;
|
||||
final String content;
|
||||
|
||||
const APIResponse(this.code, this.content) : assert(code != null);
|
||||
|
||||
List<dynamic> getArray() => jsonDecode(this.content);
|
||||
|
||||
Map<String, dynamic> getObject() => jsonDecode(this.content);
|
||||
}
|
14
lib/models/authentication_details.dart
Normal file
14
lib/models/authentication_details.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Authentication details
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class AuthenticationDetails {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
const AuthenticationDetails({@required this.email, @required this.password})
|
||||
: assert(email != null),
|
||||
assert(password != null);
|
||||
}
|
42
lib/models/config.dart
Normal file
42
lib/models/config.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Application configuration model
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
/// Configuration class
|
||||
class Config {
|
||||
final String apiServerName;
|
||||
final String apiServerUri;
|
||||
final bool apiServerSecure;
|
||||
final String serviceName;
|
||||
final String serviceToken;
|
||||
|
||||
const Config(
|
||||
{@required this.apiServerName,
|
||||
@required this.apiServerUri,
|
||||
@required this.apiServerSecure,
|
||||
@required this.serviceName,
|
||||
@required this.serviceToken})
|
||||
: assert(apiServerName != null),
|
||||
assert(apiServerUri != null),
|
||||
assert(apiServerSecure != null),
|
||||
assert(serviceName != null),
|
||||
assert(serviceToken != null);
|
||||
|
||||
/// Get and set static configuration
|
||||
static Config _config;
|
||||
|
||||
static Config get() {
|
||||
return _config;
|
||||
}
|
||||
|
||||
static void set(Config conf) {
|
||||
_config = conf;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current configuration of the application
|
||||
Config config() {
|
||||
return Config.get();
|
||||
}
|
23
lib/models/login_tokens.dart
Normal file
23
lib/models/login_tokens.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Login tokens model
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class LoginTokens {
|
||||
final String tokenOne;
|
||||
final String tokenTwo;
|
||||
|
||||
const LoginTokens(this.tokenOne, this.tokenTwo)
|
||||
: assert(tokenOne != null),
|
||||
assert(tokenTwo != null);
|
||||
|
||||
LoginTokens.fromJSON(Map<String, dynamic> json)
|
||||
: tokenOne = json["token_one"],
|
||||
tokenTwo = json["token_two"];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode({"token_one": tokenOne, "token_two": tokenTwo});
|
||||
}
|
||||
}
|
12
lib/ui/routes/home_route.dart
Normal file
12
lib/ui/routes/home_route.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Main route of the application
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class HomeRoute extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text("Home route");
|
||||
}
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
import 'package:comunic/helpers/account_helper.dart';
|
||||
import 'package:comunic/models/authentication_details.dart';
|
||||
import 'package:comunic/ui/routes/home_route.dart';
|
||||
import 'package:comunic/utils/input_utils.dart';
|
||||
import 'package:comunic/utils/intl_utils.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Login route
|
||||
@ -14,6 +18,7 @@ class LoginRoute extends StatefulWidget {
|
||||
class _LoginRouteState extends State<LoginRoute> {
|
||||
String _currEmail;
|
||||
String _currPassword;
|
||||
AuthResult _authResult;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -35,26 +40,51 @@ class _LoginRouteState extends State<LoginRoute> {
|
||||
}
|
||||
|
||||
/// Call this whenever the user request to perform login
|
||||
void _submitForm() {
|
||||
Future<void> _submitForm(BuildContext context) async {
|
||||
final loginResult = await AccountHelper().signIn(
|
||||
AuthenticationDetails(email: _currEmail, password: _currPassword));
|
||||
|
||||
if (loginResult == AuthResult.SUCCESS)
|
||||
Navigator.pushReplacement(
|
||||
context, MaterialPageRoute(builder: (b) => HomeRoute()));
|
||||
else
|
||||
setState(() {
|
||||
_authResult = loginResult;
|
||||
});
|
||||
}
|
||||
|
||||
// Build error card
|
||||
Widget _buildErrorCard() {
|
||||
if (_authResult == null)
|
||||
return null;
|
||||
|
||||
//Determine the right message
|
||||
final message = (_authResult == AuthResult.INVALID_CREDENTIALS ? tr(
|
||||
"Invalid credentials!") : (_authResult == AuthResult.TOO_MANY_ATTEMPTS
|
||||
? tr("Too many unsuccessfull login attempts! Please try again later...")
|
||||
: tr("A network error occured!")));
|
||||
|
||||
return buildErrorCard(message);
|
||||
}
|
||||
|
||||
/// Build login form
|
||||
Widget _buildLoginForm(){
|
||||
Widget _buildLoginForm() {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(tr("Please sign into your Comunic account: ")),
|
||||
Container(
|
||||
child: _buildErrorCard(),
|
||||
),
|
||||
//Email address
|
||||
TextField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr("Email address"),
|
||||
alignLabelWithHint: true,
|
||||
errorText:
|
||||
_currEmail.length > 0 && !validateEmail(_currEmail)
|
||||
errorText: _currEmail.length > 0 && !validateEmail(_currEmail)
|
||||
? tr("Invalid email address!")
|
||||
: null),
|
||||
onChanged: _emailChanged,
|
||||
@ -72,7 +102,9 @@ class _LoginRouteState extends State<LoginRoute> {
|
||||
|
||||
RaisedButton(
|
||||
child: Text(tr("Sign in")),
|
||||
onPressed: !validateEmail(_currEmail) || _currPassword.length < 3 ? null : _submitForm,
|
||||
onPressed: !validateEmail(_currEmail) || _currPassword.length < 3
|
||||
? null
|
||||
: () => _submitForm(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -83,10 +115,9 @@ class _LoginRouteState extends State<LoginRoute> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Comunic"),
|
||||
),
|
||||
body: _buildLoginForm()
|
||||
);
|
||||
appBar: AppBar(
|
||||
title: Text("Comunic"),
|
||||
),
|
||||
body: _buildLoginForm());
|
||||
}
|
||||
}
|
41
lib/utils/ui_utils.dart
Normal file
41
lib/utils/ui_utils.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// User interface utilities
|
||||
|
||||
/// Build and return a full loading page
|
||||
Widget buildLoadingPage() {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build and return an error card
|
||||
Widget buildErrorCard(String message) {
|
||||
return Card(
|
||||
elevation: 2.0,
|
||||
color: Colors.red,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(color: Colors.white),
|
||||
maxLines: null,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -81,6 +81,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -144,3 +151,4 @@ packages:
|
||||
version: "2.0.8"
|
||||
sdks:
|
||||
dart: ">=2.1.0 <3.0.0"
|
||||
flutter: ">=0.1.4 <2.0.0"
|
||||
|
@ -24,6 +24,9 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^0.1.2
|
||||
|
||||
# Preferences are useful for a lot of things (ex: login tokens)
|
||||
shared_preferences: ^0.5.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
Loading…
Reference in New Issue
Block a user