Backend: .NET 10 Clean Architecture + LiteCqrs.Net + EF Core/PostgreSQL + Identity/JWT. Frontend: React 19 + Vite + TanStack Query/Router + Tailwind v4 with a retro CRT theme. Docker/compose deployment mirroring PnvPanel's conventions, scoped down to the current base feature set.
226 lines
7.6 KiB
C#
226 lines
7.6 KiB
C#
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using TeleWave.Application.Admin.Users;
|
|
using TeleWave.Application.Auth;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Common.Models;
|
|
using TeleWave.Infrastructure.Persistence;
|
|
|
|
namespace TeleWave.Infrastructure.Identity;
|
|
|
|
internal sealed class IdentityService(
|
|
UserManager<AppUser> userManager,
|
|
SignInManager<AppUser> signInManager,
|
|
RoleManager<AppRole> roleManager,
|
|
AppDbContext dbContext
|
|
) : IIdentityService
|
|
{
|
|
public async Task<Result<Guid>> CreateUserAsync(
|
|
string userName,
|
|
string password,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
if (await userManager.FindByNameAsync(userName) is not null)
|
|
return Result.Failure<Guid>(AuthErrors.UserNameTaken);
|
|
|
|
var user = new AppUser { UserName = userName, CreatedAt = DateTimeOffset.UtcNow };
|
|
var createResult = await userManager.CreateAsync(user, password);
|
|
if (!createResult.Succeeded)
|
|
{
|
|
return Result.Failure<Guid>(
|
|
Error.Validation(
|
|
"Auth.CreateFailed",
|
|
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.Succeeded)
|
|
return Result.Failure<AuthenticatedUser>(AuthErrors.InvalidCredentials);
|
|
|
|
var role = await GetPrimaryRoleAsync(user);
|
|
return Result.Success(new AuthenticatedUser(user.Id, user.UserName!, role));
|
|
}
|
|
|
|
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);
|
|
var roleEntity = await roleManager.FindByNameAsync(role);
|
|
|
|
return new CurrentUserProfile(user.Id, user.UserName!, roleEntity?.Id ?? Guid.Empty, role, user.IsBlocked);
|
|
}
|
|
|
|
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.ChangePasswordFailed",
|
|
string.Join("; ", result.Errors.Select(e => e.Description))
|
|
)
|
|
);
|
|
}
|
|
|
|
public async Task<Result> ChangeUserNameAsync(
|
|
Guid userId,
|
|
string newUserName,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var user = await userManager.FindByIdAsync(userId.ToString());
|
|
if (user is null)
|
|
return Result.Failure(AuthErrors.Unauthorized);
|
|
|
|
var existing = await userManager.FindByNameAsync(newUserName);
|
|
if (existing is not null && existing.Id != userId)
|
|
return Result.Failure(AuthErrors.UserNameTaken);
|
|
|
|
var result = await userManager.SetUserNameAsync(user, newUserName);
|
|
return result.Succeeded
|
|
? Result.Success()
|
|
: Result.Failure(
|
|
Error.Validation(
|
|
"Auth.ChangeUserNameFailed",
|
|
string.Join("; ", result.Errors.Select(e => e.Description))
|
|
)
|
|
);
|
|
}
|
|
|
|
public async Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken)
|
|
{
|
|
var user = await userManager.FindByIdAsync(userId.ToString());
|
|
if (user is null)
|
|
return Result.Failure(UserErrors.NotFound);
|
|
|
|
var result = await userManager.DeleteAsync(user);
|
|
return result.Succeeded
|
|
? Result.Success()
|
|
: Result.Failure(
|
|
Error.Failure(
|
|
"Users.DeleteFailed",
|
|
string.Join("; ", result.Errors.Select(e => e.Description))
|
|
)
|
|
);
|
|
}
|
|
|
|
public async Task<Result> BlockUserAsync(Guid userId, CancellationToken cancellationToken)
|
|
{
|
|
var user = await userManager.FindByIdAsync(userId.ToString());
|
|
if (user is null)
|
|
return Result.Failure(UserErrors.NotFound);
|
|
|
|
user.IsBlocked = true;
|
|
await userManager.UpdateAsync(user);
|
|
return Result.Success();
|
|
}
|
|
|
|
public async Task<Result> UnblockUserAsync(Guid userId, CancellationToken cancellationToken)
|
|
{
|
|
var user = await userManager.FindByIdAsync(userId.ToString());
|
|
if (user is null)
|
|
return Result.Failure(UserErrors.NotFound);
|
|
|
|
user.IsBlocked = false;
|
|
await userManager.UpdateAsync(user);
|
|
return Result.Success();
|
|
}
|
|
|
|
public async Task<PagedList<UserSummaryDto>> ListUsersAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? search,
|
|
Guid? roleId,
|
|
bool? isBlocked,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var query =
|
|
from user in dbContext.Users
|
|
join userRole in dbContext.UserRoles on user.Id equals userRole.UserId into userRoles
|
|
from userRole in userRoles.DefaultIfEmpty()
|
|
join role in dbContext.Roles on userRole.RoleId equals role.Id into roles
|
|
from role in roles.DefaultIfEmpty()
|
|
select new { user, RoleId = (Guid?)userRole.RoleId, RoleName = role != null ? role.Name : null };
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
query = query.Where(x => x.user.UserName!.Contains(search));
|
|
|
|
if (roleId is not null)
|
|
query = query.Where(x => x.RoleId == roleId);
|
|
|
|
if (isBlocked is not null)
|
|
query = query.Where(x => x.user.IsBlocked == isBlocked);
|
|
|
|
var total = await query.CountAsync(cancellationToken);
|
|
|
|
var items = await query
|
|
.OrderByDescending(x => x.user.CreatedAt)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.Select(x => new UserSummaryDto(
|
|
x.user.Id,
|
|
x.user.UserName!,
|
|
x.RoleName ?? RoleNames.User,
|
|
x.user.IsBlocked,
|
|
x.user.CreatedAt
|
|
))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return new PagedList<UserSummaryDto>(items, total, page, pageSize);
|
|
}
|
|
|
|
public async Task<UserSummaryDto?> GetUserAsync(Guid userId, CancellationToken cancellationToken)
|
|
{
|
|
var user = await userManager.FindByIdAsync(userId.ToString());
|
|
if (user is null)
|
|
return null;
|
|
|
|
var role = await GetPrimaryRoleAsync(user);
|
|
return new UserSummaryDto(user.Id, user.UserName!, role, user.IsBlocked, user.CreatedAt);
|
|
}
|
|
|
|
private async Task<string> GetPrimaryRoleAsync(AppUser user)
|
|
{
|
|
var roles = await userManager.GetRolesAsync(user);
|
|
return roles.FirstOrDefault() ?? RoleNames.User;
|
|
}
|
|
}
|