Skip to content
Snippets Groups Projects
Commit 13142310 authored by Lucca Santangelo's avatar Lucca Santangelo
Browse files

commit

parent 57fbe09d
No related branches found
No related tags found
3 merge requests!20merge from develop,!16Feature/chat signalr,!12password recovery
Showing
with 310 additions and 32 deletions
......@@ -23,21 +23,27 @@ namespace Tsi1.Api.Controllers
private readonly IUserService _userService;
private readonly IUserTypeService _userTypeService;
private readonly ITenantService _tenantService;
public UserController(IJwtAuthManager jwtAuthManager, IUserService userService,
IUserTypeService userTypeService, ITenantService tenantService)
private readonly IEmailService _emailService;
public UserController(
IJwtAuthManager jwtAuthManager,
IUserService userService,
IUserTypeService userTypeService,
ITenantService tenantService,
IEmailService emailService)
{
_jwtAuthManager = jwtAuthManager;
_userService = userService;
_userTypeService = userTypeService;
_tenantService = tenantService;
_emailService = emailService;
}
[AllowAnonymous]
[HttpPost("Login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var resultSplit = request.UserName.Split("@");
var resultSplit = request.Username.Split("@");
if (resultSplit.Count() != 2)
{
......@@ -45,9 +51,7 @@ namespace Tsi1.Api.Controllers
}
var userName = resultSplit[0];
var tenantName = resultSplit[1];
var tenantId = await _tenantService.GetByName(tenantName);
if (tenantId.HasError)
......@@ -56,7 +60,6 @@ namespace Tsi1.Api.Controllers
}
var result = await _userService.Authenticate(userName, request.Password, tenantId.Data);
if (result.HasError)
{
return BadRequest(result.Message);
......@@ -217,5 +220,108 @@ namespace Tsi1.Api.Controllers
return Ok(result.Data);
}
[AllowAnonymous]
[HttpGet("ForgotPassword/{username}")]
public async Task<IActionResult> ForgotPassword(string username)
{
var resultSplit = username.Split("@");
if (resultSplit.Count() != 2)
{
return BadRequest(ErrorMessages.InvalidUsername);
}
username = resultSplit[0];
var tenantName = resultSplit[1];
var tenantId = await _tenantService.GetByName(tenantName);
if (tenantId.HasError)
{
return BadRequest(tenantId.Message);
}
var userResult = await _userService.GetByUsername(username, tenantId.Data);
if (userResult.HasError)
{
return BadRequest(userResult.Message);
}
var code = _jwtAuthManager.GenerateVerificationCode(username, DateTime.Now);
var result = await _emailService.SendVerificationCode(userResult.Data.Email, code);
if (result.HasError)
{
return BadRequest("Ha ocurrido un error");
}
return Ok();
}
[AllowAnonymous]
[HttpGet("VerificationCode/{username}/{code}")]
public async Task<IActionResult> VerificationCode(string username, int code)
{
var resultSplit = username.Split("@");
if (resultSplit.Count() != 2)
{
return BadRequest(ErrorMessages.InvalidUsername);
}
username = resultSplit[0];
var tenantName = resultSplit[1];
var tenantId = await _tenantService.GetByName(tenantName);
if (tenantId.HasError)
{
return BadRequest(tenantId.Message);
}
if (!_jwtAuthManager.ValidateVerificationCode(username, code))
{
return BadRequest("Código de verificación incorrecto");
}
var userResult = await _userService.GetByUsername(username, tenantId.Data);
if (userResult.HasError)
{
return BadRequest(userResult.Message);
}
var user = userResult.Data;
var claims = new[]
{
new Claim("Id", user.Id.ToString()),
new Claim("Username", user.Username),
new Claim("TenantId", user.TenantId.ToString()),
new Claim(ClaimTypes.Role, user.UserType.Name)
};
var jwtResult = _jwtAuthManager.GenerateTokens(user.Username, claims, DateTime.Now);
return Ok(new LoginResult
{
Id = user.Id,
UserName = user.Username,
Role = user.UserType.Name,
AccessToken = jwtResult.AccessToken,
RefreshToken = jwtResult.RefreshToken.TokenString
});
}
[HttpPost("RestorePassword")]
public async Task<IActionResult> RestorePassword(RestorePasswordDto dto)
{
var userId = int.Parse(HttpContext.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
var result = await _userService.UpdatePassword(userId, dto.Password);
if (result.HasError)
{
return BadRequest(result.Message);
}
return Ok();
}
}
}
......@@ -14,5 +14,8 @@ namespace Tsi1.Api.Infrastructure
void RemoveExpiredRefreshTokens(DateTime now);
void RemoveRefreshTokenByUserName(string userName);
(ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token);
int GenerateVerificationCode(string username, DateTime now);
bool ValidateVerificationCode(string username, int code);
public void RemoveExpiredVerificationCodes(DateTime now);
}
}
......@@ -15,6 +15,7 @@ namespace Tsi1.Api.Infrastructure
{
public IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary => _usersRefreshTokens.ToImmutableDictionary();
private readonly ConcurrentDictionary<string, RefreshToken> _usersRefreshTokens; // can store in a database or a distributed cache
private readonly ConcurrentDictionary<string, VerificationCode> _usersVerificationCodes;
private readonly JwtTokenConfig _jwtTokenConfig;
private readonly byte[] _secret;
......@@ -22,6 +23,7 @@ namespace Tsi1.Api.Infrastructure
{
_jwtTokenConfig = jwtTokenConfig;
_usersRefreshTokens = new ConcurrentDictionary<string, RefreshToken>();
_usersVerificationCodes = new ConcurrentDictionary<string, VerificationCode>();
_secret = Encoding.ASCII.GetBytes(jwtTokenConfig.Secret);
}
......@@ -118,6 +120,36 @@ namespace Tsi1.Api.Infrastructure
return (principal, validatedToken as JwtSecurityToken);
}
public int GenerateVerificationCode(string username, DateTime now)
{
var code = new Random().Next(1000000);
var verficationCode = new VerificationCode()
{
Username = username,
Code = code,
ExpireAt = now.AddMinutes(_jwtTokenConfig.VerificationCodeExpiration)
};
_usersVerificationCodes.AddOrUpdate(username, verficationCode, (s, t) => verficationCode);
return code;
}
public bool ValidateVerificationCode(string username, int code)
{
return _usersVerificationCodes.TryGetValue(username, out _);
}
public void RemoveExpiredVerificationCodes(DateTime now)
{
var expiredCodes = _usersVerificationCodes.Where(x => x.Value.ExpireAt < now).ToList();
foreach (var expiredCode in expiredCodes)
{
_usersVerificationCodes.TryRemove(expiredCode.Key, out _);
}
}
private static string GenerateRefreshTokenString()
{
var randomNumber = new byte[32];
......
......@@ -4,19 +4,11 @@ namespace Tsi1.Api.Infrastructure
{
public class JwtTokenConfig
{
[JsonPropertyName("secret")]
public string Secret { get; set; }
[JsonPropertyName("issuer")]
public string Issuer { get; set; }
[JsonPropertyName("audience")]
public string Audience { get; set; }
[JsonPropertyName("accessTokenExpiration")]
public int AccessTokenExpiration { get; set; }
[JsonPropertyName("refreshTokenExpiration")]
public int RefreshTokenExpiration { get; set; }
public int VerificationCodeExpiration { get; set; }
}
}
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tsi1.Api.Infrastructure
{
public class JwtVerificationCodeCache : IHostedService, IDisposable
{
private Timer _timer;
private readonly IJwtAuthManager _jwtAuthManager;
public JwtVerificationCodeCache(IJwtAuthManager jwtAuthManager)
{
_jwtAuthManager = jwtAuthManager;
}
public Task StartAsync(CancellationToken stoppingToken)
{
// remove expired refresh tokens from cache every minute
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_jwtAuthManager.RemoveExpiredVerificationCodes(DateTime.Now);
}
public Task StopAsync(CancellationToken stoppingToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
}
......@@ -11,7 +11,7 @@ namespace Tsi1.Api.Models
{
[Required]
[JsonPropertyName("username")]
public string UserName { get; set; }
public string Username { get; set; }
[Required]
[JsonPropertyName("password")]
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Tsi1.Api.Models
{
public class VerificationCode
{
public string Username { get; set; }
public int Code { get; set; }
public DateTime ExpireAt { get; set; }
}
}
......@@ -45,6 +45,8 @@ namespace Tsi1.Api
var postgreSqlSection = isElasticCloud ? "PostgreSqlCloud" : "PostgreSql";
var mongoDbSection = isElasticCloud ? "Tsi1DatabaseSettingsCloud" : "Tsi1DatabaseSettings";
var jwtTokenConfig = Configuration.GetSection("jwtTokenConfig").Get<JwtTokenConfig>();
services.AddDbContext<Tsi1Context>(x => x.UseNpgsql(Configuration.GetConnectionString(postgreSqlSection)));
services.Configure<Tsi1DatabaseSettings>(
......@@ -53,6 +55,13 @@ namespace Tsi1.Api
services.AddSingleton<ITsi1DatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<Tsi1DatabaseSettings>>().Value);
services.AddSingleton(jwtTokenConfig);
services.AddSingleton<IJwtAuthManager, JwtAuthManager>();
services.AddHostedService<JwtRefreshTokenCache>();
services.AddHostedService<JwtVerificationCodeCache>();
services.AddSingleton<IMessageService, MessageService>();
services.AddScoped<IUserService, UserService>();
......@@ -67,10 +76,7 @@ namespace Tsi1.Api
services.AddScoped<IEmailService, EmailService>();
services.AddCors();
var jwtTokenConfig = Configuration.GetSection("jwtTokenConfig").Get<JwtTokenConfig>();
services.AddSingleton(jwtTokenConfig);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
......@@ -92,9 +98,6 @@ namespace Tsi1.Api
};
});
services.AddSingleton<IJwtAuthManager, JwtAuthManager>();
services.AddHostedService<JwtRefreshTokenCache>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Tsi1 api", Version = "v1" });
......@@ -125,7 +128,7 @@ namespace Tsi1.Api
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
var mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
}
......
......@@ -14,12 +14,13 @@
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "Tsi1Db"
},
"jwtTokenConfig": {
"secret": "1234567890123456789",
"issuer": "https://localhost:5000",
"audience": "https://localhost:5000",
"accessTokenExpiration": 20,
"refreshTokenExpiration": 60
"JwtTokenConfig": {
"Secret": "1234567890123456789",
"Issuer": "https://localhost:5000",
"Audience": "https://localhost:5000",
"AccessTokenExpiration": 20,
"RefreshTokenExpiration": 60,
"VerificationCodeExpiration": 5
},
"MailSettings": {
"Mail": "tsi1.grupo2.2020@gmail.com",
......
using System;
using System.Collections.Generic;
using System.Text;
namespace Tsi1.BusinessLayer.Dtos
{
public class RestorePasswordDto
{
public string Password { get; set; }
}
}
......@@ -14,5 +14,7 @@ namespace Tsi1.BusinessLayer.Interfaces
Task<ServiceResult<bool>> SendEmailAsync(MimeMessage message);
Task<ServiceResult<bool>> NotifyNewPostOrMessage(PostCreateDto postCreateDto, List<string> mails);
public Task<ServiceResult<bool>> SendVerificationCode(string mail, int code);
}
}
......@@ -17,5 +17,9 @@ namespace Tsi1.BusinessLayer.Interfaces
Task<ServiceResult<List<UserPreviewDto>>> GetAll(int tenantId);
Task<ServiceResult<UserPreviewDto>> GetById(int userId);
Task<ServiceResult<User>> GetByUsername(string username, int tenantId);
Task<ServiceResult<bool>> UpdatePassword(int userId, string password);
}
}
......@@ -86,9 +86,22 @@ namespace Tsi1.BusinessLayer.Services
};
var result = await SendEmailAsync(message);
return result;
}
public async Task<ServiceResult<bool>> SendVerificationCode(string mail, int code)
{
var message = new MimeMessage();
message.To.Add(MailboxAddress.Parse(mail));
message.Subject = "Código de verificación";
message.Body = new TextPart("html")
{
Text = $"<p>Su código de verificación es {code}</p>"
};
var result = await SendEmailAsync(message);
return result;
}
}
}
......@@ -138,5 +138,61 @@ namespace Tsi1.BusinessLayer.Services
return result;
}
public async Task<ServiceResult<User>> GetByUsername(string username, int tenantId)
{
var result = new ServiceResult<User>();
var user = await _context.Users
.Include(x => x.UserType)
.Include(x => x.Student)
.Include(x => x.Professor)
.Where(x => x.TenantId == tenantId
&& x.Username == username)
.FirstOrDefaultAsync();
if (user == null)
{
result.HasError = true;
result.Message = string.Format(ErrorMessages.UserDoesNotExist, username);
return result;
}
var userType = user.UserType.Name;
if (userType == UserTypes.Student && user.Student == null)
{
result.HasError = true;
result.Message = string.Format(ErrorMessages.StudentDoesNotExist, username);
return result;
}
else if (userType == UserTypes.Professor && user.Professor == null)
{
result.HasError = true;
result.Message = string.Format(ErrorMessages.ProffesorDoesNotExist, username);
return result;
}
result.Data = user;
return result;
}
public async Task<ServiceResult<bool>> UpdatePassword(int userId, string password)
{
var result = new ServiceResult<bool>();
var user = await _context.Users.FirstOrDefaultAsync();
if (user == null)
{
result.HasError = true;
result.Message = string.Format(ErrorMessages.UserDoesNotExist, userId);
return result;
}
user.Password = password;
await _context.SaveChangesAsync();
return result;
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment