Compare commits
No commits in common. "78b1b1f078f79f72e244268067659ea30eef93cb" and "405bbc8d4c89506d3d9b7fe0c997e2db15fa2cc7" have entirely different histories.
78b1b1f078
...
405bbc8d4c
@ -1,7 +1,6 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Application.Users.Commands
|
||||
{
|
||||
@ -14,30 +13,25 @@ namespace API.Application.Users.Commands
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Handle(UpdateUserDTO UpdateUserDTO)
|
||||
public async Task<IActionResult> Handle(UserDTO userDTO)
|
||||
{
|
||||
List<User> existingUsers = await _repository.QueryAllUsersAsync();
|
||||
User currentUser = await _repository.QueryUserByIdAsync(UpdateUserDTO.Id);
|
||||
User currentUser = await _repository.QueryUserByIdAsync(userDTO.Id);
|
||||
|
||||
foreach (User existingUser in existingUsers)
|
||||
{
|
||||
if (existingUser.Username == UpdateUserDTO.Username && existingUser.Username != currentUser.Username)
|
||||
if (existingUser.Username == userDTO.Username && existingUser.Username != currentUser.Username)
|
||||
{
|
||||
return new ConflictObjectResult(new { message = "Username is already in use." });
|
||||
}
|
||||
|
||||
if (existingUser.Email == UpdateUserDTO.Email && existingUser.Email != currentUser.Email)
|
||||
if (existingUser.Email == userDTO.Email && existingUser.Email != currentUser.Email)
|
||||
{
|
||||
return new ConflictObjectResult(new { message = "Email is already in use." });
|
||||
}
|
||||
}
|
||||
|
||||
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(UpdateUserDTO.Password);
|
||||
|
||||
|
||||
currentUser.Username = UpdateUserDTO.Username;
|
||||
currentUser.Email = UpdateUserDTO.Email;
|
||||
currentUser.HashedPassword = hashedPassword;
|
||||
currentUser.Username = userDTO.Username;
|
||||
currentUser.Email = userDTO.Email;
|
||||
|
||||
bool success = await _repository.UpdateUserAsync(currentUser);
|
||||
if (success)
|
||||
@ -46,19 +40,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
API/Application/Users/Commands/UpdateUserPassword.cs
Normal file
57
API/Application/Users/Commands/UpdateUserPassword.cs
Normal file
@ -0,0 +1,57 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ 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;
|
||||
|
||||
@ -28,6 +29,7 @@ namespace API.Controllers
|
||||
QueryUserById queryUserById,
|
||||
CreateUser createUser,
|
||||
UpdateUser updateUser,
|
||||
UpdateUserPassword updateUserPassword,
|
||||
DeleteUser deleteUser,
|
||||
LoginUser loginUser)
|
||||
{
|
||||
@ -35,6 +37,7 @@ namespace API.Controllers
|
||||
_queryUserById = queryUserById;
|
||||
_createUser = createUser;
|
||||
_updateUser = updateUser;
|
||||
_updateUserPassword = updateUserPassword;
|
||||
_deleteUser = deleteUser;
|
||||
_loginUser = loginUser;
|
||||
}
|
||||
@ -62,9 +65,16 @@ namespace API.Controllers
|
||||
|
||||
[Authorize]
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> PutUser(UpdateUserDTO UpdateUserDTO)
|
||||
public async Task<IActionResult> PutUser(UserDTO userDTO)
|
||||
{
|
||||
return await _updateUser.Handle(UpdateUserDTO);
|
||||
return await _updateUser.Handle(userDTO);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPut("password")]
|
||||
public async Task<IActionResult> PutUserPassword(ChangePasswordDTO changePasswordDTO)
|
||||
{
|
||||
return await _updateUserPassword.Handle(changePasswordDTO);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -6,6 +6,7 @@ public class User : BaseModel
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string HashedPassword { get; set; }
|
||||
}
|
||||
|
||||
@ -29,11 +30,10 @@ public class SignUpDTO
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateUserDTO
|
||||
public class ChangePasswordDTO
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string OldPassword { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
}
|
||||
|
||||
|
@ -10,5 +10,6 @@ namespace API.Persistence.Repositories
|
||||
Task<User> QueryUserByIdAsync(string id);
|
||||
Task<User> QueryUserByEmailAsync(string email);
|
||||
Task<bool> UpdateUserAsync(User user);
|
||||
Task<bool> UpdateUserPasswordAsync(User user);
|
||||
}
|
||||
}
|
@ -57,6 +57,21 @@ namespace API.Persistence.Repositories
|
||||
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)
|
||||
{
|
||||
var user = await _context.Users.FindAsync(id);
|
||||
|
@ -36,6 +36,7 @@ namespace API
|
||||
builder.Services.AddScoped<QueryUserById>();
|
||||
builder.Services.AddScoped<CreateUser>();
|
||||
builder.Services.AddScoped<UpdateUser>();
|
||||
builder.Services.AddScoped<UpdateUserPassword>();
|
||||
builder.Services.AddScoped<DeleteUser>();
|
||||
builder.Services.AddScoped<LoginUser>();
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
@ -1,5 +1,4 @@
|
||||
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';
|
||||
@ -68,9 +67,7 @@ Future<bool> isLoggedIn(BuildContext context) async {
|
||||
|
||||
final token = prefs.getString('token');
|
||||
if (token == null){
|
||||
prefs.remove('id');
|
||||
loggedIn = false;
|
||||
user = User as User?;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -28,10 +28,8 @@ class _SideMenuState extends State<SideMenu> {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
prefs.remove('token');
|
||||
prefs.remove('id');
|
||||
setState(() {
|
||||
loggedIn = false;
|
||||
user = null;
|
||||
});
|
||||
|
||||
|
||||
|
@ -1,187 +0,0 @@
|
||||
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
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ 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});
|
||||
@ -106,18 +105,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
const SizedBox(height: 50),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
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'),
|
||||
// Add your edit action here
|
||||
},
|
||||
child: const Text('Edit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -42,7 +42,7 @@ Widget build(BuildContext context) {
|
||||
selectedIndex: 3,
|
||||
body: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Center( // Added SingleChildScrollView here
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
|
||||
child: Column(
|
||||
|
Loading…
Reference in New Issue
Block a user