Add Telegram bot integration and enhance user management features

- Introduced Telegram.Bot package for bot functionality.
- Updated user management to include Telegram linking and blocking features.
- Enhanced activation request handling with notifications via Telegram.
- Added new database entities for Telegram link tokens and login requests.
- Implemented traffic synchronization for client stats in the XuiPanelGateway.
- Updated application structure to support new test projects and improved dependency injection for Telegram services.
This commit is contained in:
Leonid Pershin
2026-07-02 01:01:03 +03:00
parent 1a8d33efa3
commit 7b6fe9ad78
142 changed files with 7570 additions and 22 deletions
@@ -39,6 +39,9 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
if (user is null)
return Result.Failure<AuthenticatedUser>(AuthErrors.InvalidCredentials);
if (user.IsBlocked)
return Result.Failure<AuthenticatedUser>(AuthErrors.UserBlocked);
var checkResult = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);
if (checkResult.IsLockedOut)
return Result.Failure<AuthenticatedUser>(AuthErrors.LockedOut);
@@ -56,7 +59,7 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
return null;
var role = await GetPrimaryRoleAsync(user);
return new CurrentUserProfile(user.Id, user.UserName!, role.Id, role.Name!, user.IsActivated, role.MaxConfigs);
return new CurrentUserProfile(user.Id, user.UserName!, role.Id, role.Name!, user.IsActivated, user.IsBlocked, role.MaxConfigs);
}
public async Task<Result> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken)
@@ -118,6 +121,118 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
return user?.Id;
}
public async Task<Result> BlockUserAsync(Guid userId, CancellationToken cancellationToken)
{
var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null)
return Result.Failure(AuthErrors.Unauthorized);
user.IsBlocked = true;
await userManager.UpdateSecurityStampAsync(user); // гасит уже выданные refresh-токены де-факто — токен привязан к пользователю, не к stamp; отзыв делает вызывающий handler явно
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(AuthErrors.Unauthorized);
user.IsBlocked = false;
return Result.Success();
}
public async Task<Result> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken)
{
var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null)
return Result.Failure(AuthErrors.Unauthorized);
var token = await userManager.GeneratePasswordResetTokenAsync(user);
var result = await userManager.ResetPasswordAsync(user, token, newPassword);
return result.Succeeded
? Result.Success()
: Result.Failure(Error.Validation(
"Auth.PasswordResetFailed", string.Join("; ", result.Errors.Select(e => e.Description))));
}
public async Task<PagedList<UserSummaryDto>> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken)
{
var query = userManager.Users.AsNoTracking();
if (!string.IsNullOrWhiteSpace(search))
query = query.Where(u => u.UserName!.Contains(search));
var total = await query.CountAsync(cancellationToken);
var users = await query
.OrderBy(u => u.UserName)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
var items = new List<UserSummaryDto>(users.Count);
foreach (var user in users)
{
var roleName = await GetPrimaryRoleNameAsync(user);
items.Add(new UserSummaryDto(user.Id, user.UserName!, roleName, user.IsActivated, user.IsBlocked, user.ActivatedAt));
}
return new PagedList<UserSummaryDto>(items, total, page, pageSize);
}
public async Task<UserStatsDto> GetUserStatsAsync(CancellationToken cancellationToken)
{
var total = await userManager.Users.CountAsync(cancellationToken);
var activated = await userManager.Users.CountAsync(u => u.IsActivated, cancellationToken);
return new UserStatsDto(total, activated);
}
public async Task<Result> LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken)
{
var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null)
return Result.Failure(AuthErrors.Unauthorized);
var alreadyLinked = await userManager.Users.AsNoTracking()
.AnyAsync(u => u.TelegramUserId == telegramUserId && u.Id != userId, cancellationToken);
if (alreadyLinked)
return Result.Failure(Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к другому аккаунту."));
user.TelegramUserId = telegramUserId;
user.TelegramUsername = telegramUsername;
user.TelegramLinkedAt = DateTimeOffset.UtcNow;
return Result.Success();
}
public async Task<Result> UnlinkTelegramAsync(Guid userId, CancellationToken cancellationToken)
{
var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null)
return Result.Failure(AuthErrors.Unauthorized);
user.TelegramUserId = null;
user.TelegramUsername = null;
user.TelegramLinkedAt = null;
return Result.Success();
}
public async Task<Guid?> FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken)
{
var user = await userManager.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.TelegramUserId == telegramUserId, cancellationToken);
return user?.Id;
}
public async Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken)
{
var user = await userManager.FindByIdAsync(userId.ToString());
return user?.TelegramUserId is not null
? new TelegramLinkInfo(true, user.TelegramUserId, user.TelegramUsername)
: new TelegramLinkInfo(false, null, null);
}
private async Task<string> GetPrimaryRoleNameAsync(AppUser user)
{
var roles = await userManager.GetRolesAsync(user);