| "</div>" + | "</div>" + | ||||
| "</div>"; | "</div>"; | ||||
| } | } | ||||
| public static string RenderRegisterPage(string url) | |||||
| { | |||||
| return "<div style=\"font-family: sans-serif\">" + | |||||
| "<div style=\"font-family: sans-serif;text-align: center;\">" + | |||||
| "<h2 style=\"color: #017397;\">Welcome to HR Center</h2>" + | |||||
| "<p style=\"font-size: 20px\">" + | |||||
| "To register, please click on the button below." + | |||||
| "</p>" + | |||||
| "<a style = \"color: white;text-decoration:none;background-color: #017397;cursor: pointer;font-size: 20px;width: 220px;text-align: center;border-radius: 5px;padding: 5px 15px;height: 25px;\" " + | |||||
| $"href=\"{url}\">" + | |||||
| " Click here to register" + | |||||
| "</a>" + | |||||
| "<p style = \"font-size: 12px; margin-top: 25px;\" >" + | |||||
| "Please do not reply to this email.This message was sent from a notification-only address that is not monitored." + | |||||
| "</p>" + | |||||
| "</div>" + | |||||
| "</div>"; | |||||
| } | |||||
| } | } | ||||
| } | } |
| namespace Diligent.WebAPI.Business.Helper | |||||
| { | |||||
| public static class StringGenerator | |||||
| { | |||||
| public static string GenerateRandomPassword(PasswordOptions opts = null) | |||||
| { | |||||
| if (opts == null) opts = new PasswordOptions() | |||||
| { | |||||
| RequiredLength = 8, | |||||
| RequiredUniqueChars = 4, | |||||
| RequireDigit = true, | |||||
| RequireLowercase = true, | |||||
| RequireNonAlphanumeric = true, | |||||
| RequireUppercase = true | |||||
| }; | |||||
| string[] randomChars = new[] { | |||||
| "ABCDEFGHJKLMNOPQRSTUVWXYZ", // uppercase | |||||
| "abcdefghijkmnopqrstuvwxyz", // lowercase | |||||
| "0123456789", // digits | |||||
| "!@$?_-" // non-alphanumeric | |||||
| }; | |||||
| Random rand = new(Environment.TickCount); | |||||
| List<char> chars = new List<char>(); | |||||
| if (opts.RequireUppercase) | |||||
| chars.Insert(rand.Next(0, chars.Count), | |||||
| randomChars[0][rand.Next(0, randomChars[0].Length)]); | |||||
| if (opts.RequireLowercase) | |||||
| chars.Insert(rand.Next(0, chars.Count), | |||||
| randomChars[1][rand.Next(0, randomChars[1].Length)]); | |||||
| if (opts.RequireDigit) | |||||
| chars.Insert(rand.Next(0, chars.Count), | |||||
| randomChars[2][rand.Next(0, randomChars[2].Length)]); | |||||
| if (opts.RequireNonAlphanumeric) | |||||
| chars.Insert(rand.Next(0, chars.Count), | |||||
| randomChars[3][rand.Next(0, randomChars[3].Length)]); | |||||
| for (int i = chars.Count; i < opts.RequiredLength | |||||
| || chars.Distinct().Count() < opts.RequiredUniqueChars; i++) | |||||
| { | |||||
| string rcs = randomChars[rand.Next(0, randomChars.Length)]; | |||||
| chars.Insert(rand.Next(0, chars.Count), | |||||
| rcs[rand.Next(0, rcs.Length)]); | |||||
| } | |||||
| return new string(chars.ToArray()); | |||||
| } | |||||
| } | |||||
| } |
| #region Model to DTO | #region Model to DTO | ||||
| CreateMap<User, UserResponseDTO>(); | CreateMap<User, UserResponseDTO>(); | ||||
| CreateMap<User, UserDetailsResponseDTO>() | |||||
| .ForMember(dest => dest.PhoneNumber, opt => opt.NullSubstitute("User has no phone number saved.")) | |||||
| .ForMember(dest => dest.Position, opt => opt.NullSubstitute("Position has not been declared yet.")) | |||||
| .ForMember(dest => dest.SocialMedias, opt => opt.NullSubstitute("User takes no part in any social media.")); | |||||
| #endregion | #endregion | ||||
| } | } | ||||
| } | } |
| namespace Diligent.WebAPI.Business.Services.Interfaces | |||||
| using Diligent.WebAPI.Contracts.DTOs.User; | |||||
| namespace Diligent.WebAPI.Business.Services.Interfaces | |||||
| { | { | ||||
| public interface IUserService | public interface IUserService | ||||
| { | { | ||||
| Task<IEnumerable<User?>> GetAll(); | Task<IEnumerable<User?>> GetAll(); | ||||
| Task<User?> GetById(int id); | Task<User?> GetById(int id); | ||||
| Task<User?> GetByEmail(string email); | |||||
| Task CreateUser(CreateUserRequestDto model); | Task CreateUser(CreateUserRequestDto model); | ||||
| Task ToggleEnable(User user); | Task ToggleEnable(User user); | ||||
| Task RemoveUser(User user); | Task RemoveUser(User user); | ||||
| Task<bool> VerifyToken(User user, string token); | |||||
| Task<ServiceResponseDTO<object>> SendRegistrationLink(InviteDTO invite); | |||||
| } | } | ||||
| } | } |
| using Diligent.WebAPI.Business.Services.Interfaces; | using Diligent.WebAPI.Business.Services.Interfaces; | ||||
| using Diligent.WebAPI.Business.Settings; | using Diligent.WebAPI.Business.Settings; | ||||
| using Diligent.WebAPI.Contracts.DTOs.User; | |||||
| using Diligent.WebAPI.Data; | using Diligent.WebAPI.Data; | ||||
| using Microsoft.AspNetCore.Identity; | using Microsoft.AspNetCore.Identity; | ||||
| using Microsoft.AspNetCore.WebUtilities; | |||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||
| using System.Web; | |||||
| namespace Diligent.WebAPI.Business.Services | namespace Diligent.WebAPI.Business.Services | ||||
| { | { | ||||
| public async Task<User?> GetById(int id) => | public async Task<User?> GetById(int id) => | ||||
| await _userManager.FindByIdAsync(id.ToString()); | await _userManager.FindByIdAsync(id.ToString()); | ||||
| public async Task<User?> GetByEmail(string email) => | |||||
| await _userManager.FindByEmailAsync(email); | |||||
| public async Task CreateUser(CreateUserRequestDto model) | public async Task CreateUser(CreateUserRequestDto model) | ||||
| { | { | ||||
| await _databaseContext.SaveChangesAsync(); | await _databaseContext.SaveChangesAsync(); | ||||
| } | } | ||||
| public async Task<ServiceResponseDTO<object>> SendRegistrationLink(InviteDTO invite) | |||||
| { | |||||
| // check if user exists | |||||
| var check = await _userManager.FindByEmailAsync(invite.Email); | |||||
| if (check != null) | |||||
| return new ServiceResponseDTO<object>() | |||||
| { | |||||
| IsError = true, | |||||
| ErrorMessage = "User already registered." | |||||
| }; | |||||
| // create template user | |||||
| // this user is disabled to log in until confirming invitation | |||||
| var user = new User | |||||
| { | |||||
| UserName = invite.Email, | |||||
| Email = invite.Email, | |||||
| FirstName = invite.FirstName, | |||||
| LastName = invite.LastName, | |||||
| IsEnabled = false | |||||
| }; | |||||
| await _userManager.CreateAsync(user, StringGenerator.GenerateRandomPassword()); | |||||
| // generate invitation token for user | |||||
| // encoded for URLs | |||||
| var token = await _userManager.GeneratePasswordResetTokenAsync(user); | |||||
| token = HttpUtility.UrlEncode(token); | |||||
| // send link | |||||
| await _emailer.SendEmailAndWriteToDbAsync(invite.Email, "Welcome", HTMLHelper.RenderRegisterPage($"{_frontEndSettings.BaseUrl}/register?token={token}&email={invite.Email}"), isHtml: true); | |||||
| await _databaseContext.SaveChangesAsync(); | |||||
| return new ServiceResponseDTO<object> | |||||
| { | |||||
| Data = new { Message = "Link has been sent!" } | |||||
| }; | |||||
| } | |||||
| public async Task<bool> VerifyToken(User user, string token) | |||||
| { | |||||
| // this method is going to be updated | |||||
| // curent new password value is static and only used for testing | |||||
| // method is not complete and is currently only used to check if valid reset token is sent | |||||
| var result = await _userManager.ResetPasswordAsync(user, token, "Nekasifra123!"); | |||||
| return result.Succeeded; | |||||
| } | |||||
| } | } | ||||
| } | } |
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Diligent.WebAPI.Contracts.DTOs.User | |||||
| { | |||||
| public class InviteDTO | |||||
| { | |||||
| public string Email { get; set; } | |||||
| public string FirstName { get; set; } | |||||
| public string LastName { get; set; } | |||||
| } | |||||
| } |
| namespace Diligent.WebAPI.Contracts.DTOs.User | |||||
| { | |||||
| public class UserDetailsResponseDTO | |||||
| { | |||||
| public int Id { get; set; } | |||||
| public string FirstName { get; set; } | |||||
| public string LastName { get; set; } | |||||
| public string Email { get; set; } | |||||
| public bool IsEnabled { get; set; } | |||||
| public string PhoneNumber { get; set; } | |||||
| public string Position { get; set; } | |||||
| public string SocialMedias { get; set; } | |||||
| } | |||||
| } |
| public string LastName { get; set; } | public string LastName { get; set; } | ||||
| public string Email { get; set; } | public string Email { get; set; } | ||||
| public bool IsEnabled { get; set; } | public bool IsEnabled { get; set; } | ||||
| //public string CVLink { get; set; } | |||||
| //public string Position { get; set; } | //public string Position { get; set; } | ||||
| } | } | ||||
| } | } |
| public string LastName { get; set; } | public string LastName { get; set; } | ||||
| public string? PasswordResetToken { get; set; } | public string? PasswordResetToken { get; set; } | ||||
| public List<Comment> Comments { get; set; } | public List<Comment> Comments { get; set; } | ||||
| public bool IsEnabled { get; set; } | |||||
| public bool? IsEnabled { get; set; } | |||||
| } | } |
| using Microsoft.EntityFrameworkCore.Migrations; | |||||
| #nullable disable | |||||
| namespace Diligent.WebAPI.Data.Migrations | |||||
| { | |||||
| public partial class DefaultDisabledUser : Migration | |||||
| { | |||||
| protected override void Up(MigrationBuilder migrationBuilder) | |||||
| { | |||||
| migrationBuilder.AlterColumn<bool>( | |||||
| name: "IsEnabled", | |||||
| table: "AspNetUsers", | |||||
| type: "bit", | |||||
| nullable: true, | |||||
| defaultValue: true, | |||||
| oldClrType: typeof(bool), | |||||
| oldType: "bit", | |||||
| oldDefaultValue: true); | |||||
| } | |||||
| protected override void Down(MigrationBuilder migrationBuilder) | |||||
| { | |||||
| migrationBuilder.AlterColumn<bool>( | |||||
| name: "IsEnabled", | |||||
| table: "AspNetUsers", | |||||
| type: "bit", | |||||
| nullable: false, | |||||
| defaultValue: true, | |||||
| oldClrType: typeof(bool), | |||||
| oldType: "bit", | |||||
| oldNullable: true, | |||||
| oldDefaultValue: true); | |||||
| } | |||||
| } | |||||
| } |
| .IsRequired() | .IsRequired() | ||||
| .HasColumnType("nvarchar(max)"); | .HasColumnType("nvarchar(max)"); | ||||
| b.Property<bool>("IsEnabled") | |||||
| b.Property<bool?>("IsEnabled") | |||||
| .ValueGeneratedOnAdd() | .ValueGeneratedOnAdd() | ||||
| .HasColumnType("bit") | .HasColumnType("bit") | ||||
| .HasDefaultValue(true); | .HasDefaultValue(true); |
| return Ok(_mapper.Map<IEnumerable<User?>, IEnumerable<UserResponseDTO>>(await _userService.GetAll())); | return Ok(_mapper.Map<IEnumerable<User?>, IEnumerable<UserResponseDTO>>(await _userService.GetAll())); | ||||
| } | } | ||||
| //[Authorize] | |||||
| [Authorize] | [Authorize] | ||||
| [HttpPost("toggleEnable/{id}")] | [HttpPost("toggleEnable/{id}")] | ||||
| public async Task<IActionResult> ToggleEnable(int id) | public async Task<IActionResult> ToggleEnable(int id) | ||||
| return Ok(user.Id); | return Ok(user.Id); | ||||
| } | } | ||||
| //[Authorize] | |||||
| [Authorize] | |||||
| [HttpGet("{id}")] | |||||
| public async Task<IActionResult> GetUser(int id) | |||||
| { | |||||
| var user = await _userService.GetById(id); | |||||
| if (user == null) | |||||
| { | |||||
| return BadRequest("User not found"); | |||||
| } | |||||
| return Ok(_mapper.Map<User, UserDetailsResponseDTO>(user)); | |||||
| } | |||||
| [Authorize] | |||||
| [HttpPost("invite")] | |||||
| public async Task<IActionResult> InviteUser([FromBody] InviteDTO invite) | |||||
| { | |||||
| var response = await _userService.SendRegistrationLink(invite); | |||||
| if (response.IsError is true) | |||||
| return BadRequest(new { message = response.ErrorMessage }); | |||||
| return Ok(response.Data); | |||||
| } | |||||
| [HttpPost("verify-invite")] | |||||
| public async Task<IActionResult> VerifyInvite(string email, string token) | |||||
| { | |||||
| // controller endpoint currently used only for testing | |||||
| // user should be enabled to log in after accepting invite and updating his account | |||||
| var user = await _userService.GetByEmail(email); | |||||
| var result = await _userService.VerifyToken(user, token); | |||||
| return Ok(result); | |||||
| } | |||||
| //[Authorize] | //[Authorize] | ||||
| [HttpPost] | [HttpPost] | ||||
| public async Task<IActionResult> CreateUser([FromBody] CreateUserRequestDto model) | public async Task<IActionResult> CreateUser([FromBody] CreateUserRequestDto model) |
| "SmtpServer": "smtp.mailtrap.io", | "SmtpServer": "smtp.mailtrap.io", | ||||
| "SmtpPort": 2525, | "SmtpPort": 2525, | ||||
| "SmtpUseSSL": true, | "SmtpUseSSL": true, | ||||
| "SmtpUsername": "460e3c49f02e37", | |||||
| "SmtpPassword": "66443869eaad55", | |||||
| "SmtpUsername": "179be7a6fd2f50", | |||||
| "SmtpPassword": "63cde15de0d5d7", | |||||
| "SmtpFrom": "noreply@hrcenter.net", | "SmtpFrom": "noreply@hrcenter.net", | ||||
| "SmtpFromName": "HRCenter Team" | "SmtpFromName": "HRCenter Team" | ||||
| }, | }, |
| "SmtpServer": "smtp.mailtrap.io", | "SmtpServer": "smtp.mailtrap.io", | ||||
| "SmtpPort": 2525, | "SmtpPort": 2525, | ||||
| "SmtpUseSSL": true, | "SmtpUseSSL": true, | ||||
| "SmtpUsername": "460e3c49f02e37", | |||||
| "SmtpPassword": "66443869eaad55", | |||||
| "SmtpUsername": "179be7a6fd2f50", | |||||
| "SmtpPassword": "63cde15de0d5d7", | |||||
| "SmtpFrom": "noreply@hrcenter.net", | "SmtpFrom": "noreply@hrcenter.net", | ||||
| "SmtpFromName": "HRCenter Team" | "SmtpFromName": "HRCenter Team" | ||||
| }, | }, |