updating everything at once works. without password dosent

This commit is contained in:
LilleBRG 2024-08-26 15:49:06 +02:00
parent b44d180284
commit 0e4c5d96cc
12 changed files with 238 additions and 100 deletions

View File

@ -1,6 +1,7 @@
using API.Models; using API.Models;
using API.Persistence.Repositories; using API.Persistence.Repositories;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;
namespace API.Application.Users.Commands namespace API.Application.Users.Commands
{ {
@ -13,25 +14,30 @@ namespace API.Application.Users.Commands
_repository = repository; _repository = repository;
} }
public async Task<IActionResult> Handle(UserDTO userDTO) public async Task<IActionResult> Handle(UpdateUserDTO UpdateUserDTO)
{ {
List<User> existingUsers = await _repository.QueryAllUsersAsync(); List<User> existingUsers = await _repository.QueryAllUsersAsync();
User currentUser = await _repository.QueryUserByIdAsync(userDTO.Id); User currentUser = await _repository.QueryUserByIdAsync(UpdateUserDTO.Id);
foreach (User existingUser in existingUsers) 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." }); 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." }); 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); bool success = await _repository.UpdateUserAsync(currentUser);
if (success) if (success)
@ -40,5 +46,19 @@ namespace API.Application.Users.Commands
return new StatusCodeResult(StatusCodes.Status500InternalServerError); 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);
}
} }
} }

View File

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

View File

@ -20,7 +20,6 @@ namespace API.Controllers
private readonly QueryUserById _queryUserById; private readonly QueryUserById _queryUserById;
private readonly CreateUser _createUser; private readonly CreateUser _createUser;
private readonly UpdateUser _updateUser; private readonly UpdateUser _updateUser;
private readonly UpdateUserPassword _updateUserPassword;
private readonly DeleteUser _deleteUser; private readonly DeleteUser _deleteUser;
private readonly LoginUser _loginUser; private readonly LoginUser _loginUser;
@ -29,7 +28,6 @@ namespace API.Controllers
QueryUserById queryUserById, QueryUserById queryUserById,
CreateUser createUser, CreateUser createUser,
UpdateUser updateUser, UpdateUser updateUser,
UpdateUserPassword updateUserPassword,
DeleteUser deleteUser, DeleteUser deleteUser,
LoginUser loginUser) LoginUser loginUser)
{ {
@ -37,7 +35,6 @@ namespace API.Controllers
_queryUserById = queryUserById; _queryUserById = queryUserById;
_createUser = createUser; _createUser = createUser;
_updateUser = updateUser; _updateUser = updateUser;
_updateUserPassword = updateUserPassword;
_deleteUser = deleteUser; _deleteUser = deleteUser;
_loginUser = loginUser; _loginUser = loginUser;
} }
@ -65,16 +62,9 @@ namespace API.Controllers
[Authorize] [Authorize]
[HttpPut] [HttpPut]
public async Task<IActionResult> PutUser(UserDTO userDTO) public async Task<IActionResult> PutUser(UpdateUserDTO UpdateUserDTO)
{ {
return await _updateUser.Handle(userDTO); return await _updateUser.Handle(UpdateUserDTO);
}
[Authorize]
[HttpPut("password")]
public async Task<IActionResult> PutUserPassword(ChangePasswordDTO changePasswordDTO)
{
return await _updateUserPassword.Handle(changePasswordDTO);
} }
[HttpPost] [HttpPost]

View File

@ -6,7 +6,6 @@ public class User : BaseModel
{ {
public string? Email { get; set; } public string? Email { get; set; }
public string? Username { get; set; } public string? Username { get; set; }
public string? Password { get; set; }
public string HashedPassword { get; set; } public string HashedPassword { get; set; }
} }
@ -30,10 +29,11 @@ public class SignUpDTO
public string Password { get; set; } public string Password { get; set; }
} }
public class ChangePasswordDTO public class UpdateUserDTO
{ {
public string Id { get; set; } public string Id { get; set; }
public string OldPassword { get; set; } public string Email { get; set; }
public string NewPassword { get; set; } public string Username { get; set; }
public string Password { get; set; }
} }

View File

@ -10,6 +10,5 @@ namespace API.Persistence.Repositories
Task<User> QueryUserByIdAsync(string id); Task<User> QueryUserByIdAsync(string id);
Task<User> QueryUserByEmailAsync(string email); Task<User> QueryUserByEmailAsync(string email);
Task<bool> UpdateUserAsync(User user); Task<bool> UpdateUserAsync(User user);
Task<bool> UpdateUserPasswordAsync(User user);
} }
} }

View File

@ -57,21 +57,6 @@ namespace API.Persistence.Repositories
return true; return true;
} }
public async Task<bool> UpdateUserPasswordAsync(User user)
{
try
{
_context.Entry(user).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
catch (Exception)
{
return false;
}
return true;
}
public async Task<bool> DeleteUserAsync(string id) public async Task<bool> DeleteUserAsync(string id)
{ {
var user = await _context.Users.FindAsync(id); var user = await _context.Users.FindAsync(id);

View File

@ -36,7 +36,6 @@ namespace API
builder.Services.AddScoped<QueryUserById>(); builder.Services.AddScoped<QueryUserById>();
builder.Services.AddScoped<CreateUser>(); builder.Services.AddScoped<CreateUser>();
builder.Services.AddScoped<UpdateUser>(); builder.Services.AddScoped<UpdateUser>();
builder.Services.AddScoped<UpdateUserPassword>();
builder.Services.AddScoped<DeleteUser>(); builder.Services.AddScoped<DeleteUser>();
builder.Services.AddScoped<LoginUser>(); builder.Services.AddScoped<LoginUser>();
builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IUserRepository, UserRepository>();

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
@ -67,7 +68,9 @@ Future<bool> isLoggedIn(BuildContext context) async {
final token = prefs.getString('token'); final token = prefs.getString('token');
if (token == null){ if (token == null){
prefs.remove('id');
loggedIn = false; loggedIn = false;
user = User as User?;
return false; return false;
} }

View File

@ -28,8 +28,10 @@ class _SideMenuState extends State<SideMenu> {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
prefs.remove('token'); prefs.remove('token');
prefs.remove('id');
setState(() { setState(() {
loggedIn = false; loggedIn = false;
user = null;
}); });

187
Mobile/lib/editprofile.dart Normal file
View File

@ -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<EditProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<EditProfilePage> {
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: <Widget>[
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
),
),
],
)
],
),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:mobile/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'base/sidemenu.dart'; import 'base/sidemenu.dart';
import 'api.dart' as api; import 'api.dart' as api;
import 'editprofile.dart';
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@ -105,7 +106,16 @@ class _ProfilePageState extends State<ProfilePage> {
const SizedBox(height: 50), const SizedBox(height: 50),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// Add your edit action here 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'), child: const Text('Edit'),
), ),

View File

@ -42,7 +42,7 @@ Widget build(BuildContext context) {
selectedIndex: 3, selectedIndex: 3,
body: Scaffold( body: Scaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( // Added SingleChildScrollView here child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400), constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
child: Column( child: Column(