diff --git a/src/Services/.gitignore b/.gitignore similarity index 100% rename from src/Services/.gitignore rename to .gitignore diff --git a/JobHubMATF.slnx b/JobHubMATF.slnx new file mode 100644 index 0000000..8e892f9 --- /dev/null +++ b/JobHubMATF.slnx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Chat.API/Chat.API.csproj b/src/Services/Chat.API/Chat.API.csproj new file mode 100644 index 0000000..e64980f --- /dev/null +++ b/src/Services/Chat.API/Chat.API.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/src/Services/Chat.API/Chat.API.http b/src/Services/Chat.API/Chat.API.http new file mode 100644 index 0000000..e63eb66 --- /dev/null +++ b/src/Services/Chat.API/Chat.API.http @@ -0,0 +1,6 @@ +@Chat.API_HostAddress = http://localhost:5075 + +GET {{Chat.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/Services/Chat.API/Controllers/MessagesController.cs b/src/Services/Chat.API/Controllers/MessagesController.cs new file mode 100644 index 0000000..73cf251 --- /dev/null +++ b/src/Services/Chat.API/Controllers/MessagesController.cs @@ -0,0 +1,44 @@ + +using Chat.API.Models; +using Chat.API.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Chat.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class MessagesController : ControllerBase + { + private readonly ChatService _chatService; + + public MessagesController(ChatService chatService) + { + _chatService = chatService; + } + [HttpPost] + public async Task SendMessage([FromBody] SendMessageRequest request) { + if (string.IsNullOrEmpty(request.Text)) + return BadRequest("Message cannot be empty"); + + var message = await _chatService.SendMessageAsync( + request.SenderId, + request.ReciverId, + request.Text + ); + + return Ok(message); + + } + + [HttpGet] + public async Task GetMessages([FromQuery] string user1, [FromQuery] string user2) + { + if (string.IsNullOrEmpty(user1) || string.IsNullOrEmpty(user2)) + return BadRequest("User IDs are required"); + + var messages = await _chatService.GetMessagesAsync(user1, user2); + + return Ok(messages); + } + } +} diff --git a/src/Services/Chat.API/Hubs/ChatHub.cs b/src/Services/Chat.API/Hubs/ChatHub.cs new file mode 100644 index 0000000..52e1406 --- /dev/null +++ b/src/Services/Chat.API/Hubs/ChatHub.cs @@ -0,0 +1,59 @@ +using Chat.API.Services; +using Microsoft.AspNetCore.SignalR; +namespace Chat.API.Hubs +{ + public class ChatHub : Hub + { + private readonly ChatService _chatService; + private static readonly Dictionary _connections = new(); + + public ChatHub(ChatService chatService) + { + _chatService = chatService; + } + public async Task SendMessage(string senderId, string reciverId, string message) + { + var savedMessage = await _chatService.SendMessageAsync(senderId, reciverId, message); + if(_connections.TryGetValue(reciverId, out var connectionId)) + { + await Clients.Client(connectionId).SendAsync("ReceiveMessage", + savedMessage.SenderId, + reciverId, + savedMessage.Text, + savedMessage.Timestamp); + } + + if (_connections.TryGetValue(senderId, out var senderConnection)) + { + await Clients.Client(senderConnection).SendAsync("ReceiveMessage", + savedMessage.SenderId, + reciverId, + savedMessage.Text, + savedMessage.Timestamp); + } + } + public override Task OnConnectedAsync() + { + var userId = Context.GetHttpContext().Request.Query["userId"]; + + if (!string.IsNullOrEmpty(userId)) + { + _connections[userId] = Context.ConnectionId; + } + + return base.OnConnectedAsync(); + } + + public override Task OnDisconnectedAsync(Exception exception) + { + var item = _connections.FirstOrDefault(x => x.Value == Context.ConnectionId); + + if (!string.IsNullOrEmpty(item.Key)) + { + _connections.Remove(item.Key); + } + + return base.OnDisconnectedAsync(exception); + } + } +} diff --git a/src/Services/Chat.API/Models/Chat.cs b/src/Services/Chat.API/Models/Chat.cs new file mode 100644 index 0000000..b06ce13 --- /dev/null +++ b/src/Services/Chat.API/Models/Chat.cs @@ -0,0 +1,21 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + + +namespace Chat.API.Models +{ + + public class Chat + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + + public string Id { get; set; } + + public string User1Id { get; set; } + + public string User2Id { get; set; } + + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Chat.API/Models/Message.cs b/src/Services/Chat.API/Models/Message.cs new file mode 100644 index 0000000..daf4a7a --- /dev/null +++ b/src/Services/Chat.API/Models/Message.cs @@ -0,0 +1,24 @@ + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Chat.API.Models +{ + + public class Message + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string ChatId { get; set; } + + public string SenderId { get; set; } + + public string Text { get; set; } + + public DateTime Timestamp { get; set; } + + } + +} \ No newline at end of file diff --git a/src/Services/Chat.API/Models/MongoDbSettings.cs b/src/Services/Chat.API/Models/MongoDbSettings.cs new file mode 100644 index 0000000..c337c68 --- /dev/null +++ b/src/Services/Chat.API/Models/MongoDbSettings.cs @@ -0,0 +1,8 @@ +namespace Chat.API.Models +{ + public class MongoDbSettings + { + public string ConnectionString { get; set; } + public string DatabaseName { get; set; } + } +} diff --git a/src/Services/Chat.API/Models/SendMessageRequest.cs b/src/Services/Chat.API/Models/SendMessageRequest.cs new file mode 100644 index 0000000..c96d8ed --- /dev/null +++ b/src/Services/Chat.API/Models/SendMessageRequest.cs @@ -0,0 +1,10 @@ +namespace Chat.API.Models +{ + public class SendMessageRequest + { + public string SenderId { get; set; } + public string ReciverId { get; set; } + + public string Text { get; set; } + } +} diff --git a/src/Services/Chat.API/Program.cs b/src/Services/Chat.API/Program.cs new file mode 100644 index 0000000..18775df --- /dev/null +++ b/src/Services/Chat.API/Program.cs @@ -0,0 +1,44 @@ +using Chat.API.Services; +using Scalar.AspNetCore; +using Chat.API.Hubs; +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); +builder.Services.AddSingleton(); +builder.Services.AddSignalR(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", + policy => + { + policy.WithOrigins("http://127.0.0.1:5500", "null") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +app.UseCors("AllowAll"); + +app.UseAuthorization(); + +app.MapControllers(); +app.MapHub("/chatHub"); + +app.Run(); diff --git a/src/Services/Chat.API/Properties/launchSettings.json b/src/Services/Chat.API/Properties/launchSettings.json new file mode 100644 index 0000000..f9ab1e8 --- /dev/null +++ b/src/Services/Chat.API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5075", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7175;http://localhost:5075", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Services/Chat.API/Services/ChatService.cs b/src/Services/Chat.API/Services/ChatService.cs new file mode 100644 index 0000000..b34a3ae --- /dev/null +++ b/src/Services/Chat.API/Services/ChatService.cs @@ -0,0 +1,99 @@ +using Chat.API.Models; +using MongoDB.Driver; + + + +namespace Chat.API.Services +{ + public class ChatService + { + private readonly IMongoCollection _messages; + private readonly IMongoCollection _chats; + + public ChatService(IConfiguration config) + { + var settings = config.GetSection("MongoDbSettings").Get(); + var client = new MongoClient(settings.ConnectionString); + var database = client.GetDatabase(settings.DatabaseName); + + _chats = database.GetCollection("Chats"); + _messages = database.GetCollection("Messages"); + } + + public async Task GetOrCreateChatAsync(string user1, string user2) + { + // Sortiranje userId-eva + var sortedUsers = new List { user1, user2 }; + sortedUsers.Sort(); + + var u1 = sortedUsers[0]; + var u2 = sortedUsers[1]; + + // Traženje postojećeg chata + var chat = await _chats + .Find(c => c.User1Id == u1 && c.User2Id == u2) + .FirstOrDefaultAsync(); + + if (chat != null) + return chat; + + // Kreiranje novog chata + var newChat = new Models.Chat + { + User1Id = u1, + User2Id = u2, + CreatedAt = DateTime.UtcNow + }; + + await _chats.InsertOneAsync(newChat); + + return newChat; + } + + public async Task SendMessageAsync(string senderId, string receiverId, string text) + { + // 1. Uzmi ili kreiraj chat + var chat = await GetOrCreateChatAsync(senderId, receiverId); + + // 2. Kreiraj poruku + var message = new Message + { + ChatId = chat.Id, + SenderId = senderId, + Text = text, + Timestamp = DateTime.UtcNow + }; + + // 3. Sačuvaj u bazu + await _messages.InsertOneAsync(message); + + return message; + } + + public async Task> GetMessagesAsync(string user1, string user2) + { + // 1. Sortiranje userId-eva + var sortedUsers = new List { user1, user2 }; + sortedUsers.Sort(); + + var u1 = sortedUsers[0]; + var u2 = sortedUsers[1]; + + // 2. Pronađi chat + var chat = await _chats + .Find(c => c.User1Id == u1 && c.User2Id == u2) + .FirstOrDefaultAsync(); + + if (chat == null) + return new List(); + + // 3. Uzmi sve poruke za taj chat + var messages = await _messages + .Find(m => m.ChatId == chat.Id) + .SortBy(m => m.Timestamp) + .ToListAsync(); + + return messages; + } + } +} diff --git a/src/Services/Chat.API/appsettings.Development.json b/src/Services/Chat.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Services/Chat.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Services/Chat.API/appsettings.json b/src/Services/Chat.API/appsettings.json new file mode 100644 index 0000000..e15d651 --- /dev/null +++ b/src/Services/Chat.API/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "MongoDbSettings": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "ChatDb" + } +} \ No newline at end of file