1
0
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:
Pierre HUBERT 2019-04-22 19:16:26 +02:00
parent b315b5ad77
commit 23f25e7704
14 changed files with 407 additions and 26 deletions

View File

@ -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;
}
}
}

View 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;
}
}

View 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);
}
}
}

View File

@ -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();
}
}

View 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";
}

View 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);
}

View 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
View 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();
}

View 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});
}
}

View 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");
}
}

View File

@ -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
View 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,
),
)
],
),
),
);
}

View File

@ -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"

View File

@ -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