import 'dart:convert'; import 'dart:typed_data'; import 'package:Envelope/utils/storage/session_cookie.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '/components/flash_message.dart'; import '/database/models/my_profile.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; class Signup extends StatelessWidget { 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 SignupResponse { final String status; final String message; const SignupResponse({ required this.status, required this.message, }); factory SignupResponse.fromJson(Map json) { return SignupResponse( status: json['status'], message: json['message'], ); } } class SignupWidget extends StatefulWidget { const SignupWidget({Key? key}) : super(key: key); @override State createState() => _SignupWidgetState(); } class _SignupWidgetState extends State { final _formKey = GlobalKey(); final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordConfirmController = TextEditingController(); final TextEditingController _serverUrlController = TextEditingController(); bool showUrlInput = false; final OutlineInputBorder inputBorderStyle = OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: const BorderSide( color: Colors.transparent, ) ); final TextStyle inputTextStyle = const TextStyle( fontSize: 18, ); @override Widget build(BuildContext context) { final ButtonStyle buttonStyle = ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.tertiary, 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: SingleChildScrollView( 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), input( _usernameController, 'Username', false, (value) { if (value == null || value.isEmpty) { return 'Create a username'; } return null; }, ), const SizedBox(height: 10), input( _passwordController, 'Password', true, (value) { if (value == null || value.isEmpty) { return 'Enter a password'; } return null; }, ), const SizedBox(height: 10), input( _passwordConfirmController, 'Confirm Password', true, (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), serverUrl(), const SizedBox(height: 15), ElevatedButton( style: buttonStyle, onPressed: () { if (!_formKey.currentState!.validate()) { return; } signUp() .then((dynamic) { Navigator. pushNamedAndRemoveUntil( context, '/home', ModalRoute.withName('/home'), ); }).catchError((error) { showMessage('Failed to signup to Envelope, please try again later', context); }); }, child: const Text('Submit'), ), ], ) ) ) ) ) ); } Widget input( TextEditingController textController, String hintText, bool password, String? Function(dynamic) validationFunction, ) { return TextFormField( controller: textController, obscureText: password, enableSuggestions: false, autocorrect: false, decoration: InputDecoration( hintText: hintText, enabledBorder: inputBorderStyle, focusedBorder: inputBorderStyle, ), style: inputTextStyle, validator: validationFunction, ); } Widget serverUrl() { if (!showUrlInput) { return Padding( padding: const EdgeInsets.only(top: 0, bottom: 10), child: Row( children: [ SizedBox( height: 10, child: IconButton( onPressed: () { setState(() { showUrlInput = true; }); }, icon: Icon( Icons.edit, color: Theme.of(context).disabledColor, ), splashRadius: 2, padding: const EdgeInsets.all(2), iconSize: 15, ), ), const SizedBox(width: 2), Column( children: [ const SizedBox(height: 10), Text( 'Server URL - $defaultServerUrl', style: TextStyle( color: Theme.of(context).disabledColor, fontSize: 12, ), ), ], ), ], ), ); } if (_serverUrlController.text == '') { _serverUrlController.text = defaultServerUrl; } return input( _serverUrlController, 'Server URL', false, (dynamic) { return null; }, ); } Future signUp() async { await MyProfile.setServerUrl(_serverUrlController.text); var keyPair = CryptoUtils.generateRSAKeyPair(); var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); String encRsaPriv = AesHelper.aesEncrypt( _passwordController.text, Uint8List.fromList(rsaPrivPem.codeUnits), ); final resp = await http.post( await MyProfile.getServerUrl('api/v1/signup'), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({ 'username': _usernameController.text, 'password': _passwordController.text, 'confirm_password': _passwordConfirmController.text, 'asymmetric_public_key': rsaPubPem, 'asymmetric_private_key': encRsaPriv, }), ); if (resp.statusCode != 200) { throw Exception('Unable to signup to envelope'); } String? rawCookie = resp.headers['set-cookie']; if (rawCookie != null) { int index = rawCookie.indexOf(';'); setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index)); } return await MyProfile.login( json.decode(resp.body), _passwordController.text, ); } }