// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.EntityFramework; using System.Linq; using System.Threading; using BCrypt.Net; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MUnique.OpenMU.Persistence.EntityFramework.Json; using MUnique.OpenMU.Persistence.EntityFramework.Model; /// /// Repository for accounts. /// internal class AccountRepository : CachingGenericRepository { /// /// Initializes a new instance of the class. /// /// The repository provider. /// The logger factory. public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory) : base(repositoryProvider, loggerFactory) { } /// public override async ValueTask GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); (this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration(); using var context = this.GetContext(); await context.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); try { var accountEntry = context.Context.ChangeTracker.Entries().FirstOrDefault(a => a.Entity.Id == id); var account = accountEntry?.Entity; if (account is null || accountEntry?.References.Any(reference => !reference.IsLoaded) is true) { if (account is not null) { context.Detach(account); } var objectLoader = new AccountJsonObjectLoader(); account = await objectLoader.LoadObjectAsync(id, context.Context, cancellationToken).ConfigureAwait(false); if (account != null && !(context.Context.Entry(account) is { } entry && entry.State != EntityState.Detached)) { context.Context.Attach(account); } } return account; } finally { await context.Context.Database.CloseConnectionAsync().ConfigureAwait(false); } } /// /// Gets the account by character name. /// /// The character name. /// The cancellation token. /// /// The account; otherwise, null. /// internal async ValueTask GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var context = this.GetContext(); var accountInfo = await context.Context.Set() .AsNoTracking() .FirstOrDefaultAsync(a => a.RawCharacters.Any(c => c.Name == characterName), cancellationToken) .ConfigureAwait(false); if (accountInfo != null) { return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); } return null; } /// /// Gets the account by login name if the password is correct. /// /// The login name. /// The password. /// The cancellation token. /// /// The account, if the password is correct. Otherwise, null. /// internal async ValueTask GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default) { using var context = this.GetContext(); return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context, cancellationToken).ConfigureAwait(false); } /// /// Authenticates the account by login name and password, returning minimal state data without loading the full account. /// /// The login name. /// The password. /// The cancellation token. /// The if credentials are valid; otherwise, null. internal async ValueTask AuthenticateAsync(string loginName, string password, CancellationToken cancellationToken = default) { using var context = this.GetContext(); cancellationToken.ThrowIfCancellationRequested(); var accountInfo = await context.Context.Set() .Where(a => a.LoginName == loginName) .Select(a => new { a.PasswordHash, a.State }) .AsNoTracking() .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (accountInfo is not null && BCrypt.Verify(password, accountInfo.PasswordHash)) { return accountInfo.State; } return null; } /// /// Gets the account by login name. /// /// The login name. /// The cancellation token. /// /// The account, if exists. Otherwise, null. /// internal async ValueTask GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default) { using var context = this.GetContext(); var accountInfo = await context.Context.Set() .Select(a => new { a.Id, a.LoginName }) .AsNoTracking() .FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false); if (accountInfo != null) { return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); } return null; } private async ValueTask LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var accountInfo = await context.Context.Set() .Select(a => new { a.Id, a.LoginName, a.PasswordHash }) .AsNoTracking() .FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false); if (accountInfo != null && BCrypt.Verify(password, accountInfo.PasswordHash)) { return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false); } return null; } }