diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 439213da5f..181d459d25 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -169,6 +169,13 @@ namespace Bit.Api.Controllers return response; } + [HttpGet("revision-date")] + public async Task GetAccountRevisionDate() + { + var userId = _userService.GetProperUserId(User); + return userId.HasValue ? (await _userService.GetAccountRevisionDateByIdAsync(userId.Value)) : (DateTime?)null; + } + [HttpGet("two-factor")] public async Task GetTwoFactor(string masterPasswordHash, TwoFactorProviderType provider) { diff --git a/src/Core/Domains/User.cs b/src/Core/Domains/User.cs index cf79a7929a..127d74e509 100644 --- a/src/Core/Domains/User.cs +++ b/src/Core/Domains/User.cs @@ -20,6 +20,7 @@ namespace Bit.Core.Domains public string TwoFactorRecoveryCode { get; set; } public string EquivalentDomains { get; set; } public string ExcludedGlobalEquivalentDomains { get; set; } + public DateTime AccountRevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Identity/UserStore.cs b/src/Core/Identity/UserStore.cs index 9a1565de70..13d574559c 100644 --- a/src/Core/Identity/UserStore.cs +++ b/src/Core/Identity/UserStore.cs @@ -153,7 +153,7 @@ namespace Bit.Core.Identity public async Task UpdateAsync(User user, CancellationToken cancellationToken = default(CancellationToken)) { - user.RevisionDate = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; await _userRepository.ReplaceAsync(user); return IdentityResult.Success; } diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index a92175dda5..7fd582ffe9 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -7,5 +7,6 @@ namespace Bit.Core.Repositories public interface IUserRepository : IRepository { Task GetByEmailAsync(string email); + Task GetAccountRevisionDateAsync(Guid id); } } diff --git a/src/Core/Repositories/SqlServer/UserRepository.cs b/src/Core/Repositories/SqlServer/UserRepository.cs index b150b55099..7a1206e886 100644 --- a/src/Core/Repositories/SqlServer/UserRepository.cs +++ b/src/Core/Repositories/SqlServer/UserRepository.cs @@ -36,6 +36,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task GetAccountRevisionDateAsync(Guid id) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadAccountRevisionDateById]", + new { Id = id }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + public override async Task ReplaceAsync(User user) { await base.ReplaceAsync(user); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 9148c94147..7e3289746d 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -3,13 +3,16 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Bit.Core.Domains; +using System.Security.Claims; namespace Bit.Core.Services { public interface IUserService { + Guid? GetProperUserId(ClaimsPrincipal principal); Task GetUserByIdAsync(string userId); Task GetUserByIdAsync(Guid userId); + Task GetAccountRevisionDateByIdAsync(Guid userId); Task SaveUserAsync(User user); Task RegisterUserAsync(User user, string masterPassword); Task SendMasterPasswordHintAsync(string email); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index a20e2043de..35b45148ba 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -10,6 +10,7 @@ using System.Linq; using Microsoft.AspNetCore.Builder; using Bit.Core.Enums; using OtpNet; +using System.Security.Claims; namespace Bit.Core.Services { @@ -56,6 +57,17 @@ namespace Bit.Core.Services _passwordValidators = passwordValidators; } + public Guid? GetProperUserId(ClaimsPrincipal principal) + { + Guid userIdGuid; + if(!Guid.TryParse(GetUserId(principal), out userIdGuid)) + { + return null; + } + + return userIdGuid; + } + public async Task GetUserByIdAsync(string userId) { Guid userIdGuid; @@ -72,6 +84,11 @@ namespace Bit.Core.Services return await _userRepository.GetByIdAsync(userId); } + public async Task GetAccountRevisionDateByIdAsync(Guid userId) + { + return await _userRepository.GetAccountRevisionDateAsync(userId); + } + public async Task SaveUserAsync(User user) { if(user.Id == default(Guid)) @@ -79,7 +96,7 @@ namespace Bit.Core.Services throw new ApplicationException("Use register method to create a new user."); } - user.RevisionDate = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; await _userRepository.ReplaceAsync(user); } @@ -152,7 +169,7 @@ namespace Bit.Core.Services user.Email = newEmail; user.EmailVerified = true; - user.RevisionDate = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; if(ciphers.Any()) { @@ -186,7 +203,7 @@ namespace Bit.Core.Services return result; } - user.RevisionDate = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; if(ciphers.Any()) { await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers);