feat: add deleted user columns in database
This commit is contained in:
		
							parent
							
								
									e95e0a79ff
								
							
						
					
					
						commit
						fa49030b06
					
				
					 17 changed files with 1254 additions and 54 deletions
				
			
		
							
								
								
									
										45
									
								
								ENDPOINTS.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								ENDPOINTS.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | # List of API endpoints and scopes | ||||||
|  | 
 | ||||||
|  | ## Scopes | ||||||
|  | 
 | ||||||
|  | - `identify`: `@me` will refer to token user (always granted) | ||||||
|  | - `user.read_privileged`: can read privileged information such as authentication methods | ||||||
|  | - `user.update`: can update the user's profile. | ||||||
|  |   **cannot** update anything locked behind `user.read_privileged` | ||||||
|  | - `member.read`: can view member list if it's hidden and enumerate unlisted members | ||||||
|  | - `member.create`: can create new members | ||||||
|  | - `member.update`: can edit and delete members | ||||||
|  | 
 | ||||||
|  | ## Meta | ||||||
|  | 
 | ||||||
|  | - [ ] GET `/meta`: gets stats and server information | ||||||
|  | 
 | ||||||
|  | ## Users | ||||||
|  | 
 | ||||||
|  | - [ ] GET `/users/{userRef}`: views current user. | ||||||
|  |   `identify` required to use `@me` as user reference. | ||||||
|  |   `user.read_privileged` required to view authentication methods. | ||||||
|  |   `member.read` required to view unlisted members. | ||||||
|  | - [ ] PATCH `/users/@me`: updates current user. `user.update` required. | ||||||
|  | - [ ] DELETE `/users/@me`: deletes current user. `*` required | ||||||
|  | - [ ] POST `/users/@me/export`: queues new data export. `*` required | ||||||
|  | - [ ] GET `/users/@me/export`: gets latest data export. `*` required | ||||||
|  | - [ ] GET `/users/@me/flags`: get all the user's flags. `identify` required | ||||||
|  | - [ ] POST `/users/@me/flags`: creates a new flag. `user.update` required | ||||||
|  | - [ ] PATCH `/users/@me/flags/{id}`: updates an existing flag. `user.update` required | ||||||
|  | - [ ] DELETE `/users/@me/flags/{id}`: deletes a user flag. `user.update` required | ||||||
|  | - [ ] POST `/users/@me/reroll`: rerolls a user's short ID. `user.update` required | ||||||
|  | 
 | ||||||
|  | ## Members | ||||||
|  | 
 | ||||||
|  | - [ ] GET `/users/{userRef}/members`: gets list of a user's members. | ||||||
|  |   if the user's member list is hidden,  | ||||||
|  |   and it is not the authenticated user (or the token doesn't have the `member.read` scope) | ||||||
|  |   returns an empty array. | ||||||
|  | - [ ] GET `/users/{userRef}/members/{memberRef}`: gets a single member. | ||||||
|  |   will always return a member if it exists, even if the member is unlisted. | ||||||
|  | - [ ] POST `/users/@me/members`: creates a new member. `member.create` required | ||||||
|  | - [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required | ||||||
|  | - [ ] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required | ||||||
|  | - [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required. | ||||||
|  | -  | ||||||
|  | @ -26,6 +26,7 @@ public class Config | ||||||
|         public string? SentryUrl { get; init; } |         public string? SentryUrl { get; init; } | ||||||
|         public bool SentryTracing { get; init; } = false; |         public bool SentryTracing { get; init; } = false; | ||||||
|         public double SentryTracesSampleRate { get; init; } = 0.0; |         public double SentryTracesSampleRate { get; init; } = 0.0; | ||||||
|  |         public bool LogQueries { get; init; } = false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class DatabaseConfig |     public class DatabaseConfig | ||||||
|  |  | ||||||
|  | @ -1,29 +1,38 @@ | ||||||
| using Foxnouns.Backend.Database; | using Foxnouns.Backend.Database; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Controllers; | namespace Foxnouns.Backend.Controllers; | ||||||
| 
 | 
 | ||||||
| [Route("/api/v2/meta")] | [Route("/api/v2/meta")] | ||||||
| public class MetaController(DatabaseContext db) : ApiControllerBase | public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBase | ||||||
| { | { | ||||||
|  |     private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; | ||||||
|  | 
 | ||||||
|     [HttpGet] |     [HttpGet] | ||||||
|     [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))] |     [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))] | ||||||
|     public async Task<IActionResult> GetMeta() |     public async Task<IActionResult> GetMeta() | ||||||
|     { |     { | ||||||
|         var userCount = await db.Users.CountAsync(); |         var now = clock.GetCurrentInstant(); | ||||||
|  |         var users = await db.Users.Select(u => u.LastActive).ToListAsync(); | ||||||
|         var memberCount = await db.Members.CountAsync(); |         var memberCount = await db.Members.CountAsync(); | ||||||
| 
 | 
 | ||||||
|         return Ok(new MetaResponse( |         return Ok(new MetaResponse( | ||||||
|             BuildInfo.Version, BuildInfo.Hash, memberCount, |             Repository, BuildInfo.Version, BuildInfo.Hash, memberCount, | ||||||
|             new UserInfo(userCount, 0, 0, 0)) |             new UserInfo( | ||||||
|  |                 users.Count, | ||||||
|  |                 users.Count(i => i > now - Duration.FromDays(30)), | ||||||
|  |                 users.Count(i => i > now - Duration.FromDays(7)), | ||||||
|  |                 users.Count(i => i > now - Duration.FromDays(1)) | ||||||
|  |             )) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpGet("/api/v2/coffee")] |     [HttpGet("/api/v2/coffee")] | ||||||
|     public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); |     public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); | ||||||
| 
 | 
 | ||||||
|     private record MetaResponse(string Version, string Hash, int Members, UserInfo Users); |     private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users); | ||||||
| 
 | 
 | ||||||
|     private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); |     private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); | ||||||
| } | } | ||||||
|  | @ -10,6 +10,7 @@ namespace Foxnouns.Backend.Database; | ||||||
| public class DatabaseContext : DbContext | public class DatabaseContext : DbContext | ||||||
| { | { | ||||||
|     private readonly NpgsqlDataSource _dataSource; |     private readonly NpgsqlDataSource _dataSource; | ||||||
|  |     private readonly ILoggerFactory? _loggerFactory; | ||||||
| 
 | 
 | ||||||
|     public DbSet<User> Users { get; set; } |     public DbSet<User> Users { get; set; } | ||||||
|     public DbSet<Member> Members { get; set; } |     public DbSet<Member> Members { get; set; } | ||||||
|  | @ -19,7 +20,7 @@ public class DatabaseContext : DbContext | ||||||
|     public DbSet<Application> Applications { get; set; } |     public DbSet<Application> Applications { get; set; } | ||||||
|     public DbSet<TemporaryKey> TemporaryKeys { get; set; } |     public DbSet<TemporaryKey> TemporaryKeys { get; set; } | ||||||
| 
 | 
 | ||||||
|     public DatabaseContext(Config config) |     public DatabaseContext(Config config, ILoggerFactory? loggerFactory) | ||||||
|     { |     { | ||||||
|         var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) |         var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) | ||||||
|         { |         { | ||||||
|  | @ -30,13 +31,15 @@ public class DatabaseContext : DbContext | ||||||
|         var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); |         var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); | ||||||
|         dataSourceBuilder.UseNodaTime(); |         dataSourceBuilder.UseNodaTime(); | ||||||
|         _dataSource = dataSourceBuilder.Build(); |         _dataSource = dataSourceBuilder.Build(); | ||||||
|  |         _loggerFactory = loggerFactory; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|         => optionsBuilder |         => optionsBuilder | ||||||
|             .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) |             .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) | ||||||
|             .UseNpgsql(_dataSource, o => o.UseNodaTime()) |             .UseNpgsql(_dataSource, o => o.UseNodaTime()) | ||||||
|             .UseSnakeCaseNamingConvention(); |             .UseSnakeCaseNamingConvention() | ||||||
|  |             .UseLoggerFactory(_loggerFactory); | ||||||
| 
 | 
 | ||||||
|     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) |     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) | ||||||
|     { |     { | ||||||
|  | @ -73,6 +76,6 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory<Data | ||||||
|             // Get the configuration as our config class |             // Get the configuration as our config class | ||||||
|             .Get<Config>() ?? new(); |             .Get<Config>() ?? new(); | ||||||
| 
 | 
 | ||||||
|         return new DatabaseContext(config); |         return new DatabaseContext(config, null); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -15,11 +15,13 @@ public static class DatabaseQueryExtensions | ||||||
|         if (Snowflake.TryParse(userRef, out var snowflake)) |         if (Snowflake.TryParse(userRef, out var snowflake)) | ||||||
|         { |         { | ||||||
|             user = await context.Users |             user = await context.Users | ||||||
|  |                 .Where(u => !u.Deleted) | ||||||
|                 .FirstOrDefaultAsync(u => u.Id == snowflake); |                 .FirstOrDefaultAsync(u => u.Id == snowflake); | ||||||
|             if (user != null) return user; |             if (user != null) return user; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         user = await context.Users |         user = await context.Users | ||||||
|  |             .Where(u => !u.Deleted) | ||||||
|             .FirstOrDefaultAsync(u => u.Username == userRef); |             .FirstOrDefaultAsync(u => u.Username == userRef); | ||||||
|         if (user != null) return user; |         if (user != null) return user; | ||||||
|         throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); |         throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); | ||||||
|  | @ -28,6 +30,7 @@ public static class DatabaseQueryExtensions | ||||||
|     public static async Task<User> ResolveUserAsync(this DatabaseContext context, Snowflake id) |     public static async Task<User> ResolveUserAsync(this DatabaseContext context, Snowflake id) | ||||||
|     { |     { | ||||||
|         var user = await context.Users |         var user = await context.Users | ||||||
|  |             .Where(u => !u.Deleted) | ||||||
|             .FirstOrDefaultAsync(u => u.Id == id); |             .FirstOrDefaultAsync(u => u.Id == id); | ||||||
|         if (user != null) return user; |         if (user != null) return user; | ||||||
|         throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound); |         throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound); | ||||||
|  | @ -37,6 +40,7 @@ public static class DatabaseQueryExtensions | ||||||
|     { |     { | ||||||
|         var member = await context.Members |         var member = await context.Members | ||||||
|             .Include(m => m.User) |             .Include(m => m.User) | ||||||
|  |             .Where(m => !m.User.Deleted) | ||||||
|             .FirstOrDefaultAsync(m => m.Id == id); |             .FirstOrDefaultAsync(m => m.Id == id); | ||||||
|         if (member != null) return member; |         if (member != null) return member; | ||||||
|         throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound); |         throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound); | ||||||
|  | @ -56,12 +60,14 @@ public static class DatabaseQueryExtensions | ||||||
|         { |         { | ||||||
|             member = await context.Members |             member = await context.Members | ||||||
|                 .Include(m => m.User) |                 .Include(m => m.User) | ||||||
|  |                 .Where(m => !m.User.Deleted) | ||||||
|                 .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId); |                 .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId); | ||||||
|             if (member != null) return member; |             if (member != null) return member; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         member = await context.Members |         member = await context.Members | ||||||
|             .Include(m => m.User) |             .Include(m => m.User) | ||||||
|  |             .Where(m => !m.User.Deleted) | ||||||
|             .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId); |             .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId); | ||||||
|         if (member != null) return member; |         if (member != null) return member; | ||||||
|         throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound); |         throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound); | ||||||
|  |  | ||||||
							
								
								
									
										515
									
								
								Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										515
									
								
								Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,515 @@ | ||||||
|  | // <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("20240712233806_AddUserLastActive")] | ||||||
|  |     partial class AddUserLastActive | ||||||
|  |     { | ||||||
|  |         /// <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.TemporaryKey", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant>("Expires") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expires"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Key") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("key"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Value") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("value"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_temporary_keys"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("Key") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_temporary_keys_key"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("temporary_keys", (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<Instant>("LastActive") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("last_active"); | ||||||
|  | 
 | ||||||
|  |                     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,30 @@ | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  | 
 | ||||||
|  | #nullable disable | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Database.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddUserLastActive : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<Instant>( | ||||||
|  |                 name: "last_active", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "timestamp with time zone", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValueSql: "now()"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "last_active", | ||||||
|  |                 table: "users"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										528
									
								
								Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										528
									
								
								Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,528 @@ | ||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | 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("20240713000719_AddDeleted")] | ||||||
|  |     partial class AddDeleted | ||||||
|  |     { | ||||||
|  |         /// <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.TemporaryKey", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<long>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  | 
 | ||||||
|  |                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant>("Expires") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expires"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Key") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("key"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("Value") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("value"); | ||||||
|  | 
 | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_temporary_keys"); | ||||||
|  | 
 | ||||||
|  |                     b.HasIndex("Key") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_temporary_keys_key"); | ||||||
|  | 
 | ||||||
|  |                     b.ToTable("temporary_keys", (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<bool>("Deleted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("deleted"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long?>("DeletedBy") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("deleted_by"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<string>("DisplayName") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("display_name"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant>("LastActive") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("last_active"); | ||||||
|  | 
 | ||||||
|  |                     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,50 @@ | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  | 
 | ||||||
|  | #nullable disable | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Database.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddDeleted : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "deleted", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.AddColumn<Instant>( | ||||||
|  |                 name: "deleted_at", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "timestamp with time zone", | ||||||
|  |                 nullable: true); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.AddColumn<long>( | ||||||
|  |                 name: "deleted_by", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "bigint", | ||||||
|  |                 nullable: true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "deleted", | ||||||
|  |                 table: "users"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "deleted_at", | ||||||
|  |                 table: "users"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "deleted_by", | ||||||
|  |                 table: "users"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -267,10 +267,26 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .HasColumnType("text") |                         .HasColumnType("text") | ||||||
|                         .HasColumnName("bio"); |                         .HasColumnName("bio"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<bool>("Deleted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("deleted"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  | 
 | ||||||
|  |                     b.Property<long?>("DeletedBy") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("deleted_by"); | ||||||
|  | 
 | ||||||
|                     b.Property<string>("DisplayName") |                     b.Property<string>("DisplayName") | ||||||
|                         .HasColumnType("text") |                         .HasColumnType("text") | ||||||
|                         .HasColumnName("display_name"); |                         .HasColumnName("display_name"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<Instant>("LastActive") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("last_active"); | ||||||
|  | 
 | ||||||
|                     b.Property<string[]>("Links") |                     b.Property<string[]>("Links") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("text[]") |                         .HasColumnType("text[]") | ||||||
|  | @ -335,7 +351,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasConstraintName("fk_members_users_user_id"); |                         .HasConstraintName("fk_members_users_user_id"); | ||||||
| 
 | 
 | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Fields#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 => |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("MemberId") |                             b1.Property<long>("MemberId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -345,7 +361,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
| 
 | 
 | ||||||
|                             b1.HasKey("MemberId"); |                             b1.HasKey("MemberId"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("members", (string)null); |                             b1.ToTable("members"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("fields"); |                             b1.ToJson("fields"); | ||||||
| 
 | 
 | ||||||
|  | @ -354,7 +370,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                                 .HasConstraintName("fk_members_members_id"); |                                 .HasConstraintName("fk_members_members_id"); | ||||||
|                         }); |                         }); | ||||||
| 
 | 
 | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Names#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 => |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("MemberId") |                             b1.Property<long>("MemberId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -364,7 +380,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
| 
 | 
 | ||||||
|                             b1.HasKey("MemberId"); |                             b1.HasKey("MemberId"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("members", (string)null); |                             b1.ToTable("members"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("names"); |                             b1.ToJson("names"); | ||||||
| 
 | 
 | ||||||
|  | @ -373,7 +389,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                                 .HasConstraintName("fk_members_members_id"); |                                 .HasConstraintName("fk_members_members_id"); | ||||||
|                         }); |                         }); | ||||||
| 
 | 
 | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Pronouns#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 => |                     b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("MemberId") |                             b1.Property<long>("MemberId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -383,7 +399,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
| 
 | 
 | ||||||
|                             b1.HasKey("MemberId"); |                             b1.HasKey("MemberId"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("members", (string)null); |                             b1.ToTable("members"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("pronouns"); |                             b1.ToJson("pronouns"); | ||||||
| 
 | 
 | ||||||
|  | @ -427,7 +443,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => |             modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => | ||||||
|                 { |                 { | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("UserId") |                             b1.Property<long>("UserId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -438,7 +454,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                             b1.HasKey("UserId") |                             b1.HasKey("UserId") | ||||||
|                                 .HasName("pk_users"); |                                 .HasName("pk_users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("users", (string)null); |                             b1.ToTable("users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("fields"); |                             b1.ToJson("fields"); | ||||||
| 
 | 
 | ||||||
|  | @ -447,7 +463,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                                 .HasConstraintName("fk_users_users_user_id"); |                                 .HasConstraintName("fk_users_users_user_id"); | ||||||
|                         }); |                         }); | ||||||
| 
 | 
 | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("UserId") |                             b1.Property<long>("UserId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -458,7 +474,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                             b1.HasKey("UserId") |                             b1.HasKey("UserId") | ||||||
|                                 .HasName("pk_users"); |                                 .HasName("pk_users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("users", (string)null); |                             b1.ToTable("users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("names"); |                             b1.ToJson("names"); | ||||||
| 
 | 
 | ||||||
|  | @ -467,7 +483,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                                 .HasConstraintName("fk_users_users_user_id"); |                                 .HasConstraintName("fk_users_users_user_id"); | ||||||
|                         }); |                         }); | ||||||
| 
 | 
 | ||||||
|                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => |                     b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => | ||||||
|                         { |                         { | ||||||
|                             b1.Property<long>("UserId") |                             b1.Property<long>("UserId") | ||||||
|                                 .HasColumnType("bigint"); |                                 .HasColumnType("bigint"); | ||||||
|  | @ -478,7 +494,7 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                             b1.HasKey("UserId") |                             b1.HasKey("UserId") | ||||||
|                                 .HasName("pk_users"); |                                 .HasName("pk_users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToTable("users", (string)null); |                             b1.ToTable("users"); | ||||||
| 
 | 
 | ||||||
|                             b1.ToJson("pronouns"); |                             b1.ToJson("pronouns"); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| using System.Text.RegularExpressions; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Database.Models; | namespace Foxnouns.Backend.Database.Models; | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +22,13 @@ public class User : BaseModel | ||||||
| 
 | 
 | ||||||
|     public List<Member> Members { get; } = []; |     public List<Member> Members { get; } = []; | ||||||
|     public List<AuthMethod> AuthMethods { get; } = []; |     public List<AuthMethod> AuthMethods { get; } = []; | ||||||
|  | 
 | ||||||
|  |     public required Instant LastActive { get; set; } | ||||||
|  | 
 | ||||||
|  |     public bool Deleted { get; set; } | ||||||
|  |     public Instant? DeletedAt { get; set; } | ||||||
|  |     public Snowflake? DeletedBy { get; set; } | ||||||
|  |     [NotMapped] public bool? SelfDelete => Deleted ? DeletedBy != null : null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| public enum UserRole | public enum UserRole | ||||||
|  |  | ||||||
|  | @ -24,6 +24,8 @@ public static class WebApplicationExtensions | ||||||
|             // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. |             // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. | ||||||
|             // Serilog doesn't disable the built-in logs, so we do it here. |             // Serilog doesn't disable the built-in logs, so we do it here. | ||||||
|             .MinimumLevel.Override("Microsoft", LogEventLevel.Information) |             .MinimumLevel.Override("Microsoft", LogEventLevel.Information) | ||||||
|  |             .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", | ||||||
|  |                 config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Warning) | ||||||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) |             .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) | ||||||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) |             .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) | ||||||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) |             .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) | ||||||
|  | @ -34,10 +36,8 @@ public static class WebApplicationExtensions | ||||||
|             logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); |             logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Log.Logger = logCfg.CreateLogger(); |  | ||||||
| 
 |  | ||||||
|         // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. |         // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. | ||||||
|         builder.Services.AddSerilog().AddSingleton(Log.Logger); |         builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger()); | ||||||
| 
 | 
 | ||||||
|         return builder; |         return builder; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Services; | namespace Foxnouns.Backend.Services; | ||||||
| 
 | 
 | ||||||
| public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) | public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) | ||||||
| { | { | ||||||
|     private readonly PasswordHasher<User> _passwordHasher = new(); |     private readonly PasswordHasher<User> _passwordHasher = new(); | ||||||
| 
 | 
 | ||||||
|  | @ -26,7 +26,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator | ||||||
|             { |             { | ||||||
|                 new AuthMethod |                 new AuthMethod | ||||||
|                     { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } |                     { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } | ||||||
|             } |             }, | ||||||
|  |             LastActive = clock.GetCurrentInstant() | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         db.Add(user); |         db.Add(user); | ||||||
|  | @ -59,7 +60,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator | ||||||
|                     Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId, |                     Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId, | ||||||
|                     RemoteUsername = remoteUsername, FediverseApplication = instance |                     RemoteUsername = remoteUsername, FediverseApplication = instance | ||||||
|                 } |                 } | ||||||
|             } |             }, | ||||||
|  |             LastActive = clock.GetCurrentInstant() | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         db.Add(user); |         db.Add(user); | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; | ||||||
| using Foxnouns.Backend.Utils; | using Foxnouns.Backend.Utils; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||||
|  | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Services; | namespace Foxnouns.Backend.Services; | ||||||
| 
 | 
 | ||||||
|  | @ -14,12 +15,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe | ||||||
|         bool renderAuthMethods = false) |         bool renderAuthMethods = false) | ||||||
|     { |     { | ||||||
|         var isSelfUser = selfUser?.Id == user.Id; |         var isSelfUser = selfUser?.Id == user.Id; | ||||||
|         var tokenCanReadHiddenMembers = token.HasScope("member.read"); |         var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; | ||||||
|         var tokenCanReadAuth = token.HasScope("user.read_privileged"); |         var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser; | ||||||
| 
 | 
 | ||||||
|         renderMembers = renderMembers && |         renderMembers = renderMembers && | ||||||
|                         (!user.ListHidden || (isSelfUser && tokenCanReadHiddenMembers)); |                         (!user.ListHidden || tokenCanReadHiddenMembers); | ||||||
|         renderAuthMethods = renderAuthMethods && isSelfUser && tokenCanReadAuth; |         renderAuthMethods = renderAuthMethods && tokenPrivileged; | ||||||
| 
 | 
 | ||||||
|         IEnumerable<Member> members = |         IEnumerable<Member> members = | ||||||
|             renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; |             renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; | ||||||
|  | @ -34,7 +35,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe | ||||||
|             : []; |             : []; | ||||||
| 
 | 
 | ||||||
|         return new UserResponse( |         return new UserResponse( | ||||||
|             user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, |             user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, | ||||||
|  |             user.Names, | ||||||
|             user.Pronouns, user.Fields, |             user.Pronouns, user.Fields, | ||||||
|             renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, |             renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, | ||||||
|             renderAuthMethods |             renderAuthMethods | ||||||
|  | @ -42,7 +44,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe | ||||||
|                     a.Id, a.AuthType, a.RemoteId, |                     a.Id, a.AuthType, a.RemoteId, | ||||||
|                     a.RemoteUsername, a.FediverseApplication?.Domain |                     a.RemoteUsername, a.FediverseApplication?.Domain | ||||||
|                 )) |                 )) | ||||||
|                 : null |                 : null, | ||||||
|  |             tokenPrivileged ? user.LastActive : null | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -63,7 +66,9 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe | ||||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] |         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||||
|         IEnumerable<MemberRendererService.PartialMember>? Members, |         IEnumerable<MemberRendererService.PartialMember>? Members, | ||||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] |         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||||
|         IEnumerable<AuthenticationMethodResponse>? AuthMethods |         IEnumerable<AuthenticationMethodResponse>? AuthMethods, | ||||||
|  |         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||||
|  |         Instant? LastActive | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     public record AuthenticationMethodResponse( |     public record AuthenticationMethodResponse( | ||||||
|  |  | ||||||
|  | @ -75,7 +75,6 @@ public static class AuthUtils | ||||||
|         } |         } | ||||||
|         catch (Exception e) |         catch (Exception e) | ||||||
|         { |         { | ||||||
|             Console.WriteLine($"Error converting string: {e}"); |  | ||||||
|             bytes = []; |             bytes = []; | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -18,11 +18,12 @@ SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0 | ||||||
| SentryTracing = true | SentryTracing = true | ||||||
| ; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all) | ; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all) | ||||||
| SentryTracesSampleRate = 1.0 | SentryTracesSampleRate = 1.0 | ||||||
|  | ; Whether to log SQL queries. Note that this is very verbose. Defaults to false. | ||||||
|  | LogQueries = false | ||||||
| 
 | 
 | ||||||
| [Database] | [Database] | ||||||
| ; The database URL in ADO.NET format. | ; The database URL in ADO.NET format. | ||||||
| Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" | 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. | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								SCOPES.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								SCOPES.md
									
										
									
									
									
								
							|  | @ -1,18 +0,0 @@ | ||||||
| # List of API endpoints and scopes |  | ||||||
| 
 |  | ||||||
| ## Scopes |  | ||||||
| 
 |  | ||||||
| - `identify`: `@me` will refer to token user (always granted) |  | ||||||
| - `user.read_privileged`: can read privileged information such as authentication methods |  | ||||||
| - `user.update`: can update the user's profile. |  | ||||||
|   **cannot** update anything locked behind `user.read_privileged` |  | ||||||
| - `member.read`: can view member list if it's hidden and enumerate unlisted members |  | ||||||
| - `member.create`: can create new members |  | ||||||
| - `member.update`: can edit and delete members |  | ||||||
| 
 |  | ||||||
| ## Users |  | ||||||
| 
 |  | ||||||
| - GET `/users/{userRef}`: `identify` required to use `@me` as user reference. |  | ||||||
|   `user.read_privileged` required to view authentication methods. |  | ||||||
|   `member.read` required to view unlisted members. |  | ||||||
| - PATCH `/users/@me`: `user.update` required. |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue