diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 40cc036..d1d9443 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -1,8 +1,106 @@ +using System.Security.Claims; +using Govor.Core.Models; +using Govor.Core.Repositories.Users; +using Govor.Data.Repositories.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; namespace Govor.API.Hubs; +[Authorize] public class ChatsHub : Hub { + private readonly IUsersRepository _usersRepository; + private readonly ILogger _logger; + + public ChatsHub(IUsersRepository usersRepository, ILogger logger) + { + _usersRepository = usersRepository; + _logger = logger; + } + + public override async Task OnConnectedAsync() + { + var userId = GetUserId(); + if (userId != Guid.Empty) + { + // Привязываем ConnectionId к UserId + await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); + _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId); + } + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = GetUserId(); + if (userId != Guid.Empty) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); + _logger.LogInformation("User {UserId} disconnected", userId); + } + await base.OnDisconnectedAsync(exception); + } + public async Task Send(string message, Guid toUserId) + { + // Валидация входных данных + if (string.IsNullOrWhiteSpace(message)) + { + _logger.LogWarning("Empty message received from user {UserId}", GetUserId()); + throw new ArgumentException("Message cannot be empty", nameof(message)); + } + + var senderId = GetUserId(); + if (senderId == Guid.Empty) + { + _logger.LogError("Could not retrieve sender userId"); + throw new InvalidOperationException("User not authenticated"); + } + + // Проверка существования получателя + try + { + await _usersRepository.FindByIdAsync(toUserId); + } + catch (NotFoundByKeyException ex) + { + _logger.LogWarning("Recipient user {ToUserId} not found", toUserId); + throw; + } + catch (ArgumentException ex) + { + _logger.LogWarning("Invalid recipient userId received from user {UserId}", GetUserId()); + throw; + } + + try + { + _logger.LogInformation("Message sent from {SenderId} to {RecipientId}: {Message}", senderId, toUserId, message); + + // Отправка сообщения отправителю и получателю + await Clients.Group(toUserId.ToString()).SendAsync("Receive", message, senderId); + await Clients.Group(senderId.ToString()).SendAsync("Receive", message, senderId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, toUserId); + throw; + } + } + + private Guid GetUserId() + { + var userIdClaim = Context.User?.FindFirst("userID")?.Value; + _logger.LogInformation("Claims: {Claims}", string.Join(", ", Context.User?.Claims.Select(c => $"{c.Type}: {c.Value}") ?? new string[0])); + + if (string.IsNullOrEmpty(userIdClaim)) + { + _logger.LogError("No userID claim found"); + return Guid.Empty; + } + + return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty; + } } \ No newline at end of file diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index 7b2c958..7943ba5 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -1,3 +1,5 @@ +using System.Text; +using Govor.API.Hubs; using Govor.API.Services; using Govor.API.Services.Authentication; using Govor.Core.Infrastructure.Extensions; @@ -11,6 +13,7 @@ using Govor.Data.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -18,14 +21,53 @@ var configuration = builder.Configuration; var services = builder.Services; builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowFrontend", policy => + { + policy.WithOrigins("http://localhost:3000", "https://localhost:3000") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + builder.Services.Configure(configuration.GetSection(nameof(JwtOption))); // Add services builder.Services.AddSignalR(); -builder.Services.AddAuthorization(); -builder.Services.AddAuthentication("Bearer") - .AddJwtBearer(); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["JwtOption:SecretKeу"]!)) + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/api/chats")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + +builder.Services.AddAuthorization(); + + builder.Services.AddControllers(); @@ -66,8 +108,8 @@ app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); +app.MapHub("/api/chats"); -app.Map("/hello", [Authorize]() => "Hello World!"); -app.Map("/", () => "Not for browser"); +app.Map("/", () => "Not for browsers"); app.Run(); \ No newline at end of file diff --git a/Govor.API/Services/Authentication/JwtService.cs b/Govor.API/Services/Authentication/JwtService.cs index fcdc578..4cbff88 100644 --- a/Govor.API/Services/Authentication/JwtService.cs +++ b/Govor.API/Services/Authentication/JwtService.cs @@ -25,7 +25,8 @@ public class JwtService : IJwtService SecurityAlgorithms.HmacSha256Signature); var token = new JwtSecurityToken(signingCredentials: singing, - expires: DateTime.UtcNow.AddHours(_jwtOption.Hours)); + expires: DateTime.UtcNow.AddHours(_jwtOption.Hours), + claims: claims); return new JwtSecurityTokenHandler().WriteToken(token); } diff --git a/Govor.Console/Govor.Console.csproj b/Govor.Console/Govor.Console.csproj index 69b50f7..e3a0a72 100644 --- a/Govor.Console/Govor.Console.csproj +++ b/Govor.Console/Govor.Console.csproj @@ -8,4 +8,14 @@ enable + + + + + + + + + + diff --git a/Govor.Console/Program.cs b/Govor.Console/Program.cs index df99ecf..2b132ae 100644 --- a/Govor.Console/Program.cs +++ b/Govor.Console/Program.cs @@ -1,14 +1,191 @@ -// See https://aka.ms/new-console-template for more information +using Microsoft.AspNetCore.SignalR.Client; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; +using Govor.Core.DTOs; -Console.WriteLine("Hello, World!"); /*==================================== *Горелые Петушки Чат *==================================== * - * [16:30] Егор >> "Привет, Питухам!" - * [16:31] Anon >> "Иди нахуй" - * - * + * [16:30] Егор >> Привет, Питухам! + * [16:31] Anon >> Че как? + * Я норм << [16:32] + *------------------------------------ * >> "Привет, Егор!" -*/ \ No newline at end of file + */ + +class Program +{ + private static HttpClient _httpClient = new HttpClient(); + private static HubConnection _hubConnection; + private static string _jwtToken; + private static Guid _userId; + private static Guid _toUserId; + + static async Task Main(string[] args) + { + Console.WriteLine("Welcome to the Chat Client!"); + Console.WriteLine("Enter your username:"); + string username = Console.ReadLine(); + Console.WriteLine("Enter your password:"); + string password = Console.ReadLine(); + + // Аутентификация и получение JWT + bool authenticated = await AuthenticateAsync(username, password); + if (!authenticated) + { + Console.WriteLine("Authentication failed. Exiting..."); + return; + } + + Console.Clear(); + + // Извлечение UserId из JWT + _userId = ExtractUserIdFromJwt(_jwtToken); + Console.WriteLine($"Authenticated successfully! Your UserId: {_userId}"); + + // Настройка SignalR + await SetupSignalRAsync(); + + // Обработка команд + Console.WriteLine("Enter command /chat {username} to send a chat message"); + Console.WriteLine("Enter commands in format: /send {message}, in chat {username}"); + Console.WriteLine("Type 'exit' to quit."); + + while (true) + { + string input = Console.ReadLine(); + if (input.ToLower() == "exit") + break; + + await ProcessCommandAsync(input); + } + + // Отключение SignalR + await _hubConnection.StopAsync(); + } + + private static async Task AuthenticateAsync(string username, string password) + { + try + { + UserDto loginData = new UserDto() + { + Name = username, + Password = password + }; + + var response = await _httpClient.PostAsJsonAsync( + "https://localhost:7155/api/Auth/login", + loginData); + + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"Login failed: {response.StatusCode}"); + return false; + } + + var json = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + _jwtToken = tokenResponse.token; + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Authentication error: {ex.Message}"); + return false; + } + } + + private static Guid ExtractUserIdFromJwt(string jwtToken) + { + try + { + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(jwtToken); + var userIdClaim = token.Claims.FirstOrDefault(c => c.Type == "sub" || c.Type == "userID")?.Value; + return Guid.TryParse(userIdClaim, out Guid userId) ? userId : Guid.Empty; + } + catch (Exception ex) + { + Console.WriteLine($"Error extracting UserId: {ex.Message}"); + return Guid.Empty; + } + } + + private static async Task SetupSignalRAsync() + { + _hubConnection = new HubConnectionBuilder() + .WithUrl("https://localhost:7155/api/chats", options => + { + options.AccessTokenProvider = () => Task.FromResult(_jwtToken); + }) + .WithAutomaticReconnect() + .Build(); + + _hubConnection.On("Receive", (message, senderId) => + { + Console.WriteLine($"[{DateTime.Now}] From {senderId}: {message}"); + }); + + try + { + await _hubConnection.StartAsync(); + Console.WriteLine("Connected to SignalR hub."); + } + catch (Exception ex) + { + Console.WriteLine($"SignalR connection error: {ex.Message}"); + } + } + + private static async Task ProcessCommandAsync(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return; + + var parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 3 || parts[0].ToLower() != "/send") + { + Console.WriteLine("Invalid command. Use: /send {toUserId} {message}"); + return; + } + + if (!Guid.TryParse(parts[1], out Guid toUserId)) + { + Console.WriteLine("Invalid toUserId format. Must be a valid Guid."); + return; + } + + string message = string.Join(" ", parts[2..]); + if (string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine("Message cannot be empty."); + return; + } + + try + { + await _hubConnection.InvokeAsync("Send", message, toUserId); + Console.WriteLine($"Message sent to {toUserId}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message: {ex.Message}"); + } + } +} + +public class LoginResponse +{ + public string token { get; set; } +} \ No newline at end of file