diff --git a/src/tvnoms-server/TvNoms.ApiService/Endpoints/UserEndpoints.cs b/src/tvnoms-server/TvNoms.ApiService/Endpoints/UserEndpoints.cs index db25ce3..fbec241 100644 --- a/src/tvnoms-server/TvNoms.ApiService/Endpoints/UserEndpoints.cs +++ b/src/tvnoms-server/TvNoms.ApiService/Endpoints/UserEndpoints.cs @@ -112,12 +112,10 @@ public class UserEndpoints : Shared.Endpoints { provider = provider.Pascalize(); - var allowedOrigins = configuration.GetSection("AllowedOrigins")?.Get() ?? Array.Empty(); + var allowedOrigins = configuration.GetSection("AllowedOrigins").Get() ?? Array.Empty(); - if (!allowedOrigins.Any(origin => Uri.Compare( - new Uri(origin, UriKind.Absolute), - new Uri(origin), UriComponents.SchemeAndServer, UriFormat.UriEscaped, - StringComparison.OrdinalIgnoreCase) == 0)) + if (allowedOrigins.All(origin => Uri.Compare(new Uri(origin, UriKind.Absolute), new Uri(origin), + UriComponents.SchemeAndServer, UriFormat.UriEscaped, StringComparison.OrdinalIgnoreCase) != 0)) throw new BadRequestException(nameof(returnUrl), $"'{nameof(returnUrl)}' is not allowed."); // Request a redirect to the external sign-in provider. diff --git a/src/tvnoms-server/TvNoms.ApiService/Program.cs b/src/tvnoms-server/TvNoms.ApiService/Program.cs index a4cfbf4..f654a95 100644 --- a/src/tvnoms-server/TvNoms.ApiService/Program.cs +++ b/src/tvnoms-server/TvNoms.ApiService/Program.cs @@ -8,12 +8,18 @@ using Microsoft.EntityFrameworkCore; using TvNoms.Server.ApiService.Shared; using TvNoms.Server.Data; using TvNoms.Server.Data.Models; +using TvNoms.Server.Services.Data.Models; +using TvNoms.Server.Services.Data.Repositories; using TvNoms.Server.Services.Identity; using TvNoms.Server.Services.Infrastructure.Messaging; using TvNoms.Server.Services.Infrastructure.Messaging.Email; using TvNoms.Server.Services.Infrastructure.Messaging.SMS; +using TvNoms.Server.Services.Infrastructure.Storage; +using TvNoms.Server.Services.Utilities; +using TvNoms.Server.Services.ViewRenderer.Razor; var builder = WebApplication.CreateBuilder(args); +var assemblies = AssemblyHelper.GetAssemblies().ToArray(); // Add service defaults & Aspire components. builder.AddServiceDefaults(); @@ -45,6 +51,11 @@ builder.Services.AddDbContext(options => { options.UseNpgsql(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(typeof(AppDbContext).Assembly.GetName().Name)); }); +builder.Services.AddAutoMapper(assemblies); +builder.Services.AddMediatR(options => { options.RegisterServicesFromAssemblies(assemblies); }); +builder.Services.AddRepositories(assemblies); +builder.Services.AddValidators(assemblies); + builder.Services.AddIdentity(options => { // Password settings. (Will be using fluent validation) options.Password.RequireDigit = false; @@ -94,18 +105,26 @@ builder.Services.AddAuthentication(options => { options.SignInScheme = IdentityConstants.ExternalScheme; builder.Configuration.GetRequiredSection("GoogleAuthOptions").Bind(options); }); - +builder.Services.AddModelBuilder(); builder.Services.AddAuthorization(); builder.Services.AddMailgunEmailSender(options => { builder.Configuration.GetRequiredSection("MailgunEmailOptions").Bind(options); }); builder.Services.AddFakeSmsSender(); +builder.Services.AddRazorViewRenderer(); + +builder.Services.AddLocalFileStorage(options => { + options.RootPath = Path.Combine(builder.Environment.WebRootPath, "uploads"); + options.WebRootPath = "/uploads"; +}); builder.Services.AddDocumentations(); +builder.Services.AddWebAppCors(); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseExceptionHandler(); +app.UseCors("WebAppPolicy"); app.MapGet("/ping", () => "pong"); diff --git a/src/tvnoms-server/TvNoms.ApiService/TvNoms.ApiService.csproj b/src/tvnoms-server/TvNoms.ApiService/TvNoms.ApiService.csproj index 080eb6b..dff7864 100644 --- a/src/tvnoms-server/TvNoms.ApiService/TvNoms.ApiService.csproj +++ b/src/tvnoms-server/TvNoms.ApiService/TvNoms.ApiService.csproj @@ -17,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/tvnoms-server/TvNoms.ApiService/appsettings.Development.json b/src/tvnoms-server/TvNoms.ApiService/appsettings.Development.json index 0923f90..d6c1dc7 100644 --- a/src/tvnoms-server/TvNoms.ApiService/appsettings.Development.json +++ b/src/tvnoms-server/TvNoms.ApiService/appsettings.Development.json @@ -16,6 +16,10 @@ "GoogleAuthOptions": { "ClientId": "547087534129-35mk786fqjgglbucjmroq9q6kgo0tvdl.apps.googleusercontent.com" }, + "AllowedHosts": "*", + "AllowedOrigins": [ + "https://tvnoms.dev.fergl.ie:3000" + ], "BearerAuthOptions": { "Secret": null, "Issuer": null, diff --git a/src/tvnoms-server/TvNoms.Data/AppDbContext.cs b/src/tvnoms-server/TvNoms.Data/AppDbContext.cs index 819ea29..1de7c23 100644 --- a/src/tvnoms-server/TvNoms.Data/AppDbContext.cs +++ b/src/tvnoms-server/TvNoms.Data/AppDbContext.cs @@ -4,16 +4,14 @@ using TvNoms.Server.Data.Models; namespace TvNoms.Server.Data; -public class AppDbContext : DbContext { - private readonly IConfiguration _configuration; +public class AppDbContext(IConfiguration configuration) : DbContext { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => + optionsBuilder.UseNpgsql(configuration.GetConnectionString("DefaultConnection")); - public AppDbContext(IConfiguration configuration) { - _configuration = configuration; - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(_configuration.GetConnectionString("DefaultConnection")); + public DbSet Users { get; set; } + public DbSet Clients { get; set; } public DbSet Shows { get; set; } public DbSet Movies { get; set; } + public DbSet Media { get; set; } } diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.Designer.cs b/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.Designer.cs deleted file mode 100644 index 3edda48..0000000 --- a/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TvNoms.Server.Data; - -#nullable disable - -namespace TvNoms.Server.Data.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20240314203159_Initial")] - partial class Initial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TvNoms.Server.Data.Models.Movie", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Movies"); - }); - - modelBuilder.Entity("TvNoms.Server.Data.Models.Show", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Shows"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.cs b/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.cs deleted file mode 100644 index 3171187..0000000 --- a/src/tvnoms-server/TvNoms.Data/Migrations/20240314203159_Initial.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TvNoms.Server.Data.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Movies", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "text", nullable: false), - DateCreated = table.Column(type: "timestamp with time zone", nullable: false), - DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Movies", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Shows", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "text", nullable: false), - DateCreated = table.Column(type: "timestamp with time zone", nullable: false), - DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Shows", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Movies"); - - migrationBuilder.DropTable( - name: "Shows"); - } - } -} diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.Designer.cs b/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.Designer.cs new file mode 100644 index 0000000..90be0cd --- /dev/null +++ b/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.Designer.cs @@ -0,0 +1,342 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TvNoms.Server.Data; + +#nullable disable + +namespace TvNoms.Server.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20240322175607_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("IpAddress") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AvatarId") + .HasColumnType("uuid"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailRequired") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastActiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Location") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PhoneNumberRequired") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AvatarId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("UserRole"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Client", b => + { + b.HasOne("TvNoms.Server.Data.Models.User", "User") + .WithMany("Clients") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.HasOne("TvNoms.Server.Data.Models.Media", "Avatar") + .WithMany() + .HasForeignKey("AvatarId"); + + b.Navigation("Avatar"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.UserRole", b => + { + b.HasOne("TvNoms.Server.Data.Models.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TvNoms.Server.Data.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.Navigation("Clients"); + + b.Navigation("Roles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.cs b/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.cs new file mode 100644 index 0000000..f274f70 --- /dev/null +++ b/src/tvnoms-server/TvNoms.Data/Migrations/20240322175607_Initial.cs @@ -0,0 +1,213 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TvNoms.Server.Data.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Media", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Size = table.Column(type: "bigint", nullable: false), + Path = table.Column(type: "text", nullable: false), + ContentType = table.Column(type: "text", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Width = table.Column(type: "integer", nullable: true), + Height = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Media", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Role", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: true), + NormalizedName = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Role", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Shows", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shows", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + AvatarId = table.Column(type: "uuid", nullable: true), + Bio = table.Column(type: "text", nullable: true), + Location = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false), + LastActiveAt = table.Column(type: "timestamp with time zone", nullable: false), + EmailRequired = table.Column(type: "boolean", nullable: false), + PhoneNumberRequired = table.Column(type: "boolean", nullable: false), + UserName = table.Column(type: "text", nullable: true), + NormalizedUserName = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + NormalizedEmail = table.Column(type: "text", nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Media_AvatarId", + column: x => x.AvatarId, + principalTable: "Media", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Clients", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ConnectionId = table.Column(type: "text", nullable: false), + ConnectionTime = table.Column(type: "timestamp with time zone", nullable: false), + IpAddress = table.Column(type: "text", nullable: true), + DeviceId = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: true), + UserAgent = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Clients", x => x.Id); + table.ForeignKey( + name: "FK_Clients_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "UserRole", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRole", x => x.Id); + table.ForeignKey( + name: "FK_UserRole_Role_RoleId", + column: x => x.RoleId, + principalTable: "Role", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRole_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Clients_UserId", + table: "Clients", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRole_RoleId", + table: "UserRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRole_UserId", + table: "UserRole", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_AvatarId", + table: "Users", + column: "AvatarId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Clients"); + + migrationBuilder.DropTable( + name: "Movies"); + + migrationBuilder.DropTable( + name: "Shows"); + + migrationBuilder.DropTable( + name: "UserRole"); + + migrationBuilder.DropTable( + name: "Role"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Media"); + } + } +} diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/AppDbContextModelSnapshot.cs b/src/tvnoms-server/TvNoms.Data/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..03595f9 --- /dev/null +++ b/src/tvnoms-server/TvNoms.Data/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,339 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TvNoms.Server.Data; + +#nullable disable + +namespace TvNoms.Server.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("IpAddress") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AvatarId") + .HasColumnType("uuid"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailRequired") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastActiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Location") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PhoneNumberRequired") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AvatarId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("UserRole"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Client", b => + { + b.HasOne("TvNoms.Server.Data.Models.User", "User") + .WithMany("Clients") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.HasOne("TvNoms.Server.Data.Models.Media", "Avatar") + .WithMany() + .HasForeignKey("AvatarId"); + + b.Navigation("Avatar"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.UserRole", b => + { + b.HasOne("TvNoms.Server.Data.Models.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TvNoms.Server.Data.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("TvNoms.Server.Data.Models.User", b => + { + b.Navigation("Clients"); + + b.Navigation("Roles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/tvnoms-server/TvNoms.Data/Migrations/TvNomsContextModelSnapshot.cs b/src/tvnoms-server/TvNoms.Data/Migrations/TvNomsContextModelSnapshot.cs deleted file mode 100644 index 4d4db26..0000000 --- a/src/tvnoms-server/TvNoms.Data/Migrations/TvNomsContextModelSnapshot.cs +++ /dev/null @@ -1,69 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TvNoms.Server.Data; - -#nullable disable - -namespace TvNoms.Server.Data.Migrations -{ - [DbContext(typeof(AppDbContext))] - partial class TvNomsContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TvNoms.Server.Data.Models.Movie", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Movies"); - }); - - modelBuilder.Entity("TvNoms.Server.Data.Models.Show", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Shows"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/tvnoms-server/TvNoms.Data/Models/User.cs b/src/tvnoms-server/TvNoms.Data/Models/User.cs index c052a65..a69de26 100644 --- a/src/tvnoms-server/TvNoms.Data/Models/User.cs +++ b/src/tvnoms-server/TvNoms.Data/Models/User.cs @@ -20,26 +20,22 @@ public class User : IdentityUser, IEntity { } public class UserRole : IdentityUserRole, IEntity { + public Guid Id { get; set; } + public virtual User User { get; set; } = default!; - public virtual Role Role { get; set; } = default!; - - Guid IEntity.Id { get; set; } } public class UserSession : IEntity { - public virtual User User { get; set; } = default!; - - public Guid UserId { get; set; } - public Guid Id { get; set; } + public virtual User User { get; set; } = default!; + public Guid UserId { get; set; } + + public string AccessTokenHash { get; set; } = default!; - public DateTimeOffset AccessTokenExpiresAt { get; set; } - public string RefreshTokenHash { get; set; } = default!; - public DateTimeOffset RefreshTokenExpiresAt { get; set; } } diff --git a/src/tvnoms-server/TvNoms.Services/Data/Repositories/ServiceCollectionExtensions.cs b/src/tvnoms-server/TvNoms.Services/Data/Repositories/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..997ea50 --- /dev/null +++ b/src/tvnoms-server/TvNoms.Services/Data/Repositories/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using TvNoms.Server.Services.Utilities; + +namespace TvNoms.Server.Services.Data.Repositories; + +public static class ServiceCollectionExtensions { + public static IServiceCollection AddRepositories(this IServiceCollection services, IEnumerable assemblies) { + var repositoryTypes = assemblies.SelectMany(_ => _.DefinedTypes).Select(_ => _.AsType()) + .Where(type => type is { IsClass: true, IsAbstract: false } && type.IsCompatibleWith(typeof(IRepository<>))); + + foreach (var concreteType in repositoryTypes) { + var matchingInterfaceType = concreteType.GetInterfaces() + .FirstOrDefault(x => string.Equals(x.Name, $"I{concreteType.Name}", StringComparison.Ordinal)); + + if (matchingInterfaceType != null) { + services.AddScoped(matchingInterfaceType, concreteType); + } + } + + return services; + } +} diff --git a/src/tvnoms-server/TvNoms.Services/Identity/ServiceCollectionExtensions.cs b/src/tvnoms-server/TvNoms.Services/Identity/ServiceCollectionExtensions.cs index 71b9ff3..79dcb1b 100644 --- a/src/tvnoms-server/TvNoms.Services/Identity/ServiceCollectionExtensions.cs +++ b/src/tvnoms-server/TvNoms.Services/Identity/ServiceCollectionExtensions.cs @@ -8,6 +8,19 @@ using TvNoms.Server.Services.Data.Services; namespace TvNoms.Server.Services.Identity; public static class ServiceCollectionExtensions { + public static IServiceCollection AddWebAppCors(this IServiceCollection services) { + services.AddCors(options => { + options.AddPolicy("WebAppPolicy", builder => { + builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .WithOrigins("https://tvnoms.dev.fergl.ie:3000/"); + }); + }); + return services; + } + public static AuthenticationBuilder AddBearer(this AuthenticationBuilder builder, Action options) { builder.Services.AddOptions().Configure( diff --git a/src/tvnoms-server/scripts/reset.sh b/src/tvnoms-server/scripts/reset.sh new file mode 100755 index 0000000..ad98335 --- /dev/null +++ b/src/tvnoms-server/scripts/reset.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +rm -rfv ./TvNoms.Data/Migrations + +dotnet ef database drop --force --startup-project ./TvNoms.ApiService/TvNoms.ApiService.csproj --project ./TvNoms.Data/TvNoms.Data.csproj +dotnet ef migrations add "Initial" --startup-project ./TvNoms.ApiService/TvNoms.ApiService.csproj --project ./TvNoms.Data/TvNoms.Data.csproj +dotnet ef database update --startup-project ./TvNoms.ApiService/TvNoms.ApiService.csproj --project ./TvNoms.Data/TvNoms.Data.csproj