<commit_before>﻿using Microsoft.AspNetCore.Identity;
using Raven.Client.Documents;
using Raven.Client.Documents.Operations;
using Raven.Client.Documents.Operations.CompareExchange;
using Raven.Client.Documents.Session;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Raven.Identity
{
    /// <summary>
    /// UserStore for entities in a RavenDB database.
    /// </summary>
    /// <typeparam name="TUser"></typeparam>
	/// <typeparam name="TRole"></typeparam>
    public class UserStore<TUser, TRole> : 
        IUserStore<TUser>, 
        IUserLoginStore<TUser>, 
        IUserClaimStore<TUser>, 
        IUserRoleStore<TUser>,
        IUserPasswordStore<TUser>, 
        IUserSecurityStampStore<TUser>, 
        IUserEmailStore<TUser>, 
        IUserLockoutStore<TUser>,
        IUserTwoFactorStore<TUser>, 
        IUserPhoneNumberStore<TUser>,
        IUserAuthenticatorKeyStore<TUser>,
        IUserAuthenticationTokenStore<TUser>,
        IUserTwoFactorRecoveryCodeStore<TUser>,
        IQueryableUserStore<TUser>
        where TUser : IdentityUser
		where TRole : IdentityRole, new()
    {
        private bool _disposed;
        private readonly Func<IAsyncDocumentSession>? getSessionFunc;
        private IAsyncDocumentSession? _session;
        private readonly bool _stableUserId;
        private readonly ILogger _logger;

        private const string emailReservationKeyPrefix = "emails/";

        /// <summary>
        /// Creates a new user store that uses the Raven document session returned from the specified session fetcher.
        /// </summary>
        /// <param name="getSession">The function that gets the Raven document session.</param>
        /// <param name="options"></param>
        /// <param name="logger"></param>
        public UserStore(Func<IAsyncDocumentSession> getSession, IOptions<RavenIdentityOptions> options, ILogger<UserStore<TUser, TRole>> logger)
        {
            this.getSessionFunc = getSession;
            this._logger = logger;
            this._stableUserId = options.Value.StableUserId;
        }

        /// <summary>
        /// Creates a new user store that uses the specified Raven document session.
        /// </summary>
        /// <param name="session"></param>
        /// <param name="options"></param>
        /// <param name="logger"></param>
        public UserStore(IAsyncDocumentSession session, IOptions<RavenIdentityOptions> options, ILogger<UserStore<TUser, TRole>> logger)
        {
            this._session = session;
            this._logger = logger;
            this._stableUserId = options.Value.StableUserId;
        }

        #region IDisposable implementation

        /// <summary>
        /// Disposes the user store.
        /// </summary>
        public void Dispose()
        {
            _disposed = true;
        }

        #endregion

        #region IUserStore implementation

        /// <inheritdoc />
        public Task<string?> GetUserIdAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Id);

        /// <inheritdoc />
        public Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.UserName);

        /// <inheritdoc />
        public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken)
        {
            user.UserName = userName;
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.UserName);

        /// <inheritdoc />
        public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
        {
            user.UserName = normalizedName.ToLowerInvariant();
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public async Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken)
        {
            ThrowIfNullDisposedCancelled(user, cancellationToken);

            // Make sure we have a valid email address.
            if (string.IsNullOrWhiteSpace(user.Email))
            {
                throw new ArgumentException("The user's email address can't be null or empty.", nameof(user));
            }

            // Make sure user email is stored in a consistent way
            user.Email = user.Email.ToLowerInvariant();

            if (string.IsNullOrEmpty(user.UserName))
            {
                user.UserName = user.Email;
            }

            cancellationToken.ThrowIfCancellationRequested();

			// See if the email address is already taken.
			// We do this using Raven's compare/exchange functionality, which works cluster-wide.
			// https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key
			//
			// Try to reserve a new user email
			// Note: This operation takes place outside of the session transaction it is a cluster-wide reservation.
            // If we are using a stable id, pass a placeholder value, we will replace it shortly
            var reservationId = "-1";
            if (!_stableUserId)
            {
                reservationId = user.Id = CreateEmailDerivedUserId(user.Email);
            }
            _logger.LogDebug("Creating email reservation for {UserEmail}", user.Email);
			var reserveEmailResult = await CreateUserKeyReservationAsync(user.Email, reservationId);
            if (!reserveEmailResult.Successful)
            {
                _logger.LogError("Error creating email reservation for {UserEmail}", user.Email);
                return IdentityResult.Failed(new[]
                {
                    new IdentityError
                    {
                        Code = "DuplicateEmail",
                        Description = $"The email address {user.Email} is already taken."
                    }
                });
            }

            // This model allows us to lookup a user by name in order to get the id
            await DbSession.StoreAsync(user, cancellationToken);

            if (_stableUserId)
            {
                var userId = DbSession.Advanced.GetDocumentId(user);
                // NOW we can update our email reservation to the server generated User Id
                var updateReservationResult = await UpdateUserKeyReservationAsync(user.Email, userId);
                if (!updateReservationResult.Successful)
                {
                    _logger.LogError("Error updating email reservation hold for {UserEmail} to {UserId}",
                        user.Email,
                        userId);
                    return IdentityResult.Failed(new[]
                    {
                        new IdentityError
                        {
                            Code = "UnknownError",
                            Description = $"The email reservation was not updated successfully."
                        }
                    });
                }
            }

            // Because this a a cluster-wide operation due to compare/exchange tokens,
            // we need to save changes here; if we can't store the user,
            // we need to roll back the email reservation.
            try
            {
                await DbSession.SaveChangesAsync(cancellationToken);
            }
            catch (Exception)
            {
                // The compare/exchange email reservation is cluster-wide, outside of the session scope.
                // We need to manually roll it back.

                try
                {
                    await this.DeleteUserKeyReservation(user.Email);
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "Caught an exception trying to remove user email reservation for {UserEmail} after save failed",
                        user.Email);
                }

                throw;
            }

            return IdentityResult.Success;
        }

        /// <inheritdoc />
        public async Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken)
        {
			ThrowIfNullDisposedCancelled(user, cancellationToken);

			// Make sure we have a valid email address.
			if (string.IsNullOrWhiteSpace(user.Email))
			{<commit_msg>Improve user create to only update their email reservation after the session save is successful<commit_after>﻿using Microsoft.AspNetCore.Identity;
using Raven.Client.Documents;
using Raven.Client.Documents.Operations;
using Raven.Client.Documents.Operations.CompareExchange;
using Raven.Client.Documents.Session;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Raven.Identity
{
    /// <summary>
    /// UserStore for entities in a RavenDB database.
    /// </summary>
    /// <typeparam name="TUser"></typeparam>
	/// <typeparam name="TRole"></typeparam>
    public class UserStore<TUser, TRole> : 
        IUserStore<TUser>, 
        IUserLoginStore<TUser>, 
        IUserClaimStore<TUser>, 
        IUserRoleStore<TUser>,
        IUserPasswordStore<TUser>, 
        IUserSecurityStampStore<TUser>, 
        IUserEmailStore<TUser>, 
        IUserLockoutStore<TUser>,
        IUserTwoFactorStore<TUser>, 
        IUserPhoneNumberStore<TUser>,
        IUserAuthenticatorKeyStore<TUser>,
        IUserAuthenticationTokenStore<TUser>,
        IUserTwoFactorRecoveryCodeStore<TUser>,
        IQueryableUserStore<TUser>
        where TUser : IdentityUser
		where TRole : IdentityRole, new()
    {
        private bool _disposed;
        private readonly Func<IAsyncDocumentSession>? getSessionFunc;
        private IAsyncDocumentSession? _session;
        private readonly bool _stableUserId;
        private readonly ILogger _logger;

        private const string emailReservationKeyPrefix = "emails/";

        /// <summary>
        /// Creates a new user store that uses the Raven document session returned from the specified session fetcher.
        /// </summary>
        /// <param name="getSession">The function that gets the Raven document session.</param>
        /// <param name="options"></param>
        /// <param name="logger"></param>
        public UserStore(Func<IAsyncDocumentSession> getSession, IOptions<RavenIdentityOptions> options, ILogger<UserStore<TUser, TRole>> logger)
        {
            this.getSessionFunc = getSession;
            this._logger = logger;
            this._stableUserId = options.Value.StableUserId;
        }

        /// <summary>
        /// Creates a new user store that uses the specified Raven document session.
        /// </summary>
        /// <param name="session"></param>
        /// <param name="options"></param>
        /// <param name="logger"></param>
        public UserStore(IAsyncDocumentSession session, IOptions<RavenIdentityOptions> options, ILogger<UserStore<TUser, TRole>> logger)
        {
            this._session = session;
            this._logger = logger;
            this._stableUserId = options.Value.StableUserId;
        }

        #region IDisposable implementation

        /// <summary>
        /// Disposes the user store.
        /// </summary>
        public void Dispose()
        {
            _disposed = true;
        }

        #endregion

        #region IUserStore implementation

        /// <inheritdoc />
        public Task<string?> GetUserIdAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Id);

        /// <inheritdoc />
        public Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.UserName);

        /// <inheritdoc />
        public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken)
        {
            user.UserName = userName;
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.UserName);

        /// <inheritdoc />
        public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
        {
            user.UserName = normalizedName.ToLowerInvariant();
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public async Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken)
        {
            ThrowIfNullDisposedCancelled(user, cancellationToken);

            // Make sure we have a valid email address.
            if (string.IsNullOrWhiteSpace(user.Email))
            {
                throw new ArgumentException("The user's email address can't be null or empty.", nameof(user));
            }

            // Make sure user email is stored in a consistent way
            user.Email = user.Email.ToLowerInvariant();

            if (string.IsNullOrEmpty(user.UserName))
            {
                user.UserName = user.Email;
            }

            cancellationToken.ThrowIfCancellationRequested();

			// See if the email address is already taken.
			// We do this using Raven's compare/exchange functionality, which works cluster-wide.
			// https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key
			//
			// Try to reserve a new user email
			// Note: This operation takes place outside of the session transaction it is a cluster-wide reservation.
            // If we are using a stable id, pass a placeholder value, we will replace it shortly
            var reservationId = "-1";
            if (!_stableUserId)
            {
                reservationId = user.Id = CreateEmailDerivedUserId(user.Email);
            }
            _logger.LogDebug("Creating email reservation for {UserEmail}", user.Email);
			var reserveEmailResult = await CreateUserKeyReservationAsync(user.Email, reservationId);
            if (!reserveEmailResult.Successful)
            {
                _logger.LogError("Error creating email reservation for {UserEmail}", user.Email);
                return IdentityResult.Failed(new[]
                {
                    new IdentityError
                    {
                        Code = "DuplicateEmail",
                        Description = $"The email address {user.Email} is already taken."
                    }
                });
            }

            // This model allows us to lookup a user by name in order to get the id
            await DbSession.StoreAsync(user, cancellationToken);

            // Because this a a cluster-wide operation due to compare/exchange tokens,
            // we need to save changes here; if we can't store the user,
            // we need to roll back the email reservation.
            try
            {
                await DbSession.SaveChangesAsync(cancellationToken);

                // Since our commit succeeded we need to update their email reservation with the actual user id
                if (_stableUserId)
                {
                    var userId = DbSession.Advanced.GetDocumentId(user);
                    // NOW we can update our email reservation to the server generated User Id
                    var updateReservationResult = await UpdateUserKeyReservationAsync(user.Email, userId);
                    if (!updateReservationResult.Successful)
                    {
                        _logger.LogError("Error updating email reservation for {UserEmail} to {UserId}",
                            user.Email,
                            userId);
                        return IdentityResult.Failed(new[]
                        {
                            new IdentityError
                            {
                                Code = "UnknownError",
                                Description = $"The email reservation was not updated successfully."
                            }
                        });
                    }
                }

            }
            catch (Exception)
            {
                // The compare/exchange email reservation is cluster-wide, outside of the session scope.
                // We need to manually roll it back.
                try
                {
                    await this.DeleteUserKeyReservation(user.Email);
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "Caught an exception trying to remove user email reservation for {UserEmail} after save failed",
                        user.Email);
                }

                throw;
            }

            return IdentityResult.Success;
        }

        /// <inheritdoc />
        public async Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken)
        {
			ThrowIfNullDisposedCancelled(user, cancellationToken);

			// Make sure we have a valid email address.
			if (string.IsNullOrWhiteSpace(user.Email))
			{
{'fraction_non_alphanumeric': 0.0847779761561942, 'fraction_numerical': 0.0003455624028105742, 'mean_word_length': 1.900283948555203, 'pattern_counts': {'":': 0, '<': 121, '<?xml version=': 0, '>': 127, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4095231', 'n_tokens_mistral': 4285, 'n_tokens_neox': 4096, 'n_words': 1133}