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 9037fa0..b1e845f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +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 a007aab..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 the same.
-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 b48a2c4..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();
diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs
index bc35f62..635dab9 100644
--- a/Foxnouns.Backend/Controllers/MembersController.cs
+++ b/Foxnouns.Backend/Controllers/MembersController.cs
@@ -121,6 +121,9 @@ public class MembersController(
CurrentUser!.Id
);
+ CurrentUser.LastActive = clock.GetCurrentInstant();
+ db.Update(CurrentUser);
+
try
{
await db.SaveChangesAsync(ct);
@@ -238,6 +241,9 @@ public class MembersController(
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 787ff66..ed9a48f 100644
--- a/Foxnouns.Backend/Controllers/UsersController.cs
+++ b/Foxnouns.Backend/Controllers/UsersController.cs
@@ -46,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
+ )
);
}
@@ -178,6 +186,8 @@ public class UsersController(
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
}
+ user.LastActive = clock.GetCurrentInstant();
+
try
{
await db.SaveChangesAsync(ct);
@@ -253,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)]
@@ -279,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 c9120f3..2bbcbc7 100644
--- a/Foxnouns.Backend/Database/DatabaseContext.cs
+++ b/Foxnouns.Backend/Database/DatabaseContext.cs
@@ -73,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)
{
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("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/20250410192220_AddAvatarMigrations.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs
new file mode 100644
index 0000000..ca88605
--- /dev/null
+++ b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Foxnouns.Backend.Database.Migrations
+{
+ ///
+ public partial class AddAvatarMigrations : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "avatar_migrated",
+ table: "users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false
+ );
+
+ migrationBuilder.AddColumn(
+ name: "avatar_migrated",
+ table: "members",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false
+ );
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(name: "avatar_migrated", table: "users");
+
+ migrationBuilder.DropColumn(name: "avatar_migrated", table: "members");
+ }
+ }
+}
diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs
index 922a599..92db9f9 100644
--- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs
+++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs
@@ -241,6 +241,10 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("text")
.HasColumnName("avatar");
+ b.Property("AvatarMigrated")
+ .HasColumnType("boolean")
+ .HasColumnName("avatar_migrated");
+
b.Property("Bio")
.HasColumnType("text")
.HasColumnName("bio");
@@ -343,6 +347,38 @@ namespace Foxnouns.Backend.Database.Migrations
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")
@@ -533,6 +569,10 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("text")
.HasColumnName("avatar");
+ b.Property("AvatarMigrated")
+ .HasColumnType("boolean")
+ .HasColumnName("avatar_migrated");
+
b.Property("Bio")
.HasColumnType("text")
.HasColumnName("bio");
@@ -750,6 +790,18 @@ namespace Foxnouns.Backend.Database.Migrations
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")
diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs
index 81a01d8..85b39f3 100644
--- a/Foxnouns.Backend/Database/Models/Member.cs
+++ b/Foxnouns.Backend/Database/Models/Member.cs
@@ -29,6 +29,9 @@ public class Member : BaseModel
public List Pronouns { get; set; } = [];
public List Fields { get; set; } = [];
+ // Only used by avatar-proxy and avatar-migration.
+ public bool AvatarMigrated { get; set; } = true;
+
public List