diff --git a/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs b/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs new file mode 100644 index 0000000..ee367da --- /dev/null +++ b/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs @@ -0,0 +1,48 @@ +using AutoFixture; +using Govor.Core.Models; +using Govor.Data; +using Govor.Data.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Govor.API.Tests.IntegrationTests.EF.Repositories; + +[TestFixture] +public class AdminsRepositoryTests +{ + private Fixture _fixture; + private DbContextOptions _options; + private int _testIteration = 0; + + [SetUp] + public void Setup() + { + _fixture = new Fixture(); + + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"DbGovor_{nameof(AdminsRepositoryTests)}_{_testIteration}") + .Options; + } + + [Test] + public async Task Given_NotEmptyDbSet_When_GetAllAsync_Then_Admins_Are_Returned() + { + // Arrange + var random = new Random(); + var admins = _fixture.CreateMany(random.Next(3, 10)).ToList(); + + await using var context = new GovorDbContext(_options); + var repository = new AdminsRepository(context); + + context.AddRange(admins); + await context.SaveChangesAsync(); + + // Act + var result = await repository.GetAllAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(admins.Count())); + Assert.That(result.Select(r => r.UserId), Is.EquivalentTo(admins.Select(r => r.UserId))); + } +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Services/AuthServiceTests.cs b/Govor.API.Tests/UnitTests/Services/AuthServiceTests.cs index ce4a958..dc65d5e 100644 --- a/Govor.API.Tests/UnitTests/Services/AuthServiceTests.cs +++ b/Govor.API.Tests/UnitTests/Services/AuthServiceTests.cs @@ -4,6 +4,8 @@ using Govor.Core.Infrastructure.Extensions; using Govor.Core.Models; using Govor.Core.Repositories.Users; using Govor.API.Services.Authentication.Interfaces; +using Govor.Core.Repositories.Admins; +using Govor.Core.Repositories.Invaites; using Moq; namespace Govor.API.Tests.UnitTests.Services; @@ -15,6 +17,9 @@ public class AuthServiceTests private Mock _passwordHasherMock; private Mock _jwtServiceMock; private Mock _usersRepositoryMock; + private Mock _adminsRepositoryMock; + private Mock _invitesRepositoryMock; + private IAccountService _accountService; [SetUp] @@ -29,7 +34,9 @@ public class AuthServiceTests _accountService = new AuthService( _usersRepositoryMock.Object, _jwtServiceMock.Object, - _passwordHasherMock.Object + _passwordHasherMock.Object, + _invitesRepositoryMock.Object, + _adminsRepositoryMock.Object ); } @@ -41,7 +48,7 @@ public class AuthServiceTests // Act & Assert Assert.ThrowsAsync(async () => await _accountService.RegistrationAsync( - _fixture.Create(), _fixture.Create())); + _fixture.Create(), _fixture.Create(), _fixture.Create())); } [Test] @@ -52,7 +59,7 @@ public class AuthServiceTests // Act & Assert Assert.DoesNotThrowAsync(async () => await _accountService.RegistrationAsync( - _fixture.Create(), _fixture.Create())); + _fixture.Create(), _fixture.Create(), _fixture.Create())); } [Test] diff --git a/Govor.API/Controllers/AuthController.cs b/Govor.API/Controllers/AuthController.cs index edc854e..b4aca2c 100644 --- a/Govor.API/Controllers/AuthController.cs +++ b/Govor.API/Controllers/AuthController.cs @@ -20,7 +20,7 @@ public class AuthController : Controller [HttpPost("register")]// api/auth/register [RequireHttps] - public async Task Register([FromBody] UserDto userDto) + public async Task Register([FromBody] RegistrationDto registrationDto) { try { @@ -29,25 +29,25 @@ public class AuthController : Controller return BadRequest(ModelState); } - var token = await _accountService.RegistrationAsync(userDto.Name, userDto.Password); - _logger.LogInformation($"Register request for {userDto.Name}"); + var token = await _accountService.RegistrationAsync(registrationDto.Name, registrationDto.Password, registrationDto.InviteLink); + _logger.LogInformation($"Register request for {registrationDto.Name}"); return Ok(new { token }); } catch (UserAlreadyExistException ex) { - _logger.LogWarning(ex, $"Registration failed for user {userDto.Name}"); + _logger.LogWarning(ex, $"Registration failed for user {registrationDto.Name}"); return BadRequest("Registration failed: user already exists."); } catch (Exception ex) { - _logger.LogError(ex, "Unexpected error during registration for user {Name}", userDto.Name); + _logger.LogError(ex, "Unexpected error during registration for user {Name}", registrationDto.Name); return StatusCode(500, "An unexpected error occurred. Please try again later."); } } [HttpPost("login")]// api/auth/login [RequireHttps] - public async Task Login([FromBody] UserDto userDto) + public async Task Login([FromBody] LoginDto userDto) { try { diff --git a/Govor.API/Controllers/UsersController.cs b/Govor.API/Controllers/UsersController.cs index 1d958c4..b903e16 100644 --- a/Govor.API/Controllers/UsersController.cs +++ b/Govor.API/Controllers/UsersController.cs @@ -7,7 +7,7 @@ namespace Govor.API.Controllers; [ApiController] [Route("api/admin/[controller]")] -//[Authorize(Roles = "Admin")] +[Authorize(Roles = "Admin")] public class UsersController : Controller { private readonly ILogger _logger; diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index ca2d75d..6876745 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using System.Text; using Govor.API.Hubs; using Govor.API.Services; @@ -8,6 +9,8 @@ using Govor.API.Services.Authentication.Interfaces; using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; +using Govor.Core.Repositories.Admins; +using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.MediasAttachments; using Govor.Core.Repositories.Messages; using Govor.Core.Repositories.Users; @@ -16,6 +19,7 @@ using Govor.Data.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); @@ -69,9 +73,6 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthorization(); - - - builder.Services.AddControllers(); builder.Services.AddScoped(); @@ -84,6 +85,8 @@ builder.Services.AddScoped, MediaAttachmentsV builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddDbContext( options => @@ -93,7 +96,32 @@ builder.Services.AddDbContext( ); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Govor API", Version = "v1" }); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer" + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + Array.Empty() + } + }); +}); + //builder.Services.AddOpenApi(); var app = builder.Build(); @@ -102,11 +130,12 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { //app.MapOpenApi(); - app.UseSwagger(); app.UseSwaggerUI(); } + + app.UseHttpsRedirection(); app.UseRouting(); @@ -117,6 +146,8 @@ app.UseAuthorization(); app.MapControllers(); app.MapHub("/api/chats"); +app.MapSwagger().RequireAuthorization(); + app.Map("/", () => "Not for browsers"); app.Run(); \ No newline at end of file diff --git a/Govor.API/Services/Authentication/AuthService.cs b/Govor.API/Services/Authentication/AuthService.cs index 47f25ec..2c662dc 100644 --- a/Govor.API/Services/Authentication/AuthService.cs +++ b/Govor.API/Services/Authentication/AuthService.cs @@ -4,6 +4,8 @@ using Govor.Core.Infrastructure.Extensions; using Govor.Core.Models; using Govor.Core.Repositories.Users; using Govor.API.Services; +using Govor.Core.Repositories.Admins; +using Govor.Core.Repositories.Invaites; namespace Govor.API.Services.Authentication; @@ -13,38 +15,64 @@ public class AuthService : IAccountService private readonly IPasswordHasher _passwordHasher; private readonly IJwtService _jwtService; private readonly IUsersRepository _usersRepository; + private readonly IInvitesRepository _invitesRepository; + private readonly IAdminsRepository _adminsRepository; - public AuthService(IUsersRepository usersRepository, IJwtService jwtService, IPasswordHasher passwordHasher) + public AuthService(IUsersRepository usersRepository, + IJwtService jwtService, + IPasswordHasher passwordHasher, + IInvitesRepository invitesRepository, + IAdminsRepository adminsRepository) { _usersRepository = usersRepository; _jwtService = jwtService; _passwordHasher = passwordHasher; + _invitesRepository = invitesRepository; + _adminsRepository = adminsRepository; } - public async Task RegistrationAsync(string name, string password) + public async Task RegistrationAsync(string name, string password, string inviteCode) { + // 1. Проверка существования имени if (await _usersRepository.ExistsUsernameAsync(name)) throw new UserAlreadyExistException(name); - + + // 2. Проверка валидности инвайта + var invite = await _invitesRepository.GetByCodeAsync(inviteCode); + + // 3. Генерация пароля var passwordHash = _passwordHasher.Hash(password); - + + // 4. Создание пользователя var user = new User { Id = Guid.NewGuid(), Username = name, - Description = string.Empty, PasswordHash = passwordHash, - CreatedOn = DateOnly.FromDateTime(DateTime.Now), + Description = string.Empty, + CreatedOn = DateOnly.FromDateTime(DateTime.UtcNow), IconId = Guid.NewGuid(), - WasOnline = DateTime.UtcNow - //Role = role == "Admin" ? "Admin" : "User" // Ограничение ролей + WasOnline = DateTime.UtcNow, + InviteId = invite.Id }; - + + // 5. Добавление пользователя await _usersRepository.AddAsync(user); - + + // 6. Назначение роли, если инвайт — админский + if (invite.IsAdmin) + { + await _adminsRepository.AddAsync(new Admin + { + UserId = user.Id + }); + } + + // 7. Генерация токена return _jwtService.GenerateJwtToken(user); } + public async Task LoginAsync(string name, string password) { if (await _usersRepository.ExistsUsernameAsync(name) == false) diff --git a/Govor.API/Services/Authentication/Interfaces/IAuthService.cs b/Govor.API/Services/Authentication/Interfaces/IAuthService.cs index 2cd87cb..ba95f13 100644 --- a/Govor.API/Services/Authentication/Interfaces/IAuthService.cs +++ b/Govor.API/Services/Authentication/Interfaces/IAuthService.cs @@ -4,6 +4,6 @@ namespace Govor.API.Services.Authentication.Interfaces; public interface IAccountService { - public Task RegistrationAsync(string name, string password); + public Task RegistrationAsync(string name, string password, string inviteCode); public Task LoginAsync(string name, string password); } \ No newline at end of file diff --git a/Govor.API/Services/Authentication/JwtService.cs b/Govor.API/Services/Authentication/JwtService.cs index 040eed9..f827790 100644 --- a/Govor.API/Services/Authentication/JwtService.cs +++ b/Govor.API/Services/Authentication/JwtService.cs @@ -3,6 +3,8 @@ using System.Security.Claims; using System.Text; using Govor.API.Services.Authentication.Interfaces; using Govor.Core.Models; +using Govor.Core.Repositories.Admins; +using Govor.Core.Repositories.Invaites; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -11,14 +13,22 @@ namespace Govor.API.Services.Authentication; public class JwtService : IJwtService { private JwtOption _jwtOption; + private IInvitesRepository _invitesRepository; public JwtService(IOptions options) { _jwtOption = options.Value; } + public string GenerateJwtToken(User user) { - Claim[] claims = [new("userID", user.Id.ToString())]; + var invite = _invitesRepository.GetByIdAsync(user.InviteId).Result; + + var claims = new[] + { + new Claim("userID", user.Id.ToString()), + new Claim(ClaimTypes.Role, invite.IsAdmin ? "Admin" : "User", ClaimValueTypes.String) + }; var singing = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOption.SecretKeу)), diff --git a/Govor.Console/Program.cs b/Govor.Console/Program.cs index 2b132ae..86f9486 100644 --- a/Govor.Console/Program.cs +++ b/Govor.Console/Program.cs @@ -74,7 +74,7 @@ class Program { try { - UserDto loginData = new UserDto() + RegistrationDto loginData = new RegistrationDto() { Name = username, Password = password diff --git a/Govor.Core/DTOs/UserDto.cs b/Govor.Core/DTOs/LoginDto.cs similarity index 94% rename from Govor.Core/DTOs/UserDto.cs rename to Govor.Core/DTOs/LoginDto.cs index d7c89a0..6ca8e67 100644 --- a/Govor.Core/DTOs/UserDto.cs +++ b/Govor.Core/DTOs/LoginDto.cs @@ -3,7 +3,7 @@ using Govor.Core.Infrastructure.Validators; namespace Govor.Core.DTOs; -public record UserDto +public class LoginDto { [Required] [StringLength(UserValidator.MAX_LENGHT_OF_NAME, diff --git a/Govor.Core/DTOs/RegistrationDto.cs b/Govor.Core/DTOs/RegistrationDto.cs new file mode 100644 index 0000000..4216c80 --- /dev/null +++ b/Govor.Core/DTOs/RegistrationDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Govor.Core.Infrastructure.Validators; + +namespace Govor.Core.DTOs; + +public record RegistrationDto +{ + [Required] + [StringLength(UserValidator.MAX_LENGHT_OF_NAME, + MinimumLength = UserValidator.MIN_LENGHT_OF_NAME, + ErrorMessage = "Username must be between 4 and 50 characters.")] + public string Name { get; init; } + [Required] + [MinLength(8)] + public string Password { get; init; } + [MinLength(8)] + public string InviteLink { get; init; } +} \ No newline at end of file diff --git a/Govor.Core/Models/Admin.cs b/Govor.Core/Models/Admin.cs new file mode 100644 index 0000000..a0ef8ca --- /dev/null +++ b/Govor.Core/Models/Admin.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Govor.Core.Models; + +public class Admin +{ + [Key] + public Guid UserId { get; set; } + public User User { get; set; } = null!; +} diff --git a/Govor.Core/Models/Invitation.cs b/Govor.Core/Models/Invitation.cs new file mode 100644 index 0000000..f04bf0c --- /dev/null +++ b/Govor.Core/Models/Invitation.cs @@ -0,0 +1,11 @@ +namespace Govor.Core.Models; + +public class Invitation +{ + public Guid Id { get; set; } + public bool IsAdmin { get; set; } + public DateTime DateCreated { get; set; } + public DateTime EndDate { get; set; } + public int MaxParticipants { get; set; } + public List Users { get; set; } = new List(); +} \ No newline at end of file diff --git a/Govor.Core/Models/User.cs b/Govor.Core/Models/User.cs index 762a4a7..56e441d 100644 --- a/Govor.Core/Models/User.cs +++ b/Govor.Core/Models/User.cs @@ -11,4 +11,6 @@ public class User public Guid IconId {get; set;} public DateOnly CreatedOn {get; set;} public DateTime WasOnline {get; set;} + public Guid InviteId {get; set;} + public Invitation? Invite { get; set; } } \ No newline at end of file diff --git a/Govor.Core/Repositories/Admins/IAdminsExist.cs b/Govor.Core/Repositories/Admins/IAdminsExist.cs new file mode 100644 index 0000000..c847400 --- /dev/null +++ b/Govor.Core/Repositories/Admins/IAdminsExist.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Admins; + +public interface IAdminsExist +{ + bool Exist(Guid guid); + bool Exist(Admin admin); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Admins/IAdminsReader.cs b/Govor.Core/Repositories/Admins/IAdminsReader.cs new file mode 100644 index 0000000..b5d01af --- /dev/null +++ b/Govor.Core/Repositories/Admins/IAdminsReader.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Admins; + +public interface IAdminsReader +{ + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Admins/IAdminsRepository.cs b/Govor.Core/Repositories/Admins/IAdminsRepository.cs new file mode 100644 index 0000000..09e9962 --- /dev/null +++ b/Govor.Core/Repositories/Admins/IAdminsRepository.cs @@ -0,0 +1,6 @@ +namespace Govor.Core.Repositories.Admins; + +public interface IAdminsRepository : IAdminsReader, IAdminsWriter, IAdminsExist +{ + +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Admins/IAdminsWriter.cs b/Govor.Core/Repositories/Admins/IAdminsWriter.cs new file mode 100644 index 0000000..2355013 --- /dev/null +++ b/Govor.Core/Repositories/Admins/IAdminsWriter.cs @@ -0,0 +1,10 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Admins; + +public interface IAdminsWriter +{ + Task AddAsync(Admin admin); + Task UpdateAsync(Admin admin); + Task DeleteAsync(Guid admin); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Invaites/IInvitesExist.cs b/Govor.Core/Repositories/Invaites/IInvitesExist.cs new file mode 100644 index 0000000..f5d747b --- /dev/null +++ b/Govor.Core/Repositories/Invaites/IInvitesExist.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Invaites; + +public interface IInvitesExist +{ + bool Exist(Invitation invitation); + bool Exist(Guid guid); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Invaites/IInvitesReader.cs b/Govor.Core/Repositories/Invaites/IInvitesReader.cs new file mode 100644 index 0000000..94e9493 --- /dev/null +++ b/Govor.Core/Repositories/Invaites/IInvitesReader.cs @@ -0,0 +1,11 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Invaites; + +public interface IInvitesReader +{ + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); + Task GetByCodeAsync(string code); + Task> GetAdminsInvitesAsync(); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Invaites/IInvitesRepository.cs b/Govor.Core/Repositories/Invaites/IInvitesRepository.cs new file mode 100644 index 0000000..832517c --- /dev/null +++ b/Govor.Core/Repositories/Invaites/IInvitesRepository.cs @@ -0,0 +1,6 @@ +namespace Govor.Core.Repositories.Invaites; + +public interface IInvitesRepository : IInvitesReader, IInvitesWriter, IInvitesExist +{ + +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Invaites/IInvitesWriter.cs b/Govor.Core/Repositories/Invaites/IInvitesWriter.cs new file mode 100644 index 0000000..b8f7487 --- /dev/null +++ b/Govor.Core/Repositories/Invaites/IInvitesWriter.cs @@ -0,0 +1,10 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Invaites; + +public interface IInvitesWriter +{ + Task AddAsync(Invitation invitation); + Task UpdateAsync(Invitation invitation); + Task RemoveAsync(Invitation invitation); +} \ No newline at end of file diff --git a/Govor.Data/Configurations/AdminConfiguration.cs b/Govor.Data/Configurations/AdminConfiguration.cs new file mode 100644 index 0000000..6d4190f --- /dev/null +++ b/Govor.Data/Configurations/AdminConfiguration.cs @@ -0,0 +1,17 @@ +using Govor.Core.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class AdminConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(a => a.UserId); + + builder.HasOne(a => a.User) + .WithOne() + .HasForeignKey(a => a.UserId); + } +} \ No newline at end of file diff --git a/Govor.Data/Configurations/InvitationConfiguration.cs b/Govor.Data/Configurations/InvitationConfiguration.cs new file mode 100644 index 0000000..540032a --- /dev/null +++ b/Govor.Data/Configurations/InvitationConfiguration.cs @@ -0,0 +1,18 @@ +using Govor.Core.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class InvitationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(i => i.Id); + + builder.HasMany(i => i.Users) + .WithOne(u => u.Invite) + .HasForeignKey(u => u.InviteId) + .OnDelete(DeleteBehavior.Cascade); + } +} \ No newline at end of file diff --git a/Govor.Data/Configurations/UserConfiguration.cs b/Govor.Data/Configurations/UserConfiguration.cs index 2f91c4d..205442d 100644 --- a/Govor.Data/Configurations/UserConfiguration.cs +++ b/Govor.Data/Configurations/UserConfiguration.cs @@ -9,5 +9,9 @@ public class UserConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + + builder.HasOne(u => u.Invite) + .WithMany(i => i.Users) + .HasForeignKey(u => u.InviteId); } } \ No newline at end of file diff --git a/Govor.Data/GovorDbContext.cs b/Govor.Data/GovorDbContext.cs index e8fec37..48a1bb5 100644 --- a/Govor.Data/GovorDbContext.cs +++ b/Govor.Data/GovorDbContext.cs @@ -8,6 +8,9 @@ namespace Govor.Data; public class GovorDbContext(DbContextOptions options) : DbContext(options) { public virtual DbSet Users { get; set; } + public virtual DbSet Admins { get; set; } + + public virtual DbSet Invitations { get; set; } public virtual DbSet Messages { get; set; } public virtual DbSet MessageViews { get; set; } @@ -20,6 +23,9 @@ public class GovorDbContext(DbContextOptions options) : DbContex protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.ApplyConfiguration(new UserConfiguration()); + modelBuilder.ApplyConfiguration(new InvitationConfiguration()); + modelBuilder.ApplyConfiguration(new AdminConfiguration()); modelBuilder.ApplyConfiguration(new MessagesConfiguration()); modelBuilder.ApplyConfiguration(new MessageReactionConfiguration()); modelBuilder.ApplyConfiguration(new MediaAttachmentsConfiguration()); diff --git a/Govor.Data/Migrations/20250624053837_FixAdminRelation.Designer.cs b/Govor.Data/Migrations/20250624053837_FixAdminRelation.Designer.cs new file mode 100644 index 0000000..c2d5107 --- /dev/null +++ b/Govor.Data/Migrations/20250624053837_FixAdminRelation.Designer.cs @@ -0,0 +1,331 @@ +// +using System; +using System.Collections.Generic; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250624053837_FixAdminRelation")] + partial class FixAdminRelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection>("Admins") + .IsRequired() + .HasColumnType("uuid[]"); + + b.PrimitiveCollection>("InviteCode") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("IsChannel") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("RecipientType") + .HasColumnType("integer"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReplyToMessageId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IconId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.Property("WasOnline") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.HasOne("Govor.Core.Models.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.HasOne("Govor.Core.Models.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250624053837_FixAdminRelation.cs b/Govor.Data/Migrations/20250624053837_FixAdminRelation.cs new file mode 100644 index 0000000..09d1e2a --- /dev/null +++ b/Govor.Data/Migrations/20250624053837_FixAdminRelation.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class FixAdminRelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Admins", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Admins", x => x.UserId); + table.ForeignKey( + name: "FK_Admins_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Admins"); + } + } +} diff --git a/Govor.Data/Migrations/20250624071942_inviteAdd.Designer.cs b/Govor.Data/Migrations/20250624071942_inviteAdd.Designer.cs new file mode 100644 index 0000000..4edcf15 --- /dev/null +++ b/Govor.Data/Migrations/20250624071942_inviteAdd.Designer.cs @@ -0,0 +1,375 @@ +// +using System; +using System.Collections.Generic; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250624071942_inviteAdd")] + partial class inviteAdd + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection>("Admins") + .IsRequired() + .HasColumnType("uuid[]"); + + b.PrimitiveCollection>("InviteCode") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("IsChannel") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("RecipientType") + .HasColumnType("integer"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReplyToMessageId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IconId") + .HasColumnType("uuid"); + + b.Property("InviteId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.Property("WasOnline") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.HasOne("Govor.Core.Models.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.HasOne("Govor.Core.Models.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250624071942_inviteAdd.cs b/Govor.Data/Migrations/20250624071942_inviteAdd.cs new file mode 100644 index 0000000..b29ffe9 --- /dev/null +++ b/Govor.Data/Migrations/20250624071942_inviteAdd.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class inviteAdd : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "InviteId", + table: "Users", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateTable( + name: "Invitations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + IsAdmin = table.Column(type: "boolean", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + MaxParticipants = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Invitations", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_InviteId", + table: "Users", + column: "InviteId"); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Invitations_InviteId", + table: "Users", + column: "InviteId", + principalTable: "Invitations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Users_Invitations_InviteId", + table: "Users"); + + migrationBuilder.DropTable( + name: "Invitations"); + + migrationBuilder.DropIndex( + name: "IX_Users_InviteId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "InviteId", + table: "Users"); + } + } +} diff --git a/Govor.Data/Migrations/20250624072330_FixinviteAdd.Designer.cs b/Govor.Data/Migrations/20250624072330_FixinviteAdd.Designer.cs new file mode 100644 index 0000000..72cedce --- /dev/null +++ b/Govor.Data/Migrations/20250624072330_FixinviteAdd.Designer.cs @@ -0,0 +1,375 @@ +// +using System; +using System.Collections.Generic; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250624072330_FixinviteAdd")] + partial class FixinviteAdd + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection>("Admins") + .IsRequired() + .HasColumnType("uuid[]"); + + b.PrimitiveCollection>("InviteCode") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("IsChannel") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("RecipientType") + .HasColumnType("integer"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReplyToMessageId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IconId") + .HasColumnType("uuid"); + + b.Property("InviteId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.Property("WasOnline") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.HasOne("Govor.Core.Models.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.HasOne("Govor.Core.Models.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250624072330_FixinviteAdd.cs b/Govor.Data/Migrations/20250624072330_FixinviteAdd.cs new file mode 100644 index 0000000..342b753 --- /dev/null +++ b/Govor.Data/Migrations/20250624072330_FixinviteAdd.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class FixinviteAdd : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Govor.Data/Migrations/20250624072548_InitialCreate.Designer.cs b/Govor.Data/Migrations/20250624072548_InitialCreate.Designer.cs new file mode 100644 index 0000000..f03fd47 --- /dev/null +++ b/Govor.Data/Migrations/20250624072548_InitialCreate.Designer.cs @@ -0,0 +1,375 @@ +// +using System; +using System.Collections.Generic; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250624072548_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection>("Admins") + .IsRequired() + .HasColumnType("uuid[]"); + + b.PrimitiveCollection>("InviteCode") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("IsChannel") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("RecipientType") + .HasColumnType("integer"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReplyToMessageId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IconId") + .HasColumnType("uuid"); + + b.Property("InviteId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.Property("WasOnline") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.HasOne("Govor.Core.Models.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.MessageView", b => + { + b.HasOne("Govor.Core.Models.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250624072548_InitialCreate.cs b/Govor.Data/Migrations/20250624072548_InitialCreate.cs new file mode 100644 index 0000000..5c791ea --- /dev/null +++ b/Govor.Data/Migrations/20250624072548_InitialCreate.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs index 3cd857e..b00839f 100644 --- a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs @@ -23,6 +23,16 @@ namespace Govor.Data.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => { b.Property("Id") @@ -89,6 +99,29 @@ namespace Govor.Data.Migrations b.ToTable("GroupMemberships"); }); + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => { b.Property("Id") @@ -228,6 +261,9 @@ namespace Govor.Data.Migrations b.Property("IconId") .HasColumnType("uuid"); + b.Property("InviteId") + .HasColumnType("uuid"); + b.Property("PasswordHash") .IsRequired() .HasColumnType("text"); @@ -241,9 +277,22 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("InviteId"); + b.ToTable("Users"); }); + modelBuilder.Entity("Govor.Core.Models.Admin", b => + { + b.HasOne("Govor.Core.Models.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => { b.HasOne("Govor.Core.Models.Message", "Message") @@ -293,6 +342,22 @@ namespace Govor.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("Govor.Core.Models.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + modelBuilder.Entity("Govor.Core.Models.Message", b => { b.Navigation("MediaAttachments"); diff --git a/Govor.Data/Repositories/AdminsRepository.cs b/Govor.Data/Repositories/AdminsRepository.cs new file mode 100644 index 0000000..7a1f7d6 --- /dev/null +++ b/Govor.Data/Repositories/AdminsRepository.cs @@ -0,0 +1,50 @@ +using Govor.Core.Infrastructure.Extensions; +using Govor.Core.Models; +using Govor.Core.Repositories.Admins; +using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace Govor.Data.Repositories; + +public class AdminsRepository(GovorDbContext context) : IAdminsRepository +{ + private GovorDbContext _context = context; + + public async Task> GetAllAsync() + { + return await _context.Admins + .AsNoTracking() + .Include(a => a.User) + .ToListOrThrowIfEmpty(new NotFoundException("Database is empty")); + } + + public Task GetByIdAsync(Guid id) + { + throw new NotImplementedException(); + } + + public Task AddAsync(Admin admin) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(Admin admin) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(Guid admin) + { + throw new NotImplementedException(); + } + + public bool Exist(Guid guid) + { + throw new NotImplementedException(); + } + + public bool Exist(Admin admin) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.Data/Repositories/InvitesRepository.cs b/Govor.Data/Repositories/InvitesRepository.cs new file mode 100644 index 0000000..0c7cc57 --- /dev/null +++ b/Govor.Data/Repositories/InvitesRepository.cs @@ -0,0 +1,52 @@ +using Govor.Core.Models; +using Govor.Core.Repositories.Invaites; + +namespace Govor.Data.Repositories; + +public class InvitesRepository : IInvitesRepository +{ + public Task> GetAllAsync() + { + throw new NotImplementedException(); + } + + public Task GetByIdAsync(Guid id) + { + throw new NotImplementedException(); + } + + public Task GetByCodeAsync(string code) + { + throw new NotImplementedException(); + } + + public Task> GetAdminsInvitesAsync() + { + throw new NotImplementedException(); + } + + public Task AddAsync(Invitation invitation) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(Invitation invitation) + { + throw new NotImplementedException(); + } + + public Task RemoveAsync(Invitation invitation) + { + throw new NotImplementedException(); + } + + public bool Exist(Invitation invitation) + { + throw new NotImplementedException(); + } + + public bool Exist(Guid guid) + { + throw new NotImplementedException(); + } +} \ No newline at end of file