diff --git a/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs b/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs new file mode 100644 index 0000000000000000000000000000000000000000..0c54af61a8bd5ac751eec8dd173a9f1140b4f5c8 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs @@ -0,0 +1,146 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Tsi1.BusinessLayer.Dtos; +using Tsi1.BusinessLayer.Interfaces; +using Tsi1.DataLayer; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.Api.SignalR +{ + [Authorize] + public class ChatHub : Hub + { + private readonly IMapper _mapper; + private readonly IHubContext<PresenceHub> _presenceHub; + private readonly PresenceTracker _tracker; + private readonly IMessageService _messageService; + private readonly IChatService _chatService; + + public ChatHub(IMapper mapper, IHubContext<PresenceHub> presenceHub, + PresenceTracker tracker, IMessageService messageService, IChatService chatService) + { + _tracker = tracker; + _presenceHub = presenceHub; + _mapper = mapper; + _messageService = messageService; + _chatService = chatService; + } + + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + + var otherUserId = int.Parse(httpContext.Request.Query["id"].ToString()); + + var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value); + var tenantId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "TenantId").Value); + + var groupName = GetGroupName(userId, otherUserId); + await Groups.AddToGroupAsync(Context.ConnectionId, groupName); + + await AddToGroup(groupName); + + var messages = await _messageService.GetMessages(userId, otherUserId, tenantId); + + await Clients.Caller.SendAsync("ReceiveMessages", messages); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + + await RemoveFromGroup(); + + await base.OnDisconnectedAsync(exception); + } + + public async Task SendMessage(MessageCreateDto newMessage) + { + var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value); + var tenantId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "TenantId").Value); + + if (userId == newMessage.ReceiverId) + throw new HubException("You cannot send messages to yourself"); + + var groupName = GetGroupName(userId, newMessage.ReceiverId); + + var group = await _chatService.GetGroupByName(groupName); + + if (group == null) + { + throw new HubException($"No existe el grupo {groupName}"); + } + + if (!group.Connections.Any(x => x.UserId == newMessage.ReceiverId)) + { + var connections = await _tracker.GetConnectionsForUser(newMessage.ReceiverId); + if (connections != null) + { + await _presenceHub.Clients.Clients(connections).SendAsync("NewMessageReceived", userId); + } + } + + newMessage.SenderId = userId; + var messageDto = await _messageService.Send(newMessage); + + await Clients.Group(groupName).SendAsync("NewMessage", messageDto); + } + + private async Task<Group> AddToGroup(string groupName) + { + var group = await _chatService.GetGroupByName(groupName); + + if (group == null) + { + throw new HubException($"No existe el grupo {groupName}"); + } + + var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value); + + var connection = new Connection + { + ConnectionId = Context.ConnectionId, + UserId = userId, + }; + + if (group == null) + { + group = new Group + { + Name = groupName, + }; + } + + group.Connections.Add(connection); + + var result = await _chatService.CreateGroup(group); + + if (result.HasError) + { + throw new HubException($"Error al crear el grupo {group}"); + } + + return group; + } + + private async Task<bool> RemoveFromGroup() + { + await _chatService.RemoveConnectionFromGroup(Context.ConnectionId); + + return true; + } + + private string GetGroupName(int callerId, int otherId) + { + var compare = callerId < otherId; + return compare ? $"{callerId}-{otherId}" : $"{otherId}-{callerId}"; + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs b/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs new file mode 100644 index 0000000000000000000000000000000000000000..e606bc97df59fe20017906c434941e3b40ca28a8 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.SignalR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Tsi1.Api.SignalR +{ + public class PresenceHub : Hub + { + private readonly PresenceTracker _tracker; + public PresenceHub(PresenceTracker tracker) + { + _tracker = tracker; + } + + public override async Task OnConnectedAsync() + { + var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value); + + var isOnline = await _tracker.UserConnected(userId, Context.ConnectionId); + if (isOnline) + await Clients.Others.SendAsync("UserIsOnline", userId); + + var currentUsers = await _tracker.GetOnlineUsers(); + await Clients.Caller.SendAsync("GetOnlineUsers", currentUsers); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value); + + var isOffline = await _tracker.UserDisconnected(userId, Context.ConnectionId); + + if (isOffline) + await Clients.Others.SendAsync("UserIsOffline", userId); + + await base.OnDisconnectedAsync(exception); + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs b/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs new file mode 100644 index 0000000000000000000000000000000000000000..f11f18db7d18e01b3a2f8b82d65f8a07dcb3db47 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Tsi1.Api.SignalR +{ + public class PresenceTracker + { + private static readonly Dictionary<int, List<string>> OnlineUsers = + new Dictionary<int, List<string>>(); + + public Task<bool> UserConnected(int userId, string connectionId) + { + bool isOnline = false; + lock (OnlineUsers) + { + if (OnlineUsers.ContainsKey(userId)) + { + OnlineUsers[userId].Add(connectionId); + } + else + { + OnlineUsers.Add(userId, new List<string> { connectionId }); + isOnline = true; + } + } + + return Task.FromResult(isOnline); + } + + public Task<bool> UserDisconnected(int userId, string connectionId) + { + bool isOffline = false; + lock (OnlineUsers) + { + if (!OnlineUsers.ContainsKey(userId)) return Task.FromResult(isOffline); + + OnlineUsers[userId].Remove(connectionId); + if (OnlineUsers[userId].Count == 0) + { + OnlineUsers.Remove(userId); + isOffline = true; + } + } + + return Task.FromResult(isOffline); + } + + public Task<int[]> GetOnlineUsers() + { + int[] onlineUsers; + lock (OnlineUsers) + { + onlineUsers = OnlineUsers.OrderBy(k => k.Key).Select(k => k.Key).ToArray(); + } + + return Task.FromResult(onlineUsers); + } + + public Task<List<string>> GetConnectionsForUser(int userId) + { + List<string> connectionIds; + lock (OnlineUsers) + { + connectionIds = OnlineUsers.GetValueOrDefault(userId); + } + + return Task.FromResult(connectionIds); + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/SignalR/VideoHub.cs b/Tsi1.Api/Tsi1.Api/SignalR/VideoHub.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e62112c24780b458c531ea9e266d70b4966b1e1 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/SignalR/VideoHub.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Tsi1.BusinessLayer.Helpers; + +namespace Tsi1.Api.SignalR +{ + [Authorize] + public class VideoHub : Hub + { + public override async Task OnConnectedAsync() + { + await Groups.AddToGroupAsync(Context.ConnectionId, Context.ConnectionId); + + await Clients.Caller.SendAsync(Context.ConnectionId); + } + + [Authorize(Roles = UserTypes.Professor)] + public async Task CreateRoom(string roomName) + { + await Groups.AddToGroupAsync(Context.ConnectionId, roomName); + + await Clients.Caller.SendAsync("CreateRoom", roomName); + } + + public async Task Join(string roomName, string offer) + { + + Console.WriteLine(offer); + await Clients.Group(roomName).SendAsync("Join", Context.ConnectionId, offer); + + await Groups.AddToGroupAsync(Context.ConnectionId, roomName); + } + + public async Task Leave(string roomName) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); + + await Clients.Group(roomName).SendAsync("Leave", Context.ConnectionId); + } + + public async Task NotifyUser(string connectionId, string answer) + { + await Clients.Group(connectionId).SendAsync("NotifyUser", Context.ConnectionId, answer); + } + + public async Task ICECandidate(string connectionId, string candidate) + { + await Clients.Group(connectionId).SendAsync("ICECandidate", Context.ConnectionId, candidate); + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Startup.cs b/Tsi1.Api/Tsi1.Api/Startup.cs index 854ba8a94b7a335050dcd3c5d35f1e8c2fa73939..135212e330271889277770863aa98754ea3dcd31 100644 --- a/Tsi1.Api/Tsi1.Api/Startup.cs +++ b/Tsi1.Api/Tsi1.Api/Startup.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Tsi1.Api.Infrastructure; +using Tsi1.Api.SignalR; using Tsi1.BusinessLayer.Helpers; using Tsi1.BusinessLayer.Interfaces; using Tsi1.BusinessLayer.Services; @@ -44,6 +45,8 @@ namespace Tsi1.Api { services.AddControllers(); + services.AddSignalR(); + var isElasticCloud = bool.Parse(Configuration.GetSection("IsElasticCloud").Value); var postgreSqlSection = isElasticCloud ? "PostgreSqlCloud" : "PostgreSql"; @@ -108,6 +111,23 @@ namespace Tsi1.Api ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; + + x.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && + path.StartsWithSegments("/hubs")) + { + context.Token = accessToken; + } + + return Task.CompletedTask; + } + }; }); services.AddSwaggerGen(c => @@ -180,6 +200,9 @@ namespace Tsi1.Api app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapHub<VideoHub>("hubs/video"); + endpoints.MapHub<PresenceHub>("hubs/presence"); + endpoints.MapHub<ChatHub>("hubs/chat"); }); } } diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs new file mode 100644 index 0000000000000000000000000000000000000000..cf332b0a11d11b5b77f9058d9d1a95488293b9ea --- /dev/null +++ b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Tsi1.BusinessLayer.Helpers; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.BusinessLayer.Interfaces +{ + public interface IChatService + { + Task<Group> GetGroupByName(string groupName); + + Task<ServiceResult<Group>> CreateGroup(Group group); + + Task<bool> RemoveConnectionFromGroup(string connectionId); + } +} diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs new file mode 100644 index 0000000000000000000000000000000000000000..4e4ac8e76aaca0017a0d35dca73cd1e57503da3e --- /dev/null +++ b/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Tsi1.BusinessLayer.Helpers; +using Tsi1.BusinessLayer.Interfaces; +using Tsi1.DataLayer; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.BusinessLayer.Services +{ + public class ChatService : IChatService + { + private readonly Tsi1Context _tsi1Context; + + public ChatService(Tsi1Context tsi1Context) + { + _tsi1Context = tsi1Context; + } + public async Task<ServiceResult<Group>> CreateGroup(Group group) + { + var result = new ServiceResult<Group>(); + + _tsi1Context.Groups.Add(group); + try + { + await _tsi1Context.SaveChangesAsync(); + } + catch (Exception) + { + result.HasError = true; + } + + return result; + } + + public async Task<Group> GetGroupByName(string groupName) + { + var result = new ServiceResult<Group>(); + + var group = await _tsi1Context.Groups.FirstOrDefaultAsync(x => x.Name == groupName); + + return group; + } + + public async Task<bool> RemoveConnectionFromGroup(string connectionId) + { + var connection = await _tsi1Context.Connections.FirstOrDefaultAsync(x => x.ConnectionId == connectionId); + + if (connection != null) + { + connection.Group = null; + await _tsi1Context.SaveChangesAsync(); + } + + return true; + } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs b/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs new file mode 100644 index 0000000000000000000000000000000000000000..2e2e9366bdd5dd35d36650f805c1672806f08f69 --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tsi1.DataLayer.Entities +{ + public class Connection + { + public string ConnectionId { get; set; } + public int UserId { get; set; } + public Group Group { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs b/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs new file mode 100644 index 0000000000000000000000000000000000000000..38291ebfd52def7d9d2e21d177a72fc191b7129c --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tsi1.DataLayer.Entities +{ + public class Group + { + public Group() + { + Connections = new List<Connection>(); + } + + public string Name { get; set; } + public ICollection<Connection> Connections { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs new file mode 100644 index 0000000000000000000000000000000000000000..7c085f952b6ad8859207d0eb4371c69d0edb2301 --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Text; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.DataLayer.EntityConfiguration +{ + public class ConnectionConfiguration : IEntityTypeConfiguration<Connection> + { + public void Configure(EntityTypeBuilder<Connection> builder) + { + builder.HasKey(x => x.ConnectionId); + + builder.Property(x => x.UserId) + .IsRequired(); + + builder.HasOne(x => x.Group) + .WithMany(x => x.Connections); + + } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e64b95e6eb1fca0fff085e53679025dfa95728a --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Text; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.DataLayer.EntityConfiguration +{ + public class GroupConfiguration : IEntityTypeConfiguration<Group> + { + public void Configure(EntityTypeBuilder<Group> builder) + { + builder.HasKey(x => x.Name); + } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.Designer.cs b/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..5dba9e6375b1de286038bffe7e78d016f6cdd791 --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.Designer.cs @@ -0,0 +1,491 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tsi1.DataLayer; + +namespace Tsi1.DataLayer.Migrations +{ + [DbContext(typeof(Tsi1Context))] + [Migration("20201029004502_added-entities-Group-and-Connection")] + partial class addedentitiesGroupandConnection + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Connection", b => + { + b.Property<string>("ConnectionId") + .HasColumnType("text"); + + b.Property<string>("GroupName") + .HasColumnType("text"); + + b.Property<int>("UserId") + .HasColumnType("integer"); + + b.HasKey("ConnectionId"); + + b.HasIndex("GroupName"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Course", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.Property<int>("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("Name", "TenantId") + .IsUnique(); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Forum", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<int>("CourseId") + .HasColumnType("integer"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId", "Name") + .IsUnique(); + + b.ToTable("Forums"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.ForumUser", b => + { + b.Property<int>("ForumId") + .HasColumnType("integer"); + + b.Property<int>("UserId") + .HasColumnType("integer"); + + b.HasKey("ForumId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ForumUsers"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Group", b => + { + b.Property<string>("Name") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Post", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp without time zone"); + + b.Property<int>("ForumId") + .HasColumnType("integer"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("character varying(100)"); + + b.Property<int>("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ForumId", "Title") + .IsUnique(); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.PostMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("character varying(10485760)"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp without time zone"); + + b.Property<int>("PostId") + .HasColumnType("integer"); + + b.Property<int>("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostMessages"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Professor", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("IdentityCard") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.Property<int>("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("IdentityCard", "TenantId") + .IsUnique(); + + b.ToTable("Professors"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.ProfessorCourse", b => + { + b.Property<int>("ProfessorId") + .HasColumnType("integer"); + + b.Property<int>("CourseId") + .HasColumnType("integer"); + + b.HasKey("ProfessorId", "CourseId"); + + b.HasIndex("CourseId"); + + b.ToTable("ProfessorCourses"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Student", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<int>("Age") + .HasColumnType("integer"); + + b.Property<string>("IdentityCard") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.Property<int>("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("IdentityCard", "TenantId") + .IsUnique(); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.StudentCourse", b => + { + b.Property<int>("StudentId") + .HasColumnType("integer"); + + b.Property<int>("CourseId") + .HasColumnType("integer"); + + b.HasKey("StudentId", "CourseId"); + + b.HasIndex("CourseId"); + + b.ToTable("StudentCourses"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Tenant", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.User", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("Email") + .IsRequired() + .HasColumnType("character varying(255)"); + + b.Property<string>("FirstName") + .IsRequired() + .HasColumnType("character varying(255)"); + + b.Property<string>("LastName") + .IsRequired() + .HasColumnType("character varying(255)"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("character varying(255)"); + + b.Property<int?>("ProfessorId") + .HasColumnType("integer"); + + b.Property<int?>("StudentId") + .HasColumnType("integer"); + + b.Property<int>("TenantId") + .HasColumnType("integer"); + + b.Property<int>("UserTypeId") + .HasColumnType("integer"); + + b.Property<string>("Username") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ProfessorId") + .IsUnique(); + + b.HasIndex("StudentId") + .IsUnique(); + + b.HasIndex("TenantId"); + + b.HasIndex("UserTypeId"); + + b.HasIndex("Username", "TenantId") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.UserType", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("UserTypes"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Connection", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Group", "Group") + .WithMany("Connections") + .HasForeignKey("GroupName"); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Course", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Tenant", "Tenant") + .WithMany("Courses") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Forum", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Course", "Course") + .WithMany("Forums") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.ForumUser", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Forum", "Forum") + .WithMany("ForumUsers") + .HasForeignKey("ForumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.User", "User") + .WithMany("ForumUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Post", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Forum", "Forum") + .WithMany("Posts") + .HasForeignKey("ForumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.User", "User") + .WithMany("Posts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.PostMessage", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Post", "Post") + .WithMany("PostMessages") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.User", "User") + .WithMany("PostMessages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Professor", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Tenant", "Tenant") + .WithMany("Professors") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.ProfessorCourse", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Course", "Course") + .WithMany("ProfessorCourses") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.Professor", "Professor") + .WithMany("ProfessorCourses") + .HasForeignKey("ProfessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.Student", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Tenant", "Tenant") + .WithMany("Students") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.StudentCourse", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Course", "Course") + .WithMany("StudentCourses") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.Student", "Student") + .WithMany("StudentCourses") + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tsi1.DataLayer.Entities.User", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Professor", "Professor") + .WithOne("User") + .HasForeignKey("Tsi1.DataLayer.Entities.User", "ProfessorId"); + + b.HasOne("Tsi1.DataLayer.Entities.Student", "Student") + .WithOne("User") + .HasForeignKey("Tsi1.DataLayer.Entities.User", "StudentId"); + + b.HasOne("Tsi1.DataLayer.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tsi1.DataLayer.Entities.UserType", "UserType") + .WithMany() + .HasForeignKey("UserTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.cs b/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.cs new file mode 100644 index 0000000000000000000000000000000000000000..dfece7bb6d2fa79e3edd1afe268602d00477a253 --- /dev/null +++ b/Tsi1.Api/Tsi1.DataLayer/Migrations/20201029004502_added-entities-Group-and-Connection.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tsi1.DataLayer.Migrations +{ + public partial class addedentitiesGroupandConnection : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Name = table.Column<string>(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "Connections", + columns: table => new + { + ConnectionId = table.Column<string>(nullable: false), + UserId = table.Column<int>(nullable: false), + GroupName = table.Column<string>(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Connections", x => x.ConnectionId); + table.ForeignKey( + name: "FK_Connections_Groups_GroupName", + column: x => x.GroupName, + principalTable: "Groups", + principalColumn: "Name", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Connections_GroupName", + table: "Connections", + column: "GroupName"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Connections"); + + migrationBuilder.DropTable( + name: "Groups"); + } + } +} diff --git a/Tsi1.Api/Tsi1.DataLayer/Migrations/Tsi1ContextModelSnapshot.cs b/Tsi1.Api/Tsi1.DataLayer/Migrations/Tsi1ContextModelSnapshot.cs index 4c6d381fbdcd5201077e62e722bd7916da810a66..9f2b80da778b9cfc5bb392f6faa481cab6f4eea7 100644 --- a/Tsi1.Api/Tsi1.DataLayer/Migrations/Tsi1ContextModelSnapshot.cs +++ b/Tsi1.Api/Tsi1.DataLayer/Migrations/Tsi1ContextModelSnapshot.cs @@ -19,6 +19,24 @@ namespace Tsi1.DataLayer.Migrations .HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Connection", b => + { + b.Property<string>("ConnectionId") + .HasColumnType("text"); + + b.Property<string>("GroupName") + .HasColumnType("text"); + + b.Property<int>("UserId") + .HasColumnType("integer"); + + b.HasKey("ConnectionId"); + + b.HasIndex("GroupName"); + + b.ToTable("Connections"); + }); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Course", b => { b.Property<int>("Id") @@ -103,6 +121,16 @@ namespace Tsi1.DataLayer.Migrations b.ToTable("ForumUsers"); }); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Group", b => + { + b.Property<string>("Name") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("Groups"); + }); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Post", b => { b.Property<int>("Id") @@ -338,6 +366,13 @@ namespace Tsi1.DataLayer.Migrations b.ToTable("UserTypes"); }); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Connection", b => + { + b.HasOne("Tsi1.DataLayer.Entities.Group", "Group") + .WithMany("Connections") + .HasForeignKey("GroupName"); + }); + modelBuilder.Entity("Tsi1.DataLayer.Entities.Course", b => { b.HasOne("Tsi1.DataLayer.Entities.Tenant", "Tenant") diff --git a/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs b/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs index 91401cfd792dbfc0ab8f62ebd7dd6e6f7277dda4..d442517b50b82f0ebd6f86812ac69f463cd16239 100644 --- a/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs +++ b/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs @@ -21,6 +21,8 @@ namespace Tsi1.DataLayer public DbSet<PostMessage> PostMessages { get; set; } public DbSet<Tenant> Tenants { get; set; } public DbSet<ForumUser> ForumUsers { get; set; } + public DbSet<Group> Groups { get; set; } + public DbSet<Connection> Connections { get; set; } public DbSet<File> Files { get; set; } @@ -40,6 +42,8 @@ namespace Tsi1.DataLayer modelBuilder.ApplyConfiguration(new PostMessageConfiguration()); modelBuilder.ApplyConfiguration(new TenantConfiguration()); modelBuilder.ApplyConfiguration(new ForumUserConfiguration()); + modelBuilder.ApplyConfiguration(new GroupConfiguration()); + modelBuilder.ApplyConfiguration(new ConnectionConfiguration()); modelBuilder.ApplyConfiguration(new FileConfiguration()); } }