diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 355b213..501bd3b 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using AskFm.BLL.DTO.UserDTOs; using AskFm.BLL.Services; using AskFm.BLL.Services.UserIdentityService; @@ -13,10 +14,12 @@ namespace AskFm.API.Controllers; public class AuthController : ControllerBase { private IAuthService _authService; + private IUserService _userService; - public AuthController(IAuthService authService) + public AuthController(IAuthService authService, IUserService userService) { _authService = authService; + _userService = userService; } [HttpPost] @@ -56,7 +59,7 @@ public async Task Login(LoginDTO login) return Ok(result); } - [HttpGet] + [HttpPost] [Route("refresh-token/{id}")] [Authorize(AuthenticationSchemes = "Bearer")] public async Task RefreshToken(int id) @@ -80,23 +83,60 @@ public async Task RefreshToken(int id) [Authorize(AuthenticationSchemes = "Bearer")] public async Task Logout(int id) { + var currentUser = await _userService.GetCurrentUserAsync(); + if (currentUser.Data == null || currentUser.Data.Id != id) + { + return BadRequest("Invalid data"); + } string refreshToken = Request.Cookies["refreshToken"]; if (string.IsNullOrEmpty(refreshToken)) { return BadRequest("token Is required"); } - ServiceResult result = await _authService.RevokeRefreshTokenAsync(id,refreshToken); - + var result = await _authService.Logout(currentUser.Data.Id,refreshToken); + if (!result.success) { return BadRequest(result.Errors); } - return Ok(result); } + + [HttpPost] + [AllowAnonymous] + [Route("forgot-password")] + public async Task ForgotPassword(ForgotPasswordDto forgotPasswordDto) + { + + var result = await _authService.ForgotPasswordAsync(forgotPasswordDto.Email); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok("Check Your Email"); + } + [HttpPost] + [AllowAnonymous] + [Route("reset-password")] + public async Task ResetPassword(ResetPasswordDto resetPasswordDto) + { + if (resetPasswordDto == null) + { + return BadRequest("Invalid Data"); + } + + var result = await _authService.ResetPasswordAsync(resetPasswordDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok("Password Reset Success"); + } private void setRefreshToken(string refreshToken,DateTime expires) { var cookieOption = new CookieOptions() diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 06e84f6..9bf0d5a 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -16,6 +16,9 @@ using AskFm.BLL.Services.UserIdentityService; using Swashbuckle.AspNetCore.SwaggerGen; using Castle.Components.DictionaryAdapter.Xml; +using Microsoft.AspNetCore.Identity.UI.Services; +using Shared; +using IEmailSender = AskFm.BLL.Services.IEmailSender; namespace AskFm.API; @@ -29,7 +32,6 @@ public static void Main(string[] args) // Add services to the container. builder.Services.AddControllers(); - // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); Env.Load(); @@ -38,12 +40,16 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } + + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddHttpContextAccessor(); + // DbContext builder.Services.AddDbContext(options => options .UseLazyLoadingProxies() .UseSqlServer(ConnectionString)); - + // ------------------------------------------------- + // Register the repositories and services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -52,11 +58,12 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddControllers(); - builder.Services.AddEndpointsApiExplorer(); + + // Configure Swagger with JWT Authentication builder.Services.AddSwaggerGen(setup => { - // Include 'SecurityScheme' to use JWT Authentication var jwtSecurityScheme = new OpenApiSecurityScheme { BearerFormat = "JWT", @@ -83,13 +90,15 @@ public static void Main(string[] args) }); + // Authentication & Authorization + JwtOptions jwtOptions = new JwtOptions { Issuer = Environment.GetEnvironmentVariable("ISSUER"), Audience = Environment.GetEnvironmentVariable("AUDIENCE"), SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), - AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")), - AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")), + AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"), + AccessRefreshTokenExpiration =builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp") }; if (jwtOptions == null) { @@ -134,15 +143,17 @@ public static void Main(string[] args) Options.Issuer = Environment.GetEnvironmentVariable("ISSUER"); Options.Audience = Environment.GetEnvironmentVariable("AUDIENCE"); Options.SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"); - Options.AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")); - Options.AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")); + Options.AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"); + Options.AccessRefreshTokenExpiration = builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp"); }); - + builder.Services.Configure(options => options.TokenLifespan = TimeSpan.FromHours(2)); + builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Bearer"; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) .AddJwtBearer( Options => { @@ -157,6 +168,21 @@ public static void Main(string[] args) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), ClockSkew = TimeSpan.FromMinutes(0) }; + Options.Events = new JwtBearerEvents + { + OnTokenValidated = async context => + { + var jti = context.Principal.Claims.FirstOrDefault(c => c.Type == "jti")?.Value; + var redis = context.HttpContext.RequestServices.GetRequiredService(); + + var cachedToken = await redis.GetCacheAsync(AppConstants.JwtCacheKey(jti)); + if (cachedToken <= 0) + { + context.Fail("Token revoked or expired"); + } + } + }; + // Enable JWT authentication for SignalR Options.Events = new JwtBearerEvents @@ -175,6 +201,9 @@ public static void Main(string[] args) }; }); builder.Services.AddAuthorization(); + + + // Identity builder.Services.AddIdentity>(options => { //password configuration @@ -204,7 +233,7 @@ public static void Main(string[] args) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - + // Redis Cache builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); @@ -212,8 +241,7 @@ public static void Main(string[] args) }); builder.Services.AddSingleton(); - - + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/AskFm/AskFm.API/appsettings.json b/AskFm/AskFm.API/appsettings.json index 0ea25cc..7749d53 100644 --- a/AskFm/AskFm.API/appsettings.json +++ b/AskFm/AskFm.API/appsettings.json @@ -9,5 +9,16 @@ "Microsoft.AspNetCore": "Warning" } }, + "ExpireTimes": { + "Jwt_Token_Exp":10, + "Refresh_Token_Exp":30 + }, + "EmailOption": { + "client": "smtp.gmail.com", + "password": "password", + "from": "email", + "port":567 + }, + "ClientUrYour App Namel": " http://localhost:5180", "AllowedHosts": "*" } diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index b92a98e..bc89689 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -13,7 +13,8 @@ + + - diff --git a/AskFm/AskFm.BLL/DTO/EmailSettings.cs b/AskFm/AskFm.BLL/DTO/EmailSettings.cs new file mode 100644 index 0000000..a7953f3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/EmailSettings.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace AskFm.BLL.DTO; + +public class EmailSettings +{ + [Required, EmailAddress] + public string From {get; set;} + [Required] + public string Client {get; set;} + [Required] + public string Password {get;set;} + [Range(1, 65535)] + public int Port {get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/RefreshToken.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs similarity index 56% rename from AskFm/AskFm.DAL/Models/RefreshToken.cs rename to AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs index d1bbc9f..cf524b2 100644 --- a/AskFm/AskFm.DAL/Models/RefreshToken.cs +++ b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs @@ -1,14 +1,11 @@ using Microsoft.EntityFrameworkCore; -namespace AskFm.DAL.Models; -[Owned] -public class RefreshToken +namespace AskFm.BLL.DTO; +public class RefreshTokenDto { public string Token { get; set; } public DateTime ExpireOn { get; set; } public bool IsExpired => DateTime.Now >= ExpireOn; - public DateTime? RevokedOn { get; set; } - public bool IsActive => RevokedOn == null && !IsExpired; - + public int ExpireAfter { get; set; } public DateTime CreatedOn { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs index 7948440..aa3ebba 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs @@ -6,6 +6,6 @@ public class AuthResponseDTO { public bool IsAuthenticated { get; set; } public string Token { get; set; } - public RefreshToken RefreshToken { get; set; } + public RefreshTokenDto RefreshToken { get; set; } public ReadUserDTO User { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs new file mode 100644 index 0000000..c3de86b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace AskFm.BLL.DTO.UserDTOs; + +public class ForgotPasswordDto +{ + [Required] + [EmailAddress] + public string Email { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs new file mode 100644 index 0000000..aa0b93b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace AskFm.BLL.DTO.UserDTOs; + +public class ResetPasswordDto +{ + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + public string Token { get; set; } + + [Required] + public string NewPassword { get; set; } + + [Required] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/EmailSender.cs b/AskFm/AskFm.BLL/Services/EmailSender.cs new file mode 100644 index 0000000..d4bf40d --- /dev/null +++ b/AskFm/AskFm.BLL/Services/EmailSender.cs @@ -0,0 +1,98 @@ +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Mail; +using AskFm.BLL.DTO; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Options; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace AskFm.BLL.Services; + +public class EmailSender : IEmailSender +{ + private readonly IConfiguration _config; + private readonly EmailSettings _emailSettings; + + // Inject IConfiguration and ILogger via the constructor + public EmailSender(IConfiguration config) + { + _config = config; + _emailSettings = new EmailSettings() + { + From = _config.GetValue("EmailOption:from"), + Client = _config.GetValue("EmailOption:client"), + Password = _config.GetValue("EmailOption:password"), + Port = _config.GetValue("EmailOption:port"), + }; + } + + public async Task> SendConfirmationLinkAsync(string email, string confirmationLink) + { + string subject = "Confirm Your Email for AskFm"; + string body = $@" +

Welcome to AskFm!

+

Thanks for registering. Please confirm your email address by clicking the link below:

+

Confirm My Email

+

If you did not create an account, you can safely ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + + public async Task> SendPasswordResetLinkAsync(string email, string resetLink) + { + string subject = "Reset Your AskFm Password"; + string body = $@" +

Password Reset Request

+

We received a request to reset your password. You can reset your password by clicking the link below:

+

Reset My Password

+

If you did not request a password reset, please ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + public async Task> SendPasswordResetCodeAsync( string email, string resetCode) + { + string subject = "Your AskFm Password Reset Code"; + string body = $@" +

Password Reset Code

+

We received a request to reset your password. Use the following code to complete the process:

+

{resetCode}

+

This code will expire shortly. If you did not request a password reset, please ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + + public async Task> SendEmailAsync(string toEmail, string subject, string htmlMessage) + { + + var client = new SmtpClient(_emailSettings.Client, 587) + { + EnableSsl = true, + Credentials = new NetworkCredential(_emailSettings.From, _emailSettings.Password) + }; + + // Create and send the email + await client.SendMailAsync( + new MailMessage(from: _emailSettings.From, + to: toEmail, + subject, + htmlMessage + )); + return await ServiceResult.Success(true); + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IEmailSender.cs b/AskFm/AskFm.BLL/Services/IEmailSender.cs new file mode 100644 index 0000000..432f1df --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Identity; + +namespace AskFm.BLL.Services; + +public interface IEmailSender +{ + public Task> SendPasswordResetLinkAsync( string email, string resetLink); + public Task> SendConfirmationLinkAsync(string email, string confirmationLink); + public Task> SendPasswordResetCodeAsync( string email, string resetCode); + Task> SendEmailAsync (string email, string subject, string message); +} diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index a6ca0f1..e7a78c5 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text; +using AskFm.BLL.DTO; using AskFm.BLL.DTO.UserDTOs; using AskFm.DAL; using AskFm.DAL.Interfaces; @@ -9,10 +10,12 @@ using Azure; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; - +using Shared; namespace AskFm.BLL.Services.UserIdentityService; public class AuthService : IAuthService @@ -20,11 +23,17 @@ public class AuthService : IAuthService private IUnitOfWork _unitOfWork; private readonly UserManager _userManager; private readonly JwtOptions _jwtOptions; + private readonly RedisCacheService _redisCacheService; + private readonly IConfiguration _configuration; + private readonly IEmailSender _emailSender; - public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions) + public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions, RedisCacheService redisCacheService, IConfiguration configuration, IEmailSender emailSender) { _unitOfWork = unitOfWork; _userManager = userManager; + _redisCacheService = redisCacheService; + _configuration = configuration; + _emailSender = emailSender; _jwtOptions = jwtOptions.Value; } @@ -40,7 +49,7 @@ public async Task> LoginAsync(LoginDTO request) } var getUser = await _userManager.FindByEmailAsync(request.Email); - + if (getUser == null) { @@ -71,21 +80,21 @@ public async Task> LoginAsync(LoginDTO request) return response; } - + public async Task> RegisterAsync(RegisterUserDTO request) { if (request == null) { - var errors = new List{ "Invalid Request Data" }; + var errors = new List { "Invalid Request Data" }; return await ServiceResult.Failure(errors); } var oldUser = _userManager.FindByEmailAsync(request.Email).Result; - if (oldUser != null && oldUser.IsDeleted ) + if (oldUser != null && oldUser.IsDeleted) { - var errors = new List{ "Email already exist" }; + var errors = new List { "Email already exist" }; return await ServiceResult.Failure(errors); } - + var newUser = new ApplicationUser() { Name = request.Name, @@ -95,16 +104,16 @@ public async Task> RegisterAsync(RegisterUserDTO AvatarPath = request.AvatarPath, LastSeen = DateTime.UtcNow }; - var createRsult = await _userManager.CreateAsync(newUser,request.Passwrod); - + var createRsult = await _userManager.CreateAsync(newUser, request.Passwrod); + if (createRsult.Succeeded == false) { - + var errors = createRsult.Errors.Select(e => e.Description).ToList(); return await ServiceResult.Failure(errors); - - + + } var response = await GetAuthToken(newUser); @@ -113,7 +122,7 @@ public async Task> RegisterAsync(RegisterUserDTO public async Task> RefreshTokenAsync(int id, string refreshToken) { - if (refreshToken == null) + if (string.IsNullOrEmpty(refreshToken)) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); @@ -125,26 +134,20 @@ public async Task> RefreshTokenAsync(int id, stri var errors = new List { "Invalid User." }; return await ServiceResult.Failure(errors); } - if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + + var oldRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (oldRefreshToken == null + || oldRefreshToken.IsExpired + || oldRefreshToken.Token != refreshToken) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); } - - var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); - if (!oldRefreshToken.IsActive) - { - var errors = new List { "InActive Token." }; - return await ServiceResult.Failure(errors); - } - - oldRefreshToken.RevokedOn = DateTime.UtcNow; - + await _redisCacheService.RemoveCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); return await GetAuthToken(user); - } - + public async Task> RevokeRefreshTokenAsync(int id, string refreshToken) { if (string.IsNullOrEmpty(refreshToken)) @@ -153,56 +156,99 @@ public async Task> RevokeRefreshTokenAsync(int id, string re return await ServiceResult.Failure(errors); } - var user = _unitOfWork.Users.GetById(id); + var user = await _unitOfWork.Users.GetByIdAsync(id); if (user == null) { var errors = new List { "Invalid User." }; return await ServiceResult.Failure(errors); } - - if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + + var userRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (userRefreshToken == null || userRefreshToken.IsExpired) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); } - - var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); - - if (!oldRefreshToken.IsActive) + + await _redisCacheService.RemoveCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + return await ServiceResult.Success(true); + + } + + public async Task> Logout(int userId, string refreshToken) + { + var result = await RevokeRefreshTokenAsync(userId, refreshToken); + if (!result.success) { - var errors = new List { "InActive Token." }; - return await ServiceResult.Failure(errors); + return result; } + await RevokeJwtToken(userId); + return await ServiceResult.Success(true); + } - oldRefreshToken.RevokedOn = DateTime.UtcNow; - - await _userManager.UpdateAsync(user); + public async Task> ForgotPasswordAsync(string email) + { + var user = await _userManager.FindByEmailAsync(email); + if (user == null) + { + var error = new List {"Invalid Email."}; + return await ServiceResult.Failure(error); + } + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var resetUrl = + $"{_configuration.GetValue("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}"; + + _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl); + return await ServiceResult.Success(true); + } + public async Task> ResetPasswordAsync(ResetPasswordDto resetPasswordDto) + { + var user = await _userManager.FindByEmailAsync(resetPasswordDto.Email); + if (user == null) + { + var error = new List { "Invalid Data" }; + return await ServiceResult.Failure(error); + } + var result = await _userManager.ResetPasswordAsync(user, resetPasswordDto.Token, resetPasswordDto.NewPassword); + if (result.Succeeded) + { + return await ServiceResult.Success(true); + } + return await ServiceResult.Failure(result.Errors.Select(e => e.Description).ToList()); } + private async Task> GetAuthToken(ApplicationUser user) { - var token = await GenerateJwtToken(user); + // Generate New JWT Token + string newTokenId = Guid.NewGuid().ToString(); + var token = await GenerateJwtToken(user, newTokenId); if (string.IsNullOrEmpty(token)) { var errors = new List { "Invalid Data" }; return await ServiceResult.Failure(errors); } - - RefreshToken refreshToken = null; - if (user.RefreshTokens !=null && user.RefreshTokens.Any(r => r.IsActive)) + var oldJwtId = await _redisCacheService.GetCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); + if (!string.IsNullOrEmpty(oldJwtId)) { - refreshToken = user.RefreshTokens.FirstOrDefault(r => r.IsActive); + await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); + await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); } - else + + await _redisCacheService.SetCacheAsync(AppConstants.JwtCacheKey(newTokenId), user.Id, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); + await _redisCacheService.SetCacheAsync(AppConstants.UserJwtCacheKey(user.Id), newTokenId, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); + + //---------------------------------- + // Use the exist refreshToken or regenerate one + + var refreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (refreshToken == null) { refreshToken = await generateRefreshToken(); - user.RefreshTokens ??= new List(); - user.RefreshTokens.Add(refreshToken); + await _redisCacheService.SetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id), + refreshToken, TimeSpan.FromDays(refreshToken.ExpireAfter)); } - - user.LastSeen = DateTime.UtcNow; - await _userManager.UpdateAsync(user); return await ServiceResult.Success(new AuthResponseDTO() { @@ -220,7 +266,7 @@ private async Task> GetAuthToken(ApplicationUser } }); } - private async Task generateRefreshToken() + private async Task generateRefreshToken() { var randomNumber = new byte[32]; @@ -228,32 +274,45 @@ private async Task generateRefreshToken() generator.GetBytes(randomNumber); - return new RefreshToken + return new RefreshTokenDto { Token = Convert.ToBase64String(randomNumber), - ExpireOn = DateTime.UtcNow.AddDays(10), - CreatedOn = DateTime.UtcNow + ExpireOn = DateTime.UtcNow.AddDays(_configuration.GetValue("ExpireTimes:Refresh_Token_Exp")), + CreatedOn = DateTime.UtcNow, + ExpireAfter = _configuration.GetValue("ExpireTimes:Refresh_Token_Exp") }; } - private Task GenerateJwtToken(ApplicationUser appUser) + private Task GenerateJwtToken(ApplicationUser appUser, string jti) { var tokenHandler = new JwtSecurityTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor(){ + var tokenDescriptor = new SecurityTokenDescriptor() + { Issuer = _jwtOptions.Issuer, Audience = _jwtOptions.Audience, Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.AccessExpiration), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SigningKey)), SecurityAlgorithms.HmacSha256), - Subject = new ClaimsIdentity(new Claim[] + Subject = new ClaimsIdentity(new Claim[] { new(ClaimTypes.Name, appUser.Name), new(ClaimTypes.Email, appUser.Email), - new("UserId", appUser.Id.ToString()) + new("UserId", appUser.Id.ToString()), + new("jti",jti) }) }; var securityToken = tokenHandler.CreateToken(tokenDescriptor); var accessToken = tokenHandler.WriteToken(securityToken); return Task.FromResult(accessToken); } + + private async Task RevokeJwtToken(int userId) + { + var oldJwtId = await _redisCacheService.GetCacheAsync(AppConstants.UserJwtCacheKey(userId)); + if (string.IsNullOrEmpty(oldJwtId)) return; + await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); + await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(userId)); + + } + } diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs index 3e6fa04..fe1cda3 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -6,8 +6,10 @@ namespace AskFm.BLL.Services.UserIdentityService; public interface IAuthService { public Task> LoginAsync(LoginDTO request); - // public Task> ResetPasswordAsync(string Email); public Task> RegisterAsync(RegisterUserDTO request); Task> RefreshTokenAsync(int id, string refreshToken); public Task> RevokeRefreshTokenAsync(int id, string refreshToken); + public Task> Logout(int userId, string refreshToken); + public Task> ForgotPasswordAsync(string email); + public Task> ResetPasswordAsync(ResetPasswordDto resetPasswordDto); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 8a00ba2..ba4f9bf 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -14,17 +14,11 @@ public interface IUserService Task> GetCurrentUserAsync(); Task> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto); Task> ResetEmail(int userId, string updatedEmail); - Task> ConfirmEmail(); /* GET Users only for now - getUserbyId - EditUser - DeleteUser - FollowUser - unfollowUser - reset password + confirm email Helper Function: getCurrentUserId */ diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs index 6d5e48f..0782b19 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs @@ -2,9 +2,9 @@ namespace AskFm.BLL.Services.UserIdentityService; public class JwtOptions { - public string Issuer { get; set; } - public string Audience { get; set; } - public string SigningKey { get; set; } + public string? Issuer { get; set; } + public string? Audience { get; set; } + public string? SigningKey { get; set; } public int AccessExpiration { get; set; } // Minutes public int AccessRefreshTokenExpiration { get; set; } // days } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs new file mode 100644 index 0000000..e1d8a3f --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs @@ -0,0 +1,810 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250915073028_remove_refersh_tokens")] + partial class remove_refersh_tokens + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs new file mode 100644 index 0000000..c3f3fc8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class remove_refersh_tokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshToken"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshToken", + columns: table => new + { + ApplicationUserId = table.Column(type: "int", nullable: false), + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CreatedOn = table.Column(type: "datetime2", nullable: false), + ExpireOn = table.Column(type: "datetime2", nullable: false), + RevokedOn = table.Column(type: "datetime2", nullable: true), + Token = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshToken", x => new { x.ApplicationUserId, x.Id }); + table.ForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index dfccc1a..6554b8b 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -587,43 +587,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => - { - b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("ApplicationUserId") - .HasColumnType("int"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); - - b1.Property("CreatedOn") - .HasColumnType("datetime2"); - - b1.Property("ExpireOn") - .HasColumnType("datetime2"); - - b1.Property("RevokedOn") - .HasColumnType("datetime2"); - - b1.Property("Token") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b1.HasKey("ApplicationUserId", "Id"); - - b1.ToTable("RefreshToken"); - - b1.WithOwner() - .HasForeignKey("ApplicationUserId"); - }); - - b.Navigation("RefreshTokens"); - }); - modelBuilder.Entity("AskFm.DAL.Models.Comment", b => { b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") diff --git a/AskFm/AskFm.DAL/Models/ApplicationUser.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs index 08f0869..2b7655c 100644 --- a/AskFm/AskFm.DAL/Models/ApplicationUser.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -29,7 +29,4 @@ public class ApplicationUser : IdentityUser, ITrackable public DateTime UpdatedAt { get; set; } public DateTime CreatedAt { get; set; } - - // tokens - public virtual ICollection? RefreshTokens { get; set; } } \ No newline at end of file diff --git a/AskFm/Shared/AppConstants.cs b/AskFm/Shared/AppConstants.cs new file mode 100644 index 0000000..fb1c834 --- /dev/null +++ b/AskFm/Shared/AppConstants.cs @@ -0,0 +1,20 @@ +namespace Shared; + +public class AppConstants +{ + + public static string UserJwtCacheKey(int userId) + { + return $"userId:{userId}:current_jti"; + } + + public static string JwtCacheKey(string id) + { + return $"jti:{id}"; + } + + public static string UserRefreshTokenCacheKey(int userId) + { + return $"refresh_token:userId:{userId}"; + } +} \ No newline at end of file