ソースを参照

implemented necessary endpoints for user invitation

pull/47/head
meris.ahmatovic 3年前
コミット
1ab38cd4f6

+ 18
- 0
Diligent.WebAPI.Business/Helper/HTMLHelper.cs ファイルの表示

@@ -26,5 +26,23 @@ namespace Diligent.WebAPI.Business.Helper
"</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>";
}
}
}

+ 54
- 0
Diligent.WebAPI.Business/Helper/StringGenerator.cs ファイルの表示

@@ -0,0 +1,54 @@
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());
}
}
}

+ 4
- 0
Diligent.WebAPI.Business/MappingProfiles/UserMappingProfile.cs ファイルの表示

@@ -17,6 +17,10 @@ namespace Diligent.WebAPI.Business.MappingProfiles

#region Model to DTO
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
}
}

+ 6
- 4
Diligent.WebAPI.Business/Services/Interfaces/IUserService.cs ファイルの表示

@@ -1,14 +1,16 @@
namespace Diligent.WebAPI.Business.Services.Interfaces
using Diligent.WebAPI.Contracts.DTOs.User;

namespace Diligent.WebAPI.Business.Services.Interfaces
{
public interface IUserService
{
Task<IEnumerable<User?>> GetAll();

Task<User?> GetById(int id);
Task<User?> GetByEmail(string email);
Task CreateUser(CreateUserRequestDto model);

Task ToggleEnable(User user);
Task RemoveUser(User user);
Task<bool> VerifyToken(User user, string token);
Task<ServiceResponseDTO<object>> SendRegistrationLink(InviteDTO invite);
}
}

+ 55
- 0
Diligent.WebAPI.Business/Services/UserService.cs ファイルの表示

@@ -1,8 +1,11 @@
using Diligent.WebAPI.Business.Services.Interfaces;
using Diligent.WebAPI.Business.Settings;
using Diligent.WebAPI.Contracts.DTOs.User;
using Diligent.WebAPI.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using System.Web;

namespace Diligent.WebAPI.Business.Services
{
@@ -35,6 +38,8 @@ namespace Diligent.WebAPI.Business.Services

public async Task<User?> GetById(int id) =>
await _userManager.FindByIdAsync(id.ToString());
public async Task<User?> GetByEmail(string email) =>
await _userManager.FindByEmailAsync(email);

public async Task CreateUser(CreateUserRequestDto model)
{
@@ -55,5 +60,55 @@ namespace Diligent.WebAPI.Business.Services

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;
}

}
}

+ 15
- 0
Diligent.WebAPI.Contracts/DTOs/User/InviteDTO.cs ファイルの表示

@@ -0,0 +1,15 @@
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; }
}
}

+ 14
- 0
Diligent.WebAPI.Contracts/DTOs/User/UserDetailsResponseDTO.cs ファイルの表示

@@ -0,0 +1,14 @@
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; }
}
}

+ 0
- 1
Diligent.WebAPI.Contracts/DTOs/User/UserResponseDTO.cs ファイルの表示

@@ -13,7 +13,6 @@ namespace Diligent.WebAPI.Contracts.DTOs.User
public string LastName { get; set; }
public string Email { get; set; }
public bool IsEnabled { get; set; }
//public string CVLink { get; set; }
//public string Position { get; set; }
}
}

+ 1
- 1
Diligent.WebAPI.Data/Entities/User.cs ファイルの表示

@@ -8,5 +8,5 @@ public class User : IdentityUser<int>
public string LastName { get; set; }
public string? PasswordResetToken { get; set; }
public List<Comment> Comments { get; set; }
public bool IsEnabled { get; set; }
public bool? IsEnabled { get; set; }
}

+ 1003
- 0
Diligent.WebAPI.Data/Migrations/20221116181306_DefaultDisabledUser.Designer.cs
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 36
- 0
Diligent.WebAPI.Data/Migrations/20221116181306_DefaultDisabledUser.cs ファイルの表示

@@ -0,0 +1,36 @@
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);
}
}
}

+ 1
- 1
Diligent.WebAPI.Data/Migrations/DatabaseContextModelSnapshot.cs ファイルの表示

@@ -553,7 +553,7 @@ namespace Diligent.WebAPI.Data.Migrations
.IsRequired()
.HasColumnType("nvarchar(max)");

b.Property<bool>("IsEnabled")
b.Property<bool?>("IsEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);

+ 38
- 2
Diligent.WebAPI.Host/Controllers/V1/UsersController.cs ファイルの表示

@@ -24,7 +24,6 @@ namespace Diligent.WebAPI.Host.Controllers.V1
return Ok(_mapper.Map<IEnumerable<User?>, IEnumerable<UserResponseDTO>>(await _userService.GetAll()));
}

//[Authorize]
[Authorize]
[HttpPost("toggleEnable/{id}")]
public async Task<IActionResult> ToggleEnable(int id)
@@ -57,7 +56,44 @@ namespace Diligent.WebAPI.Host.Controllers.V1
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]
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequestDto model)

+ 2
- 2
Diligent.WebAPI.Host/appsettings.Development.json ファイルの表示

@@ -12,8 +12,8 @@
"SmtpServer": "smtp.mailtrap.io",
"SmtpPort": 2525,
"SmtpUseSSL": true,
"SmtpUsername": "460e3c49f02e37",
"SmtpPassword": "66443869eaad55",
"SmtpUsername": "179be7a6fd2f50",
"SmtpPassword": "63cde15de0d5d7",
"SmtpFrom": "noreply@hrcenter.net",
"SmtpFromName": "HRCenter Team"
},

+ 2
- 2
Diligent.WebAPI.Host/appsettings.json ファイルの表示

@@ -12,8 +12,8 @@
"SmtpServer": "smtp.mailtrap.io",
"SmtpPort": 2525,
"SmtpUseSSL": true,
"SmtpUsername": "460e3c49f02e37",
"SmtpPassword": "66443869eaad55",
"SmtpUsername": "179be7a6fd2f50",
"SmtpPassword": "63cde15de0d5d7",
"SmtpFrom": "noreply@hrcenter.net",
"SmtpFromName": "HRCenter Team"
},

読み込み中…
キャンセル
保存