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.

313 lines
9.1 KiB

  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:Envelope/utils/storage/session_cookie.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:http/http.dart' as http;
  6. import '/components/flash_message.dart';
  7. import '/database/models/my_profile.dart';
  8. import '/utils/encryption/aes_helper.dart';
  9. import '/utils/encryption/crypto_utils.dart';
  10. class Signup extends StatelessWidget {
  11. const Signup({Key? key}) : super(key: key);
  12. @override
  13. Widget build(BuildContext context) {
  14. return Scaffold(
  15. appBar: AppBar(
  16. title: null,
  17. automaticallyImplyLeading: true,
  18. //`true` if you want Flutter to automatically add Back Button when needed,
  19. //or `false` if you want to force your own back button every where
  20. leading: IconButton(icon: const Icon(Icons.arrow_back),
  21. onPressed:() => {
  22. Navigator.pop(context)
  23. }
  24. ),
  25. backgroundColor: Colors.transparent,
  26. shadowColor: Colors.transparent,
  27. ),
  28. body: const SafeArea(
  29. child: SignupWidget(),
  30. )
  31. );
  32. }
  33. }
  34. class SignupResponse {
  35. final String status;
  36. final String message;
  37. const SignupResponse({
  38. required this.status,
  39. required this.message,
  40. });
  41. factory SignupResponse.fromJson(Map<String, dynamic> json) {
  42. return SignupResponse(
  43. status: json['status'],
  44. message: json['message'],
  45. );
  46. }
  47. }
  48. class SignupWidget extends StatefulWidget {
  49. const SignupWidget({Key? key}) : super(key: key);
  50. @override
  51. State<SignupWidget> createState() => _SignupWidgetState();
  52. }
  53. class _SignupWidgetState extends State<SignupWidget> {
  54. final _formKey = GlobalKey<FormState>();
  55. final TextEditingController _usernameController = TextEditingController();
  56. final TextEditingController _passwordController = TextEditingController();
  57. final TextEditingController _passwordConfirmController = TextEditingController();
  58. final TextEditingController _serverUrlController = TextEditingController();
  59. bool showUrlInput = false;
  60. final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
  61. borderRadius: BorderRadius.circular(5),
  62. borderSide: const BorderSide(
  63. color: Colors.transparent,
  64. )
  65. );
  66. final TextStyle inputTextStyle = const TextStyle(
  67. fontSize: 18,
  68. );
  69. @override
  70. Widget build(BuildContext context) {
  71. final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
  72. backgroundColor: Theme.of(context).colorScheme.tertiary,
  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. signUp()
  153. .then((dynamic) {
  154. Navigator.
  155. pushNamedAndRemoveUntil(
  156. context,
  157. '/home',
  158. ModalRoute.withName('/home'),
  159. );
  160. }).catchError((error) {
  161. showMessage('Failed to signup to Envelope, please try again later', context);
  162. });
  163. },
  164. child: const Text('Submit'),
  165. ),
  166. ],
  167. )
  168. )
  169. )
  170. )
  171. )
  172. );
  173. }
  174. Widget input(
  175. TextEditingController textController,
  176. String hintText,
  177. bool password,
  178. String? Function(dynamic) validationFunction,
  179. ) {
  180. return TextFormField(
  181. controller: textController,
  182. obscureText: password,
  183. enableSuggestions: false,
  184. autocorrect: false,
  185. decoration: InputDecoration(
  186. hintText: hintText,
  187. enabledBorder: inputBorderStyle,
  188. focusedBorder: inputBorderStyle,
  189. ),
  190. style: inputTextStyle,
  191. validator: validationFunction,
  192. );
  193. }
  194. Widget serverUrl() {
  195. if (!showUrlInput) {
  196. return
  197. Padding(
  198. padding: const EdgeInsets.only(top: 0, bottom: 10),
  199. child: Row(
  200. children: [
  201. SizedBox(
  202. height: 10,
  203. child: IconButton(
  204. onPressed: () {
  205. setState(() {
  206. showUrlInput = true;
  207. });
  208. },
  209. icon: Icon(
  210. Icons.edit,
  211. color: Theme.of(context).disabledColor,
  212. ),
  213. splashRadius: 2,
  214. padding: const EdgeInsets.all(2),
  215. iconSize: 15,
  216. ),
  217. ),
  218. const SizedBox(width: 2),
  219. Column(
  220. children: [
  221. const SizedBox(height: 10),
  222. Text(
  223. 'Server URL - $defaultServerUrl',
  224. style: TextStyle(
  225. color: Theme.of(context).disabledColor,
  226. fontSize: 12,
  227. ),
  228. ),
  229. ],
  230. ),
  231. ],
  232. ),
  233. );
  234. }
  235. if (_serverUrlController.text == '') {
  236. _serverUrlController.text = defaultServerUrl;
  237. }
  238. return input(
  239. _serverUrlController,
  240. 'Server URL',
  241. false,
  242. (dynamic) {
  243. return null;
  244. },
  245. );
  246. }
  247. Future<dynamic> signUp() async {
  248. await MyProfile.setServerUrl(_serverUrlController.text);
  249. var keyPair = CryptoUtils.generateRSAKeyPair();
  250. var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
  251. var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
  252. String encRsaPriv = AesHelper.aesEncrypt(
  253. _passwordController.text,
  254. Uint8List.fromList(rsaPrivPem.codeUnits),
  255. );
  256. final resp = await http.post(
  257. await MyProfile.getServerUrl('api/v1/signup'),
  258. headers: <String, String>{
  259. 'Content-Type': 'application/json; charset=UTF-8',
  260. },
  261. body: jsonEncode(<String, String>{
  262. 'username': _usernameController.text,
  263. 'password': _passwordController.text,
  264. 'confirm_password': _passwordConfirmController.text,
  265. 'asymmetric_public_key': rsaPubPem,
  266. 'asymmetric_private_key': encRsaPriv,
  267. }),
  268. );
  269. if (resp.statusCode != 200) {
  270. throw Exception('Unable to signup to envelope');
  271. }
  272. String? rawCookie = resp.headers['set-cookie'];
  273. if (rawCookie != null) {
  274. int index = rawCookie.indexOf(';');
  275. setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index));
  276. }
  277. return await MyProfile.login(
  278. json.decode(resp.body),
  279. _passwordController.text,
  280. );
  281. }
  282. }