diff --git a/API/Application/Users/Queries/QueryAllUsers.cs b/API/Application/Users/Queries/QueryAllUsers.cs index 0681417..d85b054 100644 --- a/API/Application/Users/Queries/QueryAllUsers.cs +++ b/API/Application/Users/Queries/QueryAllUsers.cs @@ -26,7 +26,7 @@ namespace API.Application.Users.Queries Id = user.Id, Email = user.Email, Username = user.Username, - ProfilePictureURL = user.ProfilePicture, + ProfilePicture = user.ProfilePicture, }).ToList(); return userDTOs; } diff --git a/API/Application/Users/Queries/QueryUserById.cs b/API/Application/Users/Queries/QueryUserById.cs index 265973a..db5814e 100644 --- a/API/Application/Users/Queries/QueryUserById.cs +++ b/API/Application/Users/Queries/QueryUserById.cs @@ -24,7 +24,7 @@ namespace API.Application.Users.Queries return new ConflictObjectResult(new { message = "No user on given Id" }); } - return new OkObjectResult(new { id = user.Id, email = user.Email, username = user.Username, profilePictureURL = user.ProfilePicture, createdAt = user.CreatedAt }); + return new OkObjectResult(new { id = user.Id, email = user.Email, username = user.Username, profilePicture = user.ProfilePicture, createdAt = user.CreatedAt }); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index ef71ba7..985d255 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -75,7 +75,7 @@ namespace API.Controllers [Authorize] [HttpPut] - public async Task PutUser(UpdateUserDTO UpdateUserDTO) + public async Task PutUser([FromForm ]UpdateUserDTO UpdateUserDTO) { return await _updateUser.Handle(UpdateUserDTO); } diff --git a/API/Models/User.cs b/API/Models/User.cs index 620ee57..9528c3c 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -17,7 +17,7 @@ public class UserDTO public string Id { get; set; } public string Email { get; set; } public string Username { get; set; } - public string ProfilePictureURL { get; set; } + public string ProfilePicture { get; set; } } diff --git a/API/Persistence/Services/R2Service.cs b/API/Persistence/Services/R2Service.cs index ff73060..8a32187 100644 --- a/API/Persistence/Services/R2Service.cs +++ b/API/Persistence/Services/R2Service.cs @@ -29,6 +29,7 @@ namespace API.Persistence.Services var request = new PutObjectRequest { InputStream = fileStream, + BucketName = "h4picturebucket", Key = fileName, DisablePayloadSigning = true }; @@ -40,7 +41,7 @@ namespace API.Persistence.Services throw new AmazonS3Exception($"Error uploading file to S3. HTTP Status Code: {response.HttpStatusCode}"); } - var imageUrl = $"https://pub-bf709b641048489ca70f693673e3e04c.r2.dev/{fileName}"; + var imageUrl = $"https://pub-bf709b641048489ca70f693673e3e04c.r2.dev/h4picturebucket/{fileName}"; return imageUrl; } } diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index ed7734f..79f3299 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; @@ -63,6 +66,92 @@ Future request(BuildContext? context, ApiService service, String method return utf8.decode(response.bodyBytes); } +Future putUser( + BuildContext? context, + ApiService service, + String method, + String path, + Map body, + File? profilePicture // The image file +) async { + final messenger = context != null ? ScaffoldMessenger.of(context) : null; + final prefs = await SharedPreferences.getInstance(); + final host = const String.fromEnvironment('AUTH_SERVICE_HOST'); + + final token = prefs.getString('token'); + final Map headers = {}; + if (token != null) headers.addAll({'Authorization': 'Bearer $token'}); + + // Create the Uri + var uri = Uri.parse(host + path); + + // Create a MultipartRequest + var request = http.MultipartRequest('PUT', uri); + request.headers.addAll(headers); + + // Add form fields + request.fields['id'] = body['id']; + request.fields['username'] = body['username']; + request.fields['email'] = body['email']; + request.fields['password'] = body['password']; + + // Attach the file to the request (if provided) + if (profilePicture != null) { + var fileStream = http.ByteStream(profilePicture.openRead()); + var length = await profilePicture.length(); + var multipartFile = http.MultipartFile( + 'ProfilePicture', // field name matches your backend DTO + fileStream, + length, + filename: profilePicture.path.split('/').last, + ); + request.files.add(multipartFile); + } + + try { + // Send the request + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + // Handle non-success responses + if (response.statusCode < 200 || response.statusCode >= 300) { + try { + final json = jsonDecode(response.body); + messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title']))); + debugPrint('API error: ' + json['message']); + } catch (e) { + debugPrint(e.toString()); + messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})'))); + } + return null; + } + + return utf8.decode(response.bodyBytes); + } catch (e) { + debugPrint(e.toString()); + messenger?.showSnackBar(const SnackBar(content: Text('Unable to connect to server'))); + return null; + } +} + + +Future _compressImage(File file) async { + final filePath = file.absolute.path; + final lastIndex = filePath.lastIndexOf(RegExp(r'.jp')); + final splitted = filePath.substring(0, lastIndex); + final outPath = "${splitted}_compressed.jpg"; + + var result = await FlutterImageCompress.compressAndGetFile( + file.absolute.path, + outPath, + quality: 80, + minWidth: 1024, + minHeight: 1024, + ); + + return File(result!.path); + } + Future isLoggedIn(BuildContext context) async { final messenger = ScaffoldMessenger.of(context); final prefs = await SharedPreferences.getInstance(); diff --git a/Mobile/lib/editprofile.dart b/Mobile/lib/editprofile.dart index 9caf4bb..e79d74a 100644 --- a/Mobile/lib/editprofile.dart +++ b/Mobile/lib/editprofile.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mobile/models.dart'; @@ -46,98 +45,68 @@ class _ProfilePageState extends State { super.dispose(); } - Future _pickImageFromGallery() async{ - final picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: ImageSource.gallery); - - if(image == null) {return;} - else{ - File compressedFile = await _compressImage(File(image.path)); + Future _pickImageFromGallery() async{ + final image = await ImagePicker().pickImage(source: ImageSource.gallery); + if (image == null) return; setState(() { - _selectedImage = compressedFile; - }); - } - + _selectedImage = File(image.path); + }); } Future _pickImageFromCamera() async{ - final picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: ImageSource.camera); - - if(image == null) {return;} - else{ - File compressedFile = await _compressImage(File(image.path)); + final image = await ImagePicker().pickImage(source: ImageSource.camera); + if (image == null) return; setState(() { - _selectedImage = compressedFile; - }); - } + _selectedImage = File(image.path); + }); } - Future _compressImage(File file) async { - final filePath = file.absolute.path; - final lastIndex = filePath.lastIndexOf(RegExp(r'.jp')); - final splitted = filePath.substring(0, lastIndex); - final outPath = "${splitted}_compressed.jpg"; + - var result = await FlutterImageCompress.compressAndGetFile( - file.absolute.path, - outPath, - quality: 80, - minWidth: 1024, - minHeight: 1024, + void _saveProfile() async { + if (passwordInput.text != confirmPasswordInput.text) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Passwords do not match'))); + return; + } + + final prefs = await SharedPreferences.getInstance(); + String? id = prefs.getString('id'); + + if (!mounted) return; + + if (id != null) { + final response = await api.putUser( + context, + api.ApiService.auth, + 'PUT', + '/api/users', + { + 'id': id, + 'username': usernameInput.text, + 'email': emailInput.text, + 'password': passwordInput.text, + }, + _selectedImage // Pass the selected image to the putUser function ); - return File(result!.path); - } - - void _saveProfile() async { - if (passwordInput.text != confirmPasswordInput.text) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Passwords do not match'))); - return; - } - - final prefs = await SharedPreferences.getInstance(); - String? id = prefs.getString('id'); - - if (!mounted) { - return; - } - if (id != null){ - final response = await api.request(context, api.ApiService.auth, 'PUT', '/api/users', { - 'id' : id, - 'username': usernameInput.text, - 'email': emailInput.text, - 'password': passwordInput.text, - 'profilePicture': _selectedImage, - }); - - if (!mounted) { - return; - } + if (!mounted) return; if (response != null) { - User updatedUser = User( - id, - emailInput.text, - usernameInput.text, - _selectedImage, - DateTime.now(), - ); setState(() { - user = updatedUser; + user = null; }); Navigator.of(context).pop(); // Close the dialog Navigator.pushReplacementNamed(context, '/profile'); - } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Something went wrong! Please contact an admin.')), ); } } - } +} + void _deleteProfile(BuildContext context) { showDialog( diff --git a/Mobile/lib/models.dart b/Mobile/lib/models.dart index d42bdc3..ce3c203 100644 --- a/Mobile/lib/models.dart +++ b/Mobile/lib/models.dart @@ -44,7 +44,7 @@ class User { String id; String email; String username; - File? profilePicture; + String profilePicture; DateTime createdAt; User( this.id, this.email, this.username, this.profilePicture, this.createdAt); diff --git a/Mobile/lib/profile.dart b/Mobile/lib/profile.dart index d52c41e..f522529 100644 --- a/Mobile/lib/profile.dart +++ b/Mobile/lib/profile.dart @@ -41,6 +41,8 @@ class _ProfilePageState extends State { Map json = jsonDecode(response); User jsonUser = User.fromJson(json); + print(jsonUser.profilePicture); + setState(() { userData = jsonUser; user = jsonUser; @@ -71,11 +73,13 @@ class _ProfilePageState extends State { : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + userData?.profilePicture != null ? const Icon( Icons.account_circle, size: 100, color: Colors.grey, - ), + ) + : Image(image: NetworkImage('https://pub-bf709b641048489ca70f693673e3e04c.r2.dev/h4picturebucket/PPb83569bef3b9470782d7b42bc4e552ff.png'), height: 100), const SizedBox(height: 20), Text( userData!.username,