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.

305 lines
8.9 KiB

  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:Envelope/components/flash_message.dart';
  4. import 'package:Envelope/models/my_profile.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:http/http.dart' as http;
  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. primary: Theme.of(context).colorScheme.surface,
  72. onPrimary: Theme.of(context).colorScheme.onSurface,
  73. minimumSize: const Size.fromHeight(50),
  74. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
  75. textStyle: TextStyle(
  76. fontSize: 20,
  77. fontWeight: FontWeight.bold,
  78. color: Theme.of(context).colorScheme.error,
  79. ),
  80. );
  81. return Center(
  82. child: SingleChildScrollView(
  83. child: Form(
  84. key: _formKey,
  85. child: Center(
  86. child: Padding(
  87. padding: const EdgeInsets.only(
  88. left: 20,
  89. right: 20,
  90. top: 0,
  91. bottom: 100,
  92. ),
  93. child: Column(
  94. mainAxisAlignment: MainAxisAlignment.center,
  95. crossAxisAlignment: CrossAxisAlignment.center,
  96. children: [
  97. Text(
  98. 'Sign Up',
  99. style: TextStyle(
  100. fontSize: 35,
  101. color: Theme.of(context).colorScheme.onBackground,
  102. ),
  103. ),
  104. const SizedBox(height: 30),
  105. input(
  106. _usernameController,
  107. 'Username',
  108. false,
  109. (value) {
  110. if (value == null || value.isEmpty) {
  111. return 'Create a username';
  112. }
  113. return null;
  114. },
  115. ),
  116. const SizedBox(height: 10),
  117. input(
  118. _passwordController,
  119. 'Password',
  120. true,
  121. (value) {
  122. if (value == null || value.isEmpty) {
  123. return 'Enter a password';
  124. }
  125. return null;
  126. },
  127. ),
  128. const SizedBox(height: 10),
  129. input(
  130. _passwordConfirmController,
  131. 'Confirm Password',
  132. true,
  133. (value) {
  134. if (value == null || value.isEmpty) {
  135. return 'Confirm your password';
  136. }
  137. if (value != _passwordController.text) {
  138. return 'Passwords do not match';
  139. }
  140. return null;
  141. },
  142. ),
  143. const SizedBox(height: 15),
  144. serverUrl(),
  145. const SizedBox(height: 15),
  146. ElevatedButton(
  147. style: buttonStyle,
  148. onPressed: () {
  149. if (!_formKey.currentState!.validate()) {
  150. return;
  151. }
  152. ScaffoldMessenger.of(context).showSnackBar(
  153. const SnackBar(content: Text('Processing Data')),
  154. );
  155. signUp()
  156. .then((dynamic) {
  157. Navigator.of(context).popUntil((route) => route.isFirst);
  158. }).catchError((error) {
  159. showMessage('Failed to signup to Envelope, please try again later', context);
  160. });
  161. },
  162. child: const Text('Submit'),
  163. ),
  164. ],
  165. )
  166. )
  167. )
  168. )
  169. )
  170. );
  171. }
  172. Widget input(
  173. TextEditingController textController,
  174. String hintText,
  175. bool password,
  176. String? Function(dynamic) validationFunction,
  177. ) {
  178. return TextFormField(
  179. controller: textController,
  180. obscureText: password,
  181. enableSuggestions: false,
  182. autocorrect: false,
  183. decoration: InputDecoration(
  184. hintText: hintText,
  185. enabledBorder: inputBorderStyle,
  186. focusedBorder: inputBorderStyle,
  187. ),
  188. style: inputTextStyle,
  189. validator: validationFunction,
  190. );
  191. }
  192. Widget serverUrl() {
  193. if (!showUrlInput) {
  194. return
  195. Padding(
  196. padding: const EdgeInsets.only(top: 0, bottom: 10),
  197. child: Row(
  198. children: [
  199. SizedBox(
  200. height: 10,
  201. child: IconButton(
  202. onPressed: () {
  203. setState(() {
  204. showUrlInput = true;
  205. });
  206. },
  207. icon: Icon(
  208. Icons.edit,
  209. color: Theme.of(context).disabledColor,
  210. ),
  211. splashRadius: 2,
  212. padding: const EdgeInsets.all(2),
  213. iconSize: 15,
  214. ),
  215. ),
  216. const SizedBox(width: 2),
  217. Column(
  218. children: [
  219. const SizedBox(height: 10),
  220. Text(
  221. 'Server URL - $defaultServerUrl',
  222. style: TextStyle(
  223. color: Theme.of(context).disabledColor,
  224. fontSize: 12,
  225. ),
  226. ),
  227. ],
  228. ),
  229. ],
  230. ),
  231. );
  232. }
  233. if (_serverUrlController.text == '') {
  234. _serverUrlController.text = defaultServerUrl;
  235. }
  236. return input(
  237. _serverUrlController,
  238. 'Server URL',
  239. false,
  240. (dynamic) {
  241. return null;
  242. },
  243. );
  244. }
  245. Future<SignupResponse> signUp() async {
  246. await MyProfile.setServerUrl(_serverUrlController.text);
  247. var keyPair = CryptoUtils.generateRSAKeyPair();
  248. var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
  249. var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
  250. String encRsaPriv = AesHelper.aesEncrypt(
  251. _passwordController.text,
  252. Uint8List.fromList(rsaPrivPem.codeUnits),
  253. );
  254. final resp = await http.post(
  255. await MyProfile.getServerUrl('api/v1/signup'),
  256. headers: <String, String>{
  257. 'Content-Type': 'application/json; charset=UTF-8',
  258. },
  259. body: jsonEncode(<String, String>{
  260. 'username': _usernameController.text,
  261. 'password': _passwordController.text,
  262. 'confirm_password': _passwordConfirmController.text,
  263. 'asymmetric_public_key': rsaPubPem,
  264. 'asymmetric_private_key': encRsaPriv,
  265. }),
  266. );
  267. SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
  268. if (resp.statusCode != 201) {
  269. throw Exception(response.message);
  270. }
  271. return response;
  272. }
  273. }