mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-05 23:34:09 +02:00
Add core models, DTOs, and services for EchoHub chat application
- Created User, Channel, Message, and ServerInfo models with necessary properties. - Added DTOs for user profiles, server information, and message handling. - Implemented JWT authentication service for user login and token generation. - Developed Entity Framework Core DbContext for database interactions. - Introduced SignalR ChatHub for real-time messaging and presence tracking. - Implemented file storage and image to ASCII conversion services. - Set up CORS and middleware for API endpoints. - Created launch settings and development configuration for server and web projects.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EchoHub.Core.DTOs;
|
||||
|
||||
namespace EchoHub.Client.Services;
|
||||
|
||||
public sealed class ApiClient : IDisposable
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private string? _token;
|
||||
|
||||
public string? Token => _token;
|
||||
public string BaseUrl { get; }
|
||||
|
||||
public ApiClient(string baseUrl)
|
||||
{
|
||||
BaseUrl = baseUrl.TrimEnd('/');
|
||||
_http = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(BaseUrl)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LoginResponse> RegisterAsync(string username, string password, string? displayName = null)
|
||||
{
|
||||
var request = new RegisterRequest(username, password, displayName);
|
||||
var response = await _http.PostAsJsonAsync("/api/auth/register", request);
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<LoginResponse>()
|
||||
?? throw new InvalidOperationException("Registration returned empty response.");
|
||||
|
||||
SetToken(result.Token);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LoginResponse> LoginAsync(string username, string password)
|
||||
{
|
||||
var request = new LoginRequest(username, password);
|
||||
var response = await _http.PostAsJsonAsync("/api/auth/login", request);
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<LoginResponse>()
|
||||
?? throw new InvalidOperationException("Login returned empty response.");
|
||||
|
||||
SetToken(result.Token);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<ChannelDto>> GetChannelsAsync()
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var channels = await _http.GetFromJsonAsync<List<ChannelDto>>("/api/channels");
|
||||
return channels ?? [];
|
||||
}
|
||||
|
||||
public async Task<ServerStatusDto?> GetServerInfoAsync()
|
||||
{
|
||||
var info = await _http.GetFromJsonAsync<ServerStatusDto>("/api/server/info");
|
||||
return info;
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto?> GetUserProfileAsync(string username)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var profile = await _http.GetFromJsonAsync<UserProfileDto>($"/api/users/{Uri.EscapeDataString(username)}/profile");
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto?> UpdateProfileAsync(UpdateProfileRequest request)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var response = await _http.PutAsJsonAsync("/api/users/profile", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<UserProfileDto>();
|
||||
}
|
||||
|
||||
public async Task<string?> UploadAvatarAsync(Stream imageStream, string fileName)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
using var content = new MultipartFormDataContent();
|
||||
using var streamContent = new StreamContent(imageStream);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(fileName));
|
||||
content.Add(streamContent, "file", fileName);
|
||||
|
||||
var response = await _http.PostAsync("/api/users/avatar", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<AvatarUploadResponse>();
|
||||
return result?.AsciiArt;
|
||||
}
|
||||
|
||||
public async Task<MessageDto?> UploadFileAsync(string channelName, Stream fileStream, string fileName)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
using var content = new MultipartFormDataContent();
|
||||
using var streamContent = new StreamContent(fileStream);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(fileName));
|
||||
content.Add(streamContent, "file", fileName);
|
||||
|
||||
var response = await _http.PostAsync($"/api/channels/{Uri.EscapeDataString(channelName)}/upload", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<MessageDto>();
|
||||
}
|
||||
|
||||
private void SetToken(string token)
|
||||
{
|
||||
_token = token;
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
private static async Task EnsureSuccessAsync(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
// Try to extract a meaningful error message from the response body
|
||||
var errorMessage = $"{(int)response.StatusCode} {response.ReasonPhrase}";
|
||||
try
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
// Try to parse {"error": "..."} format
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
if (doc.RootElement.TryGetProperty("error", out var errorProp) ||
|
||||
doc.RootElement.TryGetProperty("Error", out errorProp))
|
||||
{
|
||||
errorMessage = errorProp.GetString() ?? errorMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't parse the body, use the status code message
|
||||
}
|
||||
|
||||
throw new HttpRequestException(errorMessage);
|
||||
}
|
||||
|
||||
private void EnsureAuthenticated()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_token))
|
||||
throw new InvalidOperationException("Not authenticated. Call LoginAsync or RegisterAsync first.");
|
||||
}
|
||||
|
||||
private static string GetContentType(string fileName)
|
||||
{
|
||||
var ext = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".txt" => "text/plain",
|
||||
".pdf" => "application/pdf",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal record AvatarUploadResponse(string AsciiArt);
|
||||
@@ -0,0 +1,135 @@
|
||||
using EchoHub.Core.Constants;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace EchoHub.Client.Services;
|
||||
|
||||
public sealed class EchoHubConnection : IAsyncDisposable
|
||||
{
|
||||
private readonly HubConnection _connection;
|
||||
|
||||
public event Action<MessageDto>? OnMessageReceived;
|
||||
public event Action<string, string>? OnUserJoined;
|
||||
public event Action<string, string>? OnUserLeft;
|
||||
public event Action<ChannelDto>? OnChannelUpdated;
|
||||
public event Action<UserPresenceDto>? OnUserStatusChanged;
|
||||
public event Action<string>? OnError;
|
||||
public event Action<string>? OnConnectionStateChanged;
|
||||
|
||||
public bool IsConnected => _connection.State == HubConnectionState.Connected;
|
||||
|
||||
public EchoHubConnection(string serverUrl, string jwtToken)
|
||||
{
|
||||
var hubUrl = serverUrl.TrimEnd('/') + HubConstants.ChatHubPath;
|
||||
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl(hubUrl, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => Task.FromResult<string?>(jwtToken);
|
||||
})
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
RegisterHandlers();
|
||||
|
||||
_connection.Reconnecting += _ =>
|
||||
{
|
||||
OnConnectionStateChanged?.Invoke("Reconnecting...");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_connection.Reconnected += _ =>
|
||||
{
|
||||
OnConnectionStateChanged?.Invoke("Connected");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_connection.Closed += _ =>
|
||||
{
|
||||
OnConnectionStateChanged?.Invoke("Disconnected");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
private void RegisterHandlers()
|
||||
{
|
||||
_connection.On<MessageDto>(nameof(Core.Contracts.IEchoHubClient.ReceiveMessage), message =>
|
||||
{
|
||||
OnMessageReceived?.Invoke(message);
|
||||
});
|
||||
|
||||
_connection.On<string, string>(nameof(Core.Contracts.IEchoHubClient.UserJoined), (channelName, username) =>
|
||||
{
|
||||
OnUserJoined?.Invoke(channelName, username);
|
||||
});
|
||||
|
||||
_connection.On<string, string>(nameof(Core.Contracts.IEchoHubClient.UserLeft), (channelName, username) =>
|
||||
{
|
||||
OnUserLeft?.Invoke(channelName, username);
|
||||
});
|
||||
|
||||
_connection.On<ChannelDto>(nameof(Core.Contracts.IEchoHubClient.ChannelUpdated), channel =>
|
||||
{
|
||||
OnChannelUpdated?.Invoke(channel);
|
||||
});
|
||||
|
||||
_connection.On<UserPresenceDto>(nameof(Core.Contracts.IEchoHubClient.UserStatusChanged), presence =>
|
||||
{
|
||||
OnUserStatusChanged?.Invoke(presence);
|
||||
});
|
||||
|
||||
_connection.On<string>(nameof(Core.Contracts.IEchoHubClient.Error), message =>
|
||||
{
|
||||
OnError?.Invoke(message);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
OnConnectionStateChanged?.Invoke("Connecting...");
|
||||
await _connection.StartAsync();
|
||||
OnConnectionStateChanged?.Invoke("Connected");
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
await _connection.StopAsync();
|
||||
OnConnectionStateChanged?.Invoke("Disconnected");
|
||||
}
|
||||
|
||||
public async Task<List<MessageDto>> JoinChannelAsync(string channelName)
|
||||
{
|
||||
return await _connection.InvokeAsync<List<MessageDto>>("JoinChannel", channelName);
|
||||
}
|
||||
|
||||
public async Task LeaveChannelAsync(string channelName)
|
||||
{
|
||||
await _connection.InvokeAsync("LeaveChannel", channelName);
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(string channelName, string content)
|
||||
{
|
||||
await _connection.InvokeAsync("SendMessage", channelName, content);
|
||||
}
|
||||
|
||||
public async Task<List<MessageDto>> GetHistoryAsync(string channelName, int count = HubConstants.DefaultHistoryCount)
|
||||
{
|
||||
return await _connection.InvokeAsync<List<MessageDto>>("GetChannelHistory", channelName, count);
|
||||
}
|
||||
|
||||
public async Task UpdateStatusAsync(UserStatus status, string? statusMessage = null)
|
||||
{
|
||||
await _connection.InvokeAsync("UpdateStatus", status, statusMessage);
|
||||
}
|
||||
|
||||
public async Task<List<UserPresenceDto>> GetOnlineUsersAsync(string channelName)
|
||||
{
|
||||
return await _connection.InvokeAsync<List<UserPresenceDto>>("GetOnlineUsers", channelName);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user