Browse Source

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
pull/2/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
54068e805d
13 changed files with 301 additions and 79 deletions
  1. +52
    -0
      Backend/Api/Auth/ChangeMessageExpiry.go
  2. +17
    -8
      Backend/Api/Auth/Login.go
  3. +1
    -0
      Backend/Api/Routes.go
  4. +1
    -1
      Backend/Models/Users.go
  5. +7
    -1
      mobile/lib/components/custom_title_bar.dart
  6. +108
    -0
      mobile/lib/components/select_message_ttl.dart
  7. +5
    -1
      mobile/lib/models/my_profile.dart
  8. +0
    -1
      mobile/lib/views/authentication/login.dart
  9. +5
    -1
      mobile/lib/views/main/conversation/detail.dart
  10. +1
    -1
      mobile/lib/views/main/conversation/list.dart
  11. +57
    -56
      mobile/lib/views/main/conversation/list_item.dart
  12. +8
    -6
      mobile/lib/views/main/home.dart
  13. +39
    -3
      mobile/lib/views/main/profile/profile.dart

+ 52
- 0
Backend/Api/Auth/ChangeMessageExpiry.go View File

@ -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)
}

+ 17
- 8
Backend/Api/Auth/Login.go View File

@ -1,6 +1,7 @@
package Auth package Auth
import ( import (
"database/sql/driver"
"encoding/json" "encoding/json"
"net/http" "net/http"
"time" "time"
@ -9,7 +10,7 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
) )
type Credentials struct {
type credentials struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
@ -21,25 +22,32 @@ type loginResponse struct {
AsymmetricPrivateKey string `json:"asymmetric_private_key"` AsymmetricPrivateKey string `json:"asymmetric_private_key"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
MessageExpiryDefault string `json:"message_expiry_default"`
} }
func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, user Models.User) { func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, user Models.User) {
var ( 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" status = "success"
} }
returnJson, err = json.MarshalIndent(loginResponse{
messageExpiryRaw, _ = user.MessageExpiryDefault.Value()
messageExpiry, _ = messageExpiryRaw.(string)
returnJSON, err = json.MarshalIndent(loginResponse{
Status: status, Status: status,
Message: message, Message: message,
AsymmetricPublicKey: pubKey, AsymmetricPublicKey: pubKey,
AsymmetricPrivateKey: privKey, AsymmetricPrivateKey: privKey,
UserID: user.ID.String(), UserID: user.ID.String(),
Username: user.Username, Username: user.Username,
MessageExpiryDefault: messageExpiry,
}, "", " ") }, "", " ")
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) http.Error(w, "Error", http.StatusInternalServerError)
@ -48,12 +56,13 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey
// Return updated json // Return updated json
w.WriteHeader(code) w.WriteHeader(code)
w.Write(returnJson)
w.Write(returnJSON)
} }
// Login logs the user into the system
func Login(w http.ResponseWriter, r *http.Request) { func Login(w http.ResponseWriter, r *http.Request) {
var ( var (
creds Credentials
creds credentials
userData Models.User userData Models.User
session Models.Session session Models.Session
expiresAt time.Time expiresAt time.Time


+ 1
- 0
Backend/Api/Routes.go View File

@ -62,6 +62,7 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/check", Auth.Check).Methods("GET") authAPI.HandleFunc("/check", Auth.Check).Methods("GET")
authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST") authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST")
authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST")
authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET") authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET")


+ 1
- 1
Backend/Models/Users.go View File

@ -58,6 +58,6 @@ type User struct {
ConfirmPassword string `gorm:"-" json:"confirm_password"` ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` 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
} }

+ 7
- 1
mobile/lib/components/custom_title_bar.dart View File

@ -8,12 +8,14 @@ class CustomTitleBar extends StatelessWidget with PreferredSizeWidget {
required this.showBack, required this.showBack,
this.rightHandButton, this.rightHandButton,
this.backgroundColor, this.backgroundColor,
this.beforeBack,
}) : super(key: key); }) : super(key: key);
final Text title; final Text title;
final bool showBack; final bool showBack;
final IconButton? rightHandButton; final IconButton? rightHandButton;
final Color? backgroundColor; final Color? backgroundColor;
final Future<void> Function()? beforeBack;
@override @override
Size get preferredSize => const Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@ -59,7 +61,11 @@ class CustomTitleBar extends StatelessWidget with PreferredSizeWidget {
Widget _backButton(BuildContext context) { Widget _backButton(BuildContext context) {
return IconButton( return IconButton(
onPressed: (){
onPressed: () {
if (beforeBack != null) {
beforeBack!().then((dynamic) => Navigator.pop(context));
return;
}
Navigator.pop(context); Navigator.pop(context);
}, },
icon: Icon( icon: Icon(


+ 108
- 0
mobile/lib/components/select_message_ttl.dart View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import '/components/custom_title_bar.dart';
const Map<String, String> 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<void> Function(String messageExpiry) backCallback;
final String? currentSelected;
@override
_SelectMessageTTLState createState() => _SelectMessageTTLState();
}
class _SelectMessageTTLState extends State<SelectMessageTTL> {
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,
),
),
)
)
],
)
)
);
},
);
}
}

+ 5
- 1
mobile/lib/models/my_profile.dart View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/components/select_message_ttl.dart';
import 'package:flutter_dotenv/flutter_dotenv.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';
@ -10,7 +11,6 @@ import '/utils/encryption/crypto_utils.dart';
// TODO: Replace this with the prod url when server is deployed // TODO: Replace this with the prod url when server is deployed
String defaultServerUrl = dotenv.env['SERVER_URL'] ?? 'http://192.168.1.5:8080'; String defaultServerUrl = dotenv.env['SERVER_URL'] ?? 'http://192.168.1.5:8080';
class MyProfile { class MyProfile {
String id; String id;
String username; String username;
@ -18,6 +18,7 @@ class MyProfile {
RSAPrivateKey? privateKey; RSAPrivateKey? privateKey;
RSAPublicKey? publicKey; RSAPublicKey? publicKey;
DateTime? loggedInAt; DateTime? loggedInAt;
String messageExpiryDefault = 'no_expiry';
MyProfile({ MyProfile({
required this.id, required this.id,
@ -26,6 +27,7 @@ class MyProfile {
this.privateKey, this.privateKey,
this.publicKey, this.publicKey,
this.loggedInAt, this.loggedInAt,
required this.messageExpiryDefault,
}); });
factory MyProfile._fromJson(Map<String, dynamic> json) { factory MyProfile._fromJson(Map<String, dynamic> json) {
@ -43,6 +45,7 @@ class MyProfile {
privateKey: privateKey, privateKey: privateKey,
publicKey: publicKey, publicKey: publicKey,
loggedInAt: loggedInAt, loggedInAt: loggedInAt,
messageExpiryDefault: json['message_expiry_default']
); );
} }
@ -68,6 +71,7 @@ class MyProfile {
CryptoUtils.encodeRSAPublicKeyToPem(publicKey!) : CryptoUtils.encodeRSAPublicKeyToPem(publicKey!) :
null, null,
'logged_in_at': loggedInAt?.toIso8601String(), 'logged_in_at': loggedInAt?.toIso8601String(),
'message_expiry_default': messageExpiryDefault,
}); });
} }


+ 0
- 1
mobile/lib/views/authentication/login.dart View File

@ -192,7 +192,6 @@ class _LoginWidgetState extends State<LoginWidget> {
); );
} }
Future<dynamic> login() async { Future<dynamic> login() async {
final resp = await http.post( final resp = await http.post(
await MyProfile.getServerUrl('api/v1/login'), await MyProfile.getServerUrl('api/v1/login'),


+ 5
- 1
mobile/lib/views/main/conversation/detail.dart View File

@ -22,7 +22,11 @@ class ConversationDetail extends StatefulWidget{
class _ConversationDetailState extends State<ConversationDetail> { class _ConversationDetailState extends State<ConversationDetail> {
List<Message> messages = []; List<Message> messages = [];
MyProfile profile = MyProfile(id: '', username: '');
MyProfile profile = MyProfile(
id: '',
username: '',
messageExpiryDefault: 'no_expiry',
);
TextEditingController msgController = TextEditingController(); TextEditingController msgController = TextEditingController();


+ 1
- 1
mobile/lib/views/main/conversation/list.dart View File

@ -47,7 +47,7 @@ class _ConversationListState extends State<ConversationList> {
children: <Widget>[ children: <Widget>[
TextField( TextField(
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Search...",
hintText: 'Search...',
prefixIcon: Icon( prefixIcon: Icon(
Icons.search, Icons.search,
size: 20 size: 20


+ 57
- 56
mobile/lib/views/main/conversation/list_item.dart View File

@ -6,7 +6,7 @@ import '/models/conversations.dart';
import '/views/main/conversation/detail.dart'; import '/views/main/conversation/detail.dart';
import '/utils/time.dart'; import '/utils/time.dart';
class ConversationListItem extends StatefulWidget{
class ConversationListItem extends StatefulWidget {
final Conversation conversation; final Conversation conversation;
const ConversationListItem({ const ConversationListItem({
Key? key, Key? key,
@ -33,70 +33,71 @@ class _ConversationListItemState extends State<ConversationListItem> {
); );
})).then(onGoBack) : null; })).then(onGoBack) : null;
}, },
child: Container( 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: <Widget>[
Expanded(
child: Row(
children: <Widget>[ children: <Widget>[
CustomCircleAvatar(
initials: widget.conversation.name[0].toUpperCase(),
imagePath: null,
),
const SizedBox(width: 16),
Expanded( Expanded(
child: Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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 @override


+ 8
- 6
mobile/lib/views/main/home.dart View File

@ -24,8 +24,9 @@ class _HomeState extends State<Home> {
List<Friend> friends = []; List<Friend> friends = [];
List<Friend> friendRequests = []; List<Friend> friendRequests = [];
MyProfile profile = MyProfile( MyProfile profile = MyProfile(
id: '',
username: '',
id: '',
username: '',
messageExpiryDefault: 'no_expiry',
); );
bool isLoading = true; bool isLoading = true;
@ -34,10 +35,11 @@ class _HomeState extends State<Home> {
const ConversationList(conversations: [], friends: []), const ConversationList(conversations: [], friends: []),
FriendList(friends: const [], friendRequests: const [], callback: () {}), FriendList(friends: const [], friendRequests: const [], callback: () {}),
Profile( Profile(
profile: MyProfile(
id: '',
username: '',
)
profile: MyProfile(
id: '',
username: '',
messageExpiryDefault: 'no_expiry',
)
), ),
]; ];


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

@ -1,10 +1,14 @@
import 'dart:convert'; 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/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';
import 'package:sliding_up_panel/sliding_up_panel.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_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';
@ -144,7 +148,9 @@ class _ProfileState extends State<Profile> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 5), const SizedBox(height: 5),
TextButton.icon( TextButton.icon(
label: const Text( label: const Text(
'Disappearing Messages', 'Disappearing Messages',
@ -160,10 +166,39 @@ class _ProfileState extends State<Profile> {
) )
), ),
onPressed: () { 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), const SizedBox(height: 5),
TextButton.icon( TextButton.icon(
label: const Text( label: const Text(
'Server URL', 'Server URL',
@ -180,12 +215,13 @@ class _ProfileState extends State<Profile> {
), ),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ChangeServerUrl(
))
MaterialPageRoute(builder: (context) => const ChangeServerUrl())
); );
} }
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
TextButton.icon( TextButton.icon(
label: const Text( label: const Text(
'Change Password', 'Change Password',


Loading…
Cancel
Save