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] | ||||
| public class ApiControllerBase : ControllerBase | ||||
| { | ||||
|     internal Token? Token => HttpContext.GetToken(); | ||||
|     internal new User? User => HttpContext.GetUser(); | ||||
|     internal Token? CurrentToken => HttpContext.GetToken(); | ||||
|     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) | ||||
|     { | ||||
|         var user = await db.ResolveUserAsync(userRef); | ||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); | ||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("@me")] | ||||
|     [Authorize("identify")] | ||||
|     public async Task<IActionResult> GetMe() | ||||
|     { | ||||
|         var user = await db.ResolveUserAsync(User!.Id); | ||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); | ||||
|         var user = await db.ResolveUserAsync(CurrentUser!.Id); | ||||
|         return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("@me")] | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ using Foxnouns.Backend.Database.Models; | |||
| using Foxnouns.Backend.Extensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Diagnostics; | ||||
| using Npgsql; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Database; | ||||
|  | @ -32,6 +33,7 @@ public class DatabaseContext : DbContext | |||
| 
 | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|         => optionsBuilder | ||||
|             .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) | ||||
|             .UseNpgsql(_dataSource, o => o.UseNodaTime()) | ||||
|             .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") | ||||
|                         .HasColumnName("member_title"); | ||||
| 
 | ||||
|                     b.Property<string>("Password") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("password"); | ||||
| 
 | ||||
|                     b.Property<int>("Role") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("role"); | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ public class Application : BaseModel | |||
|         string[] redirectUrls) | ||||
|     { | ||||
|         var clientId = RandomNumberGenerator.GetHexString(32, true); | ||||
|         var clientSecret = OauthUtils.RandomToken(48); | ||||
|         var clientSecret = OauthUtils.RandomToken(); | ||||
| 
 | ||||
|         if (scopes.Except(OauthUtils.ApplicationScopes).Any()) | ||||
|         { | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ public class User : BaseModel | |||
|     public List<Field> Fields { get; set; } = []; | ||||
| 
 | ||||
|     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<AuthMethod> AuthMethods { get; } = []; | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Database; | ||||
| 
 | ||||
| [JsonConverter(typeof(JsonConverter))] | ||||
| public readonly struct Snowflake(ulong value) | ||||
| { | ||||
|     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, | ||||
|         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) | ||||
|         .AddSnowflakeGenerator() | ||||
|         .AddScoped<UserRendererService>() | ||||
|         .AddScoped<MemberRendererService>(); | ||||
|         .AddScoped<MemberRendererService>() | ||||
|         .AddScoped<AuthService>(); | ||||
| 
 | ||||
|     public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services | ||||
|         .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 Foxnouns.Backend.Database.Models; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Utils; | ||||
| 
 | ||||
|  | @ -16,13 +17,13 @@ public static class OauthUtils | |||
|     /// <summary> | ||||
|     /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. | ||||
|     /// </summary> | ||||
|     public static readonly string[] Scopes = ["identify", .. UserScopes, .. MemberScopes]; | ||||
|     public static readonly string[] Scopes = ["identify", ..UserScopes, ..MemberScopes]; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// 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. | ||||
|     /// </summary> | ||||
|     public static readonly string[] ApplicationScopes = [.. Scopes, "user", "member"]; | ||||
|     public static readonly string[] ApplicationScopes = [..Scopes, "user", "member"]; | ||||
| 
 | ||||
|     public static string[] ExpandScopes(this string[] scopes) | ||||
|     { | ||||
|  | @ -34,6 +35,21 @@ public static class OauthUtils | |||
|         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) | ||||
|     { | ||||
|         try | ||||
|  |  | |||
|  | @ -17,4 +17,4 @@ Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" | |||
| ; The timeout for opening new connections. Defaults to 5. | ||||
| Timeout = 5 | ||||
| ; The maximum number of open connections. Defaults to 50. | ||||
| MaxPoolSize = 500 | ||||
| MaxPoolSize = 50 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue