//
// 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;
}
}