diff --git a/.env b/.env index fba0b89..2af583f 100644 --- a/.env +++ b/.env @@ -2,3 +2,9 @@ sike you thought I was like that hehehehee (urp so full) + +# TODO: should have basic public-safe environment variables here +# then secret environment variables can be added via secrets in the ci script like so: +# job: inject-seccrets $ echo API_KEY={{ secrets.API_KEY }} >> .env +# then they dont have to be inserted by the docker container ( messy) + diff --git a/api/Migrations/20260321221316_InitialCreate.Designer.cs b/api/Migrations/20260321221316_InitialCreate.Designer.cs deleted file mode 100644 index 1008d5d..0000000 --- a/api/Migrations/20260321221316_InitialCreate.Designer.cs +++ /dev/null @@ -1,279 +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; - -#nullable disable - -namespace agologum_api.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20260321221316_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .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("agologumApi.Models.User", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - 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("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("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("agologumApi.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("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/Migrations/20260322011947_AddItems.Designer.cs b/api/Migrations/20260322011947_AddItems.Designer.cs deleted file mode 100644 index b40ce57..0000000 --- a/api/Migrations/20260322011947_AddItems.Designer.cs +++ /dev/null @@ -1,306 +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; - -#nullable disable - -namespace agologum_api.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20260322011947_AddItems")] - partial class AddItems - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .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("agologumApi.Models.Item", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Items"); - }); - - modelBuilder.Entity("agologumApi.Models.User", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - 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("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("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("agologumApi.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("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("agologumApi.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/Migrations/20260322011947_AddItems.cs b/api/Migrations/20260322011947_AddItems.cs deleted file mode 100644 index 324af3d..0000000 --- a/api/Migrations/20260322011947_AddItems.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace agologum_api.Migrations -{ - /// - public partial class AddItems : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Items", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - LastEditedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Items", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Items"); - } - } -} diff --git a/api/Migrations/20260322214843_AddRefreshTokens.cs b/api/Migrations/20260322214843_AddRefreshTokens.cs deleted file mode 100644 index f347d1d..0000000 --- a/api/Migrations/20260322214843_AddRefreshTokens.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace agologum_api.Migrations -{ - /// - public partial class AddRefreshTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "RefreshTokens", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Token = table.Column(type: "text", nullable: false), - UserId = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - IsRevoked = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RefreshTokens", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "RefreshTokens"); - } - } -} diff --git a/api/Migrations/20260322214843_AddRefreshTokens.Designer.cs b/api/Migrations/20260423011426_InitialMigration.Designer.cs similarity index 98% rename from api/Migrations/20260322214843_AddRefreshTokens.Designer.cs rename to api/Migrations/20260423011426_InitialMigration.Designer.cs index 488c465..24bf37e 100644 --- a/api/Migrations/20260322214843_AddRefreshTokens.Designer.cs +++ b/api/Migrations/20260423011426_InitialMigration.Designer.cs @@ -11,8 +11,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace agologum_api.Migrations { [DbContext(typeof(AppDbContext))] - [Migration("20260322214843_AddRefreshTokens")] - partial class AddRefreshTokens + [Migration("20260423011426_InitialMigration")] + partial class InitialMigration { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -252,6 +252,9 @@ namespace agologum_api.Migrations b.Property("PasswordHash") .HasColumnType("text"); + b.PrimitiveCollection("Permissions") + .HasColumnType("jsonb"); + b.Property("PhoneNumber") .HasColumnType("text"); diff --git a/api/Migrations/20260321221316_InitialCreate.cs b/api/Migrations/20260423011426_InitialMigration.cs similarity index 82% rename from api/Migrations/20260321221316_InitialCreate.cs rename to api/Migrations/20260423011426_InitialMigration.cs index da54a51..7f0a4eb 100644 --- a/api/Migrations/20260321221316_InitialCreate.cs +++ b/api/Migrations/20260423011426_InitialMigration.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace agologum_api.Migrations { /// - public partial class InitialCreate : Migration + public partial class InitialMigration : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -32,6 +32,7 @@ namespace agologum_api.Migrations { Id = table.Column(type: "text", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Permissions = table.Column(type: "jsonb", nullable: true), UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), @@ -52,6 +53,39 @@ namespace agologum_api.Migrations table.PrimaryKey("PK_AspNetUsers", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + LastEditedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Items", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Token = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + IsRevoked = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AspNetRoleClaims", columns: table => new @@ -214,6 +248,12 @@ namespace agologum_api.Migrations migrationBuilder.DropTable( name: "AspNetUserTokens"); + migrationBuilder.DropTable( + name: "Items"); + + migrationBuilder.DropTable( + name: "RefreshTokens"); + migrationBuilder.DropTable( name: "AspNetRoles"); diff --git a/api/Migrations/AppDbContextModelSnapshot.cs b/api/Migrations/AppDbContextModelSnapshot.cs index 0d019e7..0a94956 100644 --- a/api/Migrations/AppDbContextModelSnapshot.cs +++ b/api/Migrations/AppDbContextModelSnapshot.cs @@ -249,6 +249,9 @@ namespace agologum_api.Migrations b.Property("PasswordHash") .HasColumnType("text"); + b.PrimitiveCollection("Permissions") + .HasColumnType("jsonb"); + b.Property("PhoneNumber") .HasColumnType("text"); diff --git a/api/Program.cs b/api/Program.cs index 6bf5190..d4f8478 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,4 +1,5 @@ +// system usings using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -7,27 +8,33 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using System.Text; +// homeburger usings using agologumApi.Models; using agologumApi.Services; var builder = WebApplication.CreateBuilder(args); +// make sure the jwt key exists or else abort, security issue var key = builder.Configuration["Jwt:Key"]; if(key == null) return; +// connect to the sql database builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddControllers(); -// services +// add our services +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +// if this grows sufficiently large we can put elsewhere // configuration for jwt authentication builder.Services.AddIdentity() .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + .AddDefaultTokenProviders() + .AddRoles(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; @@ -44,7 +51,16 @@ builder.Services.AddAuthentication(options => { }; }); -builder.Services.AddAuthorization(); +// authorization configurations; here's where we register our permissions to policies +// TODO: this suspiciously looks able to be automated through a for loop, only if we can have a static dictionary maybe though? +builder.Services.AddAuthorization(options => { + + options.AddPolicy(Permission.SensitiveData_Read, policy => + policy.RequireClaim("permission", Permission.SensitiveData_Read)); + options.AddPolicy(Permission.SensitiveData_Modify, policy => + policy.RequireClaim("permission", Permission.SensitiveData_Modify)); + +}); // configuration for behind my nginx proxy builder.Services.Configure(options => @@ -61,6 +77,7 @@ builder.Services.Configure(options => // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +// cors; scary needs to be fixed builder.Services.AddCors(options => { options.AddPolicy("dev", @@ -69,17 +86,14 @@ builder.Services.AddCors(options => policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); - }); + }); // TODO: scary please fix this }); +// more middleware; probably uncessary at this stage builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// https://www.reddit.com/r/dotnet/comments/1h7vzbs/how_do_you_guys_handle_authorization_on_a_web_api/ -// add authorization here -// controllers will have endpoints based on authorization -// frontend is a different story - +// build app var app = builder.Build(); app.UseForwardedHeaders(); @@ -111,6 +125,7 @@ using (var scope = app.Services.CreateScope()) { Thread.Sleep(5000); } } + } app.Run(); diff --git a/api/src/Controllers/AuthController.cs b/api/src/Controllers/AuthController.cs index 8afd69e..0ecf3da 100644 --- a/api/src/Controllers/AuthController.cs +++ b/api/src/Controllers/AuthController.cs @@ -13,46 +13,62 @@ public class AuthController : ControllerBase { // identity things private readonly UserManager userManager_; private readonly SignInManager signInManager_; - + // services private readonly JwtService jwt_; + private readonly UserService userService_; - public AuthController(UserManager userManager, SignInManager signInManager, JwtService jwt) { + // class constructor (where are my initializer lists man) + public AuthController(UserManager userManager, SignInManager signInManager, JwtService jwt, UserService userService) { userManager_ = userManager; signInManager_ = signInManager; jwt_ = jwt; + userService_ = userService; } + // register endpoint [HttpPost("register")] public async Task Register(RegisterDto dto) { - var user = new User { + // create a new user out of the dto from the request + User user = new User { UserName = dto.UserName, Email = dto.Email, CreatedAt = DateTime.UtcNow // yeah why not utc }; + // assigning roles to user. create a user starting with x to give it permissions to read sensitive data + if(dto.UserName.StartsWith("x")) { + user.Permissions = new List { Permission.SensitiveData_Read }; + } + + // use Identity's user manager to add to db; error check if failed var result = await userManager_.CreateAsync(user, dto.Password); if(!result.Succeeded) return BadRequest(result.Errors); + // respond to post as necessary return CreatedAtAction( nameof(Register), new { id = user.Id } ); } + // login endpoint [HttpPost("login")] public async Task Login(LoginDto dto) { + // get the user from the database given the username var user = await userManager_.FindByNameAsync(dto.UserName); + // user not found with that name + if (user == null) return Unauthorized(); // unauthorized instead of not found to not give away info - if (user == null) return Unauthorized(); - + // use identity's password validation var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false); - + // if failed then youre not real ! if(!result.Succeeded) return Unauthorized(); - var accessToken = jwt_.GenerateJwt(user); - var refreshToken = jwt_.GenerateRefreshToken(); + // login sucess, give you an authentication token + var accessToken = await jwt_.GenerateJwt(user); + var refreshToken = jwt_.GenerateRefreshToken(); // the refresh token is good enough to refresh your access token RefreshToken newTokenObject = new RefreshToken { Token = refreshToken, UserId = user.Id, @@ -61,34 +77,44 @@ public class AuthController : ControllerBase { IsRevoked = false }; await jwt_.AddRefreshToken(newTokenObject); + // the jwt says we trust who you are and can substitute it for login + // contains permissions claims too + // return both access and refresh token return Ok(new { accessToken, refreshToken }); } + // logout endpoint [Authorize] // authorize is handled by middleware [HttpPost("logout")] public async Task Logout(string refreshTokenString) { // revoke refresh token bool success = await jwt_.RevokeRefreshToken(refreshTokenString); if(!success) return NotFound(); + // frontend refreshes page and detects logout return Ok(); } + // refresh token endpoint [HttpPost("refresh")] // allow-anonymous by default public async Task Refresh(TokenDto request) { + // reached when the frontend gets an unauthorized response and autoattempts to refresh if available + // get token from request and check if its valid RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken); if (storedToken == null) return Unauthorized(); bool valid = (storedToken.IsRevoked) || (storedToken.ExpiresAt < DateTime.UtcNow); if(!valid) return Unauthorized(); // TODO: delete the invalid token + // get user from the token and give them new tokens User? user = await jwt_.GetUser(storedToken.UserId); if(user == null) return NotFound(); - string? newAccessToken = jwt_.GenerateJwt(user); + string? newAccessToken = await jwt_.GenerateJwt(user); if(newAccessToken == null) return NotFound(); string newRefreshToken = jwt_.GenerateRefreshToken(); + // construct new token storedToken.IsRevoked = true; RefreshToken newTokenObject = new RefreshToken { Token = newRefreshToken, @@ -99,10 +125,9 @@ public class AuthController : ControllerBase { }; await jwt_.AddRefreshToken(newTokenObject); - + // return new tokens return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken }); - } // TODO diff --git a/api/src/Controllers/UsersController.cs b/api/src/Controllers/UsersController.cs new file mode 100644 index 0000000..24339b0 --- /dev/null +++ b/api/src/Controllers/UsersController.cs @@ -0,0 +1,118 @@ + +// this is basically a demo on roles +// level 0 can't access the users endpoint at all +// level 1 has read permissions +// level 2 has modify permissions + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; + +using agologumApi.Models; +using agologumApi.Services; + +[ApiController] +[Route("api/[controller]")] +public class UsersController : ControllerBase { + + private readonly UserService service_; + + public UsersController(UserService service) { + service_ = service; + } + + [Authorize(Policy = Permission.SensitiveData_Read)] + [HttpGet] + public async Task>> getUsers() { + List rawArray = await service_.GetAll(); + + List dtoArray = new List(); + + foreach(User user in rawArray) { + UserDto newDto = new UserDto(user); + dtoArray.Add(newDto); + } + + return Ok(dtoArray); + } + + [Authorize(Policy = Permission.SensitiveData_Read)] + [HttpGet("{id:int}")] + public async Task> getUser(string id) { + + var user = await service_.GetById(id); + + if (user == null) return NotFound(); + + UserDto newDto = new UserDto(user); + + return Ok(newDto); + } + + [Authorize(Policy = Permission.SensitiveData_Modify)] + [HttpDelete("{id}")] + public async Task deleteUser(string id) { + + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if(userId == id) return BadRequest(); // dont allow deletion of yourself + + var success = await service_.Delete(id); + + if (!success) return NotFound(); + + return NoContent(); + } + + [Authorize(Policy = Permission.SensitiveData_Modify)] + [HttpDelete("{id}/{permission}")] + public async Task removePermission(string id, string permission) { + + // get the user this request comes from. since it passed identity auth we can trust it + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if(permission == Permission.SensitiveData_Modify && userId == id) return BadRequest(); // dont allow permission removal of whats allowing us to re-add premissions + + // get list of permissions of that user + var user = await service_.GetById(id); + if (user == null) return NotFound(); + if(user.Permissions == null) return NotFound(); + + // verify that the requested permission exists on that user + if(!user.Permissions.Contains(permission)) return NotFound(); + + // remove the permission from the permission list + user.Permissions.Remove(permission); + + // update the user + await service_.Update(id, user); + + return NoContent(); + } + + [Authorize(Policy = Permission.SensitiveData_Modify)] + [HttpPost("{id}/{permission}")] // TODO: this was made with a single button per permission in mind, but may be better as sending an array + public async Task addPermission(string id, string permission) { + + // we'll allow the superuser to elevate their own permissions because they're the superuser + + // get list of permissions of the user + var user = await service_.GetById(id); + if (user == null) return NotFound(); + if(user.Permissions == null) return NotFound(); + + // remove add the permission to the user's permission list (if it doesnt already exist) + if(user.Permissions.Contains(permission)) return NoContent(); + user.Permissions.Add(permission); + + // update the user + await service_.Update(id, user); + + return NoContent(); + + // fyi the user will need to sign out and sign back in so the new permissions are reflected in their jwt claims + // TODO: or on the client i could issue a refresh token request after a permission api call + } + + // TODO: add controls on editing roles + +} \ No newline at end of file diff --git a/api/src/Data/AppDbContext.cs b/api/src/Data/AppDbContext.cs index 88af273..cd6b14f 100644 --- a/api/src/Data/AppDbContext.cs +++ b/api/src/Data/AppDbContext.cs @@ -10,8 +10,16 @@ public class AppDbContext : IdentityDbContext { } - // Db set for each model besides Users (DbSet