Files
AdamuSw/src/Persistence/EntityFramework/AccountRepository.cs
2026-07-14 19:00:35 +03:00

174 lines
7.0 KiB
C#

// <copyright file="AccountRepository.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
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;
/// <summary>
/// Repository for accounts.
/// </summary>
internal class AccountRepository : CachingGenericRepository<Account>
{
/// <summary>
/// Initializes a new instance of the <see cref="AccountRepository" /> class.
/// </summary>
/// <param name="repositoryProvider">The repository provider.</param>
/// <param name="loggerFactory">The logger factory.</param>
public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILoggerFactory loggerFactory)
: base(repositoryProvider, loggerFactory)
{
}
/// <inheritdoc />
public override async ValueTask<Account?> 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<Account>().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<Account>(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);
}
}
/// <summary>
/// Gets the account by character name.
/// </summary>
/// <param name="characterName">The character name.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account; otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var context = this.GetContext();
var accountInfo = await context.Context.Set<Account>()
.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;
}
/// <summary>
/// Gets the account by login name if the password is correct.
/// </summary>
/// <param name="loginName">The login name.</param>
/// <param name="password">The password.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account, if the password is correct. Otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default)
{
using var context = this.GetContext();
return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Authenticates the account by login name and password, returning minimal state data without loading the full account.
/// </summary>
/// <param name="loginName">The login name.</param>
/// <param name="password">The password.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The <see cref="DataModel.Entities.AccountState"/> if credentials are valid; otherwise, null.</returns>
internal async ValueTask<DataModel.Entities.AccountState?> AuthenticateAsync(string loginName, string password, CancellationToken cancellationToken = default)
{
using var context = this.GetContext();
cancellationToken.ThrowIfCancellationRequested();
var accountInfo = await context.Context.Set<Account>()
.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;
}
/// <summary>
/// Gets the account by login name.
/// </summary>
/// <param name="loginName">The login name.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account, if exists. Otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default)
{
using var context = this.GetContext();
var accountInfo = await context.Context.Set<Account>()
.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<Account?> LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var accountInfo = await context.Context.Set<Account>()
.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;
}
}