diff --git a/.editorconfig b/.editorconfig
index e6b41f9..22061dc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,7 +7,7 @@ resharper_not_accessed_positional_property_local_highlighting = none
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false
-csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion
+csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion
# ReSharper properties
resharper_align_multiline_binary_expressions_chain = false
diff --git a/.gitignore b/.gitignore
index 9c16977..b1e845f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,8 @@ docker/proxy-config.json
docker/frontend.env
Foxnouns.DataMigrator/apps.json
+migration-tools/avatar-proxy/config.json
+migration-tools/avatar-migrator/.env
+
+out/
+build/
diff --git a/.husky/task-runner.json b/.husky/task-runner.json
index 576b8bc..72e6fea 100644
--- a/.husky/task-runner.json
+++ b/.husky/task-runner.json
@@ -3,12 +3,8 @@
"tasks": [
{
"name": "run-prettier",
- "command": "pnpm",
- "args": [
- "prettier",
- "-w",
- "${staged}"
- ],
+ "command": "npx",
+ "args": ["prettier", "-w", "${staged}"],
"include": [
"Foxnouns.Frontend/**/*.ts",
"Foxnouns.Frontend/**/*.json",
@@ -22,13 +18,8 @@
{
"name": "run-csharpier",
"command": "dotnet",
- "args": [
- "csharpier",
- "${staged}"
- ],
- "include": [
- "**/*.cs"
- ]
+ "args": ["csharpier", "${staged}"],
+ "include": ["**/*.cs"]
}
]
}
diff --git a/DOCKER.md b/DOCKER.md
index b485eb7..b670743 100644
--- a/DOCKER.md
+++ b/DOCKER.md
@@ -1,10 +1,29 @@
-# Running with Docker
+# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)*
+
+Because SvelteKit is a pain in the ass to build in a container, and processes secrets at build time,
+there is no pre-built frontend image available.
+If you don't want to build images on your server, I recommend running the frontend outside of Docker.
+This is preconfigured in `docker-compose.prebuilt.yml`: the backend, database, and rate limiter will run in Docker,
+while the frontend is run as a normal, non-containerized service.
1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking.
2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same.
-3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do th esame.
-4. Build with `docker compose build`
-5. Run with `docker compose up`
+3. Run with `docker compose up -f docker-compose.prebuilt.yml`
+
+The backend will listen on port 5001 and metrics will be available on port 5002.
+The rate limiter (which is what should be exposed to the outside) will listen on port 5003.
+You can use `docker/Caddyfile` as an example for your reverse proxy. If you use nginx, good luck.
+
+# Running with Docker (local builds)
+
+In order to run *everything* in Docker, you'll have to build every container yourself.
+The advantage of this is that it's an all-in-one solution, where you only have to point your reverse proxy at a single container.
+The disadvantage is that you'll likely have to build the images on the server you'll be running them on.
+
+1. Configure the backend and rate limiter as in the section above.
+2. Copy `docker/frontend.example.env` to `docker/frontend.env`, and configure it.
+3. Build with `docker compose build -f docker-compose.local.yml`
+4. Run with `docker compose up -f docker-compose.local.yml`
The Caddy server will listen on `localhost:5004` for the frontend and API,
and on `localhost:5005` for the profile URL shortener.
diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs
index 0ed8b7a..461e55e 100644
--- a/Foxnouns.Backend/Config.cs
+++ b/Foxnouns.Backend/Config.cs
@@ -26,7 +26,6 @@ public class Config
public string MediaBaseUrl { get; init; } = null!;
public string Address => $"http://{Host}:{Port}";
- public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}";
public LoggingConfig Logging { get; init; } = new();
public DatabaseConfig Database { get; init; } = new();
@@ -55,6 +54,7 @@ public class Config
public bool? EnablePooling { get; init; }
public int? Timeout { get; init; }
public int? MaxPoolSize { get; init; }
+ public string Redis { get; init; } = string.Empty;
}
public class StorageConfig
@@ -99,6 +99,11 @@ public class Config
{
public int MaxMemberCount { get; init; } = 1000;
+ public int MaxFields { get; init; } = 25;
+ public int MaxFieldNameLength { get; init; } = 100;
+ public int MaxFieldEntryTextLength { get; init; } = 100;
+ public int MaxFieldEntries { get; init; } = 100;
+
public int MaxUsernameLength { get; init; } = 40;
public int MaxMemberNameLength { get; init; } = 100;
public int MaxDisplayNameLength { get; init; } = 100;
diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs
index 0d95eb2..39d3b11 100644
--- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs
+++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs
@@ -46,7 +46,7 @@ public class AuthController(
config.GoogleAuth.Enabled,
config.TumblrAuth.Enabled
);
- string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct));
+ string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());
string? discord = null;
string? google = null;
string? tumblr = null;
diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs
index bdf4b9a..8024ee6 100644
--- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs
+++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs
@@ -56,7 +56,7 @@ public class EmailAuthController(
if (!req.Email.Contains('@'))
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
- string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct);
+ string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null);
// If there's already a user with that email address, pretend we sent an email but actually ignore it
if (
diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs
index 7f40625..0442386 100644
--- a/Foxnouns.Backend/Controllers/ExportsController.cs
+++ b/Foxnouns.Backend/Controllers/ExportsController.cs
@@ -12,7 +12,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
@@ -28,13 +27,8 @@ namespace Foxnouns.Backend.Controllers;
[Authorize("identify")]
[Limit(UsableByDeletedUsers = true)]
[ApiExplorerSettings(IgnoreApi = true)]
-public class ExportsController(
- ILogger logger,
- Config config,
- IClock clock,
- DatabaseContext db,
- IQueue queue
-) : ApiControllerBase
+public class ExportsController(ILogger logger, Config config, IClock clock, DatabaseContext db)
+ : ApiControllerBase
{
private static readonly Duration MinimumTimeBetween = Duration.FromDays(1);
private readonly ILogger _logger = logger.ForContext();
@@ -80,10 +74,7 @@ public class ExportsController(
throw new ApiError.BadRequest("You can't request a new data export so soon.");
}
- queue.QueueInvocableWithPayload(
- new CreateDataExportPayload(CurrentUser.Id)
- );
-
+ CreateDataExportJob.Enqueue(CurrentUser.Id);
return NoContent();
}
}
diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs
index e976072..bed022a 100644
--- a/Foxnouns.Backend/Controllers/FlagsController.cs
+++ b/Foxnouns.Backend/Controllers/FlagsController.cs
@@ -12,7 +12,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
@@ -30,8 +29,7 @@ namespace Foxnouns.Backend.Controllers;
public class FlagsController(
DatabaseContext db,
UserRendererService userRenderer,
- ISnowflakeGenerator snowflakeGenerator,
- IQueue queue
+ ISnowflakeGenerator snowflakeGenerator
) : ApiControllerBase
{
[HttpGet]
@@ -74,10 +72,7 @@ public class FlagsController(
db.Add(flag);
await db.SaveChangesAsync();
- queue.QueueInvocableWithPayload(
- new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image)
- );
-
+ CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image));
return Accepted(userRenderer.RenderPrideFlag(flag));
}
diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs
index 8f832c1..635dab9 100644
--- a/Foxnouns.Backend/Controllers/MembersController.cs
+++ b/Foxnouns.Backend/Controllers/MembersController.cs
@@ -12,7 +12,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-using Coravel.Queuing.Interfaces;
using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
@@ -37,7 +36,6 @@ public class MembersController(
MemberRendererService memberRenderer,
ISnowflakeGenerator snowflakeGenerator,
ObjectStorageService objectStorageService,
- IQueue queue,
IClock clock,
ValidationService validationService,
Config config
@@ -81,13 +79,13 @@ public class MembersController(
("display_name", validationService.ValidateDisplayName(req.DisplayName)),
("bio", validationService.ValidateBio(req.Bio)),
("avatar", validationService.ValidateAvatar(req.Avatar)),
- .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
- .. ValidationUtils.ValidateFieldEntries(
+ .. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
+ .. validationService.ValidateFieldEntries(
req.Names?.ToArray(),
CurrentUser!.CustomPreferences,
"names"
),
- .. ValidationUtils.ValidatePronouns(
+ .. validationService.ValidatePronouns(
req.Pronouns?.ToArray(),
CurrentUser!.CustomPreferences
),
@@ -123,6 +121,9 @@ public class MembersController(
CurrentUser!.Id
);
+ CurrentUser.LastActive = clock.GetCurrentInstant();
+ db.Update(CurrentUser);
+
try
{
await db.SaveChangesAsync(ct);
@@ -139,9 +140,7 @@ public class MembersController(
if (req.Avatar != null)
{
- queue.QueueInvocableWithPayload(
- new AvatarUpdatePayload(member.Id, req.Avatar)
- );
+ MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
}
return Ok(memberRenderer.RenderMember(member, CurrentToken));
@@ -191,7 +190,7 @@ public class MembersController(
if (req.Names != null)
{
errors.AddRange(
- ValidationUtils.ValidateFieldEntries(
+ validationService.ValidateFieldEntries(
req.Names,
CurrentUser!.CustomPreferences,
"names"
@@ -203,7 +202,7 @@ public class MembersController(
if (req.Pronouns != null)
{
errors.AddRange(
- ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
+ validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
);
member.Pronouns = req.Pronouns.ToList();
}
@@ -211,7 +210,10 @@ public class MembersController(
if (req.Fields != null)
{
errors.AddRange(
- ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
+ validationService.ValidateFields(
+ req.Fields.ToList(),
+ CurrentUser!.CustomPreferences
+ )
);
member.Fields = req.Fields.ToList();
}
@@ -236,11 +238,12 @@ public class MembersController(
// so it's in a separate block to the validation above.
if (req.HasProperty(nameof(req.Avatar)))
{
- queue.QueueInvocableWithPayload(
- new AvatarUpdatePayload(member.Id, req.Avatar)
- );
+ MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
}
+ CurrentUser.LastActive = clock.GetCurrentInstant();
+ db.Update(CurrentUser);
+
try
{
await db.SaveChangesAsync();
diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs
index 1f00a7a..0166e86 100644
--- a/Foxnouns.Backend/Controllers/MetaController.cs
+++ b/Foxnouns.Backend/Controllers/MetaController.cs
@@ -13,20 +13,23 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
using System.Text.RegularExpressions;
+using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
+using Foxnouns.Backend.Services.Caching;
using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc;
namespace Foxnouns.Backend.Controllers;
[Route("/api/v2/meta")]
-public partial class MetaController(Config config) : ApiControllerBase
+public partial class MetaController(Config config, NoticeCacheService noticeCache)
+ : ApiControllerBase
{
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public IActionResult GetMeta() =>
+ public async Task GetMeta(CancellationToken ct = default) =>
Ok(
new MetaResponse(
Repository,
@@ -45,10 +48,14 @@ public partial class MetaController(Config config) : ApiControllerBase
ValidationUtils.MaxCustomPreferences,
AuthUtils.MaxAuthMethodsPerType,
FlagsController.MaxFlagCount
- )
+ ),
+ Notice: NoticeResponse(await noticeCache.GetAsync(ct))
)
);
+ private static MetaNoticeResponse? NoticeResponse(Notice? notice) =>
+ notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message);
+
[HttpGet("page/{page}")]
public async Task GetStaticPageAsync(string page, CancellationToken ct = default)
{
@@ -71,7 +78,7 @@ public partial class MetaController(Config config) : ApiControllerBase
[HttpGet("/api/v2/coffee")]
public IActionResult BrewCoffee() =>
- Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
+ StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!");
[GeneratedRegex(@"^[a-z\-_]+$")]
private static partial Regex PageRegex();
diff --git a/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs b/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
new file mode 100644
index 0000000..3d2d6bb
--- /dev/null
+++ b/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
@@ -0,0 +1,77 @@
+using Foxnouns.Backend.Database;
+using Foxnouns.Backend.Database.Models;
+using Foxnouns.Backend.Dto;
+using Foxnouns.Backend.Middleware;
+using Foxnouns.Backend.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using NodaTime;
+
+namespace Foxnouns.Backend.Controllers.Moderation;
+
+[Route("/api/v2/notices")]
+[Authorize("user.moderation")]
+[Limit(RequireModerator = true)]
+public class NoticesController(
+ DatabaseContext db,
+ UserRendererService userRenderer,
+ ISnowflakeGenerator snowflakeGenerator,
+ IClock clock
+) : ApiControllerBase
+{
+ [HttpGet]
+ public async Task GetNoticesAsync(CancellationToken ct = default)
+ {
+ List notices = await db
+ .Notices.Include(n => n.Author)
+ .OrderByDescending(n => n.Id)
+ .ToListAsync(ct);
+ return Ok(notices.Select(RenderNotice));
+ }
+
+ [HttpPost]
+ public async Task CreateNoticeAsync(CreateNoticeRequest req)
+ {
+ Instant now = clock.GetCurrentInstant();
+ if (req.StartTime < now)
+ {
+ throw new ApiError.BadRequest(
+ "Start time cannot be in the past",
+ "start_time",
+ req.StartTime
+ );
+ }
+
+ if (req.EndTime < now)
+ {
+ throw new ApiError.BadRequest(
+ "End time cannot be in the past",
+ "end_time",
+ req.EndTime
+ );
+ }
+
+ var notice = new Notice
+ {
+ Id = snowflakeGenerator.GenerateSnowflake(),
+ Message = req.Message,
+ StartTime = req.StartTime ?? clock.GetCurrentInstant(),
+ EndTime = req.EndTime,
+ Author = CurrentUser!,
+ };
+
+ db.Add(notice);
+ await db.SaveChangesAsync();
+
+ return Ok(RenderNotice(notice));
+ }
+
+ private NoticeResponse RenderNotice(Notice notice) =>
+ new(
+ notice.Id,
+ notice.Message,
+ notice.StartTime,
+ notice.EndTime,
+ userRenderer.RenderPartialUser(notice.Author)
+ );
+}
diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs
index 6ccbff0..ed9a48f 100644
--- a/Foxnouns.Backend/Controllers/UsersController.cs
+++ b/Foxnouns.Backend/Controllers/UsersController.cs
@@ -12,7 +12,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-using Coravel.Queuing.Interfaces;
using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
@@ -34,7 +33,6 @@ public class UsersController(
ILogger logger,
UserRendererService userRenderer,
ISnowflakeGenerator snowflakeGenerator,
- IQueue queue,
IClock clock,
ValidationService validationService
) : ApiControllerBase
@@ -48,7 +46,15 @@ public class UsersController(
{
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
return Ok(
- await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct)
+ await userRenderer.RenderUserAsync(
+ user,
+ CurrentUser,
+ CurrentToken,
+ renderMembers: true,
+ renderAuthMethods: true,
+ renderSettings: true,
+ ct: ct
+ )
);
}
@@ -91,7 +97,7 @@ public class UsersController(
if (req.Names != null)
{
errors.AddRange(
- ValidationUtils.ValidateFieldEntries(
+ validationService.ValidateFieldEntries(
req.Names,
CurrentUser!.CustomPreferences,
"names"
@@ -103,7 +109,7 @@ public class UsersController(
if (req.Pronouns != null)
{
errors.AddRange(
- ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
+ validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
);
user.Pronouns = req.Pronouns.ToList();
}
@@ -111,7 +117,10 @@ public class UsersController(
if (req.Fields != null)
{
errors.AddRange(
- ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
+ validationService.ValidateFields(
+ req.Fields.ToList(),
+ CurrentUser!.CustomPreferences
+ )
);
user.Fields = req.Fields.ToList();
}
@@ -174,11 +183,11 @@ public class UsersController(
// so it's in a separate block to the validation above.
if (req.HasProperty(nameof(req.Avatar)))
{
- queue.QueueInvocableWithPayload(
- new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)
- );
+ UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
}
+ user.LastActive = clock.GetCurrentInstant();
+
try
{
await db.SaveChangesAsync(ct);
@@ -254,20 +263,12 @@ public class UsersController(
}
user.CustomPreferences = preferences;
+ user.LastActive = clock.GetCurrentInstant();
await db.SaveChangesAsync(ct);
return Ok(user.CustomPreferences);
}
- [HttpGet("@me/settings")]
- [Authorize("user.read_hidden")]
- [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
- public async Task GetUserSettingsAsync(CancellationToken ct = default)
- {
- User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct);
- return Ok(user.Settings);
- }
-
[HttpPatch("@me/settings")]
[Authorize("user.read_hidden", "user.update")]
[ProducesResponseType(statusCode: StatusCodes.Status200OK)]
@@ -280,7 +281,10 @@ public class UsersController(
if (req.HasProperty(nameof(req.DarkMode)))
user.Settings.DarkMode = req.DarkMode;
+ if (req.HasProperty(nameof(req.LastReadNotice)))
+ user.Settings.LastReadNotice = req.LastReadNotice;
+ user.LastActive = clock.GetCurrentInstant();
db.Update(user);
await db.SaveChangesAsync(ct);
diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs
index ae620f2..2bbcbc7 100644
--- a/Foxnouns.Backend/Database/DatabaseContext.cs
+++ b/Foxnouns.Backend/Database/DatabaseContext.cs
@@ -64,7 +64,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
public DbSet FediverseApplications { get; init; } = null!;
public DbSet Tokens { get; init; } = null!;
public DbSet Applications { get; init; } = null!;
- public DbSet TemporaryKeys { get; init; } = null!;
public DbSet DataExports { get; init; } = null!;
public DbSet PrideFlags { get; init; } = null!;
@@ -74,6 +73,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
public DbSet Reports { get; init; } = null!;
public DbSet AuditLog { get; init; } = null!;
public DbSet Notifications { get; init; } = null!;
+ public DbSet Notices { get; init; } = null!;
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
@@ -87,7 +87,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
modelBuilder.Entity().HasIndex(u => u.Sid).IsUnique();
modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique();
modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique();
- modelBuilder.Entity().HasIndex(k => k.Key).IsUnique();
modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique();
// Two indexes on auth_methods, one for fediverse auth and one for all other types.
diff --git a/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs b/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs
new file mode 100644
index 0000000..27a8ada
--- /dev/null
+++ b/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs
@@ -0,0 +1,55 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using NodaTime;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Foxnouns.Backend.Database.Migrations
+{
+ ///
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20250304155708_RemoveTemporaryKeys")]
+ public partial class RemoveTemporaryKeys : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(name: "temporary_keys");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "temporary_keys",
+ columns: table => new
+ {
+ id = table
+ .Column(type: "bigint", nullable: false)
+ .Annotation(
+ "Npgsql:ValueGenerationStrategy",
+ NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
+ ),
+ expires = table.Column(
+ type: "timestamp with time zone",
+ nullable: false
+ ),
+ key = table.Column(type: "text", nullable: false),
+ value = table.Column(type: "text", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_temporary_keys", x => x.id);
+ }
+ );
+
+ migrationBuilder.CreateIndex(
+ name: "ix_temporary_keys_key",
+ table: "temporary_keys",
+ column: "key",
+ unique: true
+ );
+ }
+ }
+}
diff --git a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
new file mode 100644
index 0000000..d2df141
--- /dev/null
+++ b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
@@ -0,0 +1,915 @@
+//
+using System.Collections.Generic;
+using Foxnouns.Backend.Database;
+using Foxnouns.Backend.Database.Models;
+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("20250329131053_AddNotices")]
+ partial class AddNotices
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_id");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_secret");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.PrimitiveCollection("RedirectUris")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("redirect_uris");
+
+ b.PrimitiveCollection("Scopes")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("scopes");
+
+ b.HasKey("Id")
+ .HasName("pk_applications");
+
+ b.ToTable("applications", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.PrimitiveCollection("ClearedFields")
+ .HasColumnType("text[]")
+ .HasColumnName("cleared_fields");
+
+ b.Property("ModeratorId")
+ .HasColumnType("bigint")
+ .HasColumnName("moderator_id");
+
+ b.Property("ModeratorUsername")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("moderator_username");
+
+ b.Property("Reason")
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("ReportId")
+ .HasColumnType("bigint")
+ .HasColumnName("report_id");
+
+ b.Property("TargetMemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_member_id");
+
+ b.Property("TargetMemberName")
+ .HasColumnType("text")
+ .HasColumnName("target_member_name");
+
+ b.Property("TargetUserId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_user_id");
+
+ b.Property("TargetUsername")
+ .HasColumnType("text")
+ .HasColumnName("target_username");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_audit_log");
+
+ b.HasIndex("ReportId")
+ .IsUnique()
+ .HasDatabaseName("ix_audit_log_report_id");
+
+ b.ToTable("audit_log", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AuthType")
+ .HasColumnType("integer")
+ .HasColumnName("auth_type");
+
+ b.Property("FediverseApplicationId")
+ .HasColumnType("bigint")
+ .HasColumnName("fediverse_application_id");
+
+ b.Property("RemoteId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("remote_id");
+
+ b.Property("RemoteUsername")
+ .HasColumnType("text")
+ .HasColumnName("remote_username");
+
+ b.Property("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.HasIndex("AuthType", "RemoteId")
+ .IsUnique()
+ .HasDatabaseName("ix_auth_methods_auth_type_remote_id")
+ .HasFilter("fediverse_application_id IS NULL");
+
+ b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
+ .IsUnique()
+ .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
+ .HasFilter("fediverse_application_id IS NOT NULL");
+
+ b.ToTable("auth_methods", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("filename");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_data_exports");
+
+ b.HasIndex("Filename")
+ .IsUnique()
+ .HasDatabaseName("ix_data_exports_filename");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_data_exports_user_id");
+
+ b.ToTable("data_exports", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_id");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_secret");
+
+ b.Property("Domain")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("domain");
+
+ b.Property("ForceRefresh")
+ .HasColumnType("boolean")
+ .HasColumnName("force_refresh");
+
+ b.Property("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("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Avatar")
+ .HasColumnType("text")
+ .HasColumnName("avatar");
+
+ b.Property("Bio")
+ .HasColumnType("text")
+ .HasColumnName("bio");
+
+ b.Property("DisplayName")
+ .HasColumnType("text")
+ .HasColumnName("display_name");
+
+ b.Property>("Fields")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("fields");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.PrimitiveCollection("Links")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("links");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property>("Names")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("names");
+
+ b.Property>("Pronouns")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("pronouns");
+
+ b.Property("Sid")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("sid")
+ .HasDefaultValueSql("find_free_member_sid()");
+
+ b.Property("Unlisted")
+ .HasColumnType("boolean")
+ .HasColumnName("unlisted");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_members");
+
+ b.HasIndex("LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_members_legacy_id");
+
+ b.HasIndex("Sid")
+ .IsUnique()
+ .HasDatabaseName("ix_members_sid");
+
+ b.HasIndex("UserId", "Name")
+ .IsUnique()
+ .HasDatabaseName("ix_members_user_id_name");
+
+ b.ToTable("members", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("MemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("member_id");
+
+ b.Property("PrideFlagId")
+ .HasColumnType("bigint")
+ .HasColumnName("pride_flag_id");
+
+ b.HasKey("Id")
+ .HasName("pk_member_flags");
+
+ b.HasIndex("MemberId")
+ .HasDatabaseName("ix_member_flags_member_id");
+
+ b.HasIndex("PrideFlagId")
+ .HasDatabaseName("ix_member_flags_pride_flag_id");
+
+ b.ToTable("member_flags", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AuthorId")
+ .HasColumnType("bigint")
+ .HasColumnName("author_id");
+
+ b.Property("EndTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("end_time");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_time");
+
+ b.HasKey("Id")
+ .HasName("pk_notices");
+
+ b.HasIndex("AuthorId")
+ .HasDatabaseName("ix_notices_author_id");
+
+ b.ToTable("notices", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AcknowledgedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("acknowledged_at");
+
+ b.Property("LocalizationKey")
+ .HasColumnType("text")
+ .HasColumnName("localization_key");
+
+ b.Property>("LocalizationParams")
+ .IsRequired()
+ .HasColumnType("hstore")
+ .HasColumnName("localization_params");
+
+ b.Property("Message")
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("TargetId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_id");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_notifications");
+
+ b.HasIndex("TargetId")
+ .HasDatabaseName("ix_notifications_target_id");
+
+ b.ToTable("notifications", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("Hash")
+ .HasColumnType("text")
+ .HasColumnName("hash");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_pride_flags");
+
+ b.HasIndex("LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_pride_flags_legacy_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_pride_flags_user_id");
+
+ b.ToTable("pride_flags", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Context")
+ .HasColumnType("text")
+ .HasColumnName("context");
+
+ b.Property("Reason")
+ .HasColumnType("integer")
+ .HasColumnName("reason");
+
+ b.Property("ReporterId")
+ .HasColumnType("bigint")
+ .HasColumnName("reporter_id");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property("TargetMemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_member_id");
+
+ b.Property("TargetSnapshot")
+ .HasColumnType("text")
+ .HasColumnName("target_snapshot");
+
+ b.Property("TargetType")
+ .HasColumnType("integer")
+ .HasColumnName("target_type");
+
+ b.Property("TargetUserId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_reports");
+
+ b.HasIndex("ReporterId")
+ .HasDatabaseName("ix_reports_reporter_id");
+
+ b.HasIndex("TargetMemberId")
+ .HasDatabaseName("ix_reports_target_member_id");
+
+ b.HasIndex("TargetUserId")
+ .HasDatabaseName("ix_reports_target_user_id");
+
+ b.ToTable("reports", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ApplicationId")
+ .HasColumnType("bigint")
+ .HasColumnName("application_id");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_at");
+
+ b.Property("Hash")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("hash");
+
+ b.Property("ManuallyExpired")
+ .HasColumnType("boolean")
+ .HasColumnName("manually_expired");
+
+ b.PrimitiveCollection("Scopes")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("scopes");
+
+ b.Property("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("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Avatar")
+ .HasColumnType("text")
+ .HasColumnName("avatar");
+
+ b.Property("Bio")
+ .HasColumnType("text")
+ .HasColumnName("bio");
+
+ b.Property>("CustomPreferences")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("custom_preferences");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasColumnName("deleted_by");
+
+ b.Property("DisplayName")
+ .HasColumnType("text")
+ .HasColumnName("display_name");
+
+ b.Property>("Fields")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("fields");
+
+ b.Property("LastActive")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_active");
+
+ b.Property("LastSidReroll")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_sid_reroll");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.PrimitiveCollection("Links")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("links");
+
+ b.Property("ListHidden")
+ .HasColumnType("boolean")
+ .HasColumnName("list_hidden");
+
+ b.Property("MemberTitle")
+ .HasColumnType("text")
+ .HasColumnName("member_title");
+
+ b.Property>("Names")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("names");
+
+ b.Property("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property>("Pronouns")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("pronouns");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property("Settings")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("settings");
+
+ b.Property("Sid")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("sid")
+ .HasDefaultValueSql("find_free_user_sid()");
+
+ b.Property("Timezone")
+ .HasColumnType("text")
+ .HasColumnName("timezone");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_users_legacy_id");
+
+ b.HasIndex("Sid")
+ .IsUnique()
+ .HasDatabaseName("ix_users_sid");
+
+ b.HasIndex("Username")
+ .IsUnique()
+ .HasDatabaseName("ix_users_username");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("PrideFlagId")
+ .HasColumnType("bigint")
+ .HasColumnName("pride_flag_id");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_user_flags");
+
+ b.HasIndex("PrideFlagId")
+ .HasDatabaseName("ix_user_flags_pride_flag_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_user_flags_user_id");
+
+ b.ToTable("user_flags", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
+ .WithOne("AuditLogEntry")
+ .HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("fk_audit_log_reports_report_id");
+
+ b.Navigation("Report");
+ });
+
+ 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.DataExport", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
+ .WithMany("DataExports")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_data_exports_users_user_id");
+
+ 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.Navigation("User");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.Member", null)
+ .WithMany("ProfileFlags")
+ .HasForeignKey("MemberId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_member_flags_members_member_id");
+
+ b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
+ .WithMany()
+ .HasForeignKey("PrideFlagId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_member_flags_pride_flags_pride_flag_id");
+
+ b.Navigation("PrideFlag");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
+ .WithMany()
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_notices_users_author_id");
+
+ b.Navigation("Author");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
+ .WithMany()
+ .HasForeignKey("TargetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_notifications_users_target_id");
+
+ b.Navigation("Target");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.User", null)
+ .WithMany("Flags")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_pride_flags_users_user_id");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter")
+ .WithMany()
+ .HasForeignKey("ReporterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_reports_users_reporter_id");
+
+ b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember")
+ .WithMany()
+ .HasForeignKey("TargetMemberId")
+ .HasConstraintName("fk_reports_members_target_member_id");
+
+ b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser")
+ .WithMany()
+ .HasForeignKey("TargetUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_reports_users_target_user_id");
+
+ b.Navigation("Reporter");
+
+ b.Navigation("TargetMember");
+
+ b.Navigation("TargetUser");
+ });
+
+ 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.UserFlag", b =>
+ {
+ b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
+ .WithMany()
+ .HasForeignKey("PrideFlagId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_flags_pride_flags_pride_flag_id");
+
+ b.HasOne("Foxnouns.Backend.Database.Models.User", null)
+ .WithMany("ProfileFlags")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_flags_users_user_id");
+
+ b.Navigation("PrideFlag");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
+ {
+ b.Navigation("ProfileFlags");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
+ {
+ b.Navigation("AuditLogEntry");
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
+ {
+ b.Navigation("AuthMethods");
+
+ b.Navigation("DataExports");
+
+ b.Navigation("Flags");
+
+ b.Navigation("Members");
+
+ b.Navigation("ProfileFlags");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs
new file mode 100644
index 0000000..24c5166
--- /dev/null
+++ b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs
@@ -0,0 +1,56 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using NodaTime;
+
+#nullable disable
+
+namespace Foxnouns.Backend.Database.Migrations
+{
+ ///
+ public partial class AddNotices : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "notices",
+ columns: table => new
+ {
+ id = table.Column(type: "bigint", nullable: false),
+ message = table.Column(type: "text", nullable: false),
+ start_time = table.Column(
+ type: "timestamp with time zone",
+ nullable: false
+ ),
+ end_time = table.Column(
+ type: "timestamp with time zone",
+ nullable: false
+ ),
+ author_id = table.Column(type: "bigint", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_notices", x => x.id);
+ table.ForeignKey(
+ name: "fk_notices_users_author_id",
+ column: x => x.author_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade
+ );
+ }
+ );
+
+ migrationBuilder.CreateIndex(
+ name: "ix_notices_author_id",
+ table: "notices",
+ column: "author_id"
+ );
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(name: "notices");
+ }
+ }
+}
diff --git a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs
new file mode 100644
index 0000000..cb9377d
--- /dev/null
+++ b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs
@@ -0,0 +1,923 @@
+//
+using System.Collections.Generic;
+using Foxnouns.Backend.Database;
+using Foxnouns.Backend.Database.Models;
+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("20250410192220_AddAvatarMigrations")]
+ partial class AddAvatarMigrations
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_id");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_secret");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.PrimitiveCollection("RedirectUris")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("redirect_uris");
+
+ b.PrimitiveCollection("Scopes")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("scopes");
+
+ b.HasKey("Id")
+ .HasName("pk_applications");
+
+ b.ToTable("applications", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.PrimitiveCollection("ClearedFields")
+ .HasColumnType("text[]")
+ .HasColumnName("cleared_fields");
+
+ b.Property("ModeratorId")
+ .HasColumnType("bigint")
+ .HasColumnName("moderator_id");
+
+ b.Property("ModeratorUsername")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("moderator_username");
+
+ b.Property("Reason")
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("ReportId")
+ .HasColumnType("bigint")
+ .HasColumnName("report_id");
+
+ b.Property("TargetMemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_member_id");
+
+ b.Property("TargetMemberName")
+ .HasColumnType("text")
+ .HasColumnName("target_member_name");
+
+ b.Property("TargetUserId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_user_id");
+
+ b.Property("TargetUsername")
+ .HasColumnType("text")
+ .HasColumnName("target_username");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_audit_log");
+
+ b.HasIndex("ReportId")
+ .IsUnique()
+ .HasDatabaseName("ix_audit_log_report_id");
+
+ b.ToTable("audit_log", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AuthType")
+ .HasColumnType("integer")
+ .HasColumnName("auth_type");
+
+ b.Property("FediverseApplicationId")
+ .HasColumnType("bigint")
+ .HasColumnName("fediverse_application_id");
+
+ b.Property("RemoteId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("remote_id");
+
+ b.Property("RemoteUsername")
+ .HasColumnType("text")
+ .HasColumnName("remote_username");
+
+ b.Property("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.HasIndex("AuthType", "RemoteId")
+ .IsUnique()
+ .HasDatabaseName("ix_auth_methods_auth_type_remote_id")
+ .HasFilter("fediverse_application_id IS NULL");
+
+ b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
+ .IsUnique()
+ .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
+ .HasFilter("fediverse_application_id IS NOT NULL");
+
+ b.ToTable("auth_methods", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("filename");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_data_exports");
+
+ b.HasIndex("Filename")
+ .IsUnique()
+ .HasDatabaseName("ix_data_exports_filename");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_data_exports_user_id");
+
+ b.ToTable("data_exports", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_id");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("client_secret");
+
+ b.Property("Domain")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("domain");
+
+ b.Property("ForceRefresh")
+ .HasColumnType("boolean")
+ .HasColumnName("force_refresh");
+
+ b.Property("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("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Avatar")
+ .HasColumnType("text")
+ .HasColumnName("avatar");
+
+ b.Property("AvatarMigrated")
+ .HasColumnType("boolean")
+ .HasColumnName("avatar_migrated");
+
+ b.Property("Bio")
+ .HasColumnType("text")
+ .HasColumnName("bio");
+
+ b.Property("DisplayName")
+ .HasColumnType("text")
+ .HasColumnName("display_name");
+
+ b.Property>("Fields")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("fields");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.PrimitiveCollection("Links")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("links");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property>("Names")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("names");
+
+ b.Property>("Pronouns")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("pronouns");
+
+ b.Property("Sid")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("sid")
+ .HasDefaultValueSql("find_free_member_sid()");
+
+ b.Property("Unlisted")
+ .HasColumnType("boolean")
+ .HasColumnName("unlisted");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_members");
+
+ b.HasIndex("LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_members_legacy_id");
+
+ b.HasIndex("Sid")
+ .IsUnique()
+ .HasDatabaseName("ix_members_sid");
+
+ b.HasIndex("UserId", "Name")
+ .IsUnique()
+ .HasDatabaseName("ix_members_user_id_name");
+
+ b.ToTable("members", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("MemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("member_id");
+
+ b.Property("PrideFlagId")
+ .HasColumnType("bigint")
+ .HasColumnName("pride_flag_id");
+
+ b.HasKey("Id")
+ .HasName("pk_member_flags");
+
+ b.HasIndex("MemberId")
+ .HasDatabaseName("ix_member_flags_member_id");
+
+ b.HasIndex("PrideFlagId")
+ .HasDatabaseName("ix_member_flags_pride_flag_id");
+
+ b.ToTable("member_flags", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AuthorId")
+ .HasColumnType("bigint")
+ .HasColumnName("author_id");
+
+ b.Property("EndTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("end_time");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_time");
+
+ b.HasKey("Id")
+ .HasName("pk_notices");
+
+ b.HasIndex("AuthorId")
+ .HasDatabaseName("ix_notices_author_id");
+
+ b.ToTable("notices", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("AcknowledgedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("acknowledged_at");
+
+ b.Property("LocalizationKey")
+ .HasColumnType("text")
+ .HasColumnName("localization_key");
+
+ b.Property>("LocalizationParams")
+ .IsRequired()
+ .HasColumnType("hstore")
+ .HasColumnName("localization_params");
+
+ b.Property("Message")
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("TargetId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_id");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_notifications");
+
+ b.HasIndex("TargetId")
+ .HasDatabaseName("ix_notifications_target_id");
+
+ b.ToTable("notifications", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("Hash")
+ .HasColumnType("text")
+ .HasColumnName("hash");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_pride_flags");
+
+ b.HasIndex("LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_pride_flags_legacy_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_pride_flags_user_id");
+
+ b.ToTable("pride_flags", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Context")
+ .HasColumnType("text")
+ .HasColumnName("context");
+
+ b.Property("Reason")
+ .HasColumnType("integer")
+ .HasColumnName("reason");
+
+ b.Property("ReporterId")
+ .HasColumnType("bigint")
+ .HasColumnName("reporter_id");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property("TargetMemberId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_member_id");
+
+ b.Property("TargetSnapshot")
+ .HasColumnType("text")
+ .HasColumnName("target_snapshot");
+
+ b.Property("TargetType")
+ .HasColumnType("integer")
+ .HasColumnName("target_type");
+
+ b.Property("TargetUserId")
+ .HasColumnType("bigint")
+ .HasColumnName("target_user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_reports");
+
+ b.HasIndex("ReporterId")
+ .HasDatabaseName("ix_reports_reporter_id");
+
+ b.HasIndex("TargetMemberId")
+ .HasDatabaseName("ix_reports_target_member_id");
+
+ b.HasIndex("TargetUserId")
+ .HasDatabaseName("ix_reports_target_user_id");
+
+ b.ToTable("reports", (string)null);
+ });
+
+ modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("ApplicationId")
+ .HasColumnType("bigint")
+ .HasColumnName("application_id");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_at");
+
+ b.Property("Hash")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("hash");
+
+ b.Property("ManuallyExpired")
+ .HasColumnType("boolean")
+ .HasColumnName("manually_expired");
+
+ b.PrimitiveCollection("Scopes")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("scopes");
+
+ b.Property("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("Id")
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("Avatar")
+ .HasColumnType("text")
+ .HasColumnName("avatar");
+
+ b.Property("AvatarMigrated")
+ .HasColumnType("boolean")
+ .HasColumnName("avatar_migrated");
+
+ b.Property("Bio")
+ .HasColumnType("text")
+ .HasColumnName("bio");
+
+ b.Property>("CustomPreferences")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("custom_preferences");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasColumnName("deleted_by");
+
+ b.Property("DisplayName")
+ .HasColumnType("text")
+ .HasColumnName("display_name");
+
+ b.Property>("Fields")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("fields");
+
+ b.Property("LastActive")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_active");
+
+ b.Property("LastSidReroll")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_sid_reroll");
+
+ b.Property("LegacyId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasColumnName("legacy_id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.PrimitiveCollection("Links")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("links");
+
+ b.Property("ListHidden")
+ .HasColumnType("boolean")
+ .HasColumnName("list_hidden");
+
+ b.Property("MemberTitle")
+ .HasColumnType("text")
+ .HasColumnName("member_title");
+
+ b.Property>("Names")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("names");
+
+ b.Property("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property>("Pronouns")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("pronouns");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property("Settings")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("settings");
+
+ b.Property