From 54068e805d5ddf4c492f02f1ab02b8ae621e3187 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Wed, 24 Aug 2022 18:57:31 +0930 Subject: [PATCH] Update the profile page Add change password page and route Add disappearing messages page and route Add the ability to change the server URL Update the look and feel of the qr code --- Backend/Api/Auth/ChangeMessageExpiry.go | 52 ++++++++ Backend/Api/Auth/Login.go | 25 ++-- Backend/Api/Routes.go | 1 + Backend/Models/Users.go | 2 +- mobile/lib/components/custom_title_bar.dart | 8 +- mobile/lib/components/select_message_ttl.dart | 108 +++++++++++++++++ mobile/lib/models/my_profile.dart | 6 +- mobile/lib/views/authentication/login.dart | 1 - .../lib/views/main/conversation/detail.dart | 6 +- mobile/lib/views/main/conversation/list.dart | 2 +- .../views/main/conversation/list_item.dart | 113 +++++++++--------- mobile/lib/views/main/home.dart | 14 ++- mobile/lib/views/main/profile/profile.dart | 42 ++++++- 13 files changed, 301 insertions(+), 79 deletions(-) create mode 100644 Backend/Api/Auth/ChangeMessageExpiry.go create mode 100644 mobile/lib/components/select_message_ttl.dart diff --git a/Backend/Api/Auth/ChangeMessageExpiry.go b/Backend/Api/Auth/ChangeMessageExpiry.go new file mode 100644 index 0000000..8f8721f --- /dev/null +++ b/Backend/Api/Auth/ChangeMessageExpiry.go @@ -0,0 +1,52 @@ +package Auth + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +type rawChangeMessageExpiry struct { + MessageExpiry string `json:"message_exipry"` +} + +// ChangeMessageExpiry handles changing default message expiry for user +func ChangeMessageExpiry(w http.ResponseWriter, r *http.Request) { + var ( + user Models.User + changeMessageExpiry rawChangeMessageExpiry + requestBody []byte + err error + ) + + // Ignore error here, as middleware should handle auth + user, _ = CheckCookieCurrentUser(w, r) + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = json.Unmarshal(requestBody, &changeMessageExpiry) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + user.AsymmetricPrivateKey = changeMessageExpiry.MessageExpiry + + err = Database.UpdateUser( + user.ID.String(), + &user, + ) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go index 44f26e7..61225af 100644 --- a/Backend/Api/Auth/Login.go +++ b/Backend/Api/Auth/Login.go @@ -1,6 +1,7 @@ package Auth import ( + "database/sql/driver" "encoding/json" "net/http" "time" @@ -9,7 +10,7 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) -type Credentials struct { +type credentials struct { Username string `json:"username"` Password string `json:"password"` } @@ -21,25 +22,32 @@ type loginResponse struct { AsymmetricPrivateKey string `json:"asymmetric_private_key"` UserID string `json:"user_id"` Username string `json:"username"` + MessageExpiryDefault string `json:"message_expiry_default"` } func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, user Models.User) { var ( - status string = "error" - returnJson []byte - err error + status = "error" + messageExpiryRaw driver.Value + messageExpiry string + returnJSON []byte + err error ) - if code > 200 && code < 300 { + if code >= 200 && code <= 300 { status = "success" } - returnJson, err = json.MarshalIndent(loginResponse{ + messageExpiryRaw, _ = user.MessageExpiryDefault.Value() + messageExpiry, _ = messageExpiryRaw.(string) + + returnJSON, err = json.MarshalIndent(loginResponse{ Status: status, Message: message, AsymmetricPublicKey: pubKey, AsymmetricPrivateKey: privKey, UserID: user.ID.String(), Username: user.Username, + MessageExpiryDefault: messageExpiry, }, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) @@ -48,12 +56,13 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey // Return updated json w.WriteHeader(code) - w.Write(returnJson) + w.Write(returnJSON) } +// Login logs the user into the system func Login(w http.ResponseWriter, r *http.Request) { var ( - creds Credentials + creds credentials userData Models.User session Models.Session expiresAt time.Time diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index f7b8151..999a2f2 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -62,6 +62,7 @@ func InitAPIEndpoints(router *mux.Router) { authAPI.HandleFunc("/check", Auth.Check).Methods("GET") authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST") + authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST") authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET") diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go index 33d2069..685b774 100644 --- a/Backend/Models/Users.go +++ b/Backend/Models/Users.go @@ -58,6 +58,6 @@ type User struct { 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 + MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM('fifteen_min', 'thirty_min', 'one_hour', 'three_hour', 'six_hour', 'twelve_hour', 'one_day', 'three_day')"` // Stored encrypted } diff --git a/mobile/lib/components/custom_title_bar.dart b/mobile/lib/components/custom_title_bar.dart index 527b1d2..45cd96b 100644 --- a/mobile/lib/components/custom_title_bar.dart +++ b/mobile/lib/components/custom_title_bar.dart @@ -8,12 +8,14 @@ class CustomTitleBar extends StatelessWidget with PreferredSizeWidget { required this.showBack, this.rightHandButton, this.backgroundColor, + this.beforeBack, }) : super(key: key); final Text title; final bool showBack; final IconButton? rightHandButton; final Color? backgroundColor; + final Future Function()? beforeBack; @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); @@ -59,7 +61,11 @@ class CustomTitleBar extends StatelessWidget with PreferredSizeWidget { Widget _backButton(BuildContext context) { return IconButton( - onPressed: (){ + onPressed: () { + if (beforeBack != null) { + beforeBack!().then((dynamic) => Navigator.pop(context)); + return; + } Navigator.pop(context); }, icon: Icon( diff --git a/mobile/lib/components/select_message_ttl.dart b/mobile/lib/components/select_message_ttl.dart new file mode 100644 index 0000000..c2be882 --- /dev/null +++ b/mobile/lib/components/select_message_ttl.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +import '/components/custom_title_bar.dart'; + +const Map messageExpiryValues = { + 'no_expiry': 'No Expiry', + 'fifteen_min': '15 Minutes', + 'thirty_min': '30 Minutes', + 'one_hour': '1 Hour', + 'three_hour': '3 Hours', + 'six_hour': '6 Hours', + 'twelve_day': '12 Hours', + 'one_day': '1 Day', + 'three_day': '3 Days', +}; + +class SelectMessageTTL extends StatefulWidget { + const SelectMessageTTL({ + Key? key, + required this.widgetTitle, + required this.backCallback, + this.currentSelected, + }) : super(key: key); + + final String widgetTitle; + final Future Function(String messageExpiry) backCallback; + final String? currentSelected; + + @override + _SelectMessageTTLState createState() => _SelectMessageTTLState(); +} + +class _SelectMessageTTLState extends State { + String selectedExpiry = 'no_expiry'; + + @override + void initState() { + selectedExpiry = widget.currentSelected ?? 'no_expiry'; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomTitleBar( + title: Text( + widget.widgetTitle, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold + ) + ), + showBack: true, + backgroundColor: Colors.transparent, + beforeBack: () async { + widget.backCallback(selectedExpiry); + }, + ), + body: Padding( + padding: const EdgeInsets.only(top: 30), + child: list(), + ), + ); + } + + Widget list() { + return ListView.builder( + itemCount: messageExpiryValues.length, + shrinkWrap: true, + itemBuilder: (context, i) { + String key = messageExpiryValues.keys.elementAt(i); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + selectedExpiry = key; + }); + }, + + child: Padding( + padding: const EdgeInsets.only(left: 30, right: 20, top: 8, bottom: 8), + child: Row( + children: [ + selectedExpiry == key ? + const Icon(Icons.check) : + const SizedBox(width: 20), + const SizedBox(width: 16), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + messageExpiryValues[key] ?? '', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + ), + ), + ) + ) + ], + ) + ) + ); + }, + ); + } +} diff --git a/mobile/lib/models/my_profile.dart b/mobile/lib/models/my_profile.dart index 07ec14a..526e668 100644 --- a/mobile/lib/models/my_profile.dart +++ b/mobile/lib/models/my_profile.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:Envelope/components/select_message_ttl.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:pointycastle/impl.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -10,7 +11,6 @@ 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 { String id; String username; @@ -18,6 +18,7 @@ class MyProfile { RSAPrivateKey? privateKey; RSAPublicKey? publicKey; DateTime? loggedInAt; + String messageExpiryDefault = 'no_expiry'; MyProfile({ required this.id, @@ -26,6 +27,7 @@ class MyProfile { this.privateKey, this.publicKey, this.loggedInAt, + required this.messageExpiryDefault, }); factory MyProfile._fromJson(Map json) { @@ -43,6 +45,7 @@ class MyProfile { privateKey: privateKey, publicKey: publicKey, loggedInAt: loggedInAt, + messageExpiryDefault: json['message_expiry_default'] ); } @@ -68,6 +71,7 @@ class MyProfile { CryptoUtils.encodeRSAPublicKeyToPem(publicKey!) : null, 'logged_in_at': loggedInAt?.toIso8601String(), + 'message_expiry_default': messageExpiryDefault, }); } diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index be227a6..dd8e869 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -192,7 +192,6 @@ class _LoginWidgetState extends State { ); } - Future login() async { final resp = await http.post( await MyProfile.getServerUrl('api/v1/login'), diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index d0bbca2..572f855 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -22,7 +22,11 @@ class ConversationDetail extends StatefulWidget{ class _ConversationDetailState extends State { List messages = []; - MyProfile profile = MyProfile(id: '', username: ''); + MyProfile profile = MyProfile( + id: '', + username: '', + messageExpiryDefault: 'no_expiry', + ); TextEditingController msgController = TextEditingController(); diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index cabf6f0..62be875 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -47,7 +47,7 @@ class _ConversationListState extends State { children: [ TextField( decoration: const InputDecoration( - hintText: "Search...", + hintText: 'Search...', prefixIcon: Icon( Icons.search, size: 20 diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index 816b996..a94e900 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -6,7 +6,7 @@ import '/models/conversations.dart'; import '/views/main/conversation/detail.dart'; import '/utils/time.dart'; -class ConversationListItem extends StatefulWidget{ +class ConversationListItem extends StatefulWidget { final Conversation conversation; const ConversationListItem({ Key? key, @@ -33,70 +33,71 @@ class _ConversationListItemState extends State { ); })).then(onGoBack) : null; }, + child: Container( - padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10), - child: !loaded ? null : Row( + padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10), + child: !loaded ? null : Row( + children: [ + Expanded( + child: Row( children: [ + CustomCircleAvatar( + initials: widget.conversation.name[0].toUpperCase(), + imagePath: null, + ), + const SizedBox(width: 16), Expanded( - child: Row( - children: [ - CustomCircleAvatar( - initials: widget.conversation.name[0].toUpperCase(), - imagePath: null, - ), - const SizedBox(width: 16), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.conversation.name, - style: const TextStyle(fontSize: 16) - ), - recentMessage != null ? - const SizedBox(height: 2) : - const SizedBox.shrink() - , - recentMessage != null ? - Text( - recentMessage!.data, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold, - ), - ) : - const SizedBox.shrink(), - ], - ), - ), - ), + child: Align( + alignment: Alignment.centerLeft, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.conversation.name, + style: const TextStyle(fontSize: 16) + ), + recentMessage != null ? + const SizedBox(height: 2) : + const SizedBox.shrink() + , + recentMessage != null ? + Text( + recentMessage!.data, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold, + ), + ) : + const SizedBox.shrink(), + ], ), - recentMessage != null ? - Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - convertToAgo(recentMessage!.createdAt, short: true), - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - ), - ) - ): - const SizedBox.shrink(), - ], + ), ), ), + recentMessage != null ? + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + convertToAgo(recentMessage!.createdAt, short: true), + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ) + ): + const SizedBox.shrink(), ], ), ), - ); + ], + ), + ), + ); } @override diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index b069fe6..b590798 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -24,8 +24,9 @@ class _HomeState extends State { List friends = []; List friendRequests = []; MyProfile profile = MyProfile( - id: '', - username: '', + id: '', + username: '', + messageExpiryDefault: 'no_expiry', ); bool isLoading = true; @@ -34,10 +35,11 @@ class _HomeState extends State { const ConversationList(conversations: [], friends: []), FriendList(friends: const [], friendRequests: const [], callback: () {}), Profile( - profile: MyProfile( - id: '', - username: '', - ) + profile: MyProfile( + id: '', + username: '', + messageExpiryDefault: 'no_expiry', + ) ), ]; diff --git a/mobile/lib/views/main/profile/profile.dart b/mobile/lib/views/main/profile/profile.dart index bedadcf..ae7df99 100644 --- a/mobile/lib/views/main/profile/profile.dart +++ b/mobile/lib/views/main/profile/profile.dart @@ -1,10 +1,14 @@ import 'dart:convert'; +import 'package:Envelope/components/flash_message.dart'; +import 'package:Envelope/utils/storage/session_cookie.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:http/http.dart' as http; +import '/components/select_message_ttl.dart'; import '/components/custom_circle_avatar.dart'; import '/components/custom_title_bar.dart'; import '/models/my_profile.dart'; @@ -144,7 +148,9 @@ class _ProfileState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const SizedBox(height: 5), + TextButton.icon( label: const Text( 'Disappearing Messages', @@ -160,10 +166,39 @@ class _ProfileState extends State { ) ), onPressed: () { - print('Disappearing Messages'); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => SelectMessageTTL( + widgetTitle: 'Message Expiry', + currentSelected: widget.profile.messageExpiryDefault, + backCallback: (String messageExpiry) async { + widget.profile.messageExpiryDefault = messageExpiry; + + http.post( + await MyProfile.getServerUrl('api/v1/auth/message_expiry'), + headers: { + 'cookie': await getSessionCookie(), + }, + body: jsonEncode({ + 'message_expiry': messageExpiry, + }), + ).then((http.Response response) { + if (response.statusCode == 200) { + return; + } + + showMessage( + 'Could not change your default message expiry, please try again later.', + context, + ); + }); + }, + )) + ); } ), + const SizedBox(height: 5), + TextButton.icon( label: const Text( 'Server URL', @@ -180,12 +215,13 @@ class _ProfileState extends State { ), onPressed: () { Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ChangeServerUrl( - )) + MaterialPageRoute(builder: (context) => const ChangeServerUrl()) ); } ), + const SizedBox(height: 5), + TextButton.icon( label: const Text( 'Change Password',