Compare commits
2 commits
e24c4f9b00
...
7791c91960
Author | SHA1 | Date | |
---|---|---|---|
7791c91960 | |||
5e7df2e074 |
23 changed files with 484 additions and 7 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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,3 +64,11 @@
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.big-footer {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: bootstrap.shade-color(bootstrap.$dark, 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: bootstrap.shade-color(bootstrap.$light, 5%);
|
||||||
|
}
|
||||||
|
|
83
Foxnouns.Frontend/src/lib/components/Footer.svelte
Normal file
83
Foxnouns.Frontend/src/lib/components/Footer.svelte
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Meta } from "$api/models";
|
||||||
|
import Git from "svelte-bootstrap-icons/lib/Git.svelte";
|
||||||
|
import Reception4 from "svelte-bootstrap-icons/lib/Reception4.svelte";
|
||||||
|
import Newspaper from "svelte-bootstrap-icons/lib/Newspaper.svelte";
|
||||||
|
import CardText from "svelte-bootstrap-icons/lib/CardText.svelte";
|
||||||
|
import Shield from "svelte-bootstrap-icons/lib/Shield.svelte";
|
||||||
|
import Envelope from "svelte-bootstrap-icons/lib/Envelope.svelte";
|
||||||
|
import CashCoin from "svelte-bootstrap-icons/lib/CashCoin.svelte";
|
||||||
|
import Logo from "./Logo.svelte";
|
||||||
|
|
||||||
|
type Props = { meta: Meta };
|
||||||
|
let { meta }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class="big-footer mt-3 pt-3 pb-1 px-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row mb-2">
|
||||||
|
<div class="align-start flex-grow-1">
|
||||||
|
<Logo />
|
||||||
|
<ul class="mt-2 list-unstyled">
|
||||||
|
<li><strong>Version</strong> {meta.version}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="align-end">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{meta.users.total} <strong>users</strong></li>
|
||||||
|
<li>{meta.members} <strong>members</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-inline">
|
||||||
|
<a
|
||||||
|
class="list-inline-item link-underline link-underline-opacity-0"
|
||||||
|
target="_blank"
|
||||||
|
href={meta.repository}
|
||||||
|
>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Git />
|
||||||
|
Source code
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="list-inline-item link-underline link-underline-opacity-0"
|
||||||
|
target="_blank"
|
||||||
|
href="https://status.pronouns.cc"
|
||||||
|
>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Reception4 />
|
||||||
|
Status
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/about">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Envelope />
|
||||||
|
About and contact
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/tos">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<CardText />
|
||||||
|
Terms of service
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/privacy">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Shield />
|
||||||
|
Privacy policy
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/changelog">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Newspaper />
|
||||||
|
Changelog
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/donate">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<CashCoin />
|
||||||
|
Donate
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
|
@ -3,11 +3,16 @@
|
||||||
import "../app.scss";
|
import "../app.scss";
|
||||||
import type { LayoutData } from "./$types";
|
import type { LayoutData } from "./$types";
|
||||||
import Navbar from "$components/Navbar.svelte";
|
import Navbar from "$components/Navbar.svelte";
|
||||||
|
import Footer from "$components/Footer.svelte";
|
||||||
|
|
||||||
type Props = { children: Snippet; data: LayoutData };
|
type Props = { children: Snippet; data: LayoutData };
|
||||||
let { children, data }: Props = $props();
|
let { children, data }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column min-vh-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
<Navbar user={data.meUser} meta={data.meta} />
|
<Navbar user={data.meUser} meta={data.meta} />
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
<Footer meta={data.meta} />
|
||||||
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue