|
@ -2,211 +2,233 @@ import 'dart:typed_data'; |
|
|
import 'dart:convert'; |
|
|
import 'dart:convert'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:http/http.dart' as http; |
|
|
import 'package:http/http.dart' as http; |
|
|
import 'package:shared_preferences/shared_preferences.dart'; |
|
|
|
|
|
|
|
|
|
|
|
import '/utils/encryption/aes_helper.dart'; |
|
|
import '/utils/encryption/aes_helper.dart'; |
|
|
import '/utils/storage/encryption_keys.dart'; |
|
|
|
|
|
import '/utils/encryption/crypto_utils.dart'; |
|
|
import '/utils/encryption/crypto_utils.dart'; |
|
|
|
|
|
|
|
|
class SignupResponse { |
|
|
class SignupResponse { |
|
|
final String status; |
|
|
|
|
|
final String message; |
|
|
|
|
|
|
|
|
|
|
|
const SignupResponse({ |
|
|
|
|
|
required this.status, |
|
|
|
|
|
required this.message, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
factory SignupResponse.fromJson(Map<String, dynamic> json) { |
|
|
|
|
|
return SignupResponse( |
|
|
|
|
|
status: json['status'], |
|
|
|
|
|
message: json['message'], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
final String status; |
|
|
|
|
|
final String message; |
|
|
|
|
|
|
|
|
|
|
|
const SignupResponse({ |
|
|
|
|
|
required this.status, |
|
|
|
|
|
required this.message, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
factory SignupResponse.fromJson(Map<String, dynamic> json) { |
|
|
|
|
|
return SignupResponse( |
|
|
|
|
|
status: json['status'], |
|
|
|
|
|
message: json['message'], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Future<SignupResponse> signUp(context, String username, String password, String confirmPassword) async { |
|
|
Future<SignupResponse> signUp(context, String username, String password, String confirmPassword) async { |
|
|
var keyPair = CryptoUtils.generateRSAKeyPair(); |
|
|
|
|
|
|
|
|
|
|
|
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); |
|
|
|
|
|
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); |
|
|
|
|
|
|
|
|
|
|
|
var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); |
|
|
|
|
|
|
|
|
|
|
|
// TODO: Check for timeout here |
|
|
|
|
|
final resp = await http.post( |
|
|
|
|
|
Uri.parse('http://192.168.1.5:8080/api/v1/signup'), |
|
|
|
|
|
headers: <String, String>{ |
|
|
|
|
|
'Content-Type': 'application/json; charset=UTF-8', |
|
|
|
|
|
}, |
|
|
|
|
|
body: jsonEncode(<String, String>{ |
|
|
|
|
|
'username': username, |
|
|
|
|
|
'password': password, |
|
|
|
|
|
'confirm_password': confirmPassword, |
|
|
|
|
|
'asymmetric_public_key': rsaPubPem, |
|
|
|
|
|
'asymmetric_private_key': encRsaPriv, |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); |
|
|
|
|
|
|
|
|
|
|
|
if (resp.statusCode != 201) { |
|
|
|
|
|
throw Exception(response.message); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return response; |
|
|
|
|
|
|
|
|
var keyPair = CryptoUtils.generateRSAKeyPair(); |
|
|
|
|
|
|
|
|
|
|
|
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); |
|
|
|
|
|
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); |
|
|
|
|
|
|
|
|
|
|
|
var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); |
|
|
|
|
|
|
|
|
|
|
|
// TODO: Check for timeout here |
|
|
|
|
|
final resp = await http.post( |
|
|
|
|
|
Uri.parse('http://192.168.1.5:8080/api/v1/signup'), |
|
|
|
|
|
headers: <String, String>{ |
|
|
|
|
|
'Content-Type': 'application/json; charset=UTF-8', |
|
|
|
|
|
}, |
|
|
|
|
|
body: jsonEncode(<String, String>{ |
|
|
|
|
|
'username': username, |
|
|
|
|
|
'password': password, |
|
|
|
|
|
'confirm_password': confirmPassword, |
|
|
|
|
|
'asymmetric_public_key': rsaPubPem, |
|
|
|
|
|
'asymmetric_private_key': encRsaPriv, |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); |
|
|
|
|
|
|
|
|
|
|
|
if (resp.statusCode != 201) { |
|
|
|
|
|
throw Exception(response.message); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return response; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class Signup extends StatelessWidget { |
|
|
class Signup extends StatelessWidget { |
|
|
const Signup({Key? key}) : super(key: key); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
return Scaffold( |
|
|
|
|
|
backgroundColor: Colors.cyan, |
|
|
|
|
|
appBar: AppBar( |
|
|
|
|
|
title: null, |
|
|
|
|
|
automaticallyImplyLeading: true, |
|
|
|
|
|
//`true` if you want Flutter to automatically add Back Button when needed, |
|
|
|
|
|
//or `false` if you want to force your own back button every where |
|
|
|
|
|
leading: IconButton(icon: const Icon(Icons.arrow_back), |
|
|
|
|
|
//onPressed:() => Navigator.pop(context, false), |
|
|
|
|
|
onPressed:() => { |
|
|
|
|
|
Navigator.pop(context) |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
|
|
|
), |
|
|
|
|
|
body: const SafeArea( |
|
|
|
|
|
child: SignupWidget(), |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const Signup({Key? key}) : super(key: key); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
return Scaffold( |
|
|
|
|
|
appBar: AppBar( |
|
|
|
|
|
title: null, |
|
|
|
|
|
automaticallyImplyLeading: true, |
|
|
|
|
|
//`true` if you want Flutter to automatically add Back Button when needed, |
|
|
|
|
|
//or `false` if you want to force your own back button every where |
|
|
|
|
|
leading: IconButton(icon: const Icon(Icons.arrow_back), |
|
|
|
|
|
onPressed:() => { |
|
|
|
|
|
Navigator.pop(context) |
|
|
|
|
|
} |
|
|
|
|
|
), |
|
|
|
|
|
backgroundColor: Colors.transparent, |
|
|
|
|
|
shadowColor: Colors.transparent, |
|
|
|
|
|
), |
|
|
|
|
|
body: const SafeArea( |
|
|
|
|
|
child: SignupWidget(), |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class SignupWidget extends StatefulWidget { |
|
|
class SignupWidget extends StatefulWidget { |
|
|
const SignupWidget({Key? key}) : super(key: key); |
|
|
|
|
|
|
|
|
const SignupWidget({Key? key}) : super(key: key); |
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
State<SignupWidget> createState() => _SignupWidgetState(); |
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
State<SignupWidget> createState() => _SignupWidgetState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class _SignupWidgetState extends State<SignupWidget> { |
|
|
class _SignupWidgetState extends State<SignupWidget> { |
|
|
final _formKey = GlobalKey<FormState>(); |
|
|
|
|
|
|
|
|
|
|
|
TextEditingController usernameController = TextEditingController(); |
|
|
|
|
|
TextEditingController passwordController = TextEditingController(); |
|
|
|
|
|
TextEditingController passwordConfirmController = TextEditingController(); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black); |
|
|
|
|
|
|
|
|
|
|
|
final ButtonStyle _buttonStyle = ElevatedButton.styleFrom( |
|
|
|
|
|
primary: Colors.white, |
|
|
|
|
|
onPrimary: Colors.cyan, |
|
|
|
|
|
minimumSize: const Size.fromHeight(50), |
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), |
|
|
|
|
|
textStyle: const TextStyle( |
|
|
|
|
|
fontSize: 20, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
color: Colors.red, |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return Center( |
|
|
|
|
|
child: Form( |
|
|
|
|
|
key: _formKey, |
|
|
|
|
|
child: Center( |
|
|
|
|
|
child: Padding( |
|
|
|
|
|
padding: const EdgeInsets.all(15), |
|
|
|
|
|
child: Column( |
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
|
|
|
children: [ |
|
|
|
|
|
const Text('Sign Up', style: TextStyle(fontSize: 35, color: Colors.white),), |
|
|
|
|
|
const SizedBox(height: 30), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: usernameController, |
|
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
|
hintText: 'Username', |
|
|
|
|
|
), |
|
|
|
|
|
style: _inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Create a username'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 5), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: passwordController, |
|
|
|
|
|
obscureText: true, |
|
|
|
|
|
enableSuggestions: false, |
|
|
|
|
|
autocorrect: false, |
|
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
|
hintText: 'Password', |
|
|
|
|
|
), |
|
|
|
|
|
style: _inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Enter a password'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 5), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: passwordConfirmController, |
|
|
|
|
|
obscureText: true, |
|
|
|
|
|
enableSuggestions: false, |
|
|
|
|
|
autocorrect: false, |
|
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
|
hintText: 'Password', |
|
|
|
|
|
), |
|
|
|
|
|
style: _inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Confirm your password'; |
|
|
|
|
|
} |
|
|
|
|
|
if (value != passwordController.text) { |
|
|
|
|
|
return 'Passwords do not match'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 5), |
|
|
|
|
|
ElevatedButton( |
|
|
|
|
|
style: _buttonStyle, |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
if (_formKey.currentState!.validate()) { |
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar( |
|
|
|
|
|
const SnackBar(content: Text('Processing Data')), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
signUp( |
|
|
|
|
|
context, |
|
|
|
|
|
usernameController.text, |
|
|
|
|
|
passwordController.text, |
|
|
|
|
|
passwordConfirmController.text |
|
|
|
|
|
).then((value) { |
|
|
|
|
|
Navigator.of(context).popUntil((route) => route.isFirst); |
|
|
|
|
|
}).catchError((error) { |
|
|
|
|
|
print(error); // TODO: Show error on interface |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
child: const Text('Submit'), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
final _formKey = GlobalKey<FormState>(); |
|
|
|
|
|
|
|
|
|
|
|
TextEditingController usernameController = TextEditingController(); |
|
|
|
|
|
TextEditingController passwordController = TextEditingController(); |
|
|
|
|
|
TextEditingController passwordConfirmController = TextEditingController(); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
const TextStyle inputTextStyle = TextStyle( |
|
|
|
|
|
fontSize: 18, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
final OutlineInputBorder inputBorderStyle = OutlineInputBorder( |
|
|
|
|
|
borderRadius: BorderRadius.circular(5), |
|
|
|
|
|
borderSide: const BorderSide( |
|
|
|
|
|
color: Colors.transparent, |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( |
|
|
|
|
|
primary: Theme.of(context).colorScheme.surface, |
|
|
|
|
|
onPrimary: Theme.of(context).colorScheme.onSurface, |
|
|
|
|
|
minimumSize: const Size.fromHeight(50), |
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), |
|
|
|
|
|
textStyle: TextStyle( |
|
|
|
|
|
fontSize: 20, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
color: Theme.of(context).colorScheme.error, |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return Center( |
|
|
|
|
|
child: Form( |
|
|
|
|
|
key: _formKey, |
|
|
|
|
|
child: Center( |
|
|
|
|
|
child: Padding( |
|
|
|
|
|
padding: const EdgeInsets.only( |
|
|
|
|
|
left: 20, |
|
|
|
|
|
right: 20, |
|
|
|
|
|
top: 0, |
|
|
|
|
|
bottom: 100, |
|
|
|
|
|
), |
|
|
|
|
|
child: Column( |
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
|
|
|
children: [ |
|
|
|
|
|
Text( |
|
|
|
|
|
'Sign Up', |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
fontSize: 35, |
|
|
|
|
|
color: Theme.of(context).colorScheme.onBackground, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 30), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: usernameController, |
|
|
|
|
|
decoration: InputDecoration( |
|
|
|
|
|
hintText: 'Username', |
|
|
|
|
|
enabledBorder: inputBorderStyle, |
|
|
|
|
|
focusedBorder: inputBorderStyle, |
|
|
|
|
|
), |
|
|
|
|
|
style: inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Create a username'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 10), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: passwordController, |
|
|
|
|
|
obscureText: true, |
|
|
|
|
|
enableSuggestions: false, |
|
|
|
|
|
autocorrect: false, |
|
|
|
|
|
decoration: InputDecoration( |
|
|
|
|
|
hintText: 'Password', |
|
|
|
|
|
enabledBorder: inputBorderStyle, |
|
|
|
|
|
focusedBorder: inputBorderStyle, |
|
|
|
|
|
), |
|
|
|
|
|
style: inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Enter a password'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 10), |
|
|
|
|
|
TextFormField( |
|
|
|
|
|
controller: passwordConfirmController, |
|
|
|
|
|
obscureText: true, |
|
|
|
|
|
enableSuggestions: false, |
|
|
|
|
|
autocorrect: false, |
|
|
|
|
|
decoration: InputDecoration( |
|
|
|
|
|
hintText: 'Password', |
|
|
|
|
|
enabledBorder: inputBorderStyle, |
|
|
|
|
|
focusedBorder: inputBorderStyle, |
|
|
|
|
|
), |
|
|
|
|
|
style: inputTextStyle, |
|
|
|
|
|
// The validator receives the text that the user has entered. |
|
|
|
|
|
validator: (value) { |
|
|
|
|
|
if (value == null || value.isEmpty) { |
|
|
|
|
|
return 'Confirm your password'; |
|
|
|
|
|
} |
|
|
|
|
|
if (value != passwordController.text) { |
|
|
|
|
|
return 'Passwords do not match'; |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 15), |
|
|
|
|
|
ElevatedButton( |
|
|
|
|
|
style: buttonStyle, |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
if (_formKey.currentState!.validate()) { |
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar( |
|
|
|
|
|
const SnackBar(content: Text('Processing Data')), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
signUp( |
|
|
|
|
|
context, |
|
|
|
|
|
usernameController.text, |
|
|
|
|
|
passwordController.text, |
|
|
|
|
|
passwordConfirmController.text |
|
|
|
|
|
).then((value) { |
|
|
|
|
|
Navigator.of(context).popUntil((route) => route.isFirst); |
|
|
|
|
|
}).catchError((error) { |
|
|
|
|
|
print(error); // TODO: Show error on interface |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
child: const Text('Submit'), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |