feat: add debug registration endpoint, fix snowflake serialization
This commit is contained in:
		
							parent
							
								
									852036a6f7
								
							
						
					
					
						commit
						588afeec20
					
				
					 14 changed files with 646 additions and 10 deletions
				
			
		|  | @ -8,6 +8,6 @@ namespace Foxnouns.Backend.Controllers; | ||||||
| [Authenticate] | [Authenticate] | ||||||
| public class ApiControllerBase : ControllerBase | public class ApiControllerBase : ControllerBase | ||||||
| { | { | ||||||
|     internal Token? Token => HttpContext.GetToken(); |     internal Token? CurrentToken => HttpContext.GetToken(); | ||||||
|     internal new User? User => HttpContext.GetUser(); |     internal User? CurrentUser => HttpContext.GetUser(); | ||||||
| } | } | ||||||
							
								
								
									
										32
									
								
								Foxnouns.Backend/Controllers/DebugController.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								Foxnouns.Backend/Controllers/DebugController.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Foxnouns.Backend.Services; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using NodaTime; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Controllers; | ||||||
|  | 
 | ||||||
|  | [Route("/api/v2/debug")] | ||||||
|  | public class DebugController(DatabaseContext db, AuthService authSvc, IClock clock, ILogger logger) : ApiControllerBase | ||||||
|  | { | ||||||
|  |     [HttpPost("users")] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] | ||||||
|  |     public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest req) | ||||||
|  |     { | ||||||
|  |         logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); | ||||||
|  | 
 | ||||||
|  |         var user = await authSvc.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password); | ||||||
|  |         var frontendApp = await db.GetFrontendApplicationAsync(); | ||||||
|  | 
 | ||||||
|  |         var (tokenStr, token) = | ||||||
|  |             authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); | ||||||
|  |         db.Add(token); | ||||||
|  | 
 | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  | 
 | ||||||
|  |         return Ok(new AuthResponse(user.Id, user.Username, tokenStr)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public record CreateUserRequest(string Username, string Password, string Email); | ||||||
|  | 
 | ||||||
|  |     public record AuthResponse(Snowflake Id, string Username, string Token); | ||||||
|  | } | ||||||
|  | @ -13,15 +13,15 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere | ||||||
|     public async Task<IActionResult> GetUser(string userRef) |     public async Task<IActionResult> GetUser(string userRef) | ||||||
|     { |     { | ||||||
|         var user = await db.ResolveUserAsync(userRef); |         var user = await db.ResolveUserAsync(userRef); | ||||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); |         return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpGet("@me")] |     [HttpGet("@me")] | ||||||
|     [Authorize("identify")] |     [Authorize("identify")] | ||||||
|     public async Task<IActionResult> GetMe() |     public async Task<IActionResult> GetMe() | ||||||
|     { |     { | ||||||
|         var user = await db.ResolveUserAsync(User!.Id); |         var user = await db.ResolveUserAsync(CurrentUser!.Id); | ||||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); |         return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpPatch("@me")] |     [HttpPatch("@me")] | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ using Foxnouns.Backend.Database.Models; | ||||||
| using Foxnouns.Backend.Extensions; | using Foxnouns.Backend.Extensions; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Design; | using Microsoft.EntityFrameworkCore.Design; | ||||||
|  | using Microsoft.EntityFrameworkCore.Diagnostics; | ||||||
| using Npgsql; | using Npgsql; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Database; | namespace Foxnouns.Backend.Database; | ||||||
|  | @ -32,6 +33,7 @@ public class DatabaseContext : DbContext | ||||||
| 
 | 
 | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|         => optionsBuilder |         => optionsBuilder | ||||||
|  |             .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) | ||||||
|             .UseNpgsql(_dataSource, o => o.UseNodaTime()) |             .UseNpgsql(_dataSource, o => o.UseNodaTime()) | ||||||
|             .UseSnakeCaseNamingConvention(); |             .UseSnakeCaseNamingConvention(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										478
									
								
								Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,478 @@ | ||||||
|  | // <auto-generated /> | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  | 
 | ||||||
|  | #nullable disable | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Database.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(DatabaseContext))] | ||||||
|  |     [Migration("20240604142522_AddPassword")] | ||||||
|  |     partial class AddPassword | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "8.0.5") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  | 
 | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("ClientId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("client_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("ClientSecret") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("client_secret"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string[]>("RedirectUris") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text[]") | ||||||
|  |                         .HasColumnName("redirect_uris"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string[]>("Scopes") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text[]") | ||||||
|  |                         .HasColumnName("scopes"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_applications"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("applications", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<int>("AuthType") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("auth_type"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long?>("FediverseApplicationId") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("fediverse_application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("RemoteId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("remote_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("RemoteUsername") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("remote_username"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long>("UserId") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_auth_methods"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("FediverseApplicationId") | ||||||
|  |                         .HasDatabaseName("ix_auth_methods_fediverse_application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("UserId") | ||||||
|  |                         .HasDatabaseName("ix_auth_methods_user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("auth_methods", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("ClientId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("client_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("ClientSecret") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("client_secret"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Domain") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("domain"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<int>("InstanceType") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("instance_type"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_fediverse_applications"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("fediverse_applications", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Avatar") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("avatar"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Bio") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("bio"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("DisplayName") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("display_name"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string[]>("Links") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text[]") | ||||||
|  |                         .HasColumnName("links"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<bool>("Unlisted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("unlisted"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long>("UserId") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_members"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("UserId", "Name") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_members_user_id_name"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("members", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long>("ApplicationId") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant>("ExpiresAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expires_at"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<byte[]>("Hash") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("bytea") | ||||||
|  |                         .HasColumnName("hash"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<bool>("ManuallyExpired") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("manually_expired"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string[]>("Scopes") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text[]") | ||||||
|  |                         .HasColumnName("scopes"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long>("UserId") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_tokens"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("ApplicationId") | ||||||
|  |                         .HasDatabaseName("ix_tokens_application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("UserId") | ||||||
|  |                         .HasDatabaseName("ix_tokens_user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("tokens", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Avatar") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("avatar"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Bio") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("bio"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("DisplayName") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("display_name"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string[]>("Links") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text[]") | ||||||
|  |                         .HasColumnName("links"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<bool>("ListHidden") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("list_hidden"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("MemberTitle") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("member_title"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Password") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("password"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Username") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("username"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_users"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("Username") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_users_username"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("users", (string)null); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("FediverseApplicationId") | ||||||
|  |                         .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasOne("Foxnouns.Backend.Database.Models.User", "User") | ||||||
|  |                         .WithMany("AuthMethods") | ||||||
|  |                         .HasForeignKey("UserId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_auth_methods_users_user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("FediverseApplication"); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("User"); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("Foxnouns.Backend.Database.Models.User", "User") | ||||||
|  |                         .WithMany("Members") | ||||||
|  |                         .HasForeignKey("UserId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_members_users_user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("MemberId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("MemberId"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("members"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("fields"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("MemberId") | ||||||
|  |                                 .HasConstraintName("fk_members_members_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("MemberId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("MemberId"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("members"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("names"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("MemberId") | ||||||
|  |                                 .HasConstraintName("fk_members_members_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("MemberId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("MemberId"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("members"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("pronouns"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("MemberId") | ||||||
|  |                                 .HasConstraintName("fk_members_members_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Fields") | ||||||
|  |                         .IsRequired(); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Names") | ||||||
|  |                         .IsRequired(); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Pronouns") | ||||||
|  |                         .IsRequired(); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("User"); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("ApplicationId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_tokens_applications_application_id"); | ||||||
|  | 
 | ||||||
|  |                     b.HasOne("Foxnouns.Backend.Database.Models.User", "User") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("UserId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_tokens_users_user_id"); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Application"); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("User"); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => | ||||||
|  |                 { | ||||||
|  |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("UserId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("UserId") | ||||||
|  |                                 .HasName("pk_users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("fields"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("UserId") | ||||||
|  |                                 .HasConstraintName("fk_users_users_user_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("UserId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("UserId") | ||||||
|  |                                 .HasName("pk_users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("names"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("UserId") | ||||||
|  |                                 .HasConstraintName("fk_users_users_user_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => | ||||||
|  |                         { | ||||||
|  |                             b1.Property<long>("UserId") | ||||||
|  |                                 .HasColumnType("bigint"); | ||||||
|  | 
 | ||||||
|  |                             b1.Property<int>("Capacity") | ||||||
|  |                                 .HasColumnType("integer"); | ||||||
|  | 
 | ||||||
|  |                             b1.HasKey("UserId") | ||||||
|  |                                 .HasName("pk_users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToTable("users"); | ||||||
|  | 
 | ||||||
|  |                             b1.ToJson("pronouns"); | ||||||
|  | 
 | ||||||
|  |                             b1.WithOwner() | ||||||
|  |                                 .HasForeignKey("UserId") | ||||||
|  |                                 .HasConstraintName("fk_users_users_user_id"); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Fields") | ||||||
|  |                         .IsRequired(); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Names") | ||||||
|  |                         .IsRequired(); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Pronouns") | ||||||
|  |                         .IsRequired(); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => | ||||||
|  |                 { | ||||||
|  |                     b.Navigation("AuthMethods"); | ||||||
|  | 
 | ||||||
|  |                     b.Navigation("Members"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | 
 | ||||||
|  | #nullable disable | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Database.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddPassword : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "password", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "text", | ||||||
|  |                 nullable: true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "password", | ||||||
|  |                 table: "users"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -250,6 +250,10 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .HasColumnType("text") |                         .HasColumnType("text") | ||||||
|                         .HasColumnName("member_title"); |                         .HasColumnName("member_title"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<string>("Password") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("password"); | ||||||
|  | 
 | ||||||
|                     b.Property<int>("Role") |                     b.Property<int>("Role") | ||||||
|                         .HasColumnType("integer") |                         .HasColumnType("integer") | ||||||
|                         .HasColumnName("role"); |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ public class Application : BaseModel | ||||||
|         string[] redirectUrls) |         string[] redirectUrls) | ||||||
|     { |     { | ||||||
|         var clientId = RandomNumberGenerator.GetHexString(32, true); |         var clientId = RandomNumberGenerator.GetHexString(32, true); | ||||||
|         var clientSecret = OauthUtils.RandomToken(48); |         var clientSecret = OauthUtils.RandomToken(); | ||||||
| 
 | 
 | ||||||
|         if (scopes.Except(OauthUtils.ApplicationScopes).Any()) |         if (scopes.Except(OauthUtils.ApplicationScopes).Any()) | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ public class User : BaseModel | ||||||
|     public List<Field> Fields { get; set; } = []; |     public List<Field> Fields { get; set; } = []; | ||||||
| 
 | 
 | ||||||
|     public UserRole Role { get; set; } = UserRole.User; |     public UserRole Role { get; set; } = UserRole.User; | ||||||
|  |     public string? Password { get; set; } // Password may be null if the user doesn't authenticate with an email address | ||||||
| 
 | 
 | ||||||
|     public List<Member> Members { get; } = []; |     public List<Member> Members { get; } = []; | ||||||
|     public List<AuthMethod> AuthMethods { get; } = []; |     public List<AuthMethod> AuthMethods { get; } = []; | ||||||
|  |  | ||||||
|  | @ -1,9 +1,11 @@ | ||||||
| using System.Diagnostics.CodeAnalysis; | using System.Diagnostics.CodeAnalysis; | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using Newtonsoft.Json; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Database; | namespace Foxnouns.Backend.Database; | ||||||
| 
 | 
 | ||||||
|  | [JsonConverter(typeof(JsonConverter))] | ||||||
| public readonly struct Snowflake(ulong value) | public readonly struct Snowflake(ulong value) | ||||||
| { | { | ||||||
|     public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC |     public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC | ||||||
|  | @ -63,4 +65,19 @@ public readonly struct Snowflake(ulong value) | ||||||
|         convertToProviderExpression: x => x, |         convertToProviderExpression: x => x, | ||||||
|         convertFromProviderExpression: x => x |         convertFromProviderExpression: x => x | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|  |     private class JsonConverter : JsonConverter<Snowflake> | ||||||
|  |     { | ||||||
|  |         public override void WriteJson(JsonWriter writer, Snowflake value, JsonSerializer serializer) | ||||||
|  |         { | ||||||
|  |             writer.WriteValue(value.Value.ToString()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public override Snowflake ReadJson(JsonReader reader, Type objectType, Snowflake existingValue, | ||||||
|  |             bool hasExistingValue, | ||||||
|  |             JsonSerializer serializer) | ||||||
|  |         { | ||||||
|  |             return ulong.Parse((string)reader.Value!); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -65,7 +65,8 @@ public static class WebApplicationExtensions | ||||||
|         .AddSingleton<IClock>(SystemClock.Instance) |         .AddSingleton<IClock>(SystemClock.Instance) | ||||||
|         .AddSnowflakeGenerator() |         .AddSnowflakeGenerator() | ||||||
|         .AddScoped<UserRendererService>() |         .AddScoped<UserRendererService>() | ||||||
|         .AddScoped<MemberRendererService>(); |         .AddScoped<MemberRendererService>() | ||||||
|  |         .AddScoped<AuthService>(); | ||||||
| 
 | 
 | ||||||
|     public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services |     public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services | ||||||
|         .AddScoped<ErrorHandlerMiddleware>() |         .AddScoped<ErrorHandlerMiddleware>() | ||||||
|  |  | ||||||
							
								
								
									
										57
									
								
								Foxnouns.Backend/Services/AuthService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Foxnouns.Backend/Services/AuthService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | using System.Security.Cryptography; | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
|  | using Foxnouns.Backend.Utils; | ||||||
|  | using Microsoft.AspNetCore.Identity; | ||||||
|  | using NodaTime; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Services; | ||||||
|  | 
 | ||||||
|  | public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) | ||||||
|  | { | ||||||
|  |     private readonly PasswordHasher<User> _passwordHasher = new(); | ||||||
|  | 
 | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a new user with the given email address and password. | ||||||
|  |     /// This method does <i>not</i> save the resulting user, the caller must still call <see cref="M:Microsoft.EntityFrameworkCore.DbContext.SaveChanges" />. | ||||||
|  |     /// </summary> | ||||||
|  |     public async Task<User> CreateUserWithPasswordAsync(string username, string email, string password) | ||||||
|  |     { | ||||||
|  |         var user = new User | ||||||
|  |         { | ||||||
|  |             Id = snowflakeGenerator.GenerateSnowflake(), | ||||||
|  |             Username = username, | ||||||
|  |             AuthMethods = { new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         db.Add(user); | ||||||
|  |         user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); | ||||||
|  | 
 | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) | ||||||
|  |     { | ||||||
|  |         if (!OauthUtils.ValidateScopes(application, scopes)) | ||||||
|  |             throw new ApiError.BadRequest("Invalid scopes requested for this token"); | ||||||
|  | 
 | ||||||
|  |         var (token, hash) = GenerateToken(); | ||||||
|  |         return (token, new Token | ||||||
|  |         { | ||||||
|  |             Id = snowflakeGenerator.GenerateSnowflake(), | ||||||
|  |             Hash = hash, | ||||||
|  |             Application = application, | ||||||
|  |             User = user, | ||||||
|  |             ExpiresAt = expires, | ||||||
|  |             Scopes = scopes | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static (string, byte[]) GenerateToken() | ||||||
|  |     { | ||||||
|  |         var token = OauthUtils.RandomToken(48); | ||||||
|  |         var hash = SHA512.HashData(Convert.FromBase64String(token)); | ||||||
|  | 
 | ||||||
|  |         return (token, hash); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Utils; | namespace Foxnouns.Backend.Utils; | ||||||
| 
 | 
 | ||||||
|  | @ -16,13 +17,13 @@ public static class OauthUtils | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. |     /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public static readonly string[] Scopes = ["identify", .. UserScopes, .. MemberScopes]; |     public static readonly string[] Scopes = ["identify", ..UserScopes, ..MemberScopes]; | ||||||
| 
 | 
 | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes, |     /// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes, | ||||||
|     /// except for "*" which is only granted to the frontend. |     /// except for "*" which is only granted to the frontend. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public static readonly string[] ApplicationScopes = [.. Scopes, "user", "member"]; |     public static readonly string[] ApplicationScopes = [..Scopes, "user", "member"]; | ||||||
| 
 | 
 | ||||||
|     public static string[] ExpandScopes(this string[] scopes) |     public static string[] ExpandScopes(this string[] scopes) | ||||||
|     { |     { | ||||||
|  | @ -34,6 +35,21 @@ public static class OauthUtils | ||||||
|         return expandedScopes.ToArray(); |         return expandedScopes.ToArray(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private static string[] ExpandAppScopes(this string[] scopes) | ||||||
|  |     { | ||||||
|  |         var expandedScopes = scopes.ExpandScopes().ToList(); | ||||||
|  |         if (scopes.Contains("user")) expandedScopes.Add("user"); | ||||||
|  |         if (scopes.Contains("member")) expandedScopes.Add("member"); | ||||||
|  |         return expandedScopes.ToArray(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static bool ValidateScopes(Application application, string[] scopes) | ||||||
|  |     { | ||||||
|  |         var expandedScopes = scopes.ExpandScopes(); | ||||||
|  |         var appScopes = application.Scopes.ExpandAppScopes(); | ||||||
|  |         return !expandedScopes.Except(appScopes).Any(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static bool ValidateRedirectUri(string uri) |     public static bool ValidateRedirectUri(string uri) | ||||||
|     { |     { | ||||||
|         try |         try | ||||||
|  |  | ||||||
|  | @ -17,4 +17,4 @@ Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" | ||||||
| ; The timeout for opening new connections. Defaults to 5. | ; The timeout for opening new connections. Defaults to 5. | ||||||
| Timeout = 5 | Timeout = 5 | ||||||
| ; The maximum number of open connections. Defaults to 50. | ; The maximum number of open connections. Defaults to 50. | ||||||
| MaxPoolSize = 500 | MaxPoolSize = 50 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue