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
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


+ 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("/change_password", Auth.ChangePassword).Methods("POST")
authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST")
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"`
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
}

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

@ -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<void> 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(


+ 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 '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<String, dynamic> 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,
});
}


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

@ -192,7 +192,6 @@ class _LoginWidgetState extends State<LoginWidget> {
);
}
Future<dynamic> login() async {
final resp = await http.post(
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> {
List<Message> messages = [];
MyProfile profile = MyProfile(id: '', username: '');
MyProfile profile = MyProfile(
id: '',
username: '',
messageExpiryDefault: 'no_expiry',
);
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>[
TextField(
decoration: const InputDecoration(
hintText: "Search...",
hintText: 'Search...',
prefixIcon: Icon(
Icons.search,
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 '/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<ConversationListItem> {
);
})).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: <Widget>[
Expanded(
child: Row(
children: <Widget>[
CustomCircleAvatar(
initials: widget.conversation.name[0].toUpperCase(),
imagePath: null,
),
const SizedBox(width: 16),
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


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

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


Loading…
Cancel
Save