Browse Source

Add the ability to change the server URL on signup & profile page

TODO: Change server URL on login. This will only affect new devices with
existing users, so will be done later.
pull/2/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
e31b048389
19 changed files with 632 additions and 346 deletions
  1. +14
    -8
      Backend/Database/Init.go
  2. +1
    -1
      Backend/Database/Seeder/MessageSeeder.go
  3. +10
    -6
      Backend/Models/Messages.go
  4. +46
    -6
      Backend/Models/Users.go
  5. +2
    -3
      mobile/lib/components/qr_reader.dart
  6. +1
    -1
      mobile/lib/components/user_search_result.dart
  7. +1
    -0
      mobile/lib/main.dart
  8. +26
    -2
      mobile/lib/models/my_profile.dart
  9. +5
    -6
      mobile/lib/utils/storage/conversations.dart
  10. +1
    -2
      mobile/lib/utils/storage/friends.dart
  11. +3
    -4
      mobile/lib/utils/storage/messages.dart
  12. +149
    -143
      mobile/lib/views/authentication/login.dart
  13. +214
    -148
      mobile/lib/views/authentication/signup.dart
  14. +2
    -3
      mobile/lib/views/main/friend/add_search.dart
  15. +5
    -6
      mobile/lib/views/main/friend/request_list_item.dart
  16. +1
    -2
      mobile/lib/views/main/home.dart
  17. +2
    -2
      mobile/lib/views/main/profile/change_password.dart
  18. +142
    -0
      mobile/lib/views/main/profile/change_server_url.dart
  19. +7
    -3
      mobile/lib/views/main/profile/profile.dart

+ 14
- 8
Backend/Database/Init.go View File

@ -10,13 +10,14 @@ import (
) )
const ( const (
dbUrl = "postgres://postgres:@localhost:5432/envelope"
dbTestUrl = "postgres://postgres:@localhost:5432/envelope_test"
dbURL = "postgres://postgres:@localhost:5432/envelope"
dbTestURL = "postgres://postgres:@localhost:5432/envelope_test"
) )
// DB db
var DB *gorm.DB var DB *gorm.DB
func GetModels() []interface{} {
func getModels() []interface{} {
return []interface{}{ return []interface{}{
&Models.Session{}, &Models.Session{},
&Models.User{}, &Models.User{},
@ -29,6 +30,7 @@ func GetModels() []interface{} {
} }
} }
// Init initializes the database connection
func Init() { func Init() {
var ( var (
model interface{} model interface{}
@ -37,7 +39,7 @@ func Init() {
log.Println("Initializing database...") log.Println("Initializing database...")
DB, err = gorm.Open(postgres.Open(dbUrl), &gorm.Config{})
DB, err = gorm.Open(postgres.Open(dbURL), &gorm.Config{})
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
@ -45,24 +47,28 @@ func Init() {
log.Println("Running AutoMigrate...") log.Println("Running AutoMigrate...")
for _, model = range GetModels() {
DB.AutoMigrate(model)
for _, model = range getModels() {
err = DB.AutoMigrate(model)
if err != nil {
log.Fatalln(err)
}
} }
} }
// InitTest initializes the test datbase
func InitTest() { func InitTest() {
var ( var (
model interface{} model interface{}
err error err error
) )
DB, err = gorm.Open(postgres.Open(dbTestUrl), &gorm.Config{})
DB, err = gorm.Open(postgres.Open(dbTestURL), &gorm.Config{})
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
for _, model = range GetModels() {
for _, model = range getModels() {
DB.Migrator().DropTable(model) DB.Migrator().DropTable(model)
DB.AutoMigrate(model) DB.AutoMigrate(model)
} }


+ 1
- 1
Backend/Database/Seeder/MessageSeeder.go View File

@ -298,7 +298,7 @@ func SeedMessages() {
panic(err) panic(err)
} }
for i = 0; i <= 20; i++ {
for i = 0; i <= 100; i++ {
err = seedMessage( err = seedMessage(
primaryUser, primaryUser,
secondaryUser, secondaryUser,


+ 10
- 6
Backend/Models/Messages.go View File

@ -1,12 +1,14 @@
package Models package Models
import ( import (
"database/sql"
"time" "time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
// TODO: Add support for images
// MessageData holds the content of the message
// encrypted through the Message.SymmetricKey
type MessageData struct { type MessageData struct {
Base Base
Data string `gorm:"not null" json:"data"` // Stored encrypted Data string `gorm:"not null" json:"data"` // Stored encrypted
@ -14,11 +16,13 @@ type MessageData struct {
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
} }
// Message holds data pertaining to each users' message
type Message struct { type Message struct {
Base Base
MessageDataID uuid.UUID `json:"message_data_id"`
MessageData MessageData `json:"message_data"`
SymmetricKey string `json:"symmetric_key" gorm:"not null"` // Stored encrypted
AssociationKey string `json:"association_key" gorm:"not null"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this
CreatedAt time.Time `json:"created_at" gorm:"not null"`
MessageDataID uuid.UUID ` json:"message_data_id"`
MessageData MessageData ` json:"message_data"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AssociationKey string `gorm:"not null" json:"association_key"` // Stored encrypted
Expiry sql.NullTime ` json:"expiry"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
} }

+ 46
- 6
Backend/Models/Users.go View File

@ -1,10 +1,12 @@
package Models package Models
import ( import (
"database/sql/driver"
"gorm.io/gorm" "gorm.io/gorm"
) )
// Prevent updating the email if it has not changed
// BeforeUpdate prevents updating the email if it has not changed
// This stops a unique constraint error // This stops a unique constraint error
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("Username") { if !tx.Statement.Changed("Username") {
@ -13,11 +15,49 @@ func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
return nil return nil
} }
// MessageExpiry holds values for how long messages should expire by default
type MessageExpiry []uint8
const (
// MessageExpiryFifteenMin expires after 15 minutes
MessageExpiryFifteenMin = "fifteen_min"
// MessageExpiryThirtyMin expires after 30 minutes
MessageExpiryThirtyMin = "thirty_min"
// MessageExpiryOneHour expires after one hour
MessageExpiryOneHour = "one_hour"
// MessageExpiryThreeHour expires after three hours
MessageExpiryThreeHour = "three_hour"
// MessageExpirySixHour expires after six hours
MessageExpirySixHour = "six_hour"
// MessageExpiryTwelveHour expires after twelve hours
MessageExpiryTwelveHour = "twelve_hour"
// MessageExpiryOneDay expires after one day
MessageExpiryOneDay = "one_day"
// MessageExpiryThreeDay expires after three days
MessageExpiryThreeDay = "three_day"
// MessageExpiryNoExpiry never expires
MessageExpiryNoExpiry = "no_expiry"
)
// Scan new value into MessageExpiry
func (e *MessageExpiry) Scan(value interface{}) error {
*e = MessageExpiry(value.(string))
return nil
}
// Value gets value out of MessageExpiry column
func (e MessageExpiry) Value() (driver.Value, error) {
return string(e), nil
}
// User holds user data
type User struct { type User struct {
Base Base
Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"`
ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"`
Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"`
ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"`
MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"message_expiry_default" sql:"type:ENUM('fifteen_min', 'thirty_min', 'one_hour', 'three_hour', 'six_hour', 'twelve_hour', 'one_day', 'three_day')"` // Stored encrypted
} }

+ 2
- 3
mobile/lib/components/qr_reader.dart View File

@ -1,9 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:Envelope/utils/storage/session_cookie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pointycastle/impl.dart'; import 'package:pointycastle/impl.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
@ -16,6 +14,7 @@ import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/strings.dart'; import '/utils/strings.dart';
import '/utils/storage/session_cookie.dart';
import 'flash_message.dart'; import 'flash_message.dart';
class QrReader extends StatefulWidget { class QrReader extends StatefulWidget {
@ -128,7 +127,7 @@ class _QrReaderState extends State<QrReader> {
]); ]);
var resp = await http.post( var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/qr_code'),
await MyProfile.getServerUrl('api/v1/auth/friend_request/qr_code'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),


+ 1
- 1
mobile/lib/components/user_search_result.dart View File

@ -119,7 +119,7 @@ class _UserSearchResultState extends State<UserSearchResult>{
}); });
var resp = await http.post( var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request'),
await MyProfile.getServerUrl('api/v1/auth/friend_request'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
}, },


+ 1
- 0
mobile/lib/main.dart View File

@ -50,6 +50,7 @@ class MyApp extends StatelessWidget {
primaryColor: Colors.orange.shade900, primaryColor: Colors.orange.shade900,
backgroundColor: Colors.grey.shade800, backgroundColor: Colors.grey.shade800,
scaffoldBackgroundColor: Colors.grey[850], scaffoldBackgroundColor: Colors.grey[850],
disabledColor: Colors.grey[400],
colorScheme: ColorScheme( colorScheme: ColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
primary: Colors.orange.shade900, primary: Colors.orange.shade900,


+ 26
- 2
mobile/lib/models/my_profile.dart View File

@ -1,9 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/utils/encryption/aes_helper.dart';
import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pointycastle/impl.dart'; import 'package:pointycastle/impl.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart';
// TODO: Replace this with the prod url when server is deployed
String defaultServerUrl = dotenv.env['SERVER_URL'] ?? 'http://192.168.1.5:8080';
class MyProfile { class MyProfile {
String id; String id;
String username; String username;
@ -107,5 +114,22 @@ class MyProfile {
} }
return profile.privateKey!; return profile.privateKey!;
} }
static setServerUrl(String url) async {
final preferences = await SharedPreferences.getInstance();
preferences.setString('server_url', url);
}
static Future<Uri> getServerUrl(String path) async {
final preferences = await SharedPreferences.getInstance();
String? baseUrl = preferences.getString('server_url');
if (baseUrl == null) {
setServerUrl(defaultServerUrl);
return Uri.parse('$defaultServerUrl$path');
}
return Uri.parse('$baseUrl$path');
}
} }

+ 5
- 6
mobile/lib/utils/storage/conversations.dart View File

@ -1,12 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/components/flash_message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '/components/flash_message.dart';
import '/models/conversation_users.dart'; import '/models/conversation_users.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
@ -20,7 +19,7 @@ Future<void> updateConversation(Conversation conversation, { includeUsers = true
Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers); Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers);
var x = await http.put( var x = await http.put(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
await MyProfile.getServerUrl('api/v1/auth/conversations'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie, 'cookie': sessionCookie,
@ -38,7 +37,7 @@ Future<void> updateConversations() async {
// try { // try {
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
await MyProfile.getServerUrl('api/v1/auth/conversations'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
} }
@ -68,7 +67,7 @@ Future<void> updateConversations() async {
Map<String, String> params = {}; Map<String, String> params = {};
params['conversation_detail_ids'] = conversationsDetailIds.join(','); params['conversation_detail_ids'] = conversationsDetailIds.join(',');
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details');
var uri = await MyProfile.getServerUrl('api/v1/auth/conversation_details');
uri = uri.replace(queryParameters: params); uri = uri.replace(queryParameters: params);
resp = await http.get( resp = await http.get(
@ -150,7 +149,7 @@ Future<void> uploadConversation(Conversation conversation, BuildContext context)
Map<String, dynamic> conversationJson = await conversation.payloadJson(); Map<String, dynamic> conversationJson = await conversation.payloadJson();
var resp = await http.post( var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
await MyProfile.getServerUrl('api/v1/auth/conversations'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie, 'cookie': sessionCookie,


+ 1
- 2
mobile/lib/utils/storage/friends.dart View File

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
@ -15,7 +14,7 @@ Future<void> updateFriends() async {
// try { // try {
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_requests'),
await MyProfile.getServerUrl('api/v1/auth/friend_requests'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
} }


+ 3
- 4
mobile/lib/utils/storage/messages.dart View File

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -43,9 +42,9 @@ Future<void> sendMessage(Conversation conversation, String data) async {
String sessionCookie = await getSessionCookie(); String sessionCookie = await getSessionCookie();
message.payloadJson(conversation, messageId) message.payloadJson(conversation, messageId)
.then((messageJson) {
.then((messageJson) async {
return http.post( return http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'),
await MyProfile.getServerUrl('api/v1/auth/message'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie, 'cookie': sessionCookie,
@ -75,7 +74,7 @@ Future<void> updateMessageThread(Conversation conversation, {MyProfile? profile}
ConversationUser currentUser = await getConversationUser(conversation, profile.id); ConversationUser currentUser = await getConversationUser(conversation, profile.id);
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${currentUser.associationKey}'),
await MyProfile.getServerUrl('api/v1/auth/messages/${currentUser.associationKey}'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
} }


+ 149
- 143
mobile/lib/views/authentication/login.dart View File

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '/components/flash_message.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
@ -34,51 +36,26 @@ class LoginResponse {
} }
} }
Future<dynamic> login(context, String username, String password) async {
final resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': username,
'password': password,
}),
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
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), password);
}
class Login extends StatelessWidget { class Login extends StatelessWidget {
const Login({Key? key}) : super(key: key); const Login({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
title: null,
automaticallyImplyLeading: true,
leading: IconButton(icon: const Icon(Icons.arrow_back),
onPressed:() => {
Navigator.pop(context)
}
),
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
body: const SafeArea(
child: LoginWidget(),
appBar: AppBar(
title: null,
automaticallyImplyLeading: true,
leading: IconButton(icon: const Icon(Icons.arrow_back),
onPressed:() => {
Navigator.pop(context)
}
), ),
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
body: const SafeArea(
child: LoginWidget(),
),
); );
} }
} }
@ -93,125 +70,154 @@ class LoginWidget extends StatefulWidget {
class _LoginWidgetState extends State<LoginWidget> { class _LoginWidgetState extends State<LoginWidget> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const TextStyle inputTextStyle = TextStyle( const TextStyle inputTextStyle = TextStyle(
fontSize: 18,
fontSize: 18,
); );
final OutlineInputBorder inputBorderStyle = OutlineInputBorder( final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
); );
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
primary: Theme.of(context).colorScheme.surface,
onPrimary: Theme.of(context).colorScheme.onSurface,
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,
),
primary: Theme.of(context).colorScheme.surface,
onPrimary: Theme.of(context).colorScheme.onSurface,
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( return Center(
child: Form(
key: _formKey,
child: Center(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 0,
bottom: 80,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Login',
style: TextStyle(
fontSize: 35,
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 30),
TextFormField(
controller: usernameController,
decoration: InputDecoration(
hintText: 'Username',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a username';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
),
const SizedBox(height: 15),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
login(
context,
usernameController.text,
passwordController.text,
).then((val) {
Navigator.
pushNamedAndRemoveUntil(
context,
'/home',
ModalRoute.withName('/home'),
);
}).catchError((error) {
print(error); // TODO: Show error on interface
});
}
},
child: const Text('Submit'),
),
],
)
)
child: Form(
key: _formKey,
child: Center(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 0,
bottom: 80,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Login',
style: TextStyle(
fontSize: 35,
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 30),
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
hintText: 'Username',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a username';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: _passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
),
const SizedBox(height: 15),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
login()
.then((val) {
Navigator.
pushNamedAndRemoveUntil(
context,
'/home',
ModalRoute.withName('/home'),
);
}).catchError((error) {
showMessage(
'Could not login to Envelope, please try again later.',
context,
);
});
}
},
child: const Text('Submit'),
),
],
)
) )
)
) )
); );
} }
Future<dynamic> login() async {
final resp = await http.post(
await MyProfile.getServerUrl('api/v1/login'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': _usernameController.text,
'password': _passwordController.text,
}),
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
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,
);
}
} }

+ 214
- 148
mobile/lib/views/authentication/signup.dart View File

@ -1,44 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/components/flash_message.dart';
import 'package:Envelope/models/my_profile.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
Future<SignupResponse> signUp(context, String username, String password, String confirmPassword) async {
var keyPair = CryptoUtils.generateRSAKeyPair();
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
String encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits));
// TODO: Check for timeout here
final resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/signup'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': username,
'password': password,
'confirm_password': confirmPassword,
'asymmetric_public_key': rsaPubPem,
'asymmetric_private_key': encRsaPriv,
}),
);
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
if (resp.statusCode != 201) {
throw Exception(response.message);
}
return response;
}
class Signup extends StatelessWidget { class Signup extends StatelessWidget {
const Signup({Key? key}) : super(key: key); const Signup({Key? key}) : super(key: key);
@ -93,22 +63,26 @@ class SignupWidget extends StatefulWidget {
class _SignupWidgetState extends State<SignupWidget> { class _SignupWidgetState extends State<SignupWidget> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
TextEditingController _passwordConfirmController = TextEditingController();
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const TextStyle inputTextStyle = TextStyle(
fontSize: 18,
);
final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
);
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
primary: Theme.of(context).colorScheme.surface, primary: Theme.of(context).colorScheme.surface,
@ -123,117 +97,209 @@ class _SignupWidgetState extends State<SignupWidget> {
); );
return Center( return Center(
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,
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),
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
hintText: 'Username',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
const SizedBox(height: 30),
input(
_usernameController,
'Username',
false,
(value) {
if (value == null || value.isEmpty) {
return 'Create a username';
}
return null;
},
), ),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Create a username';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: _passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
const SizedBox(height: 10),
input(
_passwordController,
'Password',
true,
(value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
), ),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: _passwordConfirmController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Confirm Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
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;
},
), ),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (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),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
signUp(
context,
_usernameController.text,
_passwordController.text,
_passwordConfirmController.text
).then((value) {
Navigator.of(context).popUntil((route) => route.isFirst);
}).catchError((error) {
print(error); // TODO: Show error on interface
});
},
child: const Text('Submit'),
),
],
const SizedBox(height: 15),
serverUrl(),
const SizedBox(height: 15),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
signUp()
.then((dynamic) {
Navigator.of(context).popUntil((route) => route.isFirst);
}).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<SignupResponse> 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: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': _usernameController.text,
'password': _passwordController.text,
'confirm_password': _passwordConfirmController.text,
'asymmetric_public_key': rsaPubPem,
'asymmetric_private_key': encRsaPriv,
}),
);
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
if (resp.statusCode != 201) {
throw Exception(response.message);
}
return response;
}
} }

+ 2
- 3
mobile/lib/views/main/friend/add_search.dart View File

@ -2,12 +2,11 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
import '/components/user_search_result.dart'; import '/components/user_search_result.dart';
import '/data_models/user_search.dart'; import '/data_models/user_search.dart';
import '/models/my_profile.dart';
class FriendAddSearch extends StatefulWidget { class FriendAddSearch extends StatefulWidget {
const FriendAddSearch({ const FriendAddSearch({
@ -123,7 +122,7 @@ class _FriendAddSearchState extends State<FriendAddSearch> {
Map<String, String> params = {}; Map<String, String> params = {};
params['username'] = searchController.text; params['username'] = searchController.text;
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/users');
var uri = await MyProfile.getServerUrl('api/v1/auth/users');
uri = uri.replace(queryParameters: params); uri = uri.replace(queryParameters: params);
var resp = await http.get( var resp = await http.get(


+ 5
- 6
mobile/lib/views/main/friend/request_list_item.dart View File

@ -1,16 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/components/flash_message.dart';
import 'package:Envelope/utils/storage/database.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '/components/flash_message.dart';
import '/components/custom_circle_avatar.dart'; import '/components/custom_circle_avatar.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/utils/storage/session_cookie.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
import '/utils/storage/session_cookie.dart';
import '/utils/storage/database.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/strings.dart'; import '/utils/strings.dart';
@ -122,7 +121,7 @@ class _FriendRequestListItemState extends State<FriendRequestListItem> {
}); });
var resp = await http.post( var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/${widget.friend.id}'),
await MyProfile.getServerUrl('api/v1/auth/friend_request/${widget.friend.id}'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
}, },
@ -153,7 +152,7 @@ class _FriendRequestListItemState extends State<FriendRequestListItem> {
Future<void> rejectFriendRequest() async { Future<void> rejectFriendRequest() async {
var resp = await http.delete( var resp = await http.delete(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/${widget.friend.id}'),
await MyProfile.getServerUrl('api/v1/auth/friend_request/${widget.friend.id}'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
}, },


+ 1
- 2
mobile/lib/views/main/home.dart View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '/models/conversations.dart'; import '/models/conversations.dart';
@ -94,7 +93,7 @@ class _HomeState extends State<Home> {
int statusCode = 200; int statusCode = 200;
try { try {
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/check'),
await MyProfile.getServerUrl('api/v1/auth/check'),
headers: { headers: {
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),
} }


+ 2
- 2
mobile/lib/views/main/profile/change_password.dart View File

@ -1,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pointycastle/impl.dart'; import 'package:pointycastle/impl.dart';
import '/components/flash_message.dart'; import '/components/flash_message.dart';
import '/components/custom_title_bar.dart'; import '/components/custom_title_bar.dart';
import '/models/my_profile.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
@ -155,7 +155,7 @@ class ChangePassword extends StatelessWidget {
}); });
var resp = await http.post( var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/change_password'),
await MyProfile.getServerUrl('api/v1/auth/change_password'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'cookie': await getSessionCookie(), 'cookie': await getSessionCookie(),


+ 142
- 0
mobile/lib/views/main/profile/change_server_url.dart View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/components/custom_title_bar.dart';
import '/models/my_profile.dart';
import '/utils/storage/database.dart';
@immutable
class ChangeServerUrl extends StatefulWidget {
const ChangeServerUrl({
Key? key,
}) : super(key: key);
@override
State<ChangeServerUrl> createState() => _ChangeServerUrl();
}
class _ChangeServerUrl extends State<ChangeServerUrl> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _serverUrlController = TextEditingController();
bool invalidCurrentPassword = false;
@override
void initState() {
setUrl();
super.initState();
}
Future<void> setUrl() async {
final preferences = await SharedPreferences.getInstance();
_serverUrlController.text = preferences.getString('server_url') ?? defaultServerUrl;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomTitleBar(
title: Text(
'Profile',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold
)
),
showBack: true,
backgroundColor: Colors.transparent,
),
body: SingleChildScrollView(
child: Center(
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 30,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Change Server Url',
style: TextStyle(
fontSize: 25,
),
),
const SizedBox(height: 30),
showWarning(),
const SizedBox(height: 30),
TextFormField(
controller: _serverUrlController,
decoration: const InputDecoration(
hintText: 'Server Url',
),
// The validator receives the text that the user has entered.
validator: (String? value) {
if (value == null || !Uri.parse(value).isAbsolute) {
return 'Invalid URL';
}
return null;
},
),
const SizedBox(height: 15),
ElevatedButton(
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
},
child: const Text('CHANGE SERVER URL'),
),
],
)
)
)
)
)
);
}
Widget showWarning() {
String warning1 = '''
WARNING: Do not use this feature unless you know what you\'re doing!
''';
String warning2 = '''
Changing the server url will disconnect you from all friends and conversations on this server, and connect you to a fresh environment. This feature is intended to be used by people that are willing to host their own Envelope server, which you can find by going to \nhttps://github.com/SomeUsername/SomeRepo.\n\n
You can revert this by entering \nhttps://envelope-messenger.com\n on the login screen.
''';
return Column(
children: [
Text(
warning1,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
)
),
Text(
warning2,
textAlign: TextAlign.center,
),
],
);
}
// TODO: Write user data to new server??
Future<void> changeUrl() async {
MyProfile.setServerUrl(_serverUrlController.text);
deleteDb();
MyProfile.logout();
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
}
}

+ 7
- 3
mobile/lib/views/main/profile/profile.dart View File

@ -1,7 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:Envelope/views/main/profile/change_password.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@ -10,7 +8,10 @@ import 'package:sliding_up_panel/sliding_up_panel.dart';
import '/components/custom_circle_avatar.dart'; import '/components/custom_circle_avatar.dart';
import '/components/custom_title_bar.dart'; import '/components/custom_title_bar.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/views/main/profile/change_password.dart';
import '/views/main/profile/change_server_url.dart';
class Profile extends StatefulWidget { class Profile extends StatefulWidget {
final MyProfile profile; final MyProfile profile;
@ -178,7 +179,10 @@ class _ProfileState extends State<Profile> {
) )
), ),
onPressed: () { onPressed: () {
print('Server URL');
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ChangeServerUrl(
))
);
} }
), ),
const SizedBox(height: 5), const SizedBox(height: 5),


Loading…
Cancel
Save