diff --git a/Webshop.Api/Controllers/Customers/CustomerController.cs b/Webshop.Api/Controllers/Customers/CustomerController.cs index 51df959..8eb3676 100644 --- a/Webshop.Api/Controllers/Customers/CustomerController.cs +++ b/Webshop.Api/Controllers/Customers/CustomerController.cs @@ -1,21 +1,20 @@ -// src/Webshop.Api/Controllers/Customer/ProfileController.cs +// src/Webshop.Api/Controllers/Customer/CustomerController.cs using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; -using Webshop.Application.DTOs; // CustomerDto -using Webshop.Application.DTOs.Auth; // ChangePasswordRequestDto -using Webshop.Application.DTOs.Customers; // UpdateCustomerProfileDto -using Webshop.Application.DTOs.Email; -using Webshop.Application.Services; using System.Threading.Tasks; +using Webshop.Application; +using Webshop.Application.DTOs; +using Webshop.Application.DTOs.Auth; +using Webshop.Application.DTOs.Customers; +using Webshop.Application.DTOs.Email; using Webshop.Application.Services.Customers; -using System.ComponentModel.DataAnnotations; namespace Webshop.Api.Controllers.Customer { - [ApiController] - [Route("api/v1/customer/[controller]")] // z.B. /api/v1/customer/profile + [Route("api/v1/customer")] // Route vereinfacht, da der Controllername "Customer" ist [Authorize(Roles = "Customer")] public class CustomerController : ControllerBase { @@ -26,62 +25,89 @@ namespace Webshop.Api.Controllers.Customer _customerService = customerService; } - [HttpGet("me")] // /api/v1/customer/profile/me - public async Task> GetMyProfile() + [HttpGet("profile")] // GET /api/v1/customer/profile + [ProducesResponseType(typeof(CustomerDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task GetMyProfile() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (string.IsNullOrEmpty(userId)) return Unauthorized(new { Message = "Benutzer-ID nicht im Token gefunden." }); + if (string.IsNullOrEmpty(userId)) return Unauthorized(); - var customerProfile = await _customerService.GetMyProfileAsync(userId); - if (customerProfile == null) return NotFound(new { Message = "Kundenprofil nicht gefunden. Bitte erstellen Sie es." }); + var result = await _customerService.GetMyProfileAsync(userId); - return Ok(customerProfile); + return result.Type switch + { + ServiceResultType.Success => Ok(result.Value), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; } - [HttpPost("change-password")] // /api/v1/customer/profile/change-password - public async Task ChangePassword([FromBody] ChangePasswordRequestDto request) - { - if (!ModelState.IsValid) return BadRequest(ModelState); - - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (string.IsNullOrEmpty(userId)) return Unauthorized(new { Message = "Benutzer-ID nicht im Token gefunden." }); - - var (success, errorMessage) = await _customerService.ChangePasswordAsync(userId, request); - - if (!success) return BadRequest(new { Message = errorMessage }); - - return Ok(new { Message = "Passwort erfolgreich geändert. Bitte melden Sie sich mit dem neuen Passwort an." }); - } - - // << NEUER/AKTUALISIERTER ENDPUNKT FÜR PROFIL-AKTUALISIERUNG >> - [HttpPost("update-profile")] // /api/v1/customer/profile/update-profile + [HttpPut("profile")] // PUT /api/v1/customer/profile + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task UpdateProfile([FromBody] UpdateCustomerDto request) { if (!ModelState.IsValid) return BadRequest(ModelState); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (string.IsNullOrEmpty(userId)) return Unauthorized(new { Message = "Benutzer-ID nicht im Token gefunden." }); + if (string.IsNullOrEmpty(userId)) return Unauthorized(); - var (success, errorMessage) = await _customerService.UpdateMyProfileAsync(userId, request); + var result = await _customerService.UpdateMyProfileAsync(userId, request); - if (!success) return BadRequest(new { Message = errorMessage }); - - return Ok(new { Message = "Profil erfolgreich aktualisiert." }); + return result.Type switch + { + ServiceResultType.Success => NoContent(), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; } - [HttpPost("change-email-request")] // /api/v1/customer/profile/change-email-request - public async Task ChangeEmailRequest([FromBody] ChangeEmailRequestDto request) // DTO erstellen + [HttpPost("change-password")] // POST /api/v1/customer/change-password + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task ChangePassword([FromBody] ChangePasswordRequestDto request) { if (!ModelState.IsValid) return BadRequest(ModelState); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (string.IsNullOrEmpty(userId)) return Unauthorized(new { Message = "Benutzer-ID nicht im Token gefunden." }); + if (string.IsNullOrEmpty(userId)) return Unauthorized(); - var (success, errorMessage) = await _customerService.ChangeEmailAsync(userId, request.NewEmail, request.CurrentPassword); + var result = await _customerService.ChangePasswordAsync(userId, request); - if (!success) return BadRequest(new { Message = errorMessage }); + return result.Type switch + { + ServiceResultType.Success => Ok(new { Message = "Passwort erfolgreich geändert." }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; + } - return Ok(new { Message = errorMessage }); // Sendet Info, dass Mail gesendet wurde + [HttpPost("change-email-request")] // POST /api/v1/customer/change-email-request + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + public async Task ChangeEmailRequest([FromBody] ChangeEmailRequestDto request) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userId)) return Unauthorized(); + + var result = await _customerService.ChangeEmailAsync(userId, request.NewEmail, request.CurrentPassword); + + return result.Type switch + { + ServiceResultType.Success => Ok(new { Message = result.ErrorMessage }), // Enthält die Erfolgsmeldung + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CreateAddressDto.cs b/Webshop.Application/DTOs/Customers/CreateAddressDto.cs similarity index 100% rename from Webshop.Application/Services/Customers/CreateAddressDto.cs rename to Webshop.Application/DTOs/Customers/CreateAddressDto.cs diff --git a/Webshop.Application/Services/Customers/CustomerService.cs b/Webshop.Application/Services/Customers/CustomerService.cs index a5cb81d..3574738 100644 --- a/Webshop.Application/Services/Customers/CustomerService.cs +++ b/Webshop.Application/Services/Customers/CustomerService.cs @@ -1,19 +1,15 @@ // src/Webshop.Application/Services/Customers/CustomerService.cs using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using System.Threading.Tasks; -using Webshop.Application.DTOs; // CustomerDto -using Webshop.Application.DTOs.Auth; // ChangePasswordRequestDto -using Webshop.Application.DTOs.Customers; // UpdateCustomerProfileDto -using Webshop.Domain.Entities; // Customer Entity -using Webshop.Domain.Interfaces; // ICustomerRepository -using Webshop.Domain.Identity; // Für ApplicationUser -using System.Linq; // Für Select -using System.Collections.Generic; // Für IEnumerable -using System.Web; -using Resend; using Microsoft.Extensions.Configuration; -using System.Net.Mail; +using Resend; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Webshop.Application.DTOs; +using Webshop.Application.DTOs.Auth; +using Webshop.Application.DTOs.Customers; +using Webshop.Domain.Identity; +using Webshop.Domain.Interfaces; namespace Webshop.Application.Services.Customers { @@ -21,159 +17,115 @@ namespace Webshop.Application.Services.Customers { private readonly ICustomerRepository _customerRepository; private readonly UserManager _userManager; - private readonly IConfiguration _configuration; // << NEU: Für BaseUrl >> - private readonly IResend _resend; // << NEU >> + private readonly IConfiguration _configuration; + private readonly IResend _resend; - public CustomerService( - ICustomerRepository customerRepository, - UserManager userManager, - IConfiguration configuration, // Im Konstruktor injizieren - IResend resend) // Im Konstruktor injizieren + public CustomerService(ICustomerRepository customerRepository, UserManager userManager, IConfiguration configuration, IResend resend) { _customerRepository = customerRepository; _userManager = userManager; - _configuration = configuration; // Im Konstruktor initialisieren - _resend = resend; // Im Konstruktor initialisieren + _configuration = configuration; + _resend = resend; } - public async Task GetMyProfileAsync(string userId) + public async Task> GetMyProfileAsync(string userId) { var customer = await _customerRepository.GetByUserIdAsync(userId); - if (customer == null) return null; + if (customer == null) + return ServiceResult.Fail(ServiceResultType.NotFound, "Kundenprofil nicht gefunden."); var identityUser = await _userManager.FindByIdAsync(userId); - if (identityUser == null) return null; + if (identityUser == null) + return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzerkonto nicht gefunden."); - return new CustomerDto + var dto = new CustomerDto { Id = customer.Id, UserId = customer.AspNetUserId, FirstName = customer.FirstName, LastName = customer.LastName, - Email = identityUser.Email ?? string.Empty, // E-Mail vom ApplicationUser - PhoneNumber = identityUser.PhoneNumber, // Telefonnummer vom ApplicationUser + Email = identityUser.Email ?? string.Empty, + PhoneNumber = identityUser.PhoneNumber, DefaultShippingAddressId = customer.DefaultShippingAddressId, DefaultBillingAddressId = customer.DefaultBillingAddressId }; + return ServiceResult.Ok(dto); } - public async Task<(bool Success, string ErrorMessage)> ChangePasswordAsync(string userId, ChangePasswordRequestDto request) - { - var user = await _userManager.FindByIdAsync(userId); - if (user == null) return (false, "Benutzer nicht gefunden."); - - var result = await _userManager.ChangePasswordAsync(user, request.OldPassword, request.NewPassword); - - if (!result.Succeeded) - { - var errors = string.Join(" ", result.Errors.Select(e => e.Description)); - return (false, errors); - } - - return (true, "Passwort erfolgreich geändert."); - } - - // << NEUE IMPLEMENTIERUNG: UpdateMyProfileAsync verarbeitet alle Felder >> - public async Task<(bool Success, string ErrorMessage)> UpdateMyProfileAsync(string userId, UpdateCustomerDto profileDto) + public async Task UpdateMyProfileAsync(string userId, UpdateCustomerDto profileDto) { var customer = await _customerRepository.GetByUserIdAsync(userId); - if (customer == null) return (false, "Kundenprofil nicht gefunden."); + if (customer == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Kundenprofil nicht gefunden."); var identityUser = await _userManager.FindByIdAsync(userId); - if (identityUser == null) return (false, "Benutzerkonto nicht gefunden."); + if (identityUser == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzerkonto nicht gefunden."); - // 1. Aktuelles Passwort prüfen (Dies bleibt, da es eine gute Sicherheitspraxis für ALLE Profiländerungen ist) if (!await _userManager.CheckPasswordAsync(identityUser, profileDto.CurrentPassword)) { - return (false, "Falsches aktuelles Passwort zur Bestätigung."); + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch."); } - // 2. Felder der Customer-Entität aktualisieren (FirstName, LastName, DEFAULT ADDRESS IDs) customer.FirstName = profileDto.FirstName; customer.LastName = profileDto.LastName; customer.DefaultShippingAddressId = profileDto.DefaultShippingAddressId; customer.DefaultBillingAddressId = profileDto.DefaultBillingAddressId; - await _customerRepository.UpdateAsync(customer); // Speichert Änderungen im Customer-Profil - - // 3. Telefonnummer im ApplicationUser aktualisieren (wenn anders und nicht leer) - // E-Mail-Logik wird HIER KOMPLETT ENTFERNT. - bool identityUserChanged = false; + await _customerRepository.UpdateAsync(customer); if (!string.IsNullOrEmpty(profileDto.PhoneNumber) && identityUser.PhoneNumber != profileDto.PhoneNumber) { identityUser.PhoneNumber = profileDto.PhoneNumber; - identityUserChanged = true; - } - - if (identityUserChanged) - { var updateResult = await _userManager.UpdateAsync(identityUser); if (!updateResult.Succeeded) { var errors = string.Join(" ", updateResult.Errors.Select(e => e.Description)); - return (false, $"Fehler beim Aktualisieren der Telefonnummer: {errors}"); + return ServiceResult.Fail(ServiceResultType.Failure, $"Fehler beim Aktualisieren der Telefonnummer: {errors}"); } } - - return (true, "Profil und (optional) Telefonnummer erfolgreich aktualisiert."); + return ServiceResult.Ok(); } - public async Task<(bool Success, string ErrorMessage)> ChangeEmailAsync(string userId, string newEmail, string currentPassword) + public async Task ChangePasswordAsync(string userId, ChangePasswordRequestDto request) { var user = await _userManager.FindByIdAsync(userId); - if (user == null) return (false, "Benutzer nicht gefunden."); + if (user == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzer nicht gefunden."); - if (!await _userManager.CheckPasswordAsync(user, currentPassword)) - { - return (false, "Falsches aktuelles Passwort."); - } - - // Prüfen, ob die neue E-Mail bereits vergeben ist (außer sie ist die aktuelle E-Mail) - if (user.Email != newEmail && await _userManager.FindByEmailAsync(newEmail) != null) - { - return (false, "Die neue E-Mail-Adresse ist bereits registriert."); - } - - var token = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail); - var encodedToken = HttpUtility.UrlEncode(token); - var baseUrl = _configuration["App:BaseUrl"]; - - // Link für E-Mail-Bestätigung der Änderung - var confirmationLink = $"{baseUrl}/api/v1/auth/change-email-confirm?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={encodedToken}"; - - var message = new EmailMessage(); - message.From = "Your Webshop "; // << ANPASSEN >> - message.To.Add(newEmail); - message.Subject = "Bestätigen Sie Ihre E-Mail-Änderung für Your Webshop"; - message.HtmlBody = $@" -

E-Mail-Änderung Bestätigung

-

Sie haben eine Änderung Ihrer E-Mail-Adresse beantragt. Bitte klicken Sie auf den folgenden Link, um dies zu bestätigen:

-

{confirmationLink}

-

Wenn Sie diese Änderung nicht angefordert haben, können Sie diese E-Mail ignorieren.

-

Vielen Dank!

"; - - await _resend.EmailSendAsync(message); - - return (true, "Bestätigungs-E-Mail für die E-Mail-Änderung wurde gesendet. Bitte prüfen Sie Ihr Postfach."); - } - - public async Task<(bool Success, string ErrorMessage)> ConfirmEmailChangeAsync(string userId, string newEmail, string token) - { - var user = await _userManager.FindByIdAsync(userId); - if (user == null) return (false, "Benutzer nicht gefunden."); - - var result = await _userManager.ChangeEmailAsync(user, newEmail, HttpUtility.UrlDecode(token)); + var result = await _userManager.ChangePasswordAsync(user, request.OldPassword, request.NewPassword); if (!result.Succeeded) { var errors = string.Join(" ", result.Errors.Select(e => e.Description)); - return (false, $"E-Mail-Änderung konnte nicht bestätigt werden: {errors}"); + return ServiceResult.Fail(ServiceResultType.InvalidInput, errors); + } + return ServiceResult.Ok(); + } + + public async Task ChangeEmailAsync(string userId, string newEmail, string currentPassword) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzer nicht gefunden."); + + if (!await _userManager.CheckPasswordAsync(user, currentPassword)) + { + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch."); } - // Optional: Bestätigung der Telefonnummer zurücksetzen, wenn E-Mail geändert wurde - // user.PhoneNumberConfirmed = false; - // await _userManager.UpdateAsync(user); + if (user.Email != newEmail && await _userManager.FindByEmailAsync(newEmail) != null) + { + return ServiceResult.Fail(ServiceResultType.Conflict, "Die neue E-Mail-Adresse ist bereits registriert."); + } - return (true, "E-Mail-Adresse erfolgreich geändert und bestätigt."); + var token = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail); + var clientUrl = _configuration["App:ClientUrl"]!; + var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={HttpUtility.UrlEncode(token)}"; + + var message = new EmailMessage(); + message.From = _configuration["Resend:FromEmail"]!; + message.To.Add(newEmail); + message.Subject = "Bestätigen Sie Ihre neue E-Mail-Adresse"; + message.HtmlBody = $"

E-Mail-Änderung Bestätigung

Bitte klicken Sie auf den folgenden Link, um Ihre neue E-Mail-Adresse zu bestätigen:

Neue E-Mail-Adresse bestätigen

"; + await _resend.EmailSendAsync(message); + + // Die Erfolgsmeldung wird hier als Teil des "Ok"-Results übermittelt + return ServiceResult.Ok("Bestätigungs-E-Mail wurde an die neue Adresse gesendet. Bitte prüfen Sie Ihr Postfach."); } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs b/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs index 92e20b9..306adef 100644 --- a/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs +++ b/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs @@ -1,17 +1,17 @@ // src/Webshop.Application/Services/Customers/ICustomerService.cs using System.Threading.Tasks; -using Webshop.Application.DTOs; // CustomerDto -using Webshop.Application.DTOs.Auth; // ChangePasswordRequestDto -using Webshop.Application.DTOs.Customers; // UpdateCustomerProfileDto +using Webshop.Application; +using Webshop.Application.DTOs; +using Webshop.Application.DTOs.Auth; +using Webshop.Application.DTOs.Customers; namespace Webshop.Application.Services.Customers { public interface ICustomerService { - Task GetMyProfileAsync(string userId); - Task<(bool Success, string ErrorMessage)> ChangePasswordAsync(string userId, ChangePasswordRequestDto request); - Task<(bool Success, string ErrorMessage)> UpdateMyProfileAsync(string userId, UpdateCustomerDto profileDto); - Task<(bool Success, string ErrorMessage)> ChangeEmailAsync(string userId, string newEmail, string currentPassword); - Task<(bool Success, string ErrorMessage)> ConfirmEmailChangeAsync(string userId, string newEmail, string token); + Task> GetMyProfileAsync(string userId); + Task UpdateMyProfileAsync(string userId, UpdateCustomerDto profileDto); + Task ChangePasswordAsync(string userId, ChangePasswordRequestDto request); + Task ChangeEmailAsync(string userId, string newEmail, string currentPassword); } } \ No newline at end of file