Enhance documentation with new features: added dark/light/system theme support, instructions page, and application catalog. Updated API and domain model for app management and automatic migrations on startup. Improved frontend structure with new routes and features for user instructions and app management.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PnvPanel.Application.Auth;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
internal sealed class IdentityService(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager, RoleManager<AppRole> roleManager)
|
||||
: IIdentityService
|
||||
{
|
||||
public async Task<Result<Guid>> CreateUserAsync(string userName, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = new AppUser
|
||||
{
|
||||
UserName = userName,
|
||||
IsActivated = false,
|
||||
SubscriptionToken = GenerateSubscriptionToken(),
|
||||
};
|
||||
var createResult = await userManager.CreateAsync(user, password);
|
||||
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
return createResult.Errors.Any(e => e.Code == nameof(IdentityErrorDescriber.DuplicateUserName))
|
||||
? Result.Failure<Guid>(AuthErrors.DuplicateUserName)
|
||||
: Result.Failure<Guid>(Error.Validation(
|
||||
"Auth.RegistrationFailed",
|
||||
string.Join("; ", createResult.Errors.Select(e => e.Description))));
|
||||
}
|
||||
|
||||
await userManager.AddToRoleAsync(user, RoleNames.User);
|
||||
return Result.Success(user.Id);
|
||||
}
|
||||
|
||||
public async Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(userName);
|
||||
if (user is null)
|
||||
return Result.Failure<AuthenticatedUser>(AuthErrors.InvalidCredentials);
|
||||
|
||||
var checkResult = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);
|
||||
if (checkResult.IsLockedOut)
|
||||
return Result.Failure<AuthenticatedUser>(AuthErrors.LockedOut);
|
||||
if (!checkResult.Succeeded)
|
||||
return Result.Failure<AuthenticatedUser>(AuthErrors.InvalidCredentials);
|
||||
|
||||
var roleName = await GetPrimaryRoleNameAsync(user);
|
||||
return Result.Success(new AuthenticatedUser(user.Id, user.UserName!, roleName));
|
||||
}
|
||||
|
||||
public async Task<CurrentUserProfile?> GetProfileAsync(Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
return null;
|
||||
|
||||
var role = await GetPrimaryRoleAsync(user);
|
||||
return new CurrentUserProfile(user.Id, user.UserName!, role.Id, role.Name!, user.IsActivated, role.MaxConfigs);
|
||||
}
|
||||
|
||||
public async Task<Result> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
return Result.Failure(AuthErrors.Unauthorized);
|
||||
|
||||
var result = await userManager.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Validation(
|
||||
"Auth.PasswordChangeFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
}
|
||||
|
||||
public async Task<Result> ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
return Result.Failure(AuthErrors.Unauthorized);
|
||||
|
||||
// Мутируем уже отслеживаемую EF-сущность напрямую, без UserManager.UpdateAsync (который
|
||||
// закоммитил бы немедленно) — изменение попадёт в общий SaveChanges вместе с ActivationRequest.
|
||||
user.IsActivated = true;
|
||||
user.ActivatedAt = DateTimeOffset.UtcNow;
|
||||
user.ActivatedBy = activatedBy;
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(IReadOnlyCollection<Guid> userIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (userIds.Count == 0)
|
||||
return new Dictionary<Guid, string>();
|
||||
|
||||
return await userManager.Users
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.UserName!, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
return Result.Failure(AuthErrors.Unauthorized);
|
||||
|
||||
var result = await userManager.DeleteAsync(user);
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Failure(
|
||||
"Auth.DeleteAccountFailed", string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
}
|
||||
|
||||
public async Task<Guid?> FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.Users.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.SubscriptionToken == token, cancellationToken);
|
||||
return user?.Id;
|
||||
}
|
||||
|
||||
private async Task<string> GetPrimaryRoleNameAsync(AppUser user)
|
||||
{
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
return roles.FirstOrDefault() ?? RoleNames.User;
|
||||
}
|
||||
|
||||
private async Task<AppRole> GetPrimaryRoleAsync(AppUser user)
|
||||
{
|
||||
var roleName = await GetPrimaryRoleNameAsync(user);
|
||||
return await roleManager.FindByNameAsync(roleName)
|
||||
?? throw new InvalidOperationException($"Роль '{roleName}' не найдена.");
|
||||
}
|
||||
|
||||
private static string GenerateSubscriptionToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
|
||||
}
|
||||
Reference in New Issue
Block a user