feat(backend): initial /api/v1/users endpoint
This commit is contained in:
		
							parent
							
								
									5e7df2e074
								
							
						
					
					
						commit
						7791c91960
					
				
					 20 changed files with 385 additions and 4 deletions
				
			
		|  | @ -22,6 +22,7 @@ using Foxnouns.Backend.Services; | ||||||
| using Foxnouns.Backend.Utils; | using Foxnouns.Backend.Utils; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using XidNet; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Controllers; | namespace Foxnouns.Backend.Controllers; | ||||||
| 
 | 
 | ||||||
|  | @ -64,6 +65,7 @@ public class FlagsController( | ||||||
|         var flag = new PrideFlag |         var flag = new PrideFlag | ||||||
|         { |         { | ||||||
|             Id = snowflakeGenerator.GenerateSnowflake(), |             Id = snowflakeGenerator.GenerateSnowflake(), | ||||||
|  |             LegacyId = Xid.NewXid().ToString(), | ||||||
|             UserId = CurrentUser!.Id, |             UserId = CurrentUser!.Id, | ||||||
|             Name = req.Name, |             Name = req.Name, | ||||||
|             Description = req.Description, |             Description = req.Description, | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Storage; | using Microsoft.EntityFrameworkCore.Storage; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using XidNet; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Controllers; | namespace Foxnouns.Backend.Controllers; | ||||||
| 
 | 
 | ||||||
|  | @ -101,6 +102,7 @@ public class MembersController( | ||||||
|         var member = new Member |         var member = new Member | ||||||
|         { |         { | ||||||
|             Id = snowflakeGenerator.GenerateSnowflake(), |             Id = snowflakeGenerator.GenerateSnowflake(), | ||||||
|  |             LegacyId = Xid.NewXid().ToString(), | ||||||
|             User = CurrentUser!, |             User = CurrentUser!, | ||||||
|             Name = req.Name, |             Name = req.Name, | ||||||
|             DisplayName = req.DisplayName, |             DisplayName = req.DisplayName, | ||||||
|  |  | ||||||
|  | @ -222,7 +222,7 @@ public class UsersController( | ||||||
|             .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) |             .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) | ||||||
|             .ToDictionary(); |             .ToDictionary(); | ||||||
| 
 | 
 | ||||||
|         foreach (CustomPreferenceUpdateRequest? r in req) |         foreach (CustomPreferenceUpdateRequest r in req) | ||||||
|         { |         { | ||||||
|             if (r.Id != null && preferences.ContainsKey(r.Id.Value)) |             if (r.Id != null && preferences.ContainsKey(r.Id.Value)) | ||||||
|             { |             { | ||||||
|  | @ -233,6 +233,7 @@ public class UsersController( | ||||||
|                     Muted = r.Muted, |                     Muted = r.Muted, | ||||||
|                     Size = r.Size, |                     Size = r.Size, | ||||||
|                     Tooltip = r.Tooltip, |                     Tooltip = r.Tooltip, | ||||||
|  |                     LegacyId = preferences[r.Id.Value].LegacyId, | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|  | @ -244,6 +245,7 @@ public class UsersController( | ||||||
|                     Muted = r.Muted, |                     Muted = r.Muted, | ||||||
|                     Size = r.Size, |                     Size = r.Size, | ||||||
|                     Tooltip = r.Tooltip, |                     Tooltip = r.Tooltip, | ||||||
|  |                     LegacyId = Guid.NewGuid(), | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
|  | using Foxnouns.Backend.Services.V1; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Controllers.V1; | ||||||
|  | 
 | ||||||
|  | [Route("/api/v1/users")] | ||||||
|  | public class UsersV1Controller(UsersV1Service usersV1Service) : ApiControllerBase | ||||||
|  | { | ||||||
|  |     [HttpGet("{userRef}")] | ||||||
|  |     public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) | ||||||
|  |     { | ||||||
|  |         User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); | ||||||
|  |         return Ok(await usersV1Service.RenderUserAsync(user)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -139,6 +139,26 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) | ||||||
|         modelBuilder |         modelBuilder | ||||||
|             .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!) |             .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!) | ||||||
|             .HasName("find_free_member_sid"); |             .HasName("find_free_member_sid"); | ||||||
|  | 
 | ||||||
|  |         // Indexes for legacy IDs for APIv1 | ||||||
|  |         modelBuilder.Entity<User>().HasIndex(u => u.LegacyId).IsUnique(); | ||||||
|  |         modelBuilder.Entity<Member>().HasIndex(m => m.LegacyId).IsUnique(); | ||||||
|  |         modelBuilder.Entity<PrideFlag>().HasIndex(f => f.LegacyId).IsUnique(); | ||||||
|  | 
 | ||||||
|  |         // a UUID is not an xid, but this should always be set by the application anyway. | ||||||
|  |         // we're just setting it here to shut EFCore up because squashing migrations is for nerds | ||||||
|  |         modelBuilder | ||||||
|  |             .Entity<User>() | ||||||
|  |             .Property(u => u.LegacyId) | ||||||
|  |             .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|  |         modelBuilder | ||||||
|  |             .Entity<Member>() | ||||||
|  |             .Property(m => m.LegacyId) | ||||||
|  |             .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|  |         modelBuilder | ||||||
|  |             .Entity<PrideFlag>() | ||||||
|  |             .Property(f => f.LegacyId) | ||||||
|  |             .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// <summary> |     /// <summary> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | 
 | ||||||
|  | #nullable disable | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Database.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     [DbContext(typeof(DatabaseContext))] | ||||||
|  |     [Migration("20241225155818_AddLegacyIds")] | ||||||
|  |     public partial class AddLegacyIds : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "legacy_id", | ||||||
|  |                 table: "users", | ||||||
|  |                 type: "text", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValueSql: "gen_random_uuid()" | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "legacy_id", | ||||||
|  |                 table: "pride_flags", | ||||||
|  |                 type: "text", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValueSql: "gen_random_uuid()" | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "legacy_id", | ||||||
|  |                 table: "members", | ||||||
|  |                 type: "text", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValueSql: "gen_random_uuid()" | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_users_legacy_id", | ||||||
|  |                 table: "users", | ||||||
|  |                 column: "legacy_id", | ||||||
|  |                 unique: true | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_pride_flags_legacy_id", | ||||||
|  |                 table: "pride_flags", | ||||||
|  |                 column: "legacy_id", | ||||||
|  |                 unique: true | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_members_legacy_id", | ||||||
|  |                 table: "members", | ||||||
|  |                 column: "legacy_id", | ||||||
|  |                 unique: true | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropIndex(name: "ix_users_legacy_id", table: "users"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropIndex(name: "ix_pride_flags_legacy_id", table: "pride_flags"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropIndex(name: "ix_members_legacy_id", table: "members"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropColumn(name: "legacy_id", table: "users"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropColumn(name: "legacy_id", table: "pride_flags"); | ||||||
|  | 
 | ||||||
|  |             migrationBuilder.DropColumn(name: "legacy_id", table: "members"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -254,6 +254,13 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .HasColumnType("jsonb") |                         .HasColumnType("jsonb") | ||||||
|                         .HasColumnName("fields"); |                         .HasColumnName("fields"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<string>("LegacyId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("legacy_id") | ||||||
|  |                         .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|  | 
 | ||||||
|                     b.PrimitiveCollection<string[]>("Links") |                     b.PrimitiveCollection<string[]>("Links") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("text[]") |                         .HasColumnType("text[]") | ||||||
|  | @ -292,6 +299,10 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                     b.HasKey("Id") |                     b.HasKey("Id") | ||||||
|                         .HasName("pk_members"); |                         .HasName("pk_members"); | ||||||
| 
 | 
 | ||||||
|  |                     b.HasIndex("LegacyId") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_members_legacy_id"); | ||||||
|  | 
 | ||||||
|                     b.HasIndex("Sid") |                     b.HasIndex("Sid") | ||||||
|                         .IsUnique() |                         .IsUnique() | ||||||
|                         .HasDatabaseName("ix_members_sid"); |                         .HasDatabaseName("ix_members_sid"); | ||||||
|  | @ -386,6 +397,13 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .HasColumnType("text") |                         .HasColumnType("text") | ||||||
|                         .HasColumnName("hash"); |                         .HasColumnName("hash"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<string>("LegacyId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("legacy_id") | ||||||
|  |                         .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|  | 
 | ||||||
|                     b.Property<string>("Name") |                     b.Property<string>("Name") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("text") |                         .HasColumnType("text") | ||||||
|  | @ -398,6 +416,10 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                     b.HasKey("Id") |                     b.HasKey("Id") | ||||||
|                         .HasName("pk_pride_flags"); |                         .HasName("pk_pride_flags"); | ||||||
| 
 | 
 | ||||||
|  |                     b.HasIndex("LegacyId") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_pride_flags_legacy_id"); | ||||||
|  | 
 | ||||||
|                     b.HasIndex("UserId") |                     b.HasIndex("UserId") | ||||||
|                         .HasDatabaseName("ix_pride_flags_user_id"); |                         .HasDatabaseName("ix_pride_flags_user_id"); | ||||||
| 
 | 
 | ||||||
|  | @ -582,6 +604,13 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                         .HasColumnType("timestamp with time zone") |                         .HasColumnType("timestamp with time zone") | ||||||
|                         .HasColumnName("last_sid_reroll"); |                         .HasColumnName("last_sid_reroll"); | ||||||
| 
 | 
 | ||||||
|  |                     b.Property<string>("LegacyId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("legacy_id") | ||||||
|  |                         .HasDefaultValueSql("gen_random_uuid()"); | ||||||
|  | 
 | ||||||
|                     b.PrimitiveCollection<string[]>("Links") |                     b.PrimitiveCollection<string[]>("Links") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("text[]") |                         .HasColumnType("text[]") | ||||||
|  | @ -637,6 +666,10 @@ namespace Foxnouns.Backend.Database.Migrations | ||||||
|                     b.HasKey("Id") |                     b.HasKey("Id") | ||||||
|                         .HasName("pk_users"); |                         .HasName("pk_users"); | ||||||
| 
 | 
 | ||||||
|  |                     b.HasIndex("LegacyId") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_users_legacy_id"); | ||||||
|  | 
 | ||||||
|                     b.HasIndex("Sid") |                     b.HasIndex("Sid") | ||||||
|                         .IsUnique() |                         .IsUnique() | ||||||
|                         .HasDatabaseName("ix_users_sid"); |                         .HasDatabaseName("ix_users_sid"); | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ public class Member : BaseModel | ||||||
| { | { | ||||||
|     public required string Name { get; set; } |     public required string Name { get; set; } | ||||||
|     public string Sid { get; set; } = string.Empty; |     public string Sid { get; set; } = string.Empty; | ||||||
|  |     public required string LegacyId { get; init; } | ||||||
|     public string? DisplayName { get; set; } |     public string? DisplayName { get; set; } | ||||||
|     public string? Bio { get; set; } |     public string? Bio { get; set; } | ||||||
|     public string? Avatar { get; set; } |     public string? Avatar { get; set; } | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ namespace Foxnouns.Backend.Database.Models; | ||||||
| public class PrideFlag : BaseModel | public class PrideFlag : BaseModel | ||||||
| { | { | ||||||
|     public required Snowflake UserId { get; init; } |     public required Snowflake UserId { get; init; } | ||||||
|  |     public required string LegacyId { get; init; } | ||||||
| 
 | 
 | ||||||
|     // A null hash means the flag hasn't been processed yet. |     // A null hash means the flag hasn't been processed yet. | ||||||
|     public string? Hash { get; set; } |     public string? Hash { get; set; } | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ public class User : BaseModel | ||||||
| { | { | ||||||
|     public required string Username { get; set; } |     public required string Username { get; set; } | ||||||
|     public string Sid { get; set; } = string.Empty; |     public string Sid { get; set; } = string.Empty; | ||||||
|  |     public required string LegacyId { get; init; } | ||||||
|     public string? DisplayName { get; set; } |     public string? DisplayName { get; set; } | ||||||
|     public string? Bio { get; set; } |     public string? Bio { get; set; } | ||||||
|     public string? MemberTitle { get; set; } |     public string? MemberTitle { get; set; } | ||||||
|  | @ -69,6 +70,8 @@ public class User : BaseModel | ||||||
|         // This type is generally serialized directly, so the converter is applied here. |         // This type is generally serialized directly, so the converter is applied here. | ||||||
|         [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] |         [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] | ||||||
|         public PreferenceSize Size { get; set; } |         public PreferenceSize Size { get; set; } | ||||||
|  | 
 | ||||||
|  |         public Guid LegacyId { get; init; } = Guid.NewGuid(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static readonly Duration DeleteAfter = Duration.FromDays(30); |     public static readonly Duration DeleteAfter = Duration.FromDays(30); | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ public record UserResponse( | ||||||
|     IEnumerable<FieldEntry> Names, |     IEnumerable<FieldEntry> Names, | ||||||
|     IEnumerable<Pronoun> Pronouns, |     IEnumerable<Pronoun> Pronouns, | ||||||
|     IEnumerable<Field> Fields, |     IEnumerable<Field> Fields, | ||||||
|     Dictionary<Snowflake, User.CustomPreference> CustomPreferences, |     Dictionary<Snowflake, CustomPreferenceResponse> CustomPreferences, | ||||||
|     IEnumerable<PrideFlagResponse> Flags, |     IEnumerable<PrideFlagResponse> Flags, | ||||||
|     int? UtcOffset, |     int? UtcOffset, | ||||||
|     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, |     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, | ||||||
|  | @ -52,6 +52,14 @@ public record UserResponse( | ||||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted |     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | public record CustomPreferenceResponse( | ||||||
|  |     string Icon, | ||||||
|  |     string Tooltip, | ||||||
|  |     bool Muted, | ||||||
|  |     bool Favourite, | ||||||
|  |     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] PreferenceSize Size | ||||||
|  | ); | ||||||
|  | 
 | ||||||
| public record AuthMethodResponse( | public record AuthMethodResponse( | ||||||
|     Snowflake Id, |     Snowflake Id, | ||||||
|     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, |     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||||
|  |  | ||||||
							
								
								
									
										77
									
								
								Foxnouns.Backend/Dto/V1/User.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Foxnouns.Backend/Dto/V1/User.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | ||||||
|  | // ReSharper disable NotAccessedPositionalProperty.Global | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
|  | using Foxnouns.Backend.Services.V1; | ||||||
|  | using Newtonsoft.Json; | ||||||
|  | using Newtonsoft.Json.Converters; | ||||||
|  | using Newtonsoft.Json.Serialization; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Dto.V1; | ||||||
|  | 
 | ||||||
|  | public record UserResponse( | ||||||
|  |     string Id, | ||||||
|  |     Snowflake IdNew, | ||||||
|  |     string Sid, | ||||||
|  |     string Name, | ||||||
|  |     string? DisplayName, | ||||||
|  |     string? Bio, | ||||||
|  |     string? MemberTitle, | ||||||
|  |     string? Avatar, | ||||||
|  |     string[] Links, | ||||||
|  |     FieldEntry[] Names, | ||||||
|  |     PronounEntry[] Pronouns, | ||||||
|  |     ProfileField[] Fields, | ||||||
|  |     int? UtcOffset, | ||||||
|  |     Dictionary<Guid, CustomPreference> CustomPreferences | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | public record CustomPreference( | ||||||
|  |     string Icon, | ||||||
|  |     string Tooltip, | ||||||
|  |     [property: JsonConverter(typeof(StringEnumConverter), typeof(SnakeCaseNamingStrategy))] | ||||||
|  |         PreferenceSize Size, | ||||||
|  |     bool Muted, | ||||||
|  |     bool Favourite | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | public record ProfileField(string Name, FieldEntry[] Entries) | ||||||
|  | { | ||||||
|  |     public static ProfileField FromField( | ||||||
|  |         Field field, | ||||||
|  |         Dictionary<Snowflake, User.CustomPreference> customPreferences | ||||||
|  |     ) => new(field.Name, FieldEntry.FromEntries(field.Entries, customPreferences)); | ||||||
|  | 
 | ||||||
|  |     public static ProfileField[] FromFields( | ||||||
|  |         IEnumerable<Field> fields, | ||||||
|  |         Dictionary<Snowflake, User.CustomPreference> customPreferences | ||||||
|  |     ) => fields.Select(f => FromField(f, customPreferences)).ToArray(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | public record FieldEntry(string Value, string Status) | ||||||
|  | { | ||||||
|  |     public static FieldEntry[] FromEntries( | ||||||
|  |         IEnumerable<Foxnouns.Backend.Database.Models.FieldEntry> entries, | ||||||
|  |         Dictionary<Snowflake, User.CustomPreference> customPreferences | ||||||
|  |     ) => | ||||||
|  |         entries | ||||||
|  |             .Select(e => new FieldEntry( | ||||||
|  |                 e.Value, | ||||||
|  |                 V1Utils.TranslateStatus(e.Status, customPreferences) | ||||||
|  |             )) | ||||||
|  |             .ToArray(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | public record PronounEntry(string Pronouns, string? DisplayText, string Status) | ||||||
|  | { | ||||||
|  |     public static PronounEntry[] FromPronouns( | ||||||
|  |         IEnumerable<Pronoun> pronouns, | ||||||
|  |         Dictionary<Snowflake, User.CustomPreference> customPreferences | ||||||
|  |     ) => | ||||||
|  |         pronouns | ||||||
|  |             .Select(p => new PronounEntry( | ||||||
|  |                 p.Value, | ||||||
|  |                 p.DisplayText, | ||||||
|  |                 V1Utils.TranslateStatus(p.Status, customPreferences) | ||||||
|  |             )) | ||||||
|  |             .ToArray(); | ||||||
|  | } | ||||||
|  | @ -19,6 +19,7 @@ using Foxnouns.Backend.Jobs; | ||||||
| using Foxnouns.Backend.Middleware; | using Foxnouns.Backend.Middleware; | ||||||
| using Foxnouns.Backend.Services; | using Foxnouns.Backend.Services; | ||||||
| using Foxnouns.Backend.Services.Auth; | using Foxnouns.Backend.Services.Auth; | ||||||
|  | using Foxnouns.Backend.Services.V1; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Minio; | using Minio; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | @ -127,7 +128,9 @@ public static class WebApplicationExtensions | ||||||
|                     .AddTransient<MemberAvatarUpdateInvocable>() |                     .AddTransient<MemberAvatarUpdateInvocable>() | ||||||
|                     .AddTransient<UserAvatarUpdateInvocable>() |                     .AddTransient<UserAvatarUpdateInvocable>() | ||||||
|                     .AddTransient<CreateFlagInvocable>() |                     .AddTransient<CreateFlagInvocable>() | ||||||
|                     .AddTransient<CreateDataExportInvocable>(); |                     .AddTransient<CreateDataExportInvocable>() | ||||||
|  |                     // Legacy services | ||||||
|  |                     .AddScoped<UsersV1Service>(); | ||||||
| 
 | 
 | ||||||
|                 if (!config.Logging.EnableMetrics) |                 if (!config.Logging.EnableMetrics) | ||||||
|                     services.AddHostedService<BackgroundMetricsCollectionService>(); |                     services.AddHostedService<BackgroundMetricsCollectionService>(); | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ | ||||||
|         <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/> |         <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/> | ||||||
|         <PackageReference Include="System.Text.Json" Version="9.0.0"/> |         <PackageReference Include="System.Text.Json" Version="9.0.0"/> | ||||||
|         <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/> |         <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/> | ||||||
|  |         <PackageReference Include="Yort.Xid.Net" Version="2.0.1"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| 
 | 
 | ||||||
|     <Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation"> |     <Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation"> | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ using Foxnouns.Backend.Utils; | ||||||
| using Microsoft.AspNetCore.Identity; | using Microsoft.AspNetCore.Identity; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using XidNet; | ||||||
| 
 | 
 | ||||||
| namespace Foxnouns.Backend.Services.Auth; | namespace Foxnouns.Backend.Services.Auth; | ||||||
| 
 | 
 | ||||||
|  | @ -70,6 +71,7 @@ public class AuthService( | ||||||
|             }, |             }, | ||||||
|             LastActive = clock.GetCurrentInstant(), |             LastActive = clock.GetCurrentInstant(), | ||||||
|             Sid = null!, |             Sid = null!, | ||||||
|  |             LegacyId = Xid.NewXid().ToString(), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         db.Add(user); |         db.Add(user); | ||||||
|  | @ -116,6 +118,7 @@ public class AuthService( | ||||||
|             }, |             }, | ||||||
|             LastActive = clock.GetCurrentInstant(), |             LastActive = clock.GetCurrentInstant(), | ||||||
|             Sid = null!, |             Sid = null!, | ||||||
|  |             LegacyId = Xid.NewXid().ToString(), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         db.Add(user); |         db.Add(user); | ||||||
|  |  | ||||||
|  | @ -103,7 +103,8 @@ public class UserRendererService( | ||||||
|             user.Names, |             user.Names, | ||||||
|             user.Pronouns, |             user.Pronouns, | ||||||
|             user.Fields, |             user.Fields, | ||||||
|             user.CustomPreferences, |             user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))) | ||||||
|  |                 .ToDictionary(), | ||||||
|             flags.Select(f => RenderPrideFlag(f.PrideFlag)), |             flags.Select(f => RenderPrideFlag(f.PrideFlag)), | ||||||
|             utcOffset, |             utcOffset, | ||||||
|             user.Role, |             user.Role, | ||||||
|  | @ -130,6 +131,14 @@ public class UserRendererService( | ||||||
|                 : a.RemoteUsername |                 : a.RemoteUsername | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |     public static CustomPreferenceResponse RenderCustomPreference(User.CustomPreference pref) => | ||||||
|  |         new(pref.Icon, pref.Tooltip, pref.Muted, pref.Favourite, pref.Size); | ||||||
|  | 
 | ||||||
|  |     public static Dictionary<Snowflake, CustomPreferenceResponse> RenderCustomPreferences( | ||||||
|  |         User user | ||||||
|  |     ) => | ||||||
|  |         user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))).ToDictionary(); | ||||||
|  | 
 | ||||||
|     public PartialUser RenderPartialUser(User user) => |     public PartialUser RenderPartialUser(User user) => | ||||||
|         new( |         new( | ||||||
|             user.Id, |             user.Id, | ||||||
|  |  | ||||||
							
								
								
									
										92
									
								
								Foxnouns.Backend/Services/V1/UsersV1Service.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								Foxnouns.Backend/Services/V1/UsersV1Service.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
|  | using Foxnouns.Backend.Dto.V1; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Services.V1; | ||||||
|  | 
 | ||||||
|  | public class UsersV1Service(DatabaseContext db) | ||||||
|  | { | ||||||
|  |     public async Task<User> ResolveUserAsync( | ||||||
|  |         string userRef, | ||||||
|  |         Token? token, | ||||||
|  |         CancellationToken ct = default | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         if (userRef == "@me") | ||||||
|  |         { | ||||||
|  |             if (token == null) | ||||||
|  |             { | ||||||
|  |                 throw new ApiError.Unauthorized( | ||||||
|  |                     "This endpoint requires an authenticated user.", | ||||||
|  |                     ErrorCode.AuthenticationRequired | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return await db.Users.FirstAsync(u => u.Id == token.UserId, ct); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         User? user; | ||||||
|  |         if (Snowflake.TryParse(userRef, out Snowflake? sf)) | ||||||
|  |         { | ||||||
|  |             user = await db.Users.FirstOrDefaultAsync(u => u.Id == sf && !u.Deleted, ct); | ||||||
|  |             if (user != null) | ||||||
|  |                 return user; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         user = await db.Users.FirstOrDefaultAsync(u => u.LegacyId == userRef && !u.Deleted, ct); | ||||||
|  |         if (user != null) | ||||||
|  |             return user; | ||||||
|  | 
 | ||||||
|  |         user = await db.Users.FirstOrDefaultAsync(u => u.Username == userRef && !u.Deleted, ct); | ||||||
|  |         if (user != null) | ||||||
|  |             return user; | ||||||
|  | 
 | ||||||
|  |         throw new ApiError.NotFound( | ||||||
|  |             "No user with that ID or username found.", | ||||||
|  |             ErrorCode.UserNotFound | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async Task<UserResponse> RenderUserAsync(User user) | ||||||
|  |     { | ||||||
|  |         int? utcOffset = null; | ||||||
|  |         if ( | ||||||
|  |             user.Timezone != null | ||||||
|  |             && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz) | ||||||
|  |         ) | ||||||
|  |         { | ||||||
|  |             utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new UserResponse( | ||||||
|  |             user.LegacyId, | ||||||
|  |             user.Id, | ||||||
|  |             user.Sid, | ||||||
|  |             user.Username, | ||||||
|  |             user.DisplayName, | ||||||
|  |             user.Bio, | ||||||
|  |             user.MemberTitle, | ||||||
|  |             user.Avatar, | ||||||
|  |             user.Links, | ||||||
|  |             FieldEntry.FromEntries(user.Names, user.CustomPreferences), | ||||||
|  |             PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), | ||||||
|  |             ProfileField.FromFields(user.Fields, user.CustomPreferences), | ||||||
|  |             utcOffset, | ||||||
|  |             user.CustomPreferences.Select(x => | ||||||
|  |                     ( | ||||||
|  |                         x.Value.LegacyId, | ||||||
|  |                         new CustomPreference( | ||||||
|  |                             x.Value.Icon, | ||||||
|  |                             x.Value.Tooltip, | ||||||
|  |                             x.Value.Size, | ||||||
|  |                             x.Value.Muted, | ||||||
|  |                             x.Value.Favourite | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 .ToDictionary() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								Foxnouns.Backend/Services/V1/V1Utils.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Foxnouns.Backend/Services/V1/V1Utils.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | using Foxnouns.Backend.Database; | ||||||
|  | using Foxnouns.Backend.Database.Models; | ||||||
|  | 
 | ||||||
|  | namespace Foxnouns.Backend.Services.V1; | ||||||
|  | 
 | ||||||
|  | public static class V1Utils | ||||||
|  | { | ||||||
|  |     public static string TranslateStatus( | ||||||
|  |         string status, | ||||||
|  |         Dictionary<Snowflake, User.CustomPreference> customPreferences | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         if (!Snowflake.TryParse(status, out Snowflake? sf)) | ||||||
|  |             return status; | ||||||
|  | 
 | ||||||
|  |         return customPreferences.TryGetValue(sf.Value, out User.CustomPreference? cf) | ||||||
|  |             ? cf.LegacyId.ToString() | ||||||
|  |             : "unknown"; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -293,6 +293,12 @@ | ||||||
|           "System.Runtime": "4.3.1" |           "System.Runtime": "4.3.1" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|  |       "Yort.Xid.Net": { | ||||||
|  |         "type": "Direct", | ||||||
|  |         "requested": "[2.0.1, )", | ||||||
|  |         "resolved": "2.0.1", | ||||||
|  |         "contentHash": "+3sNX7/RKSKheVuMz9jtWLazD+R4PXpx8va2d9SdDgvKOhETbEb0VYis8K/fD1qm/qOQT57LadToSpzReGMZlw==" | ||||||
|  |       }, | ||||||
|       "BouncyCastle.Cryptography": { |       "BouncyCastle.Cryptography": { | ||||||
|         "type": "Transitive", |         "type": "Transitive", | ||||||
|         "resolved": "2.5.0", |         "resolved": "2.5.0", | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ public class UserMigrator( | ||||||
|         _user = new User |         _user = new User | ||||||
|         { |         { | ||||||
|             Id = goUser.SnowflakeId, |             Id = goUser.SnowflakeId, | ||||||
|  |             LegacyId = goUser.Id, | ||||||
|             Username = goUser.Username, |             Username = goUser.Username, | ||||||
|             DisplayName = goUser.DisplayName, |             DisplayName = goUser.DisplayName, | ||||||
|             Bio = goUser.Bio, |             Bio = goUser.Bio, | ||||||
|  | @ -139,6 +140,7 @@ public class UserMigrator( | ||||||
|                 new PrideFlag |                 new PrideFlag | ||||||
|                 { |                 { | ||||||
|                     Id = flag.SnowflakeId, |                     Id = flag.SnowflakeId, | ||||||
|  |                     LegacyId = flag.Id, | ||||||
|                     UserId = _user!.Id, |                     UserId = _user!.Id, | ||||||
|                     Hash = flag.Hash, |                     Hash = flag.Hash, | ||||||
|                     Name = flag.Name, |                     Name = flag.Name, | ||||||
|  | @ -190,6 +192,7 @@ public class UserMigrator( | ||||||
|             UserId = _user!.Id, |             UserId = _user!.Id, | ||||||
|             Name = goMember.Name, |             Name = goMember.Name, | ||||||
|             Sid = goMember.Sid, |             Sid = goMember.Sid, | ||||||
|  |             LegacyId = goMember.Id, | ||||||
|             DisplayName = goMember.DisplayName, |             DisplayName = goMember.DisplayName, | ||||||
|             Bio = goMember.Bio, |             Bio = goMember.Bio, | ||||||
|             Avatar = goMember.Avatar, |             Avatar = goMember.Avatar, | ||||||
|  | @ -235,6 +238,7 @@ public class UserMigrator( | ||||||
|                     "small" => PreferenceSize.Small, |                     "small" => PreferenceSize.Small, | ||||||
|                     _ => PreferenceSize.Normal, |                     _ => PreferenceSize.Normal, | ||||||
|                 }, |                 }, | ||||||
|  |                 LegacyId = new Guid(id), | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue