Initial commit: base slice (auth, roles, users, admin) scaffold
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.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user