diff --git a/TutorLizard.BusinessLogic/Data/JaszczurContext.cs b/TutorLizard.BusinessLogic/Data/JaszczurContext.cs index b8de0174..284b26c9 100644 --- a/TutorLizard.BusinessLogic/Data/JaszczurContext.cs +++ b/TutorLizard.BusinessLogic/Data/JaszczurContext.cs @@ -20,6 +20,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(user => user.Id) .ValueGeneratedOnAdd(); + modelBuilder.Entity() + .Property(user => user.IsActive) + .HasDefaultValue(false) + .IsRequired(); + modelBuilder.Entity() .Property(user => user.UserType) .HasConversion(); diff --git a/TutorLizard.BusinessLogic/Interfaces/Services/IUserService.cs b/TutorLizard.BusinessLogic/Interfaces/Services/IUserService.cs index 584a8e0b..40878729 100644 --- a/TutorLizard.BusinessLogic/Interfaces/Services/IUserService.cs +++ b/TutorLizard.BusinessLogic/Interfaces/Services/IUserService.cs @@ -1,12 +1,14 @@ using TutorLizard.BusinessLogic.Enums; +using TutorLizard.BusinessLogic.Models; using TutorLizard.BusinessLogic.Models.DTOs; namespace TutorLizard.BusinessLogic.Interfaces.Services; public interface IUserService { - public Task LogIn(string username, string password); - public Task RegisterUser(string userName, UserType type, string email, string password); + public Task LogIn(string username, string password); + public Task RegisterUser(string userName, UserType type, string email, string password, string activationCode); + Task ActivateUserAsync(string activationCode); public Task RegisterUserWithGoogle(string username, string email, string googleId); public Task IsTheGoogleUserRegistered(string googleId); public Task LogInWithGoogle(string username, string googleId); diff --git a/TutorLizard.BusinessLogic/Models/ActivationResult.cs b/TutorLizard.BusinessLogic/Models/ActivationResult.cs new file mode 100644 index 00000000..62a197bb --- /dev/null +++ b/TutorLizard.BusinessLogic/Models/ActivationResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TutorLizard.BusinessLogic.Models +{ + public class ActivationResult + { + public bool IsActivated { get; set; } + public string ActivationCode { get; set; } + } +} diff --git a/TutorLizard.BusinessLogic/Models/DTOs/LogInResult.cs b/TutorLizard.BusinessLogic/Models/DTOs/LogInResult.cs new file mode 100644 index 00000000..d59490a7 --- /dev/null +++ b/TutorLizard.BusinessLogic/Models/DTOs/LogInResult.cs @@ -0,0 +1,16 @@ +namespace TutorLizard.BusinessLogic.Models.DTOs +{ + public class LogInResult + { + public LogInResultCode ResultCode { get; set; } + public UserDto? User { get; set; } + } + + public enum LogInResultCode + { + Success, + UserNotFound, + InactiveAccount, + InvalidPassword + } +} diff --git a/TutorLizard.BusinessLogic/Models/DTOs/UserDto.cs b/TutorLizard.BusinessLogic/Models/DTOs/UserDto.cs index ad8f4eea..d29922b0 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/UserDto.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/UserDto.cs @@ -5,6 +5,7 @@ namespace TutorLizard.BusinessLogic.Models.DTOs; public class UserDto { public int Id { get; set; } + public bool? IsActive { get; set; } [Required(ErrorMessage = "Podaj nazwę użytkownika.")] [Display(Name = "Nazwa użytkownika")] diff --git a/TutorLizard.BusinessLogic/Models/User.cs b/TutorLizard.BusinessLogic/Models/User.cs index de152ac3..77ddbf3f 100644 --- a/TutorLizard.BusinessLogic/Models/User.cs +++ b/TutorLizard.BusinessLogic/Models/User.cs @@ -6,6 +6,7 @@ namespace TutorLizard.BusinessLogic.Models; public class User { public int Id { get; set; } + public bool IsActive { get; set; } [Required] [MinLength(5)] @@ -45,4 +46,5 @@ public User() public ICollection Ads { get; set; } = new List(); public ICollection AdRequests { get; set; } = new List(); public ICollection ScheduleItemRequests { get; set; } = new List(); + public string? ActivationCode { get; set; } } diff --git a/TutorLizard.BusinessLogic/Services/UserService.cs b/TutorLizard.BusinessLogic/Services/UserService.cs index 8e7b2e19..94067ca4 100644 --- a/TutorLizard.BusinessLogic/Services/UserService.cs +++ b/TutorLizard.BusinessLogic/Services/UserService.cs @@ -18,25 +18,43 @@ public UserService(IDbRepository userRepository) { _userRepository = userRepository; } - public async Task LogIn(string username, string password) + + public async Task LogIn(string username, string password) { var user = await _userRepository.GetAll() .FirstOrDefaultAsync(user => user.Name == username); if (user == null) { - return null; + return new LogInResult + { + ResultCode = LogInResultCode.UserNotFound + }; + } + + if (!user.IsActive) + { + return new LogInResult + { + ResultCode = LogInResultCode.InactiveAccount + }; } - var result = _passwordHasher - .VerifyHashedPassword(user, - user.PasswordHash, - password); + var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (result == PasswordVerificationResult.Success) - return user.ToDto(); + { + return new LogInResult + { + ResultCode = LogInResultCode.Success, + User = user.ToDto() + }; + } - return null; + return new LogInResult + { + ResultCode = LogInResultCode.InvalidPassword + }; } public async Task LogInWithGoogle(string username, string googleId) @@ -56,7 +74,7 @@ public UserService(IDbRepository userRepository) } - public async Task RegisterUser(string userName, UserType type, string email, string password) + public async Task RegisterUser(string userName, UserType type, string email, string password, string activationCode) { if (await _userRepository.GetAll().AnyAsync(user => user.Name == userName)) return false; @@ -66,7 +84,9 @@ public async Task RegisterUser(string userName, UserType type, string emai Name = userName, UserType = type, Email = email, - PasswordHash = password + PasswordHash = password, + ActivationCode = activationCode, + IsActive = false }; user.PasswordHash = _passwordHasher.HashPassword(user, password); @@ -101,4 +121,34 @@ public async Task IsTheGoogleUserRegistered(string googleId) return false; } + + public async Task ActivateUserAsync(string activationCode) + { + + var user = await _userRepository.GetAll() + .FirstOrDefaultAsync(u => u.ActivationCode == activationCode && u.IsActive == false); + + if (user != null) + { + user.IsActive = true; + user.ActivationCode = "ACTIVATED"; + + await _userRepository.Update(user.Id, u => + { + u.IsActive = user.IsActive; + u.ActivationCode = user.ActivationCode; + }); + + return new ActivationResult + { + IsActivated = true, + ActivationCode = activationCode + }; + } + return new ActivationResult + { + IsActivated = false, + ActivationCode = activationCode + }; + } } \ No newline at end of file diff --git a/TutorLizard.Web/Controllers/AccountController.cs b/TutorLizard.Web/Controllers/AccountController.cs index 2c01abde..c1cc9b2e 100644 --- a/TutorLizard.Web/Controllers/AccountController.cs +++ b/TutorLizard.Web/Controllers/AccountController.cs @@ -5,23 +5,31 @@ using Microsoft.AspNetCore.Mvc; using System.Security.Authentication; using System.Security.Claims; +using System.ComponentModel; +using System.Net; +using System.Net.Mail; using TutorLizard.BusinessLogic.Enums; using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models.DTOs; using TutorLizard.Web.Interfaces.Services; using TutorLizard.Web.Models; + namespace TutorLizard.Web.Controllers; public class AccountController : Controller { private readonly IUserAuthenticationService _userAuthenticationService; private readonly IUiMessagesService _uiMessagesService; + private readonly IUserService _userService; public AccountController(IUserAuthenticationService userAuthenticationService, - IUiMessagesService uiMessagesService) + IUiMessagesService uiMessagesService, + IUserService userService) { _userAuthenticationService = userAuthenticationService; _uiMessagesService = uiMessagesService; + _userService = userService; } public IActionResult Index() @@ -111,14 +119,39 @@ public async Task Login(LoginModel model) try { - if (ModelState.IsValid && await _userAuthenticationService.LogInAsync(model.UserName, model.Password)) + if (ModelState.IsValid) { - _uiMessagesService.ShowSuccessMessage("Jesteś zalogowany/a."); - if (string.IsNullOrEmpty(returnUrl)) + var logInResult = await _userAuthenticationService.LogInAsync(model.UserName, model.Password); + + switch (logInResult.ResultCode) { - return RedirectToAction("Index", "Home"); + case BusinessLogic.Models.DTOs.LogInResultCode.Success: + _uiMessagesService.ShowSuccessMessage("Jesteś zalogowany/a."); + if (string.IsNullOrEmpty(returnUrl)) + { + return RedirectToAction("Index", "Home"); + } + return Redirect(returnUrl); + + case BusinessLogic.Models.DTOs.LogInResultCode.UserNotFound: + case BusinessLogic.Models.DTOs.LogInResultCode.InvalidPassword: + _uiMessagesService.ShowFailureMessage("Logowanie nieudane. Nieprawidłowa nazwa użytkownika lub hasło."); + return RedirectToAction(nameof(Login), new { returnUrl = returnUrl }); + + case BusinessLogic.Models.DTOs.LogInResultCode.InactiveAccount: + _uiMessagesService.ShowFailureMessage("Logowanie nieudane. Konto nie jest aktywne."); + return LocalRedirect("/Home/Index"); + + default: + throw new InvalidEnumArgumentException(argumentName: nameof(BusinessLogic.Models.DTOs.LogInResult.ResultCode), + invalidValue: (int)logInResult.ResultCode, + enumClass: typeof(BusinessLogic.Models.DTOs.LogInResult)); } - return Redirect(returnUrl); + } + else + { + _uiMessagesService.ShowFailureMessage("Logowanie nieudane. Proszę wypełnić poprawnie formularz."); + return RedirectToAction(nameof(Login), new { returnUrl = returnUrl }); } } catch @@ -126,9 +159,8 @@ public async Task Login(LoginModel model) _uiMessagesService.ShowFailureMessage("Logowanie nieudane."); return LocalRedirect("/Home/Index"); } - _uiMessagesService.ShowFailureMessage("Logowanie nieudane."); - return RedirectToAction(nameof(Login), new { returnUrl = returnUrl }); } + [Authorize] public async Task Logout() { @@ -140,16 +172,30 @@ public IActionResult Register() { return View(); } + [HttpPost] [ValidateAntiForgeryToken] public async Task Register(RegisterUserModel model) { try { - if (ModelState.IsValid - && await _userAuthenticationService.RegisterUser(model.UserName, UserType.Regular, model.Email, model.Password)) + if (!ModelState.IsValid) { - _uiMessagesService.ShowSuccessMessage("Użytkownik zarejestrowany."); + var errors = ModelState.Values.SelectMany(v => v.Errors); + foreach (var error in errors) + { + Console.WriteLine(error.ErrorMessage); + } + return View(model); + } + + var (registrationResult, activationCode) = await _userAuthenticationService.RegisterUser( + model.UserName, UserType.Regular, model.Email, model.Password); + + if (registrationResult) + { + _userAuthenticationService.SendActivationEmail(model.Email, activationCode); + _uiMessagesService.ShowSuccessMessage("Wysłano mail aktywacyjny."); return LocalRedirect("/Home/Index"); } } @@ -162,6 +208,23 @@ public async Task Register(RegisterUserModel model) return LocalRedirect("/Home/Index"); } + [HttpGet] + public async Task ActivateAccount(string activationCode) + { + var result = await _userService.ActivateUserAsync(activationCode); + + if (result.IsActivated) + { + _uiMessagesService.ShowSuccessMessage("Atywacja udana."); + return View("ActivateAccount"); + } + else + { + _uiMessagesService.ShowFailureMessage("Aktywacja konta nie powiodła się."); + return LocalRedirect("/Home/Index"); + } + } + public IActionResult AccessDenied() { return View(); diff --git a/TutorLizard.Web/Interfaces/Services/IUserAuthenticationService.cs b/TutorLizard.Web/Interfaces/Services/IUserAuthenticationService.cs index e05ee514..45a6bc6f 100644 --- a/TutorLizard.Web/Interfaces/Services/IUserAuthenticationService.cs +++ b/TutorLizard.Web/Interfaces/Services/IUserAuthenticationService.cs @@ -6,10 +6,11 @@ public interface IUserAuthenticationService { int? GetLoggedInUserId(); - public Task LogInAsync(string username, string password); + Task LogInAsync(string username, string password); public Task LogOutAsync(); - public Task RegisterUser(string username, UserType type, string email, string password); public Task IsGoogleUserRegistered(string googleid); public Task RegisterUserWithGoogle(string username, string email, string googleId); public Task LogInWithGoogleAsync(string username, string googleId); + Task<(bool, string)> RegisterUser(string username, UserType type, string email, string password); + void SendActivationEmail(string email, string activationCode); } diff --git a/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.Designer.cs b/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.Designer.cs new file mode 100644 index 00000000..69e4f217 --- /dev/null +++ b/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.Designer.cs @@ -0,0 +1,345 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TutorLizard.BusinessLogic.Data; + +#nullable disable + +namespace TutorLizard.Web.Migrations +{ + [DbContext(typeof(JaszczurContext))] + [Migration("20240606152246_Add-Activation-Emails")] + partial class AddActivationEmails + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.Ad", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(3000) + .HasColumnType("nvarchar(3000)"); + + b.Property("IsRemote") + .HasColumnType("bit"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Price") + .HasPrecision(7, 2) + .HasColumnType("decimal(7,2)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TutorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("TutorId"); + + b.ToTable("Ads"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.AdRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdId") + .HasColumnType("int"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("IsRemote") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReplyMessage") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("StudentId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AdId"); + + b.HasIndex("StudentId"); + + b.ToTable("AdRequests"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.ScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdId") + .HasColumnType("int"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdId"); + + b.ToTable("ScheduleItems"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.ScheduleItemRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("IsRemote") + .HasColumnType("bit"); + + b.Property("ScheduleItemId") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleItemId"); + + b.HasIndex("StudentId"); + + b.ToTable("ScheduleItemRequests"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActivationCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserType") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.Ad", b => + { + b.HasOne("TutorLizard.BusinessLogic.Models.Category", "Category") + .WithMany("Ads") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TutorLizard.BusinessLogic.Models.User", "User") + .WithMany("Ads") + .HasForeignKey("TutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.AdRequest", b => + { + b.HasOne("TutorLizard.BusinessLogic.Models.Ad", "Ad") + .WithMany("AdRequests") + .HasForeignKey("AdId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TutorLizard.BusinessLogic.Models.User", "User") + .WithMany("AdRequests") + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ad"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.ScheduleItem", b => + { + b.HasOne("TutorLizard.BusinessLogic.Models.Ad", "Ad") + .WithMany("ScheduleItems") + .HasForeignKey("AdId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ad"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.ScheduleItemRequest", b => + { + b.HasOne("TutorLizard.BusinessLogic.Models.ScheduleItem", "ScheduleItem") + .WithMany("ScheduleItemRequests") + .HasForeignKey("ScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TutorLizard.BusinessLogic.Models.User", "User") + .WithMany("ScheduleItemRequests") + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ScheduleItem"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.Ad", b => + { + b.Navigation("AdRequests"); + + b.Navigation("ScheduleItems"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.Category", b => + { + b.Navigation("Ads"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.ScheduleItem", b => + { + b.Navigation("ScheduleItemRequests"); + }); + + modelBuilder.Entity("TutorLizard.BusinessLogic.Models.User", b => + { + b.Navigation("AdRequests"); + + b.Navigation("Ads"); + + b.Navigation("ScheduleItemRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.cs b/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.cs new file mode 100644 index 00000000..14a7d81c --- /dev/null +++ b/TutorLizard.Web/Migrations/20240606152246_Add-Activation-Emails.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TutorLizard.Web.Migrations +{ + /// + public partial class AddActivationEmails : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ActivationCode", + table: "Users", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IsActive", + table: "Users", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ActivationCode", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsActive", + table: "Users"); + } + } +} diff --git a/TutorLizard.Web/Migrations/JaszczurContextModelSnapshot.cs b/TutorLizard.Web/Migrations/JaszczurContextModelSnapshot.cs index bcaf54e5..ba34cd76 100644 --- a/TutorLizard.Web/Migrations/JaszczurContextModelSnapshot.cs +++ b/TutorLizard.Web/Migrations/JaszczurContextModelSnapshot.cs @@ -208,6 +208,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("ActivationCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("DateCreated") .HasColumnType("datetime2"); @@ -216,6 +220,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("GoogleId") .HasColumnType("nvarchar(max)"); diff --git a/TutorLizard.Web/Models/EmailSettings.cs b/TutorLizard.Web/Models/EmailSettings.cs new file mode 100644 index 00000000..481bb807 --- /dev/null +++ b/TutorLizard.Web/Models/EmailSettings.cs @@ -0,0 +1,13 @@ +namespace TutorLizard.Web.Models +{ + public class EmailSettings + { + public string FromAddress { get; set; } + public string FromPassword { get; set; } + public string ActivationLink { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public bool EnableSsl { get; set; } + } + +} diff --git a/TutorLizard.Web/Program.cs b/TutorLizard.Web/Program.cs index 73c66df3..a85940ec 100644 --- a/TutorLizard.Web/Program.cs +++ b/TutorLizard.Web/Program.cs @@ -7,6 +7,7 @@ using TutorLizard.BusinessLogic.Options; using TutorLizard.BusinessLogic.Services; using TutorLizard.Web.Interfaces.Services; +using TutorLizard.Web.Models; using TutorLizard.Web.Services; var builder = WebApplication.CreateBuilder(args); @@ -68,7 +69,9 @@ b => b.MigrationsAssembly("TutorLizard.Web")); }); + builder.Services.AddTutorLizardDbRepositories(); +builder.Services.Configure(builder.Configuration.GetSection("EmailSettings")); @@ -97,6 +100,18 @@ app.UseAuthorization(); app.UseHttpsRedirection(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + name: "ActivateAccount", + pattern: "{controller=Account}/{action=ActivateAccount}/{activationCode?}"); +}); + + + app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); diff --git a/TutorLizard.Web/Services/UserAuthenticationService.cs b/TutorLizard.Web/Services/UserAuthenticationService.cs index 65c5ea38..c5ed6092 100644 --- a/TutorLizard.Web/Services/UserAuthenticationService.cs +++ b/TutorLizard.Web/Services/UserAuthenticationService.cs @@ -1,8 +1,16 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Mail; using System.Security.Claims; +using TutorLizard.BusinessLogic.Data; using TutorLizard.BusinessLogic.Enums; +using TutorLizard.BusinessLogic.Interfaces.Data.Repositories; using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models; +using TutorLizard.Web.Models; namespace TutorLizard.BusinessLogic.Services; @@ -10,47 +18,50 @@ public class UserAuthenticationService : IUserAuthenticationService { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IUserService _userService; + private readonly EmailSettings _emailSettings; + private readonly IDbRepository _userRepository; - public UserAuthenticationService(IHttpContextAccessor httpContextAccessor, IUserService userService) + public UserAuthenticationService(IHttpContextAccessor httpContextAccessor, IUserService userService, IOptions emailSettings, IDbRepository userRepository) { _httpContextAccessor = httpContextAccessor; _userService = userService; + _emailSettings = emailSettings.Value; + _userRepository = userRepository; } - public async Task LogInAsync(string username, string password) + + public async Task LogInAsync(string username, string password) { - var user = await _userService.LogIn(username, password); + var logInResult = await _userService.LogIn(username, password); - if (user is null) + if (logInResult.ResultCode == Models.DTOs.LogInResultCode.Success && logInResult.User != null) { - return false; - } - - var claims = new List + var claims = new List { - new Claim(ClaimTypes.Email, user.Email), - new Claim(ClaimTypes.Name, user.Name), - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), - new Claim(ClaimTypes.Role, user.UserType.ToString()) + new Claim(ClaimTypes.Email, logInResult.User.Email), + new Claim(ClaimTypes.Name, logInResult.User.Name), + new Claim(ClaimTypes.NameIdentifier, logInResult.User.Id.ToString()), + new Claim(ClaimTypes.Role, logInResult.User.UserType.ToString()) }; - var claimsIdentity = new ClaimsIdentity( - claims, CookieAuthenticationDefaults.AuthenticationScheme); - - var authProperties = new AuthenticationProperties - { - AllowRefresh = true, - ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10), - IsPersistent = true, - }; - - if (_httpContextAccessor.HttpContext is null) - return false; - - await _httpContextAccessor.HttpContext.SignInAsync("CookieAuth", - new ClaimsPrincipal(claimsIdentity), - authProperties); + var claimsIdentity = new ClaimsIdentity( + claims, CookieAuthenticationDefaults.AuthenticationScheme); + + var authProperties = new AuthenticationProperties + { + AllowRefresh = true, + ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10), + IsPersistent = true, + }; + + if (_httpContextAccessor.HttpContext != null) + { + await _httpContextAccessor.HttpContext.SignInAsync("CookieAuth", + new ClaimsPrincipal(claimsIdentity), + authProperties); + } + } - return true; + return logInResult; } public async Task LogInWithGoogleAsync(string username, string googleId) @@ -99,9 +110,17 @@ public async Task LogOutAsync() await _httpContextAccessor.HttpContext.SignOutAsync("CookieAuth"); } - public Task RegisterUser(string username, UserType type, string email, string password) + public async Task<(bool, string)> RegisterUser(string username, UserType type, string email, string password) + { + string activationCode = GenerateActivationCode(); + bool result = await _userService.RegisterUser(username, type, email, password, activationCode); + + return (result, activationCode); + } + + private string GenerateActivationCode() { - return _userService.RegisterUser(username, type, email, password); + return Guid.NewGuid().ToString(); } public Task RegisterUserWithGoogle(string username, string email, string googleId) @@ -132,4 +151,33 @@ public async Task IsGoogleUserRegistered(string googleid) } return userId; } + + public void SendActivationEmail(string userEmail, string activationCode) + { + var fromAddress = new MailAddress(_emailSettings.FromAddress, "Tutor Lizard"); + var toAddress = new MailAddress(userEmail); + var fromPassword = _emailSettings.FromPassword; + + string subject = "Aktywacja konta"; + string body = $"Cześć tu zespół Tutor Lizard, \naby aktywować swoje konto, kliknij poniższy link: {_emailSettings.ActivationLink}{activationCode}"; + + var smtp = new SmtpClient + { + Host = _emailSettings.Host, + Port = _emailSettings.Port, + EnableSsl = _emailSettings.EnableSsl, + DeliveryMethod = SmtpDeliveryMethod.Network, + UseDefaultCredentials = false, + Credentials = new NetworkCredential(fromAddress.Address, fromPassword) + }; + using (var message = new MailMessage(fromAddress, toAddress) + { + Subject = subject, + Body = body + }) + { + smtp.Send(message); + } + } + } diff --git a/TutorLizard.Web/Views/Account/ActivateAccount.cshtml b/TutorLizard.Web/Views/Account/ActivateAccount.cshtml new file mode 100644 index 00000000..c11389f5 --- /dev/null +++ b/TutorLizard.Web/Views/Account/ActivateAccount.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Aktywacja konta"; +} +

@ViewData["Title"]

+ +

Twoje konto zostało aktywowane.

\ No newline at end of file