diff --git a/API/Application/Users/Commands/UpdateUser.cs b/API/Application/Users/Commands/UpdateUser.cs index 1b5165f..c530041 100644 --- a/API/Application/Users/Commands/UpdateUser.cs +++ b/API/Application/Users/Commands/UpdateUser.cs @@ -1,6 +1,7 @@ using API.Models; using API.Persistence.Repositories; using Microsoft.AspNetCore.Mvc; +using System.Text.RegularExpressions; namespace API.Application.Users.Commands { @@ -13,25 +14,30 @@ namespace API.Application.Users.Commands _repository = repository; } - public async Task Handle(UserDTO userDTO) + public async Task Handle(UpdateUserDTO UpdateUserDTO) { List existingUsers = await _repository.QueryAllUsersAsync(); - User currentUser = await _repository.QueryUserByIdAsync(userDTO.Id); + User currentUser = await _repository.QueryUserByIdAsync(UpdateUserDTO.Id); foreach (User existingUser in existingUsers) { - if (existingUser.Username == userDTO.Username && existingUser.Username != currentUser.Username) + if (existingUser.Username == UpdateUserDTO.Username && existingUser.Username != currentUser.Username) { return new ConflictObjectResult(new { message = "Username is already in use." }); } - if (existingUser.Email == userDTO.Email && existingUser.Email != currentUser.Email) + if (existingUser.Email == UpdateUserDTO.Email && existingUser.Email != currentUser.Email) { return new ConflictObjectResult(new { message = "Email is already in use." }); } } - currentUser.Username = userDTO.Username; - currentUser.Email = userDTO.Email; + + string hashedPassword = BCrypt.Net.BCrypt.HashPassword(UpdateUserDTO.Password); + + + currentUser.Username = UpdateUserDTO.Username; + currentUser.Email = UpdateUserDTO.Email; + currentUser.HashedPassword = hashedPassword; bool success = await _repository.UpdateUserAsync(currentUser); if (success) @@ -40,5 +46,19 @@ namespace API.Application.Users.Commands return new StatusCodeResult(StatusCodes.Status500InternalServerError); } + private bool IsPasswordSecure(string password) + { + var hasUpperCase = new Regex(@"[A-Z]+"); + var hasLowerCase = new Regex(@"[a-z]+"); + var hasDigits = new Regex(@"[0-9]+"); + var hasSpecialChar = new Regex(@"[\W_]+"); + var hasMinimum8Chars = new Regex(@".{8,}"); + + return hasUpperCase.IsMatch(password) + && hasLowerCase.IsMatch(password) + && hasDigits.IsMatch(password) + && hasSpecialChar.IsMatch(password) + && hasMinimum8Chars.IsMatch(password); + } } } diff --git a/API/Application/Users/Commands/UpdateUserPassword.cs b/API/Application/Users/Commands/UpdateUserPassword.cs deleted file mode 100644 index 4a2efb1..0000000 --- a/API/Application/Users/Commands/UpdateUserPassword.cs +++ /dev/null @@ -1,57 +0,0 @@ -using API.Models; -using API.Persistence.Repositories; -using Microsoft.AspNetCore.Mvc; -using System.Text.RegularExpressions; - -namespace API.Application.Users.Commands -{ - public class UpdateUserPassword - { - private readonly IUserRepository _repository; - - public UpdateUserPassword(IUserRepository repository) - { - _repository = repository; - } - - public async Task Handle(ChangePasswordDTO changePasswordDTO) - { - if (!IsPasswordSecure(changePasswordDTO.NewPassword)) - { - return new ConflictObjectResult(new { message = "New Password is not secure." }); - } - - User currentUser = await _repository.QueryUserByIdAsync(changePasswordDTO.Id); - if (currentUser == null || !BCrypt.Net.BCrypt.Verify(changePasswordDTO.OldPassword, currentUser.HashedPassword)) - { - return new UnauthorizedObjectResult(new { message = "Old Password is incorrect" }); - } - string hashedPassword = BCrypt.Net.BCrypt.HashPassword(changePasswordDTO.NewPassword); - - currentUser.HashedPassword = hashedPassword; - - bool success = await _repository.UpdateUserPasswordAsync(currentUser); - if (success) - return new OkResult(); - else - return new StatusCodeResult(StatusCodes.Status500InternalServerError); - } - - private bool IsPasswordSecure(string password) - { - var hasUpperCase = new Regex(@"[A-Z]+"); - var hasLowerCase = new Regex(@"[a-z]+"); - var hasDigits = new Regex(@"[0-9]+"); - var hasSpecialChar = new Regex(@"[\W_]+"); - var hasMinimum8Chars = new Regex(@".{8,}"); - - return hasUpperCase.IsMatch(password) - && hasLowerCase.IsMatch(password) - && hasDigits.IsMatch(password) - && hasSpecialChar.IsMatch(password) - && hasMinimum8Chars.IsMatch(password); - } - - - } -} diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 1a0f564..460274e 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -20,7 +20,6 @@ namespace API.Controllers private readonly QueryUserById _queryUserById; private readonly CreateUser _createUser; private readonly UpdateUser _updateUser; - private readonly UpdateUserPassword _updateUserPassword; private readonly DeleteUser _deleteUser; private readonly LoginUser _loginUser; @@ -29,7 +28,6 @@ namespace API.Controllers QueryUserById queryUserById, CreateUser createUser, UpdateUser updateUser, - UpdateUserPassword updateUserPassword, DeleteUser deleteUser, LoginUser loginUser) { @@ -37,7 +35,6 @@ namespace API.Controllers _queryUserById = queryUserById; _createUser = createUser; _updateUser = updateUser; - _updateUserPassword = updateUserPassword; _deleteUser = deleteUser; _loginUser = loginUser; } @@ -65,16 +62,9 @@ namespace API.Controllers [Authorize] [HttpPut] - public async Task PutUser(UserDTO userDTO) + public async Task PutUser(UpdateUserDTO UpdateUserDTO) { - return await _updateUser.Handle(userDTO); - } - - [Authorize] - [HttpPut("password")] - public async Task PutUserPassword(ChangePasswordDTO changePasswordDTO) - { - return await _updateUserPassword.Handle(changePasswordDTO); + return await _updateUser.Handle(UpdateUserDTO); } [HttpPost] diff --git a/API/Models/User.cs b/API/Models/User.cs index 4cac344..99072d0 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -6,7 +6,6 @@ public class User : BaseModel { public string? Email { get; set; } public string? Username { get; set; } - public string? Password { get; set; } public string HashedPassword { get; set; } } @@ -30,10 +29,11 @@ public class SignUpDTO public string Password { get; set; } } -public class ChangePasswordDTO +public class UpdateUserDTO { public string Id { get; set; } - public string OldPassword { get; set; } - public string NewPassword { get; set; } + public string Email { get; set; } + public string Username { get; set; } + public string Password { get; set; } } diff --git a/API/Persistence/Repositories/IUserRepository.cs b/API/Persistence/Repositories/IUserRepository.cs index 51548ee..690210f 100644 --- a/API/Persistence/Repositories/IUserRepository.cs +++ b/API/Persistence/Repositories/IUserRepository.cs @@ -10,6 +10,5 @@ namespace API.Persistence.Repositories Task QueryUserByIdAsync(string id); Task QueryUserByEmailAsync(string email); Task UpdateUserAsync(User user); - Task UpdateUserPasswordAsync(User user); } } \ No newline at end of file diff --git a/API/Persistence/Repositories/UserRepository.cs b/API/Persistence/Repositories/UserRepository.cs index 26f0e33..da8f833 100644 --- a/API/Persistence/Repositories/UserRepository.cs +++ b/API/Persistence/Repositories/UserRepository.cs @@ -57,21 +57,6 @@ namespace API.Persistence.Repositories return true; } - public async Task UpdateUserPasswordAsync(User user) - { - try - { - _context.Entry(user).State = EntityState.Modified; - await _context.SaveChangesAsync(); - } - catch (Exception) - { - return false; - } - - return true; - } - public async Task DeleteUserAsync(string id) { var user = await _context.Users.FindAsync(id); diff --git a/API/Program.cs b/API/Program.cs index 22885e1..b38647b 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -36,7 +36,6 @@ namespace API builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index 204bee4..a53c222 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:mobile/models.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; @@ -67,7 +68,9 @@ Future isLoggedIn(BuildContext context) async { final token = prefs.getString('token'); if (token == null){ + prefs.remove('id'); loggedIn = false; + user = User as User?; return false; } diff --git a/Mobile/lib/base/sidemenu.dart b/Mobile/lib/base/sidemenu.dart index 65cda94..824d210 100644 --- a/Mobile/lib/base/sidemenu.dart +++ b/Mobile/lib/base/sidemenu.dart @@ -28,8 +28,10 @@ class _SideMenuState extends State { final prefs = await SharedPreferences.getInstance(); prefs.remove('token'); + prefs.remove('id'); setState(() { loggedIn = false; + user = null; }); diff --git a/Mobile/lib/editprofile.dart b/Mobile/lib/editprofile.dart new file mode 100644 index 0000000..bb438c6 --- /dev/null +++ b/Mobile/lib/editprofile.dart @@ -0,0 +1,187 @@ +import 'dart:math'; +import 'dart:developer' as useMAN; +import 'package:flutter/material.dart'; +import 'package:mobile/models.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'api.dart' as api; +import 'base/variables.dart'; + +class EditProfilePage extends StatefulWidget { + final User? userData; + + const EditProfilePage({super.key, required this.userData}); + + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + TextEditingController usernameInput = TextEditingController(); + TextEditingController emailInput = TextEditingController(); + TextEditingController passwordInput = TextEditingController(); + TextEditingController confirmPasswordInput = TextEditingController(); + + + @override + void initState() { + super.initState(); + // Initialize the controllers with existing data + usernameInput.text = widget.userData!.username; + emailInput.text = widget.userData!.email; + } + + @override + void dispose() { + // Dispose of the controllers when the widget is disposed + usernameInput.dispose(); + emailInput.dispose(); + passwordInput.dispose(); + confirmPasswordInput.dispose(); + super.dispose(); + } + + 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'); + + final response = await api + .request(context, api.ApiService.auth, 'PUT', '/api/users', { + 'id' : id, + 'username': usernameInput.text, + 'email': emailInput.text, + 'password': passwordInput.text, + }); + + useMAN.log('data'); + + + if (response!.isEmpty) { + prefs.remove('token'); + loggedIn = true; + user = User( + id!, + emailInput.text, + usernameInput.text, + DateTime.now(), + ); + Navigator.of(context).pop(); // Close the dialog + + } + else{ + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Something went wrong! Please contact an admin.')), + ); + } + } + + void _deleteProfile(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Confirm Deletion'), + content: Text('Are you sure you want to delete your profile?'), + actions: [ + TextButton( + child: Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + TextButton( + child: Text('Delete'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + onPressed: () async { + final prefs = await SharedPreferences.getInstance(); + String? id = prefs.getString('id'); + + final response = await api + .request(context, api.ApiService.auth, 'DELETE', '/api/users/$id', null); + + if (response!.isEmpty) { + prefs.remove('token'); + prefs.remove('id'); + setState(() { + loggedIn = false; + user = null; + }); + Navigator.of(context).pop(); // Close the dialog + Navigator.of(context).pop(); + Navigator.pushReplacementNamed(context, '/register'); + } + else{ + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Something went wrong! Please contact an admin.')), + ); + } + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Edit Profile'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); // Navigates back when the back button is pressed + }, + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + controller: usernameInput, + decoration: InputDecoration(labelText: 'Name'), + ), + TextField( + controller: emailInput, + decoration: InputDecoration(labelText: 'Email'), + ), + TextField( + controller: passwordInput, + decoration: InputDecoration(labelText: 'New password'), + ), + TextField( + controller: confirmPasswordInput, + decoration: InputDecoration(labelText: 'Repeat new password'), + ), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _saveProfile, // Save and pop + child: Text('Save'), + ), + SizedBox(width: 10), + ElevatedButton( + onPressed: () => _deleteProfile(context), + child: Text('Delete'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.red, // Red text + ), + ), + ], + ) + ], + ), + ), + ); + } +} + diff --git a/Mobile/lib/profile.dart b/Mobile/lib/profile.dart index e76be05..129506e 100644 --- a/Mobile/lib/profile.dart +++ b/Mobile/lib/profile.dart @@ -6,6 +6,7 @@ import 'package:mobile/models.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'base/sidemenu.dart'; import 'api.dart' as api; +import 'editprofile.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @@ -105,9 +106,18 @@ class _ProfilePageState extends State { const SizedBox(height: 50), ElevatedButton( onPressed: () { - // Add your edit action here - }, - child: const Text('Edit'), + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.9, // 90% height + child: EditProfilePage(userData: user), + ); + }, + ); + }, + child: const Text('Edit'), ), ], ), diff --git a/Mobile/lib/register.dart b/Mobile/lib/register.dart index b8156e9..6eb4036 100644 --- a/Mobile/lib/register.dart +++ b/Mobile/lib/register.dart @@ -42,7 +42,7 @@ Widget build(BuildContext context) { selectedIndex: 3, body: Scaffold( body: SingleChildScrollView( - child: Center( // Added SingleChildScrollView here + child: Center( child: Container( constraints: const BoxConstraints(minWidth: 100, maxWidth: 400), child: Column(