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.

297 lines
8.4 KiB

  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. import 'package:http/http.dart' as http;
  4. import '/components/flash_message.dart';
  5. import '/database/models/my_profile.dart';
  6. import '/utils/storage/session_cookie.dart';
  7. class LoginResponse {
  8. final String userId;
  9. final String username;
  10. final String publicKey;
  11. final String privateKey;
  12. final String symmetricKey;
  13. final String? imageLink;
  14. const LoginResponse({
  15. required this.publicKey,
  16. required this.privateKey,
  17. required this.symmetricKey,
  18. required this.userId,
  19. required this.username,
  20. this.imageLink,
  21. });
  22. factory LoginResponse.fromJson(Map<String, dynamic> json) {
  23. return LoginResponse(
  24. userId: json['user_id'],
  25. username: json['username'],
  26. publicKey: json['asymmetric_public_key'],
  27. privateKey: json['asymmetric_private_key'],
  28. symmetricKey: json['symmetric_key'],
  29. imageLink: json['image_link'],
  30. );
  31. }
  32. }
  33. class Login extends StatelessWidget {
  34. const Login({Key? key}) : super(key: key);
  35. @override
  36. Widget build(BuildContext context) {
  37. return Scaffold(
  38. appBar: AppBar(
  39. title: null,
  40. automaticallyImplyLeading: true,
  41. leading: IconButton(icon: const Icon(Icons.arrow_back),
  42. onPressed:() => {
  43. Navigator.pop(context)
  44. }
  45. ),
  46. backgroundColor: Colors.transparent,
  47. shadowColor: Colors.transparent,
  48. ),
  49. body: const SafeArea(
  50. child: LoginWidget(),
  51. ),
  52. );
  53. }
  54. }
  55. class LoginWidget extends StatefulWidget {
  56. const LoginWidget({Key? key}) : super(key: key);
  57. @override
  58. State<LoginWidget> createState() => _LoginWidgetState();
  59. }
  60. class _LoginWidgetState extends State<LoginWidget> {
  61. final _formKey = GlobalKey<FormState>();
  62. final TextEditingController _usernameController = TextEditingController();
  63. final TextEditingController _passwordController = TextEditingController();
  64. final TextEditingController _urlController = TextEditingController();
  65. bool _changedUrl = false;
  66. String _initialServerUrl = '';
  67. @override
  68. void initState() {
  69. MyProfile.getBaseServerUrl().then((String url) {
  70. _urlController.text = url;
  71. _initialServerUrl = url;
  72. });
  73. super.initState();
  74. }
  75. @override
  76. Widget build(BuildContext context) {
  77. const TextStyle inputTextStyle = TextStyle(
  78. fontSize: 18,
  79. );
  80. final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
  81. borderRadius: BorderRadius.circular(5),
  82. borderSide: const BorderSide(
  83. color: Colors.transparent,
  84. )
  85. );
  86. final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
  87. backgroundColor: Theme.of(context).colorScheme.tertiary,
  88. minimumSize: const Size.fromHeight(50),
  89. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
  90. textStyle: TextStyle(
  91. fontSize: 20,
  92. fontWeight: FontWeight.bold,
  93. color: Theme.of(context).colorScheme.error,
  94. ),
  95. );
  96. return Center(
  97. child: Form(
  98. key: _formKey,
  99. child: Center(
  100. child: Padding(
  101. padding: const EdgeInsets.only(
  102. left: 20,
  103. right: 20,
  104. top: 0,
  105. bottom: 80,
  106. ),
  107. child: Column(
  108. mainAxisAlignment: MainAxisAlignment.center,
  109. crossAxisAlignment: CrossAxisAlignment.center,
  110. children: [
  111. Text(
  112. 'Login',
  113. style: TextStyle(
  114. fontSize: 35,
  115. color: Theme.of(context).colorScheme.onBackground,
  116. ),
  117. ),
  118. const SizedBox(height: 30),
  119. TextFormField(
  120. controller: _usernameController,
  121. decoration: InputDecoration(
  122. hintText: 'Username',
  123. enabledBorder: inputBorderStyle,
  124. focusedBorder: inputBorderStyle,
  125. ),
  126. style: inputTextStyle,
  127. // The validator receives the text that the user has entered.
  128. validator: (value) {
  129. if (value == null || value.isEmpty) {
  130. return 'Enter a username';
  131. }
  132. return null;
  133. },
  134. ),
  135. const SizedBox(height: 10),
  136. TextFormField(
  137. controller: _passwordController,
  138. obscureText: true,
  139. enableSuggestions: false,
  140. autocorrect: false,
  141. decoration: InputDecoration(
  142. hintText: 'Password',
  143. enabledBorder: inputBorderStyle,
  144. focusedBorder: inputBorderStyle,
  145. ),
  146. style: inputTextStyle,
  147. // The validator receives the text that the user has entered.
  148. validator: (value) {
  149. if (value == null || value.isEmpty) {
  150. return 'Enter a password';
  151. }
  152. return null;
  153. },
  154. ),
  155. const SizedBox(height: 15),
  156. changeUrl(),
  157. const SizedBox(height: 20),
  158. ElevatedButton(
  159. style: buttonStyle,
  160. onPressed: () {
  161. if (_formKey.currentState!.validate()) {
  162. login()
  163. .then((val) {
  164. Navigator.
  165. pushNamedAndRemoveUntil(
  166. context,
  167. '/home',
  168. ModalRoute.withName('/home'),
  169. );
  170. }).catchError((error) {
  171. showMessage(
  172. 'Could not login to Envelope, please try again later.',
  173. context,
  174. );
  175. });
  176. }
  177. },
  178. child: const Text('Submit'),
  179. ),
  180. ],
  181. )
  182. )
  183. )
  184. )
  185. );
  186. }
  187. Widget changeUrl() {
  188. if (_changedUrl) {
  189. return TextFormField(
  190. controller: _urlController,
  191. decoration: InputDecoration(
  192. hintText: 'URL',
  193. enabledBorder: OutlineInputBorder(
  194. borderRadius: BorderRadius.circular(5),
  195. borderSide: const BorderSide(
  196. color: Colors.transparent,
  197. )
  198. ),
  199. focusedBorder: OutlineInputBorder(
  200. borderRadius: BorderRadius.circular(5),
  201. borderSide: const BorderSide(
  202. color: Colors.transparent,
  203. )
  204. ),
  205. ),
  206. style: const TextStyle(
  207. fontSize: 18,
  208. ),
  209. // The validator receives the text that the user has entered.
  210. validator: (value) {
  211. if (value == null || value.isEmpty) {
  212. return 'Enter a URL';
  213. }
  214. return null;
  215. },
  216. );
  217. }
  218. return GestureDetector(
  219. onTap: () {
  220. setState(() {
  221. _changedUrl = true;
  222. });
  223. },
  224. child: Row(
  225. mainAxisAlignment: MainAxisAlignment.end,
  226. children: [
  227. Text(
  228. 'Change URL',
  229. style: TextStyle(
  230. color: Theme.of(context).disabledColor,
  231. ),
  232. ),
  233. const SizedBox(width: 10),
  234. Icon(
  235. Icons.edit,
  236. color: Theme.of(context).disabledColor,
  237. size: 14,
  238. ),
  239. const SizedBox(width: 10),
  240. ],
  241. )
  242. );
  243. }
  244. Future<dynamic> login() async {
  245. if (_urlController.text != _initialServerUrl) {
  246. await MyProfile.setServerUrl(_urlController.text);
  247. }
  248. final resp = await http.post(
  249. await MyProfile.getServerUrl('api/v1/login'),
  250. headers: <String, String>{
  251. 'Content-Type': 'application/json; charset=UTF-8',
  252. },
  253. body: jsonEncode(<String, String>{
  254. 'username': _usernameController.text,
  255. 'password': _passwordController.text,
  256. }),
  257. );
  258. if (resp.statusCode != 200) {
  259. throw Exception(resp.body);
  260. }
  261. String? rawCookie = resp.headers['set-cookie'];
  262. if (rawCookie != null) {
  263. int index = rawCookie.indexOf(';');
  264. setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index));
  265. }
  266. return await MyProfile.login(
  267. json.decode(resp.body),
  268. _passwordController.text,
  269. );
  270. }
  271. }