Encrypted messaging app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
8.8 KiB

  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:flutter/material.dart';
  4. import 'package:http/http.dart' as http;
  5. import '/components/flash_message.dart';
  6. import '/database/models/my_profile.dart';
  7. import '/utils/encryption/aes_helper.dart';
  8. import '/utils/encryption/crypto_utils.dart';
  9. class Signup extends StatelessWidget {
  10. const Signup({Key? key}) : super(key: key);
  11. @override
  12. Widget build(BuildContext context) {
  13. return Scaffold(
  14. appBar: AppBar(
  15. title: null,
  16. automaticallyImplyLeading: true,
  17. //`true` if you want Flutter to automatically add Back Button when needed,
  18. //or `false` if you want to force your own back button every where
  19. leading: IconButton(icon: const Icon(Icons.arrow_back),
  20. onPressed:() => {
  21. Navigator.pop(context)
  22. }
  23. ),
  24. backgroundColor: Colors.transparent,
  25. shadowColor: Colors.transparent,
  26. ),
  27. body: const SafeArea(
  28. child: SignupWidget(),
  29. )
  30. );
  31. }
  32. }
  33. class SignupResponse {
  34. final String status;
  35. final String message;
  36. const SignupResponse({
  37. required this.status,
  38. required this.message,
  39. });
  40. factory SignupResponse.fromJson(Map<String, dynamic> json) {
  41. return SignupResponse(
  42. status: json['status'],
  43. message: json['message'],
  44. );
  45. }
  46. }
  47. class SignupWidget extends StatefulWidget {
  48. const SignupWidget({Key? key}) : super(key: key);
  49. @override
  50. State<SignupWidget> createState() => _SignupWidgetState();
  51. }
  52. class _SignupWidgetState extends State<SignupWidget> {
  53. final _formKey = GlobalKey<FormState>();
  54. final TextEditingController _usernameController = TextEditingController();
  55. final TextEditingController _passwordController = TextEditingController();
  56. final TextEditingController _passwordConfirmController = TextEditingController();
  57. final TextEditingController _serverUrlController = TextEditingController();
  58. bool showUrlInput = false;
  59. final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
  60. borderRadius: BorderRadius.circular(5),
  61. borderSide: const BorderSide(
  62. color: Colors.transparent,
  63. )
  64. );
  65. final TextStyle inputTextStyle = const TextStyle(
  66. fontSize: 18,
  67. );
  68. @override
  69. Widget build(BuildContext context) {
  70. final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
  71. backgroundColor: Theme.of(context).colorScheme.tertiary,
  72. minimumSize: const Size.fromHeight(50),
  73. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
  74. textStyle: TextStyle(
  75. fontSize: 20,
  76. fontWeight: FontWeight.bold,
  77. color: Theme.of(context).colorScheme.error,
  78. ),
  79. );
  80. return Center(
  81. child: SingleChildScrollView(
  82. child: Form(
  83. key: _formKey,
  84. child: Center(
  85. child: Padding(
  86. padding: const EdgeInsets.only(
  87. left: 20,
  88. right: 20,
  89. top: 0,
  90. bottom: 100,
  91. ),
  92. child: Column(
  93. mainAxisAlignment: MainAxisAlignment.center,
  94. crossAxisAlignment: CrossAxisAlignment.center,
  95. children: [
  96. Text(
  97. 'Sign Up',
  98. style: TextStyle(
  99. fontSize: 35,
  100. color: Theme.of(context).colorScheme.onBackground,
  101. ),
  102. ),
  103. const SizedBox(height: 30),
  104. input(
  105. _usernameController,
  106. 'Username',
  107. false,
  108. (value) {
  109. if (value == null || value.isEmpty) {
  110. return 'Create a username';
  111. }
  112. return null;
  113. },
  114. ),
  115. const SizedBox(height: 10),
  116. input(
  117. _passwordController,
  118. 'Password',
  119. true,
  120. (value) {
  121. if (value == null || value.isEmpty) {
  122. return 'Enter a password';
  123. }
  124. return null;
  125. },
  126. ),
  127. const SizedBox(height: 10),
  128. input(
  129. _passwordConfirmController,
  130. 'Confirm Password',
  131. true,
  132. (value) {
  133. if (value == null || value.isEmpty) {
  134. return 'Confirm your password';
  135. }
  136. if (value != _passwordController.text) {
  137. return 'Passwords do not match';
  138. }
  139. return null;
  140. },
  141. ),
  142. const SizedBox(height: 15),
  143. serverUrl(),
  144. const SizedBox(height: 15),
  145. ElevatedButton(
  146. style: buttonStyle,
  147. onPressed: () {
  148. if (!_formKey.currentState!.validate()) {
  149. return;
  150. }
  151. ScaffoldMessenger.of(context).showSnackBar(
  152. const SnackBar(content: Text('Processing Data')),
  153. );
  154. signUp()
  155. .then((dynamic) {
  156. Navigator.of(context).popUntil((route) => route.isFirst);
  157. }).catchError((error) {
  158. showMessage('Failed to signup to Envelope, please try again later', context);
  159. });
  160. },
  161. child: const Text('Submit'),
  162. ),
  163. ],
  164. )
  165. )
  166. )
  167. )
  168. )
  169. );
  170. }
  171. Widget input(
  172. TextEditingController textController,
  173. String hintText,
  174. bool password,
  175. String? Function(dynamic) validationFunction,
  176. ) {
  177. return TextFormField(
  178. controller: textController,
  179. obscureText: password,
  180. enableSuggestions: false,
  181. autocorrect: false,
  182. decoration: InputDecoration(
  183. hintText: hintText,
  184. enabledBorder: inputBorderStyle,
  185. focusedBorder: inputBorderStyle,
  186. ),
  187. style: inputTextStyle,
  188. validator: validationFunction,
  189. );
  190. }
  191. Widget serverUrl() {
  192. if (!showUrlInput) {
  193. return
  194. Padding(
  195. padding: const EdgeInsets.only(top: 0, bottom: 10),
  196. child: Row(
  197. children: [
  198. SizedBox(
  199. height: 10,
  200. child: IconButton(
  201. onPressed: () {
  202. setState(() {
  203. showUrlInput = true;
  204. });
  205. },
  206. icon: Icon(
  207. Icons.edit,
  208. color: Theme.of(context).disabledColor,
  209. ),
  210. splashRadius: 2,
  211. padding: const EdgeInsets.all(2),
  212. iconSize: 15,
  213. ),
  214. ),
  215. const SizedBox(width: 2),
  216. Column(
  217. children: [
  218. const SizedBox(height: 10),
  219. Text(
  220. 'Server URL - $defaultServerUrl',
  221. style: TextStyle(
  222. color: Theme.of(context).disabledColor,
  223. fontSize: 12,
  224. ),
  225. ),
  226. ],
  227. ),
  228. ],
  229. ),
  230. );
  231. }
  232. if (_serverUrlController.text == '') {
  233. _serverUrlController.text = defaultServerUrl;
  234. }
  235. return input(
  236. _serverUrlController,
  237. 'Server URL',
  238. false,
  239. (dynamic) {
  240. return null;
  241. },
  242. );
  243. }
  244. Future<SignupResponse> signUp() async {
  245. await MyProfile.setServerUrl(_serverUrlController.text);
  246. var keyPair = CryptoUtils.generateRSAKeyPair();
  247. var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
  248. var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
  249. String encRsaPriv = AesHelper.aesEncrypt(
  250. _passwordController.text,
  251. Uint8List.fromList(rsaPrivPem.codeUnits),
  252. );
  253. final resp = await http.post(
  254. await MyProfile.getServerUrl('api/v1/signup'),
  255. headers: <String, String>{
  256. 'Content-Type': 'application/json; charset=UTF-8',
  257. },
  258. body: jsonEncode(<String, String>{
  259. 'username': _usernameController.text,
  260. 'password': _passwordController.text,
  261. 'confirm_password': _passwordConfirmController.text,
  262. 'asymmetric_public_key': rsaPubPem,
  263. 'asymmetric_private_key': encRsaPriv,
  264. }),
  265. );
  266. SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
  267. if (resp.statusCode != 201) {
  268. throw Exception(response.message);
  269. }
  270. return response;
  271. }
  272. }