diff --git a/Server/Settings/Jwt.cs b/Server/Configurations/Jwt.cs similarity index 89% rename from Server/Settings/Jwt.cs rename to Server/Configurations/Jwt.cs index d22de73..ca85f88 100644 --- a/Server/Settings/Jwt.cs +++ b/Server/Configurations/Jwt.cs @@ -1,4 +1,4 @@ -namespace Server.Settings; +namespace Server.Configurations; public class Jwt { diff --git a/Server/Configurations/MapperInitializer.cs b/Server/Configurations/MapperInitializer.cs new file mode 100644 index 0000000..84b7433 --- /dev/null +++ b/Server/Configurations/MapperInitializer.cs @@ -0,0 +1,31 @@ +using AutoMapper; +using Server.Models; +using SharedModels.DataTransferObjects; +using Route = Server.Models.Route; + +namespace Server.Configurations; + +public class MapperInitializer : Profile +{ + public MapperInitializer() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/Server/Controllers/AuthenticationController.cs b/Server/Controllers/AuthenticationController.cs index 2686c59..2f25cd9 100644 --- a/Server/Controllers/AuthenticationController.cs +++ b/Server/Controllers/AuthenticationController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Server.Configurations; using Server.Services; -using Server.Settings; using SharedModels.Requests; using SharedModels.Responses; diff --git a/Server/Controllers/CountryManagementController.cs b/Server/Controllers/CountryManagementController.cs new file mode 100644 index 0000000..e38177f --- /dev/null +++ b/Server/Controllers/CountryManagementController.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Mvc; +using Server.Data; +using Server.Services; +using SharedModels.DataTransferObjects; + +namespace Server.Controllers; + +[Route("api/countries")] +[ApiController] +public class CountryManagementController : ControllerBase +{ + private readonly ICountryManagementService _countryManagementService; + + public CountryManagementController(ICountryManagementService countryManagementService) + { + _countryManagementService = countryManagementService; + } + + [HttpPost] + public async Task AddCountry(CreateCountryDto country) + { + var result = await _countryManagementService.AddCountry(country); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return CreatedAtAction(nameof(GetCountry), new {id = result.country.Id}, result.country); + } + + [HttpGet] + public async Task GetCountries() + { + var result = await _countryManagementService.GetCountries(); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result.countries); + } + + [HttpGet("{id}")] + public async Task GetCountry(int id) + { + if (!await _countryManagementService.IsCountryExists(id)) + { + return NotFound(); + } + + var result = await _countryManagementService.GetCountry(id); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result.country); + } + + [HttpPut("{id}")] + public async Task UpdateRoute(int id, UpdateCountryDto country) + { + if (id != country.Id) + { + return BadRequest(); + } + + var result = await _countryManagementService.UpdateCountry(country); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task DeleteRoute(int id) + { + if (!await _countryManagementService.IsCountryExists(id)) + { + return NotFound(); + } + + var result = await _countryManagementService.DeleteCountry(id); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return NoContent(); + } +} diff --git a/Server/Controllers/SecuredController.cs b/Server/Controllers/SecuredController.cs deleted file mode 100644 index 3b369c5..0000000 --- a/Server/Controllers/SecuredController.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using SharedModels.Responses; - -namespace Server.Controllers; - -[Authorize(Roles = "Admin")] -[Route("api/secured")] -[ApiController] -public class SecuredController : ControllerBase -{ - [HttpGet] - public async Task GetSecuredData() - { - return Ok(new ResponseBase - { - Message = "This Secured Data is available only for Authenticated Users with Admin role." - }); - } -} \ No newline at end of file diff --git a/Server/Controllers/TestingController.cs b/Server/Controllers/TestingController.cs new file mode 100644 index 0000000..06595cb --- /dev/null +++ b/Server/Controllers/TestingController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Server.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class TestingController : ControllerBase +{ + [HttpPost("timezone")] + public async Task SetTimeZone(string timeZone) + { + var cookieOptions = new CookieOptions() + { + Expires = DateTimeOffset.MaxValue + }; + Response.Cookies.Append("timeZone", timeZone, cookieOptions); + return Ok(); + } + + [HttpGet("timezone")] + public async Task GetTimeZone() + { + if (Request.Cookies.TryGetValue("timeZone", out string? tz)) + { + return Ok(tz); + } + + return NotFound(); + } +} + diff --git a/Server/Data/ApplicationDbContext.cs b/Server/Data/ApplicationDbContext.cs index 36e4220..68b0a40 100644 --- a/Server/Data/ApplicationDbContext.cs +++ b/Server/Data/ApplicationDbContext.cs @@ -1,13 +1,37 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Server.Models; +using Route = Server.Models.Route; namespace Server.Data; -public class ApplicationDbContext : IdentityDbContext +public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } + + public DbSet Companies { get; set; } + public DbSet Vehicles { get; set; } + public DbSet VehicleEnrollments { get; set; } + public DbSet Routes { get; set; } + public DbSet RouteAddresses { get; set; } + public DbSet
Addresses { get; set; } + public DbSet Cities { get; set; } + public DbSet States { get; set; } + public DbSet Countries { get; set; } + + public DbSet Tickets { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(ra => new {ra.RouteId, ra.AddressId}); + + modelBuilder.Entity() + .HasKey(t => new {t.UserId, t.VehicleEnrollmentId}); + } } \ No newline at end of file diff --git a/Server/Data/ApplicationDbContextSeed.cs b/Server/Data/ApplicationDbContextSeed.cs index 12697bb..3ce011b 100644 --- a/Server/Data/ApplicationDbContextSeed.cs +++ b/Server/Data/ApplicationDbContextSeed.cs @@ -6,7 +6,7 @@ namespace Server.Data; public class ApplicationDbContextSeed { - public static async Task SeedEssentialsAsync(UserManager userManager, + public static async Task SeedEssentialsAsync(UserManager userManager, RoleManager roleManager) { //Seed Roles @@ -14,7 +14,7 @@ public class ApplicationDbContextSeed await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.User.ToString())); //Seed Default User - var defaultUser = new ApplicationUser + var defaultUser = new User { UserName = Authorization.DefaultUsername, Email = Authorization.DefaultEmail, diff --git a/Server/Migrations/20221010172816_Main_Infrastructure.Designer.cs b/Server/Migrations/20221010172816_Main_Infrastructure.Designer.cs new file mode 100644 index 0000000..9af5ea1 --- /dev/null +++ b/Server/Migrations/20221010172816_Main_Infrastructure.Designer.cs @@ -0,0 +1,703 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221010172816_Main_Infrastructure")] + partial class Main_Infrastructure + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StateId"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Countries"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IntendedDepartureTimeOnlyUtc") + .HasColumnType("time without time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.Property("RouteId") + .HasColumnType("integer"); + + b.Property("AddressId") + .HasColumnType("integer"); + + b.Property("CostToNextCity") + .HasColumnType("double precision"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TimeSpanToNextCity") + .HasColumnType("interval"); + + b.Property("WaitTimeSpan") + .HasColumnType("interval"); + + b.HasKey("RouteId", "AddressId"); + + b.HasIndex("AddressId"); + + b.ToTable("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.ToTable("States"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.Property("IsReturned") + .HasColumnType("boolean"); + + b.Property("PurchaseDateTimeUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "VehicleEnrollmentId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CompanyId") + .HasColumnType("integer"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTimeSpan") + .HasColumnType("interval"); + + b.Property("DepartureDateOnly") + .HasColumnType("date"); + + b.Property("RouteId") + .HasColumnType("integer"); + + b.Property("VehicleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RouteId"); + + b.HasIndex("VehicleId"); + + b.ToTable("VehicleEnrollments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.HasOne("Server.Models.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.HasOne("Server.Models.State", "State") + .WithMany("Cities") + .HasForeignKey("StateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.HasOne("Server.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.HasOne("Server.Models.Address", "Address") + .WithMany("RouteAddresses") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.HasOne("Server.Models.Country", "Country") + .WithMany("States") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.HasOne("Server.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany() + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("UserId") + .HasColumnType("text"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Created") + .HasColumnType("timestamp with time zone"); + + b1.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b1.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("UserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.HasOne("Server.Models.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.HasOne("Server.Models.Route", "Route") + .WithMany() + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.Navigation("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Navigation("States"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Navigation("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Navigation("Cities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20221010172816_Main_Infrastructure.cs b/Server/Migrations/20221010172816_Main_Infrastructure.cs new file mode 100644 index 0000000..77c9e8b --- /dev/null +++ b/Server/Migrations/20221010172816_Main_Infrastructure.cs @@ -0,0 +1,340 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + public partial class Main_Infrastructure : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + table: "RefreshToken"); + + migrationBuilder.RenameColumn( + name: "ApplicationUserId", + table: "RefreshToken", + newName: "UserId"); + + migrationBuilder.CreateTable( + name: "Companies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + OwnerId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Companies", x => x.Id); + table.ForeignKey( + name: "FK_Companies_AspNetUsers_OwnerId", + column: x => x.OwnerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Countries", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Countries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Routes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Type = table.Column(type: "text", nullable: false), + IntendedDepartureTimeOnlyUtc = table.Column(type: "time without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Routes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Vehicles", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Number = table.Column(type: "text", nullable: false), + Type = table.Column(type: "text", nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + CompanyId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Vehicles", x => x.Id); + table.ForeignKey( + name: "FK_Vehicles_Companies_CompanyId", + column: x => x.CompanyId, + principalTable: "Companies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "States", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + CountryId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_States", x => x.Id); + table.ForeignKey( + name: "FK_States_Countries_CountryId", + column: x => x.CountryId, + principalTable: "Countries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "VehicleEnrollments", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + VehicleId = table.Column(type: "integer", nullable: false), + RouteId = table.Column(type: "integer", nullable: false), + DepartureDateOnly = table.Column(type: "date", nullable: false), + DelayTimeSpan = table.Column(type: "interval", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VehicleEnrollments", x => x.Id); + table.ForeignKey( + name: "FK_VehicleEnrollments_Routes_RouteId", + column: x => x.RouteId, + principalTable: "Routes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_VehicleEnrollments_Vehicles_VehicleId", + column: x => x.VehicleId, + principalTable: "Vehicles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Cities", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + StateId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Cities", x => x.Id); + table.ForeignKey( + name: "FK_Cities_States_StateId", + column: x => x.StateId, + principalTable: "States", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + VehicleEnrollmentId = table.Column(type: "integer", nullable: false), + PurchaseDateTimeUtc = table.Column(type: "timestamp with time zone", nullable: false), + IsReturned = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => new { x.UserId, x.VehicleEnrollmentId }); + table.ForeignKey( + name: "FK_Tickets_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_VehicleEnrollments_VehicleEnrollmentId", + column: x => x.VehicleEnrollmentId, + principalTable: "VehicleEnrollments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Latitude = table.Column(type: "double precision", nullable: false), + Longitude = table.Column(type: "double precision", nullable: false), + CityId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_Cities_CityId", + column: x => x.CityId, + principalTable: "Cities", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RouteAddresses", + columns: table => new + { + RouteId = table.Column(type: "integer", nullable: false), + AddressId = table.Column(type: "integer", nullable: false), + Order = table.Column(type: "integer", nullable: false), + TimeSpanToNextCity = table.Column(type: "interval", nullable: false), + WaitTimeSpan = table.Column(type: "interval", nullable: false), + CostToNextCity = table.Column(type: "double precision", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RouteAddresses", x => new { x.RouteId, x.AddressId }); + table.ForeignKey( + name: "FK_RouteAddresses_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RouteAddresses_Routes_RouteId", + column: x => x.RouteId, + principalTable: "Routes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CityId", + table: "Addresses", + column: "CityId"); + + migrationBuilder.CreateIndex( + name: "IX_Cities_StateId", + table: "Cities", + column: "StateId"); + + migrationBuilder.CreateIndex( + name: "IX_Companies_OwnerId", + table: "Companies", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_RouteAddresses_AddressId", + table: "RouteAddresses", + column: "AddressId"); + + migrationBuilder.CreateIndex( + name: "IX_States_CountryId", + table: "States", + column: "CountryId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_VehicleEnrollmentId", + table: "Tickets", + column: "VehicleEnrollmentId"); + + migrationBuilder.CreateIndex( + name: "IX_VehicleEnrollments_RouteId", + table: "VehicleEnrollments", + column: "RouteId"); + + migrationBuilder.CreateIndex( + name: "IX_VehicleEnrollments_VehicleId", + table: "VehicleEnrollments", + column: "VehicleId"); + + migrationBuilder.CreateIndex( + name: "IX_Vehicles_CompanyId", + table: "Vehicles", + column: "CompanyId"); + + migrationBuilder.AddForeignKey( + name: "FK_RefreshToken_AspNetUsers_UserId", + table: "RefreshToken", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_RefreshToken_AspNetUsers_UserId", + table: "RefreshToken"); + + migrationBuilder.DropTable( + name: "RouteAddresses"); + + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "VehicleEnrollments"); + + migrationBuilder.DropTable( + name: "Cities"); + + migrationBuilder.DropTable( + name: "Routes"); + + migrationBuilder.DropTable( + name: "Vehicles"); + + migrationBuilder.DropTable( + name: "States"); + + migrationBuilder.DropTable( + name: "Companies"); + + migrationBuilder.DropTable( + name: "Countries"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "RefreshToken", + newName: "ApplicationUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + table: "RefreshToken", + column: "ApplicationUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Server/Migrations/ApplicationDbContextModelSnapshot.cs b/Server/Migrations/ApplicationDbContextModelSnapshot.cs index b64ef52..1ffb312 100644 --- a/Server/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Server/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Server.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -154,7 +154,191 @@ namespace Server.Migrations b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Server.Models.ApplicationUser", b => + modelBuilder.Entity("Server.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StateId"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Countries"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IntendedDepartureTimeOnlyUtc") + .HasColumnType("time without time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.Property("RouteId") + .HasColumnType("integer"); + + b.Property("AddressId") + .HasColumnType("integer"); + + b.Property("CostToNextCity") + .HasColumnType("double precision"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TimeSpanToNextCity") + .HasColumnType("interval"); + + b.Property("WaitTimeSpan") + .HasColumnType("interval"); + + b.HasKey("RouteId", "AddressId"); + + b.HasIndex("AddressId"); + + b.ToTable("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.ToTable("States"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.Property("IsReturned") + .HasColumnType("boolean"); + + b.Property("PurchaseDateTimeUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "VehicleEnrollmentId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Server.Models.User", b => { b.Property("Id") .HasColumnType("text"); @@ -224,6 +408,64 @@ namespace Server.Migrations b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CompanyId") + .HasColumnType("integer"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTimeSpan") + .HasColumnType("interval"); + + b.Property("DepartureDateOnly") + .HasColumnType("date"); + + b.Property("RouteId") + .HasColumnType("integer"); + + b.Property("VehicleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RouteId"); + + b.HasIndex("VehicleId"); + + b.ToTable("VehicleEnrollments"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -235,7 +477,7 @@ namespace Server.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Server.Models.ApplicationUser", null) + b.HasOne("Server.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -244,7 +486,7 @@ namespace Server.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Server.Models.ApplicationUser", null) + b.HasOne("Server.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -259,7 +501,7 @@ namespace Server.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Server.Models.ApplicationUser", null) + b.HasOne("Server.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -268,18 +510,100 @@ namespace Server.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Server.Models.ApplicationUser", null) + b.HasOne("Server.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Server.Models.ApplicationUser", b => + modelBuilder.Entity("Server.Models.Address", b => { - b.OwnsMany("Server.Entities.RefreshToken", "RefreshTokens", b1 => + b.HasOne("Server.Models.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.HasOne("Server.Models.State", "State") + .WithMany("Cities") + .HasForeignKey("StateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.HasOne("Server.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.HasOne("Server.Models.Address", "Address") + .WithMany("RouteAddresses") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.HasOne("Server.Models.Country", "Country") + .WithMany("States") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.HasOne("Server.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany() + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 => { - b1.Property("ApplicationUserId") + b1.Property("UserId") .HasColumnType("text"); b1.Property("Id") @@ -301,16 +625,76 @@ namespace Server.Migrations .IsRequired() .HasColumnType("text"); - b1.HasKey("ApplicationUserId", "Id"); + b1.HasKey("UserId", "Id"); b1.ToTable("RefreshToken"); b1.WithOwner() - .HasForeignKey("ApplicationUserId"); + .HasForeignKey("UserId"); }); b.Navigation("RefreshTokens"); }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.HasOne("Server.Models.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.HasOne("Server.Models.Route", "Route") + .WithMany() + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.Navigation("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Navigation("States"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Navigation("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Navigation("Cities"); + }); #pragma warning restore 612, 618 } } diff --git a/Server/Models/Address.cs b/Server/Models/Address.cs new file mode 100644 index 0000000..7f003a5 --- /dev/null +++ b/Server/Models/Address.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class Address +{ + [Key] + public int Id { get; set; } + + public string Name { get; set; } = null!; + public double Latitude { get; set; } + public double Longitude { get; set; } + + [ForeignKey("CityId")] + public int CityId { get; set; } + public City? City { get; set; } + + public virtual IList RouteAddresses { get; set; } = null!; + + public override string ToString() + { + return $"{City.State.Country.Name}, {City.State.Name}, " + + $"{City.Name}, {this.Name}"; + } +} \ No newline at end of file diff --git a/Server/Models/City.cs b/Server/Models/City.cs new file mode 100644 index 0000000..1927105 --- /dev/null +++ b/Server/Models/City.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class City +{ + [Key] + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public virtual IList
? Addresses { get; set; } + + [ForeignKey("StateId")] + public int StateId { get; set; } + public State? State { get; set; } +} \ No newline at end of file diff --git a/Server/Models/Company.cs b/Server/Models/Company.cs new file mode 100644 index 0000000..204731b --- /dev/null +++ b/Server/Models/Company.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Models; + +public class Company +{ + [Key] + public int Id { get; set; } + + public string Name { get; set; } = null!; + + [ForeignKey("OwnerId")] + public string OwnerId { get; set; } = null!; + public User Owner { get; set; } = null!; + + public List? Vehicles { get; set; } +} \ No newline at end of file diff --git a/Server/Models/Country.cs b/Server/Models/Country.cs new file mode 100644 index 0000000..77b533b --- /dev/null +++ b/Server/Models/Country.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class Country +{ + [Key] + public int Id { get; set; } + + public string Code { get; set; } = null!; + public string Name { get; set; } = null!; + + public virtual IList States { get; set; } = null!; +} \ No newline at end of file diff --git a/Server/Entities/RefreshToken.cs b/Server/Models/RefreshToken.cs similarity index 93% rename from Server/Entities/RefreshToken.cs rename to Server/Models/RefreshToken.cs index b261401..940584f 100644 --- a/Server/Entities/RefreshToken.cs +++ b/Server/Models/RefreshToken.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace Server.Entities; +namespace Server.Models; [Owned] public class RefreshToken diff --git a/Server/Models/Route.cs b/Server/Models/Route.cs new file mode 100644 index 0000000..a910e9c --- /dev/null +++ b/Server/Models/Route.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class Route +{ + [Key] + public int Id { get; set; } + + public string Type { get; set; } = null!; + public TimeOnly IntendedDepartureTimeOnlyUtc { get; set; } + + public virtual IList RouteAddresses { get; set; } = null!; +} \ No newline at end of file diff --git a/Server/Models/RouteAddress.cs b/Server/Models/RouteAddress.cs new file mode 100644 index 0000000..d72de61 --- /dev/null +++ b/Server/Models/RouteAddress.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class RouteAddress +{ + [ForeignKey("RouteId")] + public int RouteId { get; set; } + public Route Route { get; set; } = null!; + + [ForeignKey("AddressId")] + public int AddressId { get; set; } + public Address Address { get; set; } = null!; + + public int Order { get; set; } + public TimeSpan TimeSpanToNextCity { get; set; } + public TimeSpan WaitTimeSpan { get; set; } + public double CostToNextCity { get; set; } +} \ No newline at end of file diff --git a/Server/Models/State.cs b/Server/Models/State.cs new file mode 100644 index 0000000..2db6547 --- /dev/null +++ b/Server/Models/State.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SharedModels.DataTransferObjects; + +namespace Server.Models; + +public class State +{ + [Key] + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public virtual IList Cities { get; set; } = null!; + + [ForeignKey("CountryId")] + public int CountryId { get; set; } + public Country? Country { get; set; } = null!; +} \ No newline at end of file diff --git a/Server/Models/Ticket.cs b/Server/Models/Ticket.cs new file mode 100644 index 0000000..e798c84 --- /dev/null +++ b/Server/Models/Ticket.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Models; + +public class Ticket +{ + [ForeignKey("UserId")] + public string UserId { get; set; } = null!; + public User User { get; set; } = null!; + + [ForeignKey("VehicleEnrollmentId")] + public int VehicleEnrollmentId { get; set; } + public VehicleEnrollment VehicleEnrollment { get; set; } = null!; + + public DateTime PurchaseDateTimeUtc { get; set; } + public bool IsReturned { get; set; } = false; +} \ No newline at end of file diff --git a/Server/Models/ApplicationUser.cs b/Server/Models/User.cs similarity index 76% rename from Server/Models/ApplicationUser.cs rename to Server/Models/User.cs index 8c86e56..425ed96 100644 --- a/Server/Models/ApplicationUser.cs +++ b/Server/Models/User.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Identity; -using Server.Entities; namespace Server.Models; -public class ApplicationUser : IdentityUser +public class User : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } diff --git a/Server/Models/Vehicle.cs b/Server/Models/Vehicle.cs new file mode 100644 index 0000000..f060717 --- /dev/null +++ b/Server/Models/Vehicle.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Models; + +public class Vehicle +{ + [Key] + public int Id { get; set; } + + public string Number { get; set; } = null!; + public string Type { get; set; } = null!; + public int Capacity { get; set; } + + [ForeignKey("CompanyId")] + public int CompanyId { get; set; } + public Company Company { get; set; } = null!; +} \ No newline at end of file diff --git a/Server/Models/VehicleEnrollment.cs b/Server/Models/VehicleEnrollment.cs new file mode 100644 index 0000000..1f455ee --- /dev/null +++ b/Server/Models/VehicleEnrollment.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Models; + +public class VehicleEnrollment +{ + [Key] + public int Id { get; set; } + + [ForeignKey("VehicleId")] + public int VehicleId { get; set; } + public Vehicle Vehicle { get; set; } = null!; + + [ForeignKey("RouteId")] + public int RouteId { get; set; } + public Route Route { get; set; } = null!; + + public DateOnly DepartureDateOnly { get; set; } + public TimeSpan DelayTimeSpan { get; set; } +} \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index 0a5293b..f72a731 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -4,16 +4,18 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Newtonsoft.Json; +using Server.Configurations; using Server.Data; using Server.Models; using Server.Services; -using Server.Settings; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers().AddNewtonsoftJson(options => + options.SerializerSettings.Formatting = Formatting.Indented); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -42,19 +44,18 @@ builder.Services.AddSwaggerGen(options => { }); var corsPolicyName = "defaultCorsPolicy"; -builder.Services.AddCors(options => -{ +builder.Services.AddCors(options => { options.AddPolicy(corsPolicyName, policy => policy.WithOrigins("http://localhost:4200").AllowCredentials() .AllowAnyHeader().AllowAnyMethod()); }); -//Configuration from AppSettings +// Configuration from AppSettings builder.Services.Configure(builder.Configuration.GetSection("Jwt")); -//User Manager Service -builder.Services.AddIdentity().AddEntityFrameworkStores(); +// User Manager Service +builder.Services.AddIdentity().AddEntityFrameworkStores(); builder.Services.AddScoped(); -//Adding Authentication - JWT +// Adding Authentication - JWT builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; @@ -78,7 +79,12 @@ builder.Services.AddAuthentication(options => { }); builder.Services.AddAuthorization(); -//Adding DB Context with PostgreSQL +builder.Services.AddAutoMapper(typeof(MapperInitializer)); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Adding DB Context with PostgreSQL var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); @@ -96,7 +102,7 @@ app.UseHttpsRedirection(); // Data seeding // using var scope = app.Services.CreateScope(); -// var userManager = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager))!; +// var userManager = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager))!; // var roleManager = (RoleManager)scope.ServiceProvider.GetService(typeof(RoleManager))!; // await ApplicationDbContextSeed.SeedEssentialsAsync(userManager, roleManager); diff --git a/Server/Server.csproj b/Server/Server.csproj index c048fdc..f68f53f 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -7,21 +7,26 @@ - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs index 1bd15a9..7f5e3c5 100644 --- a/Server/Services/AuthenticationService.cs +++ b/Server/Services/AuthenticationService.cs @@ -6,10 +6,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Server.Configurations; using Server.Constants; -using Server.Entities; using Server.Models; -using Server.Settings; using SharedModels.Requests; using SharedModels.Responses; @@ -17,11 +16,11 @@ namespace Server.Services; public class AuthenticationService : IAuthenticationService { - private readonly UserManager _userManager; + private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly Jwt _jwt; - public AuthenticationService(UserManager userManager, + public AuthenticationService(UserManager userManager, RoleManager roleManager, IOptions jwt) { _userManager = userManager; @@ -37,7 +36,7 @@ public class AuthenticationService : IAuthenticationService return (false, $"Email {regRequest.Email} is already registered."); } - var user = new ApplicationUser {UserName = regRequest.Username, Email = regRequest.Email}; + var user = new User {UserName = regRequest.Username, Email = regRequest.Email}; var result = await _userManager.CreateAsync(user, regRequest.Password); if (!result.Succeeded) @@ -154,7 +153,7 @@ public class AuthenticationService : IAuthenticationService return true; } - private async Task CreateJwtToken(ApplicationUser user) + private async Task CreateJwtToken(User user) { var userClaims = await _userManager.GetClaimsAsync(user); var roles = await _userManager.GetRolesAsync(user); diff --git a/Server/Services/CountryManagementService.cs b/Server/Services/CountryManagementService.cs new file mode 100644 index 0000000..20ee6bc --- /dev/null +++ b/Server/Services/CountryManagementService.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; +using Server.Data; +using Server.Models; +using SharedModels.DataTransferObjects; + +namespace Server.Services; + +public class CountryManagementService : ICountryManagementService +{ + private readonly ApplicationDbContext _dbContext; + private readonly IMapper _mapper; + + public CountryManagementService(ApplicationDbContext dbContext, + IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task<(bool isSucceed, string message, CountryDto country)> AddCountry(CreateCountryDto createCountryDto) + { + var country = _mapper.Map(createCountryDto); + + await _dbContext.Countries.AddAsync(country); + await _dbContext.SaveChangesAsync(); + + return (true, String.Empty, _mapper.Map(country)); + } + + public async Task<(bool isSucceed, string message, IEnumerable countries)> GetCountries() + { + throw new NotImplementedException(); + } + + public async Task<(bool isSucceed, string message, CountryDto country)> GetCountry(int id) + { + var dbCountry = await _dbContext.Countries.Where(c => c.Id == id) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + if (dbCountry == null) + { + return (false, $"Country doesn't exist", null)!; + } + + return (true, "", dbCountry); + } + + public async Task<(bool isSucceed, string message, CountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto) + { + var country = _mapper.Map(updateCountryDto); + _dbContext.Entry(country).State = EntityState.Modified; + + try + { + await _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await IsCountryExists(updateCountryDto.Id)) + { + return (false, $"Country with id:{updateCountryDto.Id} doesn't exist", null)!; + } + + throw; + } + + var dbCountry = await _dbContext.Countries.FirstOrDefaultAsync(c => c.Id == country.Id); + + return (true, String.Empty, _mapper.Map(dbCountry)); + } + + public async Task<(bool isSucceed, string message)> DeleteCountry(int id) + { + var dbCountry = await _dbContext.Countries.FirstOrDefaultAsync(c => c.Id == id); + + if (dbCountry == null) + { + return (false, $"Country with id:{id} doesn't exist"); + } + + _dbContext.Countries.Remove(dbCountry); + await _dbContext.SaveChangesAsync(); + + return (true, String.Empty); + } + + public async Task IsCountryExists(int id) + { + return await _dbContext.Countries.AnyAsync(c => c.Id == id); + } +} \ No newline at end of file diff --git a/Server/Services/DateTimeService.cs b/Server/Services/DateTimeService.cs new file mode 100644 index 0000000..1b41fbe --- /dev/null +++ b/Server/Services/DateTimeService.cs @@ -0,0 +1,30 @@ +namespace Server.Services; + +public class DateTimeService : IDateTimeService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public DateTimeService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public bool TryToGetTimeZoneInfoFromCookie(out TimeZoneInfo? timeZoneInfo) + { + if (_httpContextAccessor.HttpContext == null) + { + timeZoneInfo = null; + return false; + } + + if (!_httpContextAccessor.HttpContext.Request.Cookies.TryGetValue( + "timeZone", out string? timeZoneId)) + { + timeZoneInfo = null; + return false; + } + + timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId!); + return true; + } +} \ No newline at end of file diff --git a/Server/Services/ICountryManagementService.cs b/Server/Services/ICountryManagementService.cs new file mode 100644 index 0000000..2eca47b --- /dev/null +++ b/Server/Services/ICountryManagementService.cs @@ -0,0 +1,14 @@ +using SharedModels.DataTransferObjects; + +namespace Server.Services; + +public interface ICountryManagementService +{ + Task<(bool isSucceed, string message, CountryDto country)> AddCountry(CreateCountryDto createCountryDto); + + Task<(bool isSucceed, string message, IEnumerable countries)> GetCountries(); + Task<(bool isSucceed, string message, CountryDto country)> GetCountry(int id); + Task<(bool isSucceed, string message, CountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto); + Task<(bool isSucceed, string message)> DeleteCountry(int id); + Task IsCountryExists(int id); +} \ No newline at end of file diff --git a/Server/Services/IDateTimeService.cs b/Server/Services/IDateTimeService.cs new file mode 100644 index 0000000..9ea20a7 --- /dev/null +++ b/Server/Services/IDateTimeService.cs @@ -0,0 +1,6 @@ +namespace Server.Services; + +public interface IDateTimeService +{ + bool TryToGetTimeZoneInfoFromCookie(out TimeZoneInfo? timeZoneInfo); +} \ No newline at end of file diff --git a/Server/appsettings.json b/Server/appsettings.json index c4e1c0a..47ce3e1 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;" + "DefaultConnection": "Host=localhost;Database=auto.bus;Username=postgres;Password=postgres;" }, "Jwt": { "Key": "Secret which will never be exposed", diff --git a/SharedModels/DataTransferObjects/AddressDto.cs b/SharedModels/DataTransferObjects/AddressDto.cs new file mode 100644 index 0000000..fa5945b --- /dev/null +++ b/SharedModels/DataTransferObjects/AddressDto.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class AddressDto : CreateAddressDto +{ + public int Id { get; set; } + + public CityDto City { get; set; } = null!; + + public virtual IList RouteAddresses { get; set; } = null!; +} + +public class CreateAddressDto +{ + [Required] + [StringLength(maximumLength: 250, ErrorMessage = "Address name is too long")] + public string Name { get; set; } = null!; + + [Required] + [Range(-90, 90, ErrorMessage = "Latitude must be in range(-90, 90)")] + public double Latitude { get; set; } + + [Required] + [Range(-180, 180, ErrorMessage = "Longitude must be in range(-180, 180)")] + public double Longitude { get; set; } + + [Required] + public int CityId { get; set; } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/CityDto.cs b/SharedModels/DataTransferObjects/CityDto.cs new file mode 100644 index 0000000..04c8582 --- /dev/null +++ b/SharedModels/DataTransferObjects/CityDto.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class CityDto : CreateCityDto +{ + public int Id { get; set; } + + public StateDto State { get; set; } = null!; + + public virtual IList? Addresses { get; set; } +} + +public class CreateCityDto +{ + [Required] + [StringLength(maximumLength: 40, ErrorMessage = "City Name is too long")] + public string Name { get; set; } = null!; + + [Required] + public int StateId { get; set; } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/CountryDto.cs b/SharedModels/DataTransferObjects/CountryDto.cs new file mode 100644 index 0000000..cd9f865 --- /dev/null +++ b/SharedModels/DataTransferObjects/CountryDto.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class CountryDto : CreateCountryDto +{ + public int Id { get; set; } + + public virtual IList States { get; set; } = null!; +} + +public class CreateCountryDto +{ + [Required] + [StringLength(maximumLength: 2, MinimumLength = 2, ErrorMessage = "The Code field must be 2 characters long")] + public string Code { get; set; } = null!; + + [Required] + [StringLength(maximumLength: 56, ErrorMessage = "The Name field must be shorter than 56 characters")] + public string Name { get; set; } = null!; +} + +public class UpdateCountryDto : CreateCountryDto +{ + [Required] + public int Id { get; set; } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/RouteAddressDto.cs b/SharedModels/DataTransferObjects/RouteAddressDto.cs new file mode 100644 index 0000000..0fa3774 --- /dev/null +++ b/SharedModels/DataTransferObjects/RouteAddressDto.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class RouteAddressDto : CreateRouteAddressDto +{ + public int Id { get; set; } + + public RouteDto Route { get; set; } = null!; + + public AddressDto Address { get; set; } = null!; +} + +public class CreateRouteAddressDto +{ + [Required] + public int RouteId { get; set; } + + [Required] + public int AddressId { get; set; } + + [Required] + public int Order { get; set; } + + [Required] + [DataType(DataType.Duration)] + public TimeSpan TimeSpanToNextCity { get; set; } + + [Required] + [DataType(DataType.Duration)] + public TimeSpan WaitTimeSpan { get; set; } + + [Required] + [DataType(DataType.Currency)] + public double CostToNextCity { get; set; } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/RouteDto.cs b/SharedModels/DataTransferObjects/RouteDto.cs new file mode 100644 index 0000000..fbb6ea3 --- /dev/null +++ b/SharedModels/DataTransferObjects/RouteDto.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class RouteDto : CreateRouteDto +{ + public int Id { get; set; } + + public virtual IList RouteAddresses { get; set; } = null!; +} + +public class CreateRouteDto +{ + [Required] + public string Type { get; set; } = null!; + + [Required] + public TimeOnly IntendedDepartureTimeOnlyUtc { get; set; } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/StateDto.cs b/SharedModels/DataTransferObjects/StateDto.cs new file mode 100644 index 0000000..a8d6cc2 --- /dev/null +++ b/SharedModels/DataTransferObjects/StateDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.DataTransferObjects; + +public class StateDto : CreateStateDto +{ + public int Id { get; set; } + public CountryDto Country { get; set; } = null!; + + public virtual IList Cities { get; set; } = null!; +} + +public class CreateStateDto +{ + [Required] + [StringLength(maximumLength: 40, ErrorMessage = "State Name is too long")] + public string Name { get; set; } = null!; + + [Required] + public int CountryId { get; set; } +} \ No newline at end of file diff --git a/SharedModels/SharedModels.csproj b/SharedModels/SharedModels.csproj index 4a175d7..eb2460e 100644 --- a/SharedModels/SharedModels.csproj +++ b/SharedModels/SharedModels.csproj @@ -6,8 +6,4 @@ enable - - - - diff --git a/auto.bus REST API.sln b/auto.bus API.sln similarity index 100% rename from auto.bus REST API.sln rename to auto.bus API.sln