Added custom error message for when interactions failpull/1/head
@ -0,0 +1,75 @@ | |||
package Friends | |||
import ( | |||
"encoding/json" | |||
"io/ioutil" | |||
"net/http" | |||
"time" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||
"github.com/gorilla/mux" | |||
) | |||
// AcceptFriendRequest accepts friend requests | |||
func AcceptFriendRequest(w http.ResponseWriter, r *http.Request) { | |||
var ( | |||
oldFriendRequest Models.FriendRequest | |||
newFriendRequest Models.FriendRequest | |||
urlVars map[string]string | |||
friendRequestID string | |||
requestBody []byte | |||
ok bool | |||
err error | |||
) | |||
urlVars = mux.Vars(r) | |||
friendRequestID, ok = urlVars["requestID"] | |||
if !ok { | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
oldFriendRequest, err = Database.GetFriendRequestByID(friendRequestID) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
oldFriendRequest.AcceptedAt.Time = time.Now() | |||
oldFriendRequest.AcceptedAt.Valid = true | |||
requestBody, err = ioutil.ReadAll(r.Body) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
err = json.Unmarshal(requestBody, &newFriendRequest) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
err = Database.UpdateFriendRequest(&oldFriendRequest) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
newFriendRequest.AcceptedAt.Time = time.Now() | |||
newFriendRequest.AcceptedAt.Valid = true | |||
err = Database.CreateFriendRequest(&newFriendRequest) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
w.WriteHeader(http.StatusNoContent) | |||
} |
@ -0,0 +1,44 @@ | |||
package Friends | |||
import ( | |||
"net/http" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||
"github.com/gorilla/mux" | |||
) | |||
// RejectFriendRequest rejects friend requests | |||
func RejectFriendRequest(w http.ResponseWriter, r *http.Request) { | |||
var ( | |||
friendRequest Models.FriendRequest | |||
urlVars map[string]string | |||
friendRequestID string | |||
ok bool | |||
err error | |||
) | |||
urlVars = mux.Vars(r) | |||
friendRequestID, ok = urlVars["requestID"] | |||
if !ok { | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
friendRequest, err = Database.GetFriendRequestByID(friendRequestID) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
err = Database.DeleteFriendRequest(&friendRequest) | |||
if err != nil { | |||
http.Error(w, "Error", http.StatusInternalServerError) | |||
w.WriteHeader(http.StatusInternalServerError) | |||
return | |||
} | |||
w.WriteHeader(http.StatusNoContent) | |||
} |
@ -0,0 +1,56 @@ | |||
package Users | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"net/url" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||
) | |||
// SearchUsers searches a for a user by username | |||
func SearchUsers(w http.ResponseWriter, r *http.Request) { | |||
var ( | |||
user Models.User | |||
query url.Values | |||
rawUsername []string | |||
username string | |||
returnJSON []byte | |||
ok bool | |||
err error | |||
) | |||
query = r.URL.Query() | |||
rawUsername, ok = query["username"] | |||
if !ok { | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
if len(rawUsername) != 1 { | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
username = rawUsername[0] | |||
user, err = Database.GetUserByUsername(username) | |||
if err != nil { | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
user.Password = "" | |||
user.AsymmetricPrivateKey = "" | |||
returnJSON, err = json.MarshalIndent(user, "", " ") | |||
if err != nil { | |||
panic(err) | |||
http.Error(w, "Not Found", http.StatusNotFound) | |||
return | |||
} | |||
w.WriteHeader(http.StatusOK) | |||
w.Write(returnJSON) | |||
} |
@ -1,20 +1,19 @@ | |||
package Models | |||
import ( | |||
"time" | |||
"database/sql" | |||
"github.com/gofrs/uuid" | |||
) | |||
// Set with Friend being the requestee, and RequestFromID being the requester | |||
// FriendRequest Set with Friend being the requestee, and RequestFromID being the requester | |||
type FriendRequest struct { | |||
Base | |||
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` | |||
User User ` json:"user"` | |||
UserUsername string ` json:"user_username"` | |||
FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted | |||
FriendUsername string ` json:"friend_username"` | |||
FriendPublicAsymmetricKey string ` json:"asymmetric_public_key"` | |||
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted | |||
AcceptedAt time.Time ` json:"accepted_at"` | |||
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` | |||
User User ` json:"user"` | |||
FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted | |||
FriendUsername string ` json:"friend_username"` // Stored encrypted | |||
FriendPublicAsymmetricKey string ` json:"asymmetric_public_key"` // Stored encrypted | |||
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted | |||
AcceptedAt sql.NullTime ` json:"accepted_at"` | |||
} |
@ -0,0 +1,213 @@ | |||
import 'dart:math' as math; | |||
import 'package:flutter/material.dart'; | |||
class ExpandableFab extends StatefulWidget { | |||
const ExpandableFab({ | |||
Key? key, | |||
this.initialOpen, | |||
required this.distance, | |||
required this.icon, | |||
required this.children, | |||
}) : super(key: key); | |||
final bool? initialOpen; | |||
final double distance; | |||
final Icon icon; | |||
final List<Widget> children; | |||
@override | |||
State<ExpandableFab> createState() => _ExpandableFabState(); | |||
} | |||
class _ExpandableFabState extends State<ExpandableFab> | |||
with SingleTickerProviderStateMixin { | |||
late final AnimationController _controller; | |||
late final Animation<double> _expandAnimation; | |||
bool _open = false; | |||
@override | |||
void initState() { | |||
super.initState(); | |||
_open = widget.initialOpen ?? false; | |||
_controller = AnimationController( | |||
value: _open ? 1.0 : 0.0, | |||
duration: const Duration(milliseconds: 250), | |||
vsync: this, | |||
); | |||
_expandAnimation = CurvedAnimation( | |||
curve: Curves.fastOutSlowIn, | |||
reverseCurve: Curves.easeOutQuad, | |||
parent: _controller, | |||
); | |||
} | |||
@override | |||
void dispose() { | |||
_controller.dispose(); | |||
super.dispose(); | |||
} | |||
void _toggle() { | |||
setState(() { | |||
_open = !_open; | |||
if (_open) { | |||
_controller.forward(); | |||
} else { | |||
_controller.reverse(); | |||
} | |||
}); | |||
} | |||
@override | |||
Widget build(BuildContext context) { | |||
return SizedBox.expand( | |||
child: Stack( | |||
alignment: Alignment.bottomRight, | |||
clipBehavior: Clip.none, | |||
children: [ | |||
_buildTapToCloseFab(), | |||
..._buildExpandingActionButtons(), | |||
_buildTapToOpenFab(), | |||
], | |||
), | |||
); | |||
} | |||
Widget _buildTapToCloseFab() { | |||
return SizedBox( | |||
width: 56.0, | |||
height: 56.0, | |||
child: Center( | |||
child: Material( | |||
shape: const CircleBorder(), | |||
clipBehavior: Clip.antiAlias, | |||
elevation: 4.0, | |||
child: InkWell( | |||
onTap: _toggle, | |||
child: Padding( | |||
padding: const EdgeInsets.all(8.0), | |||
child: Icon( | |||
Icons.close, | |||
color: Theme.of(context).primaryColor, | |||
), | |||
), | |||
), | |||
), | |||
), | |||
); | |||
} | |||
List<Widget> _buildExpandingActionButtons() { | |||
final children = <Widget>[]; | |||
final count = widget.children.length; | |||
final step = 60.0 / (count - 1); | |||
for (var i = 0, angleInDegrees = 15.0; | |||
i < count; | |||
i++, angleInDegrees += step) { | |||
children.add( | |||
_ExpandingActionButton( | |||
directionInDegrees: angleInDegrees, | |||
maxDistance: widget.distance, | |||
progress: _expandAnimation, | |||
child: widget.children[i], | |||
), | |||
); | |||
} | |||
return children; | |||
} | |||
Widget _buildTapToOpenFab() { | |||
return IgnorePointer( | |||
ignoring: _open, | |||
child: AnimatedContainer( | |||
transformAlignment: Alignment.center, | |||
transform: Matrix4.diagonal3Values( | |||
_open ? 0.7 : 1.0, | |||
_open ? 0.7 : 1.0, | |||
1.0, | |||
), | |||
duration: const Duration(milliseconds: 250), | |||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut), | |||
child: AnimatedOpacity( | |||
opacity: _open ? 0.0 : 1.0, | |||
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut), | |||
duration: const Duration(milliseconds: 250), | |||
child: FloatingActionButton( | |||
onPressed: _toggle, | |||
backgroundColor: Theme.of(context).colorScheme.primary, | |||
child: widget.icon, | |||
), | |||
), | |||
), | |||
); | |||
} | |||
} | |||
@immutable | |||
class _ExpandingActionButton extends StatelessWidget { | |||
const _ExpandingActionButton({ | |||
required this.directionInDegrees, | |||
required this.maxDistance, | |||
required this.progress, | |||
required this.child, | |||
}); | |||
final double directionInDegrees; | |||
final double maxDistance; | |||
final Animation<double> progress; | |||
final Widget child; | |||
@override | |||
Widget build(BuildContext context) { | |||
return AnimatedBuilder( | |||
animation: progress, | |||
builder: (context, child) { | |||
final offset = Offset.fromDirection( | |||
directionInDegrees * (math.pi / 180.0), | |||
progress.value * maxDistance, | |||
); | |||
return Positioned( | |||
right: 4.0 + offset.dx, | |||
bottom: 4.0 + offset.dy, | |||
child: Transform.rotate( | |||
angle: (1.0 - progress.value) * math.pi / 2, | |||
child: child!, | |||
), | |||
); | |||
}, | |||
child: FadeTransition( | |||
opacity: progress, | |||
child: child, | |||
), | |||
); | |||
} | |||
} | |||
class ActionButton extends StatelessWidget { | |||
const ActionButton({ | |||
Key? key, | |||
this.onPressed, | |||
required this.icon, | |||
}) : super(key: key); | |||
final VoidCallback? onPressed; | |||
final Widget icon; | |||
@override | |||
Widget build(BuildContext context) { | |||
final theme = Theme.of(context); | |||
return Material( | |||
shape: const CircleBorder(), | |||
clipBehavior: Clip.antiAlias, | |||
color: theme.colorScheme.secondary, | |||
elevation: 4.0, | |||
child: IconButton( | |||
onPressed: onPressed, | |||
icon: icon, | |||
color: theme.colorScheme.onSecondary, | |||
), | |||
); | |||
} | |||
} |
@ -0,0 +1,72 @@ | |||
import 'package:flutter/material.dart'; | |||
@immutable | |||
class CustomTitleBar extends StatelessWidget with PreferredSizeWidget { | |||
const CustomTitleBar({ | |||
Key? key, | |||
required this.title, | |||
required this.showBack, | |||
this.rightHandButton, | |||
this.backgroundColor, | |||
}) : super(key: key); | |||
final Text title; | |||
final bool showBack; | |||
final IconButton? rightHandButton; | |||
final Color? backgroundColor; | |||
@override | |||
Size get preferredSize => const Size.fromHeight(kToolbarHeight); | |||
@override | |||
Widget build(BuildContext context) { | |||
return AppBar( | |||
elevation: 0, | |||
automaticallyImplyLeading: false, | |||
backgroundColor: | |||
backgroundColor != null ? | |||
backgroundColor! : | |||
Theme.of(context).appBarTheme.backgroundColor, | |||
flexibleSpace: SafeArea( | |||
child: Container( | |||
padding: const EdgeInsets.only(right: 16), | |||
child: Row( | |||
children: <Widget>[ | |||
showBack ? | |||
_backButton(context) : | |||
const SizedBox.shrink(), | |||
showBack ? | |||
const SizedBox(width: 2,) : | |||
const SizedBox(width: 15), | |||
Expanded( | |||
child: Column( | |||
crossAxisAlignment: CrossAxisAlignment.start, | |||
mainAxisAlignment: MainAxisAlignment.center, | |||
children: <Widget>[ | |||
title, | |||
], | |||
), | |||
), | |||
rightHandButton != null ? | |||
rightHandButton! : | |||
const SizedBox.shrink(), | |||
], | |||
), | |||
), | |||
), | |||
); | |||
} | |||
Widget _backButton(BuildContext context) { | |||
return IconButton( | |||
onPressed: (){ | |||
Navigator.pop(context); | |||
}, | |||
icon: Icon( | |||
Icons.arrow_back, | |||
color: Theme.of(context).appBarTheme.iconTheme?.color, | |||
), | |||
); | |||
} | |||
} | |||
@ -0,0 +1,60 @@ | |||
import 'package:flutter/material.dart'; | |||
class FlashMessage extends StatelessWidget { | |||
const FlashMessage({ | |||
Key? key, | |||
required this.message, | |||
}) : super(key: key); | |||
final String message; | |||
@override | |||
Widget build(BuildContext context) { | |||
final theme = Theme.of(context); | |||
return Stack( | |||
clipBehavior: Clip.none, | |||
children: <Widget>[ | |||
Container( | |||
padding: const EdgeInsets.all(16), | |||
height: 90, | |||
decoration: BoxDecoration( | |||
borderRadius: const BorderRadius.all(Radius.circular(20)), | |||
color: theme.colorScheme.onError, | |||
), | |||
child: Column( | |||
children: <Widget>[ | |||
Text( | |||
'Error', | |||
style: TextStyle( | |||
color: theme.colorScheme.error, | |||
fontSize: 18 | |||
), | |||
), | |||
Text( | |||
message, | |||
style: TextStyle( | |||
color: theme.colorScheme.error, | |||
fontSize: 14 | |||
), | |||
), | |||
], | |||
), | |||
), | |||
] | |||
); | |||
} | |||
} | |||
void showMessage(String message, BuildContext context) { | |||
ScaffoldMessenger.of(context).showSnackBar( | |||
SnackBar( | |||
content: FlashMessage( | |||
message: message, | |||
), | |||
behavior: SnackBarBehavior.floating, | |||
backgroundColor: Colors.transparent, | |||
elevation: 0, | |||
), | |||
); | |||
} |
@ -0,0 +1,137 @@ | |||
import 'dart:convert'; | |||
import 'dart:typed_data'; | |||
import 'package:flutter/material.dart'; | |||
import 'package:flutter_dotenv/flutter_dotenv.dart'; | |||
import 'package:http/http.dart' as http; | |||
import 'package:pointycastle/impl.dart'; | |||
import '/components/custom_circle_avatar.dart'; | |||
import '/data_models/user_search.dart'; | |||
import '/models/my_profile.dart'; | |||
import '/utils/encryption/aes_helper.dart'; | |||
import '/utils/storage/session_cookie.dart'; | |||
import '/utils/strings.dart'; | |||
import '/utils/encryption/crypto_utils.dart'; | |||
@immutable | |||
class UserSearchResult extends StatefulWidget { | |||
final UserSearch user; | |||
const UserSearchResult({ | |||
Key? key, | |||
required this.user, | |||
}) : super(key: key); | |||
@override | |||
_UserSearchResultState createState() => _UserSearchResultState(); | |||
} | |||
class _UserSearchResultState extends State<UserSearchResult>{ | |||
bool showFailed = false; | |||
@override | |||
Widget build(BuildContext context) { | |||
return Center( | |||
child: Padding( | |||
padding: const EdgeInsets.only(top: 30), | |||
child: Column( | |||
crossAxisAlignment: CrossAxisAlignment.center, | |||
children: <Widget>[ | |||
CustomCircleAvatar( | |||
initials: widget.user.username[0].toUpperCase(), | |||
icon: const Icon(Icons.person, size: 80), | |||
imagePath: null, | |||
radius: 50, | |||
), | |||
const SizedBox(height: 10), | |||
Text( | |||
widget.user.username, | |||
style: const TextStyle( | |||
fontSize: 35, | |||
), | |||
), | |||
const SizedBox(height: 30), | |||
TextButton( | |||
onPressed: sendFriendRequest, | |||
child: Text( | |||
'Send Friend Request', | |||
style: TextStyle( | |||
color: Theme.of(context).colorScheme.onPrimary, | |||
fontSize: 20, | |||
), | |||
), | |||
style: ButtonStyle( | |||
backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary), | |||
padding: MaterialStateProperty.all<EdgeInsets>( | |||
const EdgeInsets.only(left: 20, right: 20, top: 8, bottom: 8)), | |||
), | |||
), | |||
showFailed ? const SizedBox(height: 20) : const SizedBox.shrink(), | |||
failedMessage(context), | |||
], | |||
), | |||
), | |||
); | |||
} | |||
Widget failedMessage(BuildContext context) { | |||
if (!showFailed) { | |||
return const SizedBox.shrink(); | |||
} | |||
return Text( | |||
'Failed to send friend request', | |||
style: TextStyle( | |||
color: Theme.of(context).colorScheme.error, | |||
fontSize: 16, | |||
), | |||
); | |||
} | |||
Future<void> sendFriendRequest() async { | |||
MyProfile profile = await MyProfile.getProfile(); | |||
String publicKeyString = CryptoUtils.encodeRSAPublicKeyToPem(profile.publicKey!); | |||
RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(widget.user.publicKey); | |||
final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); | |||
String payloadJson = jsonEncode({ | |||
'user_id': widget.user.id, | |||
'friend_id': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(profile.id.codeUnits), | |||
friendPublicKey, | |||
)), | |||
'friend_username': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(profile.username.codeUnits), | |||
friendPublicKey, | |||
)), | |||
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(symmetricKey), | |||
friendPublicKey, | |||
)), | |||
'asymmetric_public_key': AesHelper.aesEncrypt( | |||
symmetricKey, | |||
Uint8List.fromList(publicKeyString.codeUnits), | |||
), | |||
}); | |||
var resp = await http.post( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request'), | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
}, | |||
body: payloadJson, | |||
); | |||
if (resp.statusCode != 200) { | |||
showFailed = true; | |||
setState(() {}); | |||
return; | |||
} | |||
Navigator.pop(context); | |||
} | |||
} |
@ -0,0 +1,20 @@ | |||
class UserSearch { | |||
String id; | |||
String username; | |||
String publicKey; | |||
UserSearch({ | |||
required this.id, | |||
required this.username, | |||
required this.publicKey, | |||
}); | |||
factory UserSearch.fromJson(Map<String, dynamic> json) { | |||
return UserSearch( | |||
id: json['id'], | |||
username: json['username'], | |||
publicKey: json['asymmetric_public_key'], | |||
); | |||
} | |||
} |
@ -0,0 +1,151 @@ | |||
import 'dart:convert'; | |||
import 'package:flutter/material.dart'; | |||
import 'package:http/http.dart' as http; | |||
import 'package:flutter_dotenv/flutter_dotenv.dart'; | |||
import '/utils/storage/session_cookie.dart'; | |||
import '/components/user_search_result.dart'; | |||
import '/data_models/user_search.dart'; | |||
class FriendAddSearch extends StatefulWidget { | |||
const FriendAddSearch({ | |||
Key? key, | |||
}) : super(key: key); | |||
@override | |||
State<FriendAddSearch> createState() => _FriendAddSearchState(); | |||
} | |||
class _FriendAddSearchState extends State<FriendAddSearch> { | |||
UserSearch? user; | |||
Text centerMessage = const Text('Search to add friends...'); | |||
TextEditingController searchController = TextEditingController(); | |||
@override | |||
Widget build(BuildContext context) { | |||
return Scaffold( | |||
appBar: AppBar( | |||
elevation: 0, | |||
automaticallyImplyLeading: false, | |||
flexibleSpace: SafeArea( | |||
child: Container( | |||
padding: const EdgeInsets.only(right: 16), | |||
child: Row( | |||
children: <Widget>[ | |||
IconButton( | |||
onPressed: () { | |||
Navigator.pop(context); | |||
}, | |||
icon: Icon( | |||
Icons.arrow_back, | |||
color: Theme.of(context).appBarTheme.iconTheme?.color, | |||
), | |||
), | |||
const SizedBox(width: 2), | |||
Expanded( | |||
child: Column( | |||
crossAxisAlignment: CrossAxisAlignment.start, | |||
mainAxisAlignment: MainAxisAlignment.center, | |||
children: <Widget>[ | |||
Text( | |||
'Add Friends', | |||
style: TextStyle( | |||
fontSize: 16, | |||
fontWeight: FontWeight.w600, | |||
color: Theme.of(context).appBarTheme.toolbarTextStyle?.color | |||
) | |||
), | |||
], | |||
) | |||
) | |||
] | |||
), | |||
), | |||
), | |||
), | |||
body: Stack( | |||
children: <Widget>[ | |||
Padding( | |||
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), | |||
child: TextField( | |||
autofocus: true, | |||
decoration: InputDecoration( | |||
hintText: 'Search...', | |||
prefixIcon: const Icon( | |||
Icons.search, | |||
size: 20 | |||
), | |||
suffixIcon: Padding( | |||
padding: const EdgeInsets.only(top: 4, bottom: 4, right: 8), | |||
child: OutlinedButton( | |||
style: ButtonStyle( | |||
backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.secondary), | |||
foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.onSecondary), | |||
shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0))), | |||
elevation: MaterialStateProperty.all(4), | |||
), | |||
onPressed: searchUsername, | |||
child: const Icon(Icons.search, size: 25), | |||
), | |||
), | |||
), | |||
controller: searchController, | |||
), | |||
), | |||
Padding( | |||
padding: const EdgeInsets.only(top: 90), | |||
child: showFriend(), | |||
), | |||
], | |||
), | |||
); | |||
} | |||
Widget showFriend() { | |||
if (user == null) { | |||
return Center( | |||
child: centerMessage, | |||
); | |||
} | |||
return UserSearchResult( | |||
user: user!, | |||
); | |||
} | |||
Future<void> searchUsername() async { | |||
if (searchController.text.isEmpty) { | |||
return; | |||
} | |||
Map<String, String> params = {}; | |||
params['username'] = searchController.text; | |||
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/users'); | |||
uri = uri.replace(queryParameters: params); | |||
var resp = await http.get( | |||
uri, | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
} | |||
); | |||
if (resp.statusCode != 200) { | |||
user = null; | |||
centerMessage = const Text('User not found'); | |||
setState(() {}); | |||
return; | |||
} | |||
user = UserSearch.fromJson( | |||
jsonDecode(resp.body) | |||
); | |||
setState(() {}); | |||
FocusScope.of(context).unfocus(); | |||
searchController.clear(); | |||
} | |||
} |
@ -0,0 +1,180 @@ | |||
import 'dart:convert'; | |||
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_dotenv/flutter_dotenv.dart'; | |||
import 'package:http/http.dart' as http; | |||
import '/components/custom_circle_avatar.dart'; | |||
import '/models/friends.dart'; | |||
import '/utils/storage/session_cookie.dart'; | |||
import '/models/my_profile.dart'; | |||
import '/utils/encryption/aes_helper.dart'; | |||
import '/utils/encryption/crypto_utils.dart'; | |||
import '/utils/strings.dart'; | |||
class FriendRequestListItem extends StatefulWidget{ | |||
final Friend friend; | |||
final Function callback; | |||
const FriendRequestListItem({ | |||
Key? key, | |||
required this.friend, | |||
required this.callback, | |||
}) : super(key: key); | |||
@override | |||
_FriendRequestListItemState createState() => _FriendRequestListItemState(); | |||
} | |||
class _FriendRequestListItemState extends State<FriendRequestListItem> { | |||
@override | |||
Widget build(BuildContext context) { | |||
return GestureDetector( | |||
behavior: HitTestBehavior.opaque, | |||
onTap: () async { | |||
}, | |||
child: Container( | |||
padding: const EdgeInsets.only(left: 16,right: 10,top: 0,bottom: 20), | |||
child: Row( | |||
children: <Widget>[ | |||
Expanded( | |||
child: Row( | |||
children: <Widget>[ | |||
CustomCircleAvatar( | |||
initials: widget.friend.username[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.friend.username, style: const TextStyle(fontSize: 16)), | |||
], | |||
), | |||
), | |||
), | |||
), | |||
SizedBox( | |||
height: 30, | |||
width: 30, | |||
child: IconButton( | |||
onPressed: () { acceptFriendRequest(context); }, | |||
icon: const Icon(Icons.check), | |||
padding: const EdgeInsets.all(0), | |||
splashRadius: 20, | |||
), | |||
), | |||
const SizedBox(width: 6), | |||
SizedBox( | |||
height: 30, | |||
width: 30, | |||
child: IconButton( | |||
onPressed: rejectFriendRequest, | |||
icon: const Icon(Icons.cancel), | |||
padding: const EdgeInsets.all(0), | |||
splashRadius: 20, | |||
), | |||
), | |||
], | |||
), | |||
), | |||
], | |||
), | |||
), | |||
); | |||
} | |||
Future<void> acceptFriendRequest(BuildContext context) async { | |||
MyProfile profile = await MyProfile.getProfile(); | |||
String publicKeyString = CryptoUtils.encodeRSAPublicKeyToPem(profile.publicKey!); | |||
final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); | |||
String payloadJson = jsonEncode({ | |||
'user_id': widget.friend.userId, | |||
'friend_id': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(profile.id.codeUnits), | |||
widget.friend.publicKey, | |||
)), | |||
'friend_username': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(profile.username.codeUnits), | |||
widget.friend.publicKey, | |||
)), | |||
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( | |||
Uint8List.fromList(symmetricKey), | |||
widget.friend.publicKey, | |||
)), | |||
'asymmetric_public_key': AesHelper.aesEncrypt( | |||
symmetricKey, | |||
Uint8List.fromList(publicKeyString.codeUnits), | |||
), | |||
}); | |||
var resp = await http.post( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/${widget.friend.id}'), | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
}, | |||
body: payloadJson, | |||
); | |||
if (resp.statusCode != 204) { | |||
showMessage( | |||
'Failed to accept friend request, please try again later', | |||
context | |||
); | |||
return; | |||
} | |||
final db = await getDatabaseConnection(); | |||
widget.friend.acceptedAt = DateTime.now(); | |||
await db.update( | |||
'friends', | |||
widget.friend.toMap(), | |||
where: 'id = ?', | |||
whereArgs: [widget.friend.id], | |||
); | |||
widget.callback(); | |||
} | |||
Future<void> rejectFriendRequest() async { | |||
var resp = await http.delete( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/${widget.friend.id}'), | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
}, | |||
); | |||
if (resp.statusCode != 204) { | |||
showMessage( | |||
'Failed to decline friend request, please try again later', | |||
context | |||
); | |||
return; | |||
} | |||
final db = await getDatabaseConnection(); | |||
await db.delete( | |||
'friends', | |||
where: 'id = ?', | |||
whereArgs: [widget.friend.id], | |||
); | |||
widget.callback(); | |||
} | |||
} |