Compare commits

..

No commits in common. "main" and "v2025.3.0" have entirely different histories.

76 changed files with 4478 additions and 10337 deletions

View file

@ -7,7 +7,7 @@ resharper_not_accessed_positional_property_local_highlighting = none
# Microsoft .NET properties # Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false csharp_new_line_before_members_in_object_initializers = false
csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion
# ReSharper properties # ReSharper properties
resharper_align_multiline_binary_expressions_chain = false resharper_align_multiline_binary_expressions_chain = false

2
.gitignore vendored
View file

@ -14,8 +14,6 @@ docker/proxy-config.json
docker/frontend.env docker/frontend.env
Foxnouns.DataMigrator/apps.json Foxnouns.DataMigrator/apps.json
migration-tools/avatar-proxy/config.json
migration-tools/avatar-migrator/.env
out/ out/
build/ build/

View file

@ -3,8 +3,12 @@
"tasks": [ "tasks": [
{ {
"name": "run-prettier", "name": "run-prettier",
"command": "npx", "command": "pnpm",
"args": ["prettier", "-w", "${staged}"], "args": [
"prettier",
"-w",
"${staged}"
],
"include": [ "include": [
"Foxnouns.Frontend/**/*.ts", "Foxnouns.Frontend/**/*.ts",
"Foxnouns.Frontend/**/*.json", "Foxnouns.Frontend/**/*.json",
@ -18,8 +22,13 @@
{ {
"name": "run-csharpier", "name": "run-csharpier",
"command": "dotnet", "command": "dotnet",
"args": ["csharpier", "${staged}"], "args": [
"include": ["**/*.cs"] "csharpier",
"${staged}"
],
"include": [
"**/*.cs"
]
} }
] ]
} }

View file

@ -1,29 +1,10 @@
# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)* # Running with Docker
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. 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. 2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same.
3. Run with `docker compose up -f docker-compose.prebuilt.yml` 3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do the same.
4. Build with `docker compose build`
The backend will listen on port 5001 and metrics will be available on port 5002. 5. Run with `docker compose up`
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, The Caddy server will listen on `localhost:5004` for the frontend and API,
and on `localhost:5005` for the profile URL shortener. and on `localhost:5005` for the profile URL shortener.

View file

@ -26,6 +26,7 @@ public class Config
public string MediaBaseUrl { get; init; } = null!; public string MediaBaseUrl { get; init; } = null!;
public string Address => $"http://{Host}:{Port}"; public string Address => $"http://{Host}:{Port}";
public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}";
public LoggingConfig Logging { get; init; } = new(); public LoggingConfig Logging { get; init; } = new();
public DatabaseConfig Database { get; init; } = new(); public DatabaseConfig Database { get; init; } = new();

View file

@ -121,9 +121,6 @@ public class MembersController(
CurrentUser!.Id CurrentUser!.Id
); );
CurrentUser.LastActive = clock.GetCurrentInstant();
db.Update(CurrentUser);
try try
{ {
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
@ -241,9 +238,6 @@ public class MembersController(
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar)); MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
} }
CurrentUser.LastActive = clock.GetCurrentInstant();
db.Update(CurrentUser);
try try
{ {
await db.SaveChangesAsync(); await db.SaveChangesAsync();

View file

@ -13,23 +13,20 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto; using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Services.Caching;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Foxnouns.Backend.Controllers; namespace Foxnouns.Backend.Controllers;
[Route("/api/v2/meta")] [Route("/api/v2/meta")]
public partial class MetaController(Config config, NoticeCacheService noticeCache) public partial class MetaController(Config config) : ApiControllerBase
: ApiControllerBase
{ {
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
[HttpGet] [HttpGet]
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)] [ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetMeta(CancellationToken ct = default) => public IActionResult GetMeta() =>
Ok( Ok(
new MetaResponse( new MetaResponse(
Repository, Repository,
@ -48,14 +45,10 @@ public partial class MetaController(Config config, NoticeCacheService noticeCach
ValidationUtils.MaxCustomPreferences, ValidationUtils.MaxCustomPreferences,
AuthUtils.MaxAuthMethodsPerType, AuthUtils.MaxAuthMethodsPerType,
FlagsController.MaxFlagCount 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}")] [HttpGet("page/{page}")]
public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default) public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default)
{ {
@ -78,7 +71,7 @@ public partial class MetaController(Config config, NoticeCacheService noticeCach
[HttpGet("/api/v2/coffee")] [HttpGet("/api/v2/coffee")]
public IActionResult BrewCoffee() => public IActionResult BrewCoffee() =>
StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!"); Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
[GeneratedRegex(@"^[a-z\-_]+$")] [GeneratedRegex(@"^[a-z\-_]+$")]
private static partial Regex PageRegex(); private static partial Regex PageRegex();

View file

@ -1,77 +0,0 @@
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<IActionResult> GetNoticesAsync(CancellationToken ct = default)
{
List<Notice> notices = await db
.Notices.Include(n => n.Author)
.OrderByDescending(n => n.Id)
.ToListAsync(ct);
return Ok(notices.Select(RenderNotice));
}
[HttpPost]
public async Task<IActionResult> 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)
);
}

View file

@ -46,15 +46,7 @@ public class UsersController(
{ {
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
return Ok( return Ok(
await userRenderer.RenderUserAsync( await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct)
user,
CurrentUser,
CurrentToken,
renderMembers: true,
renderAuthMethods: true,
renderSettings: true,
ct: ct
)
); );
} }
@ -186,8 +178,6 @@ public class UsersController(
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
} }
user.LastActive = clock.GetCurrentInstant();
try try
{ {
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
@ -263,12 +253,20 @@ public class UsersController(
} }
user.CustomPreferences = preferences; user.CustomPreferences = preferences;
user.LastActive = clock.GetCurrentInstant();
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return Ok(user.CustomPreferences); return Ok(user.CustomPreferences);
} }
[HttpGet("@me/settings")]
[Authorize("user.read_hidden")]
[ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> GetUserSettingsAsync(CancellationToken ct = default)
{
User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct);
return Ok(user.Settings);
}
[HttpPatch("@me/settings")] [HttpPatch("@me/settings")]
[Authorize("user.read_hidden", "user.update")] [Authorize("user.read_hidden", "user.update")]
[ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)]
@ -281,10 +279,7 @@ public class UsersController(
if (req.HasProperty(nameof(req.DarkMode))) if (req.HasProperty(nameof(req.DarkMode)))
user.Settings.DarkMode = 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); db.Update(user);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);

View file

@ -73,7 +73,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
public DbSet<Report> Reports { get; init; } = null!; public DbSet<Report> Reports { get; init; } = null!;
public DbSet<AuditLogEntry> AuditLog { get; init; } = null!; public DbSet<AuditLogEntry> AuditLog { get; init; } = null!;
public DbSet<Notification> Notifications { get; init; } = null!; public DbSet<Notification> Notifications { get; init; } = null!;
public DbSet<Notice> Notices { get; init; } = null!;
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{ {

View file

@ -1,915 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_id");
b.Property<string>("ClientSecret")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_secret");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("redirect_uris");
b.PrimitiveCollection<string[]>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.PrimitiveCollection<string[]>("ClearedFields")
.HasColumnType("text[]")
.HasColumnName("cleared_fields");
b.Property<long>("ModeratorId")
.HasColumnType("bigint")
.HasColumnName("moderator_id");
b.Property<string>("ModeratorUsername")
.IsRequired()
.HasColumnType("text")
.HasColumnName("moderator_username");
b.Property<string>("Reason")
.HasColumnType("text")
.HasColumnName("reason");
b.Property<long?>("ReportId")
.HasColumnType("bigint")
.HasColumnName("report_id");
b.Property<long?>("TargetMemberId")
.HasColumnType("bigint")
.HasColumnName("target_member_id");
b.Property<string>("TargetMemberName")
.HasColumnType("text")
.HasColumnName("target_member_name");
b.Property<long?>("TargetUserId")
.HasColumnType("bigint")
.HasColumnName("target_user_id");
b.Property<string>("TargetUsername")
.HasColumnType("text")
.HasColumnName("target_username");
b.Property<int>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<int>("AuthType")
.HasColumnType("integer")
.HasColumnName("auth_type");
b.Property<long?>("FediverseApplicationId")
.HasColumnType("bigint")
.HasColumnName("fediverse_application_id");
b.Property<string>("RemoteId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("remote_id");
b.Property<string>("RemoteUsername")
.HasColumnType("text")
.HasColumnName("remote_username");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_auth_methods");
b.HasIndex("FediverseApplicationId")
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
b.HasIndex("UserId")
.HasDatabaseName("ix_auth_methods_user_id");
b.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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("text")
.HasColumnName("filename");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_id");
b.Property<string>("ClientSecret")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_secret");
b.Property<string>("Domain")
.IsRequired()
.HasColumnType("text")
.HasColumnName("domain");
b.Property<bool>("ForceRefresh")
.HasColumnType("boolean")
.HasColumnName("force_refresh");
b.Property<int>("InstanceType")
.HasColumnType("integer")
.HasColumnName("instance_type");
b.HasKey("Id")
.HasName("pk_fediverse_applications");
b.ToTable("fediverse_applications", (string)null);
});
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Avatar")
.HasColumnType("text")
.HasColumnName("avatar");
b.Property<string>("Bio")
.HasColumnType("text")
.HasColumnName("bio");
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<List<Field>>("Fields")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("fields");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.PrimitiveCollection<string[]>("Links")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<List<FieldEntry>>("Names")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("names");
b.Property<List<Pronoun>>("Pronouns")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("pronouns");
b.Property<string>("Sid")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("sid")
.HasDefaultValueSql("find_free_member_sid()");
b.Property<bool>("Unlisted")
.HasColumnType("boolean")
.HasColumnName("unlisted");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_members");
b.HasIndex("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<long>("AuthorId")
.HasColumnType("bigint")
.HasColumnName("author_id");
b.Property<Instant>("EndTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("end_time");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<Instant>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<Instant?>("AcknowledgedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("acknowledged_at");
b.Property<string>("LocalizationKey")
.HasColumnType("text")
.HasColumnName("localization_key");
b.Property<Dictionary<string, string>>("LocalizationParams")
.IsRequired()
.HasColumnType("hstore")
.HasColumnName("localization_params");
b.Property<string>("Message")
.HasColumnType("text")
.HasColumnName("message");
b.Property<long>("TargetId")
.HasColumnType("bigint")
.HasColumnName("target_id");
b.Property<int>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<string>("Hash")
.HasColumnType("text")
.HasColumnName("hash");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Context")
.HasColumnType("text")
.HasColumnName("context");
b.Property<int>("Reason")
.HasColumnType("integer")
.HasColumnName("reason");
b.Property<long>("ReporterId")
.HasColumnType("bigint")
.HasColumnName("reporter_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<long?>("TargetMemberId")
.HasColumnType("bigint")
.HasColumnName("target_member_id");
b.Property<string>("TargetSnapshot")
.HasColumnType("text")
.HasColumnName("target_snapshot");
b.Property<int>("TargetType")
.HasColumnType("integer")
.HasColumnName("target_type");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<long>("ApplicationId")
.HasColumnType("bigint")
.HasColumnName("application_id");
b.Property<Instant>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<byte[]>("Hash")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hash");
b.Property<bool>("ManuallyExpired")
.HasColumnType("boolean")
.HasColumnName("manually_expired");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("scopes");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_tokens");
b.HasIndex("ApplicationId")
.HasDatabaseName("ix_tokens_application_id");
b.HasIndex("UserId")
.HasDatabaseName("ix_tokens_user_id");
b.ToTable("tokens", (string)null);
});
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Avatar")
.HasColumnType("text")
.HasColumnName("avatar");
b.Property<string>("Bio")
.HasColumnType("text")
.HasColumnName("bio");
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("custom_preferences");
b.Property<bool>("Deleted")
.HasColumnType("boolean")
.HasColumnName("deleted");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasColumnName("deleted_by");
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<List<Field>>("Fields")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("fields");
b.Property<Instant>("LastActive")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_active");
b.Property<Instant>("LastSidReroll")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_sid_reroll");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.PrimitiveCollection<string[]>("Links")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("links");
b.Property<bool>("ListHidden")
.HasColumnType("boolean")
.HasColumnName("list_hidden");
b.Property<string>("MemberTitle")
.HasColumnType("text")
.HasColumnName("member_title");
b.Property<List<FieldEntry>>("Names")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("names");
b.Property<string>("Password")
.HasColumnType("text")
.HasColumnName("password");
b.Property<List<Pronoun>>("Pronouns")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("pronouns");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<UserSettings>("Settings")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("settings");
b.Property<string>("Sid")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("sid")
.HasDefaultValueSql("find_free_user_sid()");
b.Property<string>("Timezone")
.HasColumnType("text")
.HasColumnName("timezone");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("PrideFlagId")
.HasColumnType("bigint")
.HasColumnName("pride_flag_id");
b.Property<long>("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
}
}
}

View file

@ -1,56 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace Foxnouns.Backend.Database.Migrations
{
/// <inheritdoc />
public partial class AddNotices : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "notices",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false),
message = table.Column<string>(type: "text", nullable: false),
start_time = table.Column<Instant>(
type: "timestamp with time zone",
nullable: false
),
end_time = table.Column<Instant>(
type: "timestamp with time zone",
nullable: false
),
author_id = table.Column<long>(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"
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "notices");
}
}
}

View file

@ -1,923 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_id");
b.Property<string>("ClientSecret")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_secret");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("redirect_uris");
b.PrimitiveCollection<string[]>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.PrimitiveCollection<string[]>("ClearedFields")
.HasColumnType("text[]")
.HasColumnName("cleared_fields");
b.Property<long>("ModeratorId")
.HasColumnType("bigint")
.HasColumnName("moderator_id");
b.Property<string>("ModeratorUsername")
.IsRequired()
.HasColumnType("text")
.HasColumnName("moderator_username");
b.Property<string>("Reason")
.HasColumnType("text")
.HasColumnName("reason");
b.Property<long?>("ReportId")
.HasColumnType("bigint")
.HasColumnName("report_id");
b.Property<long?>("TargetMemberId")
.HasColumnType("bigint")
.HasColumnName("target_member_id");
b.Property<string>("TargetMemberName")
.HasColumnType("text")
.HasColumnName("target_member_name");
b.Property<long?>("TargetUserId")
.HasColumnType("bigint")
.HasColumnName("target_user_id");
b.Property<string>("TargetUsername")
.HasColumnType("text")
.HasColumnName("target_username");
b.Property<int>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<int>("AuthType")
.HasColumnType("integer")
.HasColumnName("auth_type");
b.Property<long?>("FediverseApplicationId")
.HasColumnType("bigint")
.HasColumnName("fediverse_application_id");
b.Property<string>("RemoteId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("remote_id");
b.Property<string>("RemoteUsername")
.HasColumnType("text")
.HasColumnName("remote_username");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_auth_methods");
b.HasIndex("FediverseApplicationId")
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
b.HasIndex("UserId")
.HasDatabaseName("ix_auth_methods_user_id");
b.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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("text")
.HasColumnName("filename");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_id");
b.Property<string>("ClientSecret")
.IsRequired()
.HasColumnType("text")
.HasColumnName("client_secret");
b.Property<string>("Domain")
.IsRequired()
.HasColumnType("text")
.HasColumnName("domain");
b.Property<bool>("ForceRefresh")
.HasColumnType("boolean")
.HasColumnName("force_refresh");
b.Property<int>("InstanceType")
.HasColumnType("integer")
.HasColumnName("instance_type");
b.HasKey("Id")
.HasName("pk_fediverse_applications");
b.ToTable("fediverse_applications", (string)null);
});
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Avatar")
.HasColumnType("text")
.HasColumnName("avatar");
b.Property<bool>("AvatarMigrated")
.HasColumnType("boolean")
.HasColumnName("avatar_migrated");
b.Property<string>("Bio")
.HasColumnType("text")
.HasColumnName("bio");
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<List<Field>>("Fields")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("fields");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.PrimitiveCollection<string[]>("Links")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<List<FieldEntry>>("Names")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("names");
b.Property<List<Pronoun>>("Pronouns")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("pronouns");
b.Property<string>("Sid")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("sid")
.HasDefaultValueSql("find_free_member_sid()");
b.Property<bool>("Unlisted")
.HasColumnType("boolean")
.HasColumnName("unlisted");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_members");
b.HasIndex("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<long>("AuthorId")
.HasColumnType("bigint")
.HasColumnName("author_id");
b.Property<Instant>("EndTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("end_time");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<Instant>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<Instant?>("AcknowledgedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("acknowledged_at");
b.Property<string>("LocalizationKey")
.HasColumnType("text")
.HasColumnName("localization_key");
b.Property<Dictionary<string, string>>("LocalizationParams")
.IsRequired()
.HasColumnType("hstore")
.HasColumnName("localization_params");
b.Property<string>("Message")
.HasColumnType("text")
.HasColumnName("message");
b.Property<long>("TargetId")
.HasColumnType("bigint")
.HasColumnName("target_id");
b.Property<int>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<string>("Hash")
.HasColumnType("text")
.HasColumnName("hash");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Context")
.HasColumnType("text")
.HasColumnName("context");
b.Property<int>("Reason")
.HasColumnType("integer")
.HasColumnName("reason");
b.Property<long>("ReporterId")
.HasColumnType("bigint")
.HasColumnName("reporter_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<long?>("TargetMemberId")
.HasColumnType("bigint")
.HasColumnName("target_member_id");
b.Property<string>("TargetSnapshot")
.HasColumnType("text")
.HasColumnName("target_snapshot");
b.Property<int>("TargetType")
.HasColumnType("integer")
.HasColumnName("target_type");
b.Property<long>("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<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<long>("ApplicationId")
.HasColumnType("bigint")
.HasColumnName("application_id");
b.Property<Instant>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<byte[]>("Hash")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hash");
b.Property<bool>("ManuallyExpired")
.HasColumnType("boolean")
.HasColumnName("manually_expired");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("scopes");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_tokens");
b.HasIndex("ApplicationId")
.HasDatabaseName("ix_tokens_application_id");
b.HasIndex("UserId")
.HasDatabaseName("ix_tokens_user_id");
b.ToTable("tokens", (string)null);
});
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("Avatar")
.HasColumnType("text")
.HasColumnName("avatar");
b.Property<bool>("AvatarMigrated")
.HasColumnType("boolean")
.HasColumnName("avatar_migrated");
b.Property<string>("Bio")
.HasColumnType("text")
.HasColumnName("bio");
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("custom_preferences");
b.Property<bool>("Deleted")
.HasColumnType("boolean")
.HasColumnName("deleted");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasColumnName("deleted_by");
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<List<Field>>("Fields")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("fields");
b.Property<Instant>("LastActive")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_active");
b.Property<Instant>("LastSidReroll")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_sid_reroll");
b.Property<string>("LegacyId")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("legacy_id")
.HasDefaultValueSql("gen_random_uuid()");
b.PrimitiveCollection<string[]>("Links")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("links");
b.Property<bool>("ListHidden")
.HasColumnType("boolean")
.HasColumnName("list_hidden");
b.Property<string>("MemberTitle")
.HasColumnType("text")
.HasColumnName("member_title");
b.Property<List<FieldEntry>>("Names")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("names");
b.Property<string>("Password")
.HasColumnType("text")
.HasColumnName("password");
b.Property<List<Pronoun>>("Pronouns")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("pronouns");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<UserSettings>("Settings")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("settings");
b.Property<string>("Sid")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasColumnName("sid")
.HasDefaultValueSql("find_free_user_sid()");
b.Property<string>("Timezone")
.HasColumnType("text")
.HasColumnName("timezone");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("PrideFlagId")
.HasColumnType("bigint")
.HasColumnName("pride_flag_id");
b.Property<long>("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
}
}
}

View file

@ -1,38 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Foxnouns.Backend.Database.Migrations
{
/// <inheritdoc />
public partial class AddAvatarMigrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "avatar_migrated",
table: "users",
type: "boolean",
nullable: false,
defaultValue: false
);
migrationBuilder.AddColumn<bool>(
name: "avatar_migrated",
table: "members",
type: "boolean",
nullable: false,
defaultValue: false
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "avatar_migrated", table: "users");
migrationBuilder.DropColumn(name: "avatar_migrated", table: "members");
}
}
}

View file

@ -241,10 +241,6 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("avatar"); .HasColumnName("avatar");
b.Property<bool>("AvatarMigrated")
.HasColumnType("boolean")
.HasColumnName("avatar_migrated");
b.Property<string>("Bio") b.Property<string>("Bio")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("bio"); .HasColumnName("bio");
@ -347,38 +343,6 @@ namespace Foxnouns.Backend.Database.Migrations
b.ToTable("member_flags", (string)null); b.ToTable("member_flags", (string)null);
}); });
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<long>("AuthorId")
.HasColumnType("bigint")
.HasColumnName("author_id");
b.Property<Instant>("EndTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("end_time");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<Instant>("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 => modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@ -569,10 +533,6 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("avatar"); .HasColumnName("avatar");
b.Property<bool>("AvatarMigrated")
.HasColumnType("boolean")
.HasColumnName("avatar_migrated");
b.Property<string>("Bio") b.Property<string>("Bio")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("bio"); .HasColumnName("bio");
@ -790,18 +750,6 @@ namespace Foxnouns.Backend.Database.Migrations
b.Navigation("PrideFlag"); 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 => modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
{ {
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target") b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")

View file

@ -29,9 +29,6 @@ public class Member : BaseModel
public List<Pronoun> Pronouns { get; set; } = []; public List<Pronoun> Pronouns { get; set; } = [];
public List<Field> Fields { get; set; } = []; public List<Field> Fields { get; set; } = [];
// Only used by avatar-proxy and avatar-migration.
public bool AvatarMigrated { get; set; } = true;
public List<MemberFlag> ProfileFlags { get; set; } = []; public List<MemberFlag> ProfileFlags { get; set; } = [];
public Snowflake UserId { get; init; } public Snowflake UserId { get; init; }

View file

@ -1,13 +0,0 @@
using NodaTime;
namespace Foxnouns.Backend.Database.Models;
public class Notice : BaseModel
{
public required string Message { get; set; }
public required Instant StartTime { get; set; }
public required Instant EndTime { get; set; }
public Snowflake AuthorId { get; init; }
public User Author { get; init; } = null!;
}

View file

@ -57,9 +57,6 @@ public class User : BaseModel
public Instant? DeletedAt { get; set; } public Instant? DeletedAt { get; set; }
public Snowflake? DeletedBy { get; set; } public Snowflake? DeletedBy { get; set; }
// Only used by avatar-proxy and avatar-migration.
public bool AvatarMigrated { get; set; } = true;
[NotMapped] [NotMapped]
public bool? SelfDelete => Deleted ? DeletedBy != null : null; public bool? SelfDelete => Deleted ? DeletedBy != null : null;
@ -98,5 +95,4 @@ public enum PreferenceSize
public class UserSettings public class UserSettings
{ {
public bool? DarkMode { get; set; } public bool? DarkMode { get; set; }
public Snowflake? LastReadNotice { get; set; }
} }

View file

@ -14,8 +14,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// ReSharper disable NotAccessedPositionalProperty.Global // ReSharper disable NotAccessedPositionalProperty.Global
using Foxnouns.Backend.Database;
namespace Foxnouns.Backend.Dto; namespace Foxnouns.Backend.Dto;
public record MetaResponse( public record MetaResponse(
@ -24,12 +22,9 @@ public record MetaResponse(
string Hash, string Hash,
int Members, int Members,
UserInfoResponse Users, UserInfoResponse Users,
LimitsResponse Limits, LimitsResponse Limits
MetaNoticeResponse? Notice
); );
public record MetaNoticeResponse(Snowflake Id, string Message);
public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
public record LimitsResponse( public record LimitsResponse(

View file

@ -122,13 +122,3 @@ public record QueryUserResponse(
); );
public record QuerySensitiveUserDataRequest(string Reason); public record QuerySensitiveUserDataRequest(string Reason);
public record NoticeResponse(
Snowflake Id,
string Message,
Instant StartTime,
Instant EndTime,
PartialUser Author
);
public record CreateNoticeRequest(string Message, Instant? StartTime, Instant EndTime);

View file

@ -49,8 +49,7 @@ public record UserResponse(
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastSidReroll, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastSidReroll,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Suspended, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Suspended,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] UserSettings? Settings
); );
public record CustomPreferenceResponse( public record CustomPreferenceResponse(
@ -80,7 +79,6 @@ public record PartialUser(
public class UpdateUserSettingsRequest : PatchRequest public class UpdateUserSettingsRequest : PatchRequest
{ {
public bool? DarkMode { get; init; } public bool? DarkMode { get; init; }
public Snowflake? LastReadNotice { get; init; }
} }
public class CustomPreferenceUpdateRequest public class CustomPreferenceUpdateRequest

View file

@ -15,18 +15,14 @@
using Coravel; using Coravel;
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Services.Auth;
using Foxnouns.Backend.Services.Caching;
using Foxnouns.Backend.Services.V1; using Foxnouns.Backend.Services.V1;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Http.Resilience;
using Minio; using Minio;
using NodaTime; using NodaTime;
using Polly;
using Prometheus; using Prometheus;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
@ -104,40 +100,6 @@ public static class WebApplicationExtensions
builder.Host.ConfigureServices( builder.Host.ConfigureServices(
(ctx, services) => (ctx, services) =>
{ {
// create a single HTTP client for all requests.
// it's also configured with a retry mechanism, so that requests aren't immediately lost to the void if they fail
services.AddSingleton<HttpClient>(_ =>
{
// ReSharper disable once SuggestVarOrType_Elsewhere
var retryPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(
new HttpRetryStrategyOptions
{
BackoffType = DelayBackoffType.Linear,
MaxRetryAttempts = 3,
}
)
.Build();
var resilienceHandler = new ResilienceHandler(retryPipeline)
{
InnerHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
},
};
var client = new HttpClient(resilienceHandler);
client.DefaultRequestHeaders.Remove("User-Agent");
client.DefaultRequestHeaders.Remove("Accept");
client.DefaultRequestHeaders.Add(
"User-Agent",
$"pronouns.cc/{BuildInfo.Version}"
);
client.DefaultRequestHeaders.Add("Accept", "application/json");
return client;
});
services services
.AddQueue() .AddQueue()
.AddSmtpMailer(ctx.Configuration) .AddSmtpMailer(ctx.Configuration)
@ -164,7 +126,6 @@ public static class WebApplicationExtensions
.AddScoped<ObjectStorageService>() .AddScoped<ObjectStorageService>()
.AddTransient<DataCleanupService>() .AddTransient<DataCleanupService>()
.AddTransient<ValidationService>() .AddTransient<ValidationService>()
.AddSingleton<NoticeCacheService>()
// Background services // Background services
.AddHostedService<PeriodicTasksService>() .AddHostedService<PeriodicTasksService>()
// Transient jobs // Transient jobs
@ -199,6 +160,9 @@ public static class WebApplicationExtensions
public static async Task Initialize(this WebApplication app, string[] args) public static async Task Initialize(this WebApplication app, string[] args)
{ {
// Read version information from .version in the repository root
await BuildInfo.ReadBuildInfo();
app.Services.ConfigureQueue() app.Services.ConfigureQueue()
.LogQueuedTaskProgress(app.Services.GetRequiredService<ILogger<IQueue>>()); .LogQueuedTaskProgress(app.Services.GetRequiredService<ILogger<IQueue>>());

View file

@ -25,13 +25,12 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2"/> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2"/>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.2.0"/>
<PackageReference Include="MimeKit" Version="4.10.0"/> <PackageReference Include="MimeKit" Version="4.10.0"/>
<PackageReference Include="Minio" Version="6.0.4"/> <PackageReference Include="Minio" Version="6.0.4"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NodaTime" Version="3.2.1"/> <PackageReference Include="NodaTime" Version="3.2.1"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.3"/>
<PackageReference Include="Npgsql.Json.NET" Version="9.0.3"/> <PackageReference Include="Npgsql.Json.NET" Version="9.0.3"/>
<PackageReference Include="prometheus-net" Version="8.2.1"/> <PackageReference Include="prometheus-net" Version="8.2.1"/>
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
@ -39,14 +38,14 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Scalar.AspNetCore" Version="2.0.26"/> <PackageReference Include="Scalar.AspNetCore" Version="2.0.18"/>
<PackageReference Include="Sentry.AspNetCore" Version="5.3.0"/> <PackageReference Include="Sentry.AspNetCore" Version="5.2.0"/>
<PackageReference Include="Serilog" Version="4.2.0"/> <PackageReference Include="Serilog" Version="4.2.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/> <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7"/> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.31"/> <PackageReference Include="StackExchange.Redis" Version="2.8.24"/>
<PackageReference Include="System.Text.Json" Version="9.0.2"/> <PackageReference Include="System.Text.Json" Version="9.0.2"/>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
<PackageReference Include="Yort.Xid.Net" Version="2.0.1"/> <PackageReference Include="Yort.Xid.Net" Version="2.0.1"/>

View file

@ -27,7 +27,6 @@ using NodaTime.Text;
namespace Foxnouns.Backend.Jobs; namespace Foxnouns.Backend.Jobs;
public class CreateDataExportJob( public class CreateDataExportJob(
HttpClient client,
DatabaseContext db, DatabaseContext db,
IClock clock, IClock clock,
UserRendererService userRenderer, UserRendererService userRenderer,
@ -37,6 +36,7 @@ public class CreateDataExportJob(
ILogger logger ILogger logger
) )
{ {
private static readonly HttpClient Client = new();
private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>(); private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>();
public static void Enqueue(Snowflake userId) public static void Enqueue(Snowflake userId)
@ -201,7 +201,7 @@ public class CreateDataExportJob(
if (s3Path == null) if (s3Path == null)
return; return;
HttpResponseMessage resp = await client.GetAsync(s3Path); HttpResponseMessage resp = await Client.GetAsync(s3Path);
if (resp.StatusCode != HttpStatusCode.OK) if (resp.StatusCode != HttpStatusCode.OK)
{ {
_logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path); _logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path);

View file

@ -67,7 +67,6 @@ public class MemberAvatarUpdateJob(
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");
member.Avatar = hash; member.Avatar = hash;
member.AvatarMigrated = true;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
if (prevHash != null && prevHash != hash) if (prevHash != null && prevHash != hash)

View file

@ -68,7 +68,6 @@ public class UserAvatarUpdateJob(
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");
user.Avatar = hash; user.Avatar = hash;
user.AvatarMigrated = true;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
if (prevHash != null && prevHash != hash) if (prevHash != null && prevHash != hash)

View file

@ -34,9 +34,6 @@ Config config = builder.AddConfiguration();
builder.AddSerilog(); builder.AddSerilog();
// Read version information from .version in the repository root
await BuildInfo.ReadBuildInfo();
builder builder
.WebHost.UseSentry(opts => .WebHost.UseSentry(opts =>
{ {
@ -68,13 +65,14 @@ builder
{ {
NamingStrategy = new SnakeCaseNamingStrategy(), NamingStrategy = new SnakeCaseNamingStrategy(),
}; };
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
}) })
.ConfigureApiBehaviorOptions(options => .ConfigureApiBehaviorOptions(options =>
{ {
options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( // the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine)
new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() options.InvalidModelStateResponseFactory = (ActionContext actionContext) =>
); new BadRequestObjectResult(
new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson()
);
}); });
builder builder

View file

@ -253,14 +253,14 @@ public class AuthService(
{ {
AssertValidAuthType(authType, app); AssertValidAuthType(authType, app);
// This is already checked when generating an add account state, but we check it here too just in case. // This is already checked when
int currentCount = await db int currentCount = await db
.AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType)
.CountAsync(ct); .CountAsync(ct);
if (currentCount >= AuthUtils.MaxAuthMethodsPerType) if (currentCount >= AuthUtils.MaxAuthMethodsPerType)
{ {
throw new ApiError.BadRequest( throw new ApiError.BadRequest(
$"Too many linked accounts of this type, maximum of {AuthUtils.MaxAuthMethodsPerType} per account." "Too many linked accounts of this type, maximum of 3 per account."
); );
} }

View file

@ -25,20 +25,20 @@ namespace Foxnouns.Backend.Services.Auth;
public partial class FediverseAuthService public partial class FediverseAuthService
{ {
private string MastodonRedirectUri(string instance) => private string MastodonRedirectUri(string instance) =>
$"{config.BaseUrl}/auth/callback/mastodon/{instance}"; $"{_config.BaseUrl}/auth/callback/mastodon/{instance}";
private async Task<FediverseApplication> CreateMastodonApplicationAsync( private async Task<FediverseApplication> CreateMastodonApplicationAsync(
string instance, string instance,
Snowflake? existingAppId = null Snowflake? existingAppId = null
) )
{ {
HttpResponseMessage resp = await client.PostAsJsonAsync( HttpResponseMessage resp = await _client.PostAsJsonAsync(
$"https://{instance}/api/v1/apps", $"https://{instance}/api/v1/apps",
new CreateMastodonApplicationRequest( new CreateMastodonApplicationRequest(
$"pronouns.cc (+{config.BaseUrl})", $"pronouns.cc (+{_config.BaseUrl})",
MastodonRedirectUri(instance), MastodonRedirectUri(instance),
"read read:accounts", "read read:accounts",
config.BaseUrl _config.BaseUrl
) )
); );
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
@ -58,19 +58,19 @@ public partial class FediverseAuthService
{ {
app = new FediverseApplication app = new FediverseApplication
{ {
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
ClientId = mastodonApp.ClientId, ClientId = mastodonApp.ClientId,
ClientSecret = mastodonApp.ClientSecret, ClientSecret = mastodonApp.ClientSecret,
Domain = instance, Domain = instance,
InstanceType = FediverseInstanceType.MastodonApi, InstanceType = FediverseInstanceType.MastodonApi,
}; };
db.Add(app); _db.Add(app);
} }
else else
{ {
app = app =
await db.FediverseApplications.FindAsync(existingAppId) await _db.FediverseApplications.FindAsync(existingAppId)
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
app.ClientId = mastodonApp.ClientId; app.ClientId = mastodonApp.ClientId;
@ -78,7 +78,7 @@ public partial class FediverseAuthService
app.InstanceType = FediverseInstanceType.MastodonApi; app.InstanceType = FediverseInstanceType.MastodonApi;
} }
await db.SaveChangesAsync(); await _db.SaveChangesAsync();
return app; return app;
} }
@ -90,9 +90,9 @@ public partial class FediverseAuthService
) )
{ {
if (state != null) if (state != null)
await keyCacheService.ValidateAuthStateAsync(state); await _keyCacheService.ValidateAuthStateAsync(state);
HttpResponseMessage tokenResp = await client.PostAsync( HttpResponseMessage tokenResp = await _client.PostAsync(
MastodonTokenUri(app.Domain), MastodonTokenUri(app.Domain),
new FormUrlEncodedContent( new FormUrlEncodedContent(
new Dictionary<string, string> new Dictionary<string, string>
@ -123,7 +123,7 @@ public partial class FediverseAuthService
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain)); var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain));
req.Headers.Add("Authorization", $"Bearer {token}"); req.Headers.Add("Authorization", $"Bearer {token}");
HttpResponseMessage currentUserResp = await client.SendAsync(req); HttpResponseMessage currentUserResp = await _client.SendAsync(req);
currentUserResp.EnsureSuccessStatusCode(); currentUserResp.EnsureSuccessStatusCode();
FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>(); FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
if (user == null) if (user == null)
@ -151,7 +151,7 @@ public partial class FediverseAuthService
app = await CreateMastodonApplicationAsync(app.Domain, app.Id); app = await CreateMastodonApplicationAsync(app.Domain, app.Id);
} }
state ??= HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync()); state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
return $"https://{app.Domain}/oauth/authorize?response_type=code" return $"https://{app.Domain}/oauth/authorize?response_type=code"
+ $"&client_id={app.ClientId}" + $"&client_id={app.ClientId}"

View file

@ -34,11 +34,11 @@ public partial class FediverseAuthService
Snowflake? existingAppId = null Snowflake? existingAppId = null
) )
{ {
HttpResponseMessage resp = await client.PostAsJsonAsync( HttpResponseMessage resp = await _client.PostAsJsonAsync(
MisskeyAppUri(instance), MisskeyAppUri(instance),
new CreateMisskeyApplicationRequest( new CreateMisskeyApplicationRequest(
$"pronouns.cc (+{config.BaseUrl})", $"pronouns.cc (+{_config.BaseUrl})",
$"pronouns.cc on {config.BaseUrl}", $"pronouns.cc on {_config.BaseUrl}",
["read:account"], ["read:account"],
MastodonRedirectUri(instance) MastodonRedirectUri(instance)
) )
@ -60,19 +60,19 @@ public partial class FediverseAuthService
{ {
app = new FediverseApplication app = new FediverseApplication
{ {
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
ClientId = misskeyApp.Id, ClientId = misskeyApp.Id,
ClientSecret = misskeyApp.Secret, ClientSecret = misskeyApp.Secret,
Domain = instance, Domain = instance,
InstanceType = FediverseInstanceType.MisskeyApi, InstanceType = FediverseInstanceType.MisskeyApi,
}; };
db.Add(app); _db.Add(app);
} }
else else
{ {
app = app =
await db.FediverseApplications.FindAsync(existingAppId) await _db.FediverseApplications.FindAsync(existingAppId)
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
app.ClientId = misskeyApp.Id; app.ClientId = misskeyApp.Id;
@ -80,7 +80,7 @@ public partial class FediverseAuthService
app.InstanceType = FediverseInstanceType.MisskeyApi; app.InstanceType = FediverseInstanceType.MisskeyApi;
} }
await db.SaveChangesAsync(); await _db.SaveChangesAsync();
return app; return app;
} }
@ -96,7 +96,7 @@ public partial class FediverseAuthService
private async Task<FediverseUser> GetMisskeyUserAsync(FediverseApplication app, string code) private async Task<FediverseUser> GetMisskeyUserAsync(FediverseApplication app, string code)
{ {
HttpResponseMessage resp = await client.PostAsJsonAsync( HttpResponseMessage resp = await _client.PostAsJsonAsync(
MisskeyTokenUri(app.Domain), MisskeyTokenUri(app.Domain),
new GetMisskeySessionUserKeyRequest(app.ClientSecret, code) new GetMisskeySessionUserKeyRequest(app.ClientSecret, code)
); );
@ -130,7 +130,7 @@ public partial class FediverseAuthService
app = await CreateMisskeyApplicationAsync(app.Domain, app.Id); app = await CreateMisskeyApplicationAsync(app.Domain, app.Id);
} }
HttpResponseMessage resp = await client.PostAsJsonAsync( HttpResponseMessage resp = await _client.PostAsJsonAsync(
MisskeyGenerateSessionUri(app.Domain), MisskeyGenerateSessionUri(app.Domain),
new CreateMisskeySessionUriRequest(app.ClientSecret) new CreateMisskeySessionUriRequest(app.ClientSecret)
); );

View file

@ -19,17 +19,37 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Foxnouns.Backend.Services.Auth; namespace Foxnouns.Backend.Services.Auth;
public partial class FediverseAuthService( public partial class FediverseAuthService
ILogger logger,
Config config,
DatabaseContext db,
HttpClient client,
KeyCacheService keyCacheService,
ISnowflakeGenerator snowflakeGenerator
)
{ {
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
private readonly ILogger _logger = logger.ForContext<FediverseAuthService>();
private readonly HttpClient _client;
private readonly ILogger _logger;
private readonly Config _config;
private readonly DatabaseContext _db;
private readonly KeyCacheService _keyCacheService;
private readonly ISnowflakeGenerator _snowflakeGenerator;
public FediverseAuthService(
ILogger logger,
Config config,
DatabaseContext db,
KeyCacheService keyCacheService,
ISnowflakeGenerator snowflakeGenerator
)
{
_logger = logger.ForContext<FediverseAuthService>();
_config = config;
_db = db;
_keyCacheService = keyCacheService;
_snowflakeGenerator = snowflakeGenerator;
_client = new HttpClient();
_client.DefaultRequestHeaders.Remove("User-Agent");
_client.DefaultRequestHeaders.Remove("Accept");
_client.DefaultRequestHeaders.Add("User-Agent", $"pronouns.cc/{BuildInfo.Version}");
_client.DefaultRequestHeaders.Add("Accept", "application/json");
}
public async Task<string> GenerateAuthUrlAsync( public async Task<string> GenerateAuthUrlAsync(
string instance, string instance,
@ -50,7 +70,7 @@ public partial class FediverseAuthService(
public async Task<FediverseApplication> GetApplicationAsync(string instance) public async Task<FediverseApplication> GetApplicationAsync(string instance)
{ {
FediverseApplication? app = await db.FediverseApplications.FirstOrDefaultAsync(a => FediverseApplication? app = await _db.FediverseApplications.FirstOrDefaultAsync(a =>
a.Domain == instance a.Domain == instance
); );
if (app != null) if (app != null)
@ -72,7 +92,7 @@ public partial class FediverseAuthService(
{ {
_logger.Debug("Requesting software name for fediverse instance {Instance}", instance); _logger.Debug("Requesting software name for fediverse instance {Instance}", instance);
HttpResponseMessage wellKnownResp = await client.GetAsync( HttpResponseMessage wellKnownResp = await _client.GetAsync(
new Uri($"https://{instance}/.well-known/nodeinfo") new Uri($"https://{instance}/.well-known/nodeinfo")
); );
wellKnownResp.EnsureSuccessStatusCode(); wellKnownResp.EnsureSuccessStatusCode();
@ -87,7 +107,7 @@ public partial class FediverseAuthService(
); );
} }
HttpResponseMessage nodeInfoResp = await client.GetAsync(nodeInfoUrl); HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl);
nodeInfoResp.EnsureSuccessStatusCode(); nodeInfoResp.EnsureSuccessStatusCode();
PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>(); PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>();

View file

@ -27,7 +27,7 @@ public partial class RemoteAuthService
) )
{ {
var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; var redirectUri = $"{config.BaseUrl}/auth/callback/discord";
HttpResponseMessage resp = await client.PostAsync( HttpResponseMessage resp = await _httpClient.PostAsync(
_discordTokenUri, _discordTokenUri,
new FormUrlEncodedContent( new FormUrlEncodedContent(
new Dictionary<string, string> new Dictionary<string, string>
@ -59,7 +59,7 @@ public partial class RemoteAuthService
var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri);
req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}"); req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
HttpResponseMessage resp2 = await client.SendAsync(req, ct); HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
resp2.EnsureSuccessStatusCode(); resp2.EnsureSuccessStatusCode();
DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct); DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
if (user == null) if (user == null)

View file

@ -28,7 +28,7 @@ public partial class RemoteAuthService
) )
{ {
var redirectUri = $"{config.BaseUrl}/auth/callback/google"; var redirectUri = $"{config.BaseUrl}/auth/callback/google";
HttpResponseMessage resp = await client.PostAsync( HttpResponseMessage resp = await _httpClient.PostAsync(
_googleTokenUri, _googleTokenUri,
new FormUrlEncodedContent( new FormUrlEncodedContent(
new Dictionary<string, string> new Dictionary<string, string>

View file

@ -29,7 +29,7 @@ public partial class RemoteAuthService
) )
{ {
var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr"; var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr";
HttpResponseMessage resp = await client.PostAsync( HttpResponseMessage resp = await _httpClient.PostAsync(
_tumblrTokenUri, _tumblrTokenUri,
new FormUrlEncodedContent( new FormUrlEncodedContent(
new Dictionary<string, string> new Dictionary<string, string>
@ -62,7 +62,7 @@ public partial class RemoteAuthService
var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri); var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri);
req.Headers.Add("Authorization", $"Bearer {token.AccessToken}"); req.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
HttpResponseMessage resp2 = await client.SendAsync(req, ct); HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
if (!resp2.IsSuccessStatusCode) if (!resp2.IsSuccessStatusCode)
{ {
string respBody = await resp2.Content.ReadAsStringAsync(ct); string respBody = await resp2.Content.ReadAsStringAsync(ct);

View file

@ -25,7 +25,6 @@ using Microsoft.EntityFrameworkCore;
namespace Foxnouns.Backend.Services.Auth; namespace Foxnouns.Backend.Services.Auth;
public partial class RemoteAuthService( public partial class RemoteAuthService(
HttpClient client,
Config config, Config config,
ILogger logger, ILogger logger,
DatabaseContext db, DatabaseContext db,
@ -33,6 +32,7 @@ public partial class RemoteAuthService(
) )
{ {
private readonly ILogger _logger = logger.ForContext<RemoteAuthService>(); private readonly ILogger _logger = logger.ForContext<RemoteAuthService>();
private readonly HttpClient _httpClient = new();
public record RemoteUser(string Id, string Username); public record RemoteUser(string Id, string Username);

View file

@ -1,39 +0,0 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace Foxnouns.Backend.Services.Caching;
public class NoticeCacheService(IServiceProvider serviceProvider, IClock clock, ILogger logger)
: SingletonCacheService<Notice>(serviceProvider, clock, logger)
{
public override Duration MaxAge { get; init; } = Duration.FromMinutes(5);
public override Func<
DatabaseContext,
CancellationToken,
Task<Notice?>
> FetchFunc { get; init; } =
async (db, ct) =>
await db
.Notices.Where(n =>
n.StartTime < clock.GetCurrentInstant() && n.EndTime > clock.GetCurrentInstant()
)
.OrderByDescending(n => n.Id)
.FirstOrDefaultAsync(ct);
}

View file

@ -1,63 +0,0 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using NodaTime;
namespace Foxnouns.Backend.Services.Caching;
public abstract class SingletonCacheService<T>(
IServiceProvider serviceProvider,
IClock clock,
ILogger logger
)
where T : class
{
private T? _item;
private Instant _lastUpdated = Instant.MinValue;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly ILogger _logger = logger.ForContext<SingletonCacheService<T>>();
public virtual Duration MaxAge { get; init; } = Duration.FromMinutes(5);
public virtual Func<DatabaseContext, CancellationToken, Task<T?>> FetchFunc { get; init; } =
(_, __) => Task.FromResult<T?>(null);
public async Task<T?> GetAsync(CancellationToken ct = default)
{
await _semaphore.WaitAsync(ct);
try
{
if (_lastUpdated > clock.GetCurrentInstant() - MaxAge)
{
return _item;
}
_logger.Debug("Cached item of type {Type} is expired, fetching it.", typeof(T));
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
await using DatabaseContext db =
scope.ServiceProvider.GetRequiredService<DatabaseContext>();
T? item = await FetchFunc(db, ct);
_item = item;
_lastUpdated = clock.GetCurrentInstant();
return item;
}
finally
{
_semaphore.Release();
}
}
}

View file

@ -39,6 +39,8 @@ public class KeyCacheService(Config config)
public async Task DeleteKeyAsync(string key) => public async Task DeleteKeyAsync(string key) =>
await Multiplexer.GetDatabase().KeyDeleteAsync(key); await Multiplexer.GetDatabase().KeyDeleteAsync(key);
public Task DeleteExpiredKeysAsync(CancellationToken ct) => Task.CompletedTask;
public async Task SetKeyAsync<T>(string key, T obj, Duration expiresAt) public async Task SetKeyAsync<T>(string key, T obj, Duration expiresAt)
where T : class where T : class
{ {

View file

@ -33,9 +33,11 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
// The type is literally written on the same line, we can just use `var` // The type is literally written on the same line, we can just use `var`
// ReSharper disable SuggestVarOrType_SimpleTypes // ReSharper disable SuggestVarOrType_SimpleTypes
var keyCacheService = scope.ServiceProvider.GetRequiredService<KeyCacheService>();
var dataCleanupService = scope.ServiceProvider.GetRequiredService<DataCleanupService>(); var dataCleanupService = scope.ServiceProvider.GetRequiredService<DataCleanupService>();
// ReSharper restore SuggestVarOrType_SimpleTypes // ReSharper restore SuggestVarOrType_SimpleTypes
await keyCacheService.DeleteExpiredKeysAsync(ct);
await dataCleanupService.InvokeAsync(ct); await dataCleanupService.InvokeAsync(ct);
} }
} }

View file

@ -33,7 +33,6 @@ public class UserRendererService(
bool renderMembers = true, bool renderMembers = true,
bool renderAuthMethods = false, bool renderAuthMethods = false,
string? overrideSid = null, string? overrideSid = null,
bool renderSettings = false,
CancellationToken ct = default CancellationToken ct = default
) => ) =>
await RenderUserInnerAsync( await RenderUserInnerAsync(
@ -43,7 +42,6 @@ public class UserRendererService(
renderMembers, renderMembers,
renderAuthMethods, renderAuthMethods,
overrideSid, overrideSid,
renderSettings,
ct ct
); );
@ -54,7 +52,6 @@ public class UserRendererService(
bool renderMembers = true, bool renderMembers = true,
bool renderAuthMethods = false, bool renderAuthMethods = false,
string? overrideSid = null, string? overrideSid = null,
bool renderSettings = false,
CancellationToken ct = default CancellationToken ct = default
) )
{ {
@ -65,7 +62,6 @@ public class UserRendererService(
renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers); renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers);
renderAuthMethods = renderAuthMethods && tokenPrivileged; renderAuthMethods = renderAuthMethods && tokenPrivileged;
renderSettings = renderSettings && tokenHidden;
IEnumerable<Member> members = renderMembers IEnumerable<Member> members = renderMembers
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
@ -121,8 +117,7 @@ public class UserRendererService(
tokenHidden ? user.LastSidReroll : null, tokenHidden ? user.LastSidReroll : null,
tokenHidden ? user.Timezone ?? "<none>" : null, tokenHidden ? user.Timezone ?? "<none>" : null,
tokenHidden ? user is { Deleted: true, DeletedBy: not null } : null, tokenHidden ? user is { Deleted: true, DeletedBy: not null } : null,
tokenHidden ? user.Deleted : null, tokenHidden ? user.Deleted : null
renderSettings ? user.Settings : null
); );
} }

View file

@ -31,7 +31,6 @@ public partial class ValidationService
"settings", "settings",
"pronouns.cc", "pronouns.cc",
"pronounscc", "pronounscc",
"null",
]; ];
private static readonly string[] InvalidMemberNames = private static readonly string[] InvalidMemberNames =
@ -39,10 +38,8 @@ public partial class ValidationService
// these break routing outright // these break routing outright
".", ".",
"..", "..",
// TODO: remove this? i'm not sure if /@[username]/edit will redirect to settings // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
"edit", "edit",
// this breaks the frontend, somehow
"null",
]; ];
public ValidationError? ValidateUsername(string username) public ValidationError? ValidateUsername(string username)

View file

@ -155,18 +155,6 @@
"Microsoft.Extensions.Primitives": "9.0.2" "Microsoft.Extensions.Primitives": "9.0.2"
} }
}, },
"Microsoft.Extensions.Http.Resilience": {
"type": "Direct",
"requested": "[9.2.0, )",
"resolved": "9.2.0",
"contentHash": "Km+YyCuk1IaeOsAzPDygtgsUOh3Fi89hpA18si0tFJmpSBf9aKzP9ffV5j7YOoVDvRWirpumXAPQzk1inBsvKw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
"Microsoft.Extensions.Http.Diagnostics": "9.2.0",
"Microsoft.Extensions.ObjectPool": "9.0.2",
"Microsoft.Extensions.Resilience": "9.2.0"
}
},
"MimeKit": { "MimeKit": {
"type": "Direct", "type": "Direct",
"requested": "[4.10.0, )", "requested": "[4.10.0, )",
@ -205,23 +193,23 @@
}, },
"Npgsql.EntityFrameworkCore.PostgreSQL": { "Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.4, )", "requested": "[9.0.3, )",
"resolved": "9.0.4", "resolved": "9.0.3",
"contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", "contentHash": "1A6HpMPbzK+quxdtug1aDHI4BSRTgpi7OaDt8WQh7SFJd2sSQ0nNTZ7sYrwyxVf4AdKdN7XJL9tpiiJjRUaa4g==",
"dependencies": { "dependencies": {
"Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)",
"Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)",
"Npgsql": "9.0.3" "Npgsql": "9.0.2"
} }
}, },
"Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.4, )", "requested": "[9.0.3, )",
"resolved": "9.0.4", "resolved": "9.0.3",
"contentHash": "QZ80CL3c9xzC83eVMWYWa1RcFZA6HJtpMAKFURlmz+1p0OyysSe8R6f/4sI9vk/nwqF6Fkw3lDgku/xH6HcJYg==", "contentHash": "Eks1o3NfIbS3EHhrXC0QABrQab7CJ64C2+kF0YJWLwlH/tu3ExrgrSLpLI6INdeMYcLr2PXu71LjJsrQNVciYg==",
"dependencies": { "dependencies": {
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.4", "Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.3",
"Npgsql.NodaTime": "9.0.3" "Npgsql.NodaTime": "9.0.2"
} }
}, },
"Npgsql.Json.NET": { "Npgsql.Json.NET": {
@ -261,18 +249,18 @@
}, },
"Scalar.AspNetCore": { "Scalar.AspNetCore": {
"type": "Direct", "type": "Direct",
"requested": "[2.0.26, )", "requested": "[2.0.18, )",
"resolved": "2.0.26", "resolved": "2.0.18",
"contentHash": "0tKBFM7quBq0ifgRWo7eTTVpiTbnwpf/6ygtb/aYVuo0D2gMsYknAJRqEhH8HFBqzntNiYpzHbQSf2b+VAA8sA==" "contentHash": "nS8Sw6wRO1A/dARn3q9R6znIBfddJcmAczI5uMROBGWkO2KG/ad/Ld+UeUePTxGr1+6humJSOxI7An+q4q3oGA=="
}, },
"Sentry.AspNetCore": { "Sentry.AspNetCore": {
"type": "Direct", "type": "Direct",
"requested": "[5.3.0, )", "requested": "[5.2.0, )",
"resolved": "5.3.0", "resolved": "5.2.0",
"contentHash": "zC2yhwQB0laYWGXLYDCsiKSIqleaEK3fUH9Z5t8Bgvfs2nGX0mHmh9oPqNAAbkVGvni56mhgHHCBxN/kpfkawA==", "contentHash": "vEKanBDOxCnEQrcMq3j47z8HOblRfiyJotdm9Fyc24cmLrLsTYZnWWprCYstt++M9bGSXYf4jrM2aaWxgJ8aww==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Binder": "9.0.0", "Microsoft.Extensions.Configuration.Binder": "9.0.0",
"Sentry.Extensions.Logging": "5.3.0" "Sentry.Extensions.Logging": "5.2.0"
} }
}, },
"Serilog": { "Serilog": {
@ -317,15 +305,15 @@
}, },
"SixLabors.ImageSharp": { "SixLabors.ImageSharp": {
"type": "Direct", "type": "Direct",
"requested": "[3.1.7, )", "requested": "[3.1.6, )",
"resolved": "3.1.7", "resolved": "3.1.6",
"contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" "contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA=="
}, },
"StackExchange.Redis": { "StackExchange.Redis": {
"type": "Direct", "type": "Direct",
"requested": "[2.8.31, )", "requested": "[2.8.24, )",
"resolved": "2.8.31", "resolved": "2.8.24",
"contentHash": "RCHVQa9Zke8k0oBgJn1Yl6BuYy8i6kv+sdMObiH60nOwD6QvWAjxdDwOm+LO78E8WsGiPqgOuItkz98fPS6haQ==", "contentHash": "GWllmsFAtLyhm4C47cOCipGxyEi1NQWTFUHXnJ8hiHOsK/bH3T5eLkWPVW+LRL6jDiB3g3izW3YEHgLuPoJSyA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Pipelines.Sockets.Unofficial": "2.2.8" "Pipelines.Sockets.Unofficial": "2.2.8"
@ -549,16 +537,6 @@
"Microsoft.Extensions.Logging": "9.0.2" "Microsoft.Extensions.Logging": "9.0.2"
} }
}, },
"Microsoft.Extensions.AmbientMetadata.Application": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "GMCX3zybUB22aAADjYPXrWhhd1HNMkcY5EcFAJnXy/4k5pPpJ6TS4VRl37xfrtosNyzbpO2SI7pd2Q5PvggSdg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "9.0.2",
"Microsoft.Extensions.Hosting.Abstractions": "9.0.2",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
}
},
"Microsoft.Extensions.Caching.Abstractions": { "Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.2",
@ -567,22 +545,13 @@
"Microsoft.Extensions.Primitives": "9.0.2" "Microsoft.Extensions.Primitives": "9.0.2"
} }
}, },
"Microsoft.Extensions.Compliance.Abstractions": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "Te+N4xphDlGIS90lKJMZyezFiMWKLAtYV2/M8gGJG4thH6xyC7LWhMzgz2+tWMehxwZlBUq2D9DvVpjKBZFTPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
"Microsoft.Extensions.ObjectPool": "9.0.2"
}
},
"Microsoft.Extensions.Configuration": { "Microsoft.Extensions.Configuration": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==", "contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2", "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
"Microsoft.Extensions.Primitives": "9.0.2" "Microsoft.Extensions.Primitives": "9.0.0"
} }
}, },
"Microsoft.Extensions.Configuration.Abstractions": { "Microsoft.Extensions.Configuration.Abstractions": {
@ -595,10 +564,10 @@
}, },
"Microsoft.Extensions.Configuration.Binder": { "Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==", "contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2" "Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
} }
}, },
"Microsoft.Extensions.DependencyInjection": { "Microsoft.Extensions.DependencyInjection": {
@ -614,14 +583,6 @@
"resolved": "9.0.2", "resolved": "9.0.2",
"contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg=="
}, },
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "WcwfTpl3IcPcaahTVEaJwMUg1eWog1SkIA6jQZZFqMXiMX9/tVkhNB6yzUQmBdGWdlWDDRKpOmK7T7x1Uu05pQ==",
"dependencies": {
"Microsoft.Extensions.Hosting.Abstractions": "9.0.2"
}
},
"Microsoft.Extensions.DependencyModel": { "Microsoft.Extensions.DependencyModel": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.2",
@ -629,74 +590,54 @@
}, },
"Microsoft.Extensions.Diagnostics": { "Microsoft.Extensions.Diagnostics": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==", "contentHash": "0CF9ZrNw5RAlRfbZuVIvzzhP8QeWqHiUmMBU/2H7Nmit8/vwP3/SbHeEctth7D4Gz2fBnEbokPc1NU8/j/1ZLw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.2", "Microsoft.Extensions.Configuration": "9.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0"
} }
}, },
"Microsoft.Extensions.Diagnostics.Abstractions": { "Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==", "contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.2" "Microsoft.Extensions.Options": "9.0.0"
}
},
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "et5JevHsLv1w1O1Zhb6LiUfai/nmDRzIHnbrZJdzLsIbbMCKTZpeHuANYIppAD//n12KvgOne05j4cu0GhG9gw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
} }
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==", "contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "9.0.2" "Microsoft.Extensions.Primitives": "9.0.0"
} }
}, },
"Microsoft.Extensions.Hosting.Abstractions": { "Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==", "contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2", "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.2", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.2" "Microsoft.Extensions.Logging.Abstractions": "9.0.0"
} }
}, },
"Microsoft.Extensions.Http": { "Microsoft.Extensions.Http": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==", "contentHash": "DqI4q54U4hH7bIAq9M5a/hl5Odr/KBAoaZ0dcT4OgutD8dook34CbkvAfAIzkMVjYXiL+E5ul9etwwqiX4PHGw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2", "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Diagnostics": "9.0.2", "Microsoft.Extensions.Diagnostics": "9.0.0",
"Microsoft.Extensions.Logging": "9.0.2", "Microsoft.Extensions.Logging": "9.0.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.2", "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.2" "Microsoft.Extensions.Options": "9.0.0"
}
},
"Microsoft.Extensions.Http.Diagnostics": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "Eeup1LuD5hVk5SsKAuX1D7I9sF380MjrNG10IaaauRLOmrRg8rq2TA8PYTXVBXf3MLkZ6m2xpBqRbZdxf8ygkg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0",
"Microsoft.Extensions.Http": "9.0.2",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2",
"Microsoft.Extensions.Telemetry": "9.2.0",
"System.IO.Pipelines": "9.0.2"
} }
}, },
"Microsoft.Extensions.Logging": { "Microsoft.Extensions.Logging": {
@ -719,23 +660,23 @@
}, },
"Microsoft.Extensions.Logging.Configuration": { "Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==", "contentHash": "H05HiqaNmg6GjH34ocYE9Wm1twm3Oz2aXZko8GTwGBzM7op2brpAA8pJ5yyD1OpS1mXUtModBYOlcZ/wXeWsSg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.2", "Microsoft.Extensions.Configuration": "9.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2", "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
"Microsoft.Extensions.Configuration.Binder": "9.0.2", "Microsoft.Extensions.Configuration.Binder": "9.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Logging": "9.0.2", "Microsoft.Extensions.Logging": "9.0.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.2", "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.2", "Microsoft.Extensions.Options": "9.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0"
} }
}, },
"Microsoft.Extensions.ObjectPool": { "Microsoft.Extensions.ObjectPool": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "7.0.0",
"contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw==" "contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
}, },
"Microsoft.Extensions.Options": { "Microsoft.Extensions.Options": {
"type": "Transitive", "type": "Transitive",
@ -748,14 +689,14 @@
}, },
"Microsoft.Extensions.Options.ConfigurationExtensions": { "Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "9.0.0",
"contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==", "contentHash": "Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2", "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
"Microsoft.Extensions.Configuration.Binder": "9.0.2", "Microsoft.Extensions.Configuration.Binder": "9.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.2", "Microsoft.Extensions.Options": "9.0.0",
"Microsoft.Extensions.Primitives": "9.0.2" "Microsoft.Extensions.Primitives": "9.0.0"
} }
}, },
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
@ -763,42 +704,6 @@
"resolved": "9.0.2", "resolved": "9.0.2",
"contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA==" "contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA=="
}, },
"Microsoft.Extensions.Resilience": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "dyaM+Jeznh/i21bOrrRs3xceFfn0571EOjOq95dRXmL1rHDLC4ExhACJ2xipRBP6g1AgRNqmryi+hMrVWWgmlg==",
"dependencies": {
"Microsoft.Extensions.Diagnostics": "9.0.2",
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": "9.2.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2",
"Microsoft.Extensions.Telemetry.Abstractions": "9.2.0",
"Polly.Extensions": "8.4.2",
"Polly.RateLimiting": "8.4.2"
}
},
"Microsoft.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "4+bw7W4RrAMrND9TxonnSmzJOdXiPxljoda8OPJiReIN607mKCc0t0Mf28sHNsTujO1XQw28wsI0poxeeQxohw==",
"dependencies": {
"Microsoft.Extensions.AmbientMetadata.Application": "9.2.0",
"Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0",
"Microsoft.Extensions.Logging.Configuration": "9.0.2",
"Microsoft.Extensions.ObjectPool": "9.0.2",
"Microsoft.Extensions.Telemetry.Abstractions": "9.2.0"
}
},
"Microsoft.Extensions.Telemetry.Abstractions": {
"type": "Transitive",
"resolved": "9.2.0",
"contentHash": "kEl+5G3RqS20XaEhHh/nOugcjKEK+rgVtMJra1iuwNzdzQXElelf3vu8TugcT7rIZ/T4T76EKW1OX/fmlxz4hw==",
"dependencies": {
"Microsoft.Extensions.Compliance.Abstractions": "9.2.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
"Microsoft.Extensions.ObjectPool": "9.0.2",
"Microsoft.Extensions.Options": "9.0.2"
}
},
"Microsoft.NETCore.Platforms": { "Microsoft.NETCore.Platforms": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.1.1", "resolved": "1.1.1",
@ -840,11 +745,11 @@
}, },
"Npgsql.NodaTime": { "Npgsql.NodaTime": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.2",
"contentHash": "PMWXCft/iw+5A7eCeMcy6YZXBst6oeisbCkv2JMQVG4SAFa5vQaf6K2voXzUJCqzwOFcCWs+oT42w2uMDFpchw==", "contentHash": "jURb6VGmmR3pPae2N3HrUixSZ/U5ovqZgg/qo3m5Rq/q0m2fpxbZcsHZo21s5MLa/AfJAx4hcFMY98D4RtLdcg==",
"dependencies": { "dependencies": {
"NodaTime": "3.2.0", "NodaTime": "3.2.0",
"Npgsql": "9.0.3" "Npgsql": "9.0.2"
} }
}, },
"Pipelines.Sockets.Unofficial": { "Pipelines.Sockets.Unofficial": {
@ -855,44 +760,20 @@
"System.IO.Pipelines": "5.0.1" "System.IO.Pipelines": "5.0.1"
} }
}, },
"Polly.Core": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g=="
},
"Polly.Extensions": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Polly.Core": "8.4.2"
}
},
"Polly.RateLimiting": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==",
"dependencies": {
"Polly.Core": "8.4.2",
"System.Threading.RateLimiting": "8.0.0"
}
},
"Sentry": { "Sentry": {
"type": "Transitive", "type": "Transitive",
"resolved": "5.3.0", "resolved": "5.2.0",
"contentHash": "zlBIP7YmYxySwcgapLMj1gdxPEz9rwdrOa4Yjub/TzcAaMQXusRH9hY4CE6pu0EIibZ7C7Hhjhr6xOTlyK8gFQ==" "contentHash": "b3aZSOU2CjlIIFRtPRbXParKQ+9PF+JOqkSD7Gxq6PiR07t1rnK+crPtdrWMXfW6PVo/s67trCJ+fuLsgTeADw=="
}, },
"Sentry.Extensions.Logging": { "Sentry.Extensions.Logging": {
"type": "Transitive", "type": "Transitive",
"resolved": "5.3.0", "resolved": "5.2.0",
"contentHash": "DPN6NXvO4LTH21UM2gUFJwSwVa/fuT3B/UZmQyfSfecqViXrZO7WFuKz/h592YUoGNCumyt8x045bxbz6j9btg==", "contentHash": "546bHsERKY7/pG5T4mVIp6WbHnQPMst6VDuxSaeU5DhQHLfh7KhgMmkdZ4Xvdlr95fvWk5/bX2xbipy6qoh/1A==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Binder": "9.0.0", "Microsoft.Extensions.Configuration.Binder": "9.0.0",
"Microsoft.Extensions.Http": "9.0.0", "Microsoft.Extensions.Http": "9.0.0",
"Microsoft.Extensions.Logging.Configuration": "9.0.0", "Microsoft.Extensions.Logging.Configuration": "9.0.0",
"Sentry": "5.3.0" "Sentry": "5.2.0"
} }
}, },
"Serilog.Extensions.Hosting": { "Serilog.Extensions.Hosting": {
@ -1020,8 +901,8 @@
}, },
"System.IO.Pipelines": { "System.IO.Pipelines": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.2", "resolved": "7.0.0",
"contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ==" "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
}, },
"System.Reactive": { "System.Reactive": {
"type": "Transitive", "type": "Transitive",
@ -1059,11 +940,6 @@
"type": "Transitive", "type": "Transitive",
"resolved": "7.0.0", "resolved": "7.0.0",
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
},
"System.Threading.RateLimiting": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
} }
} }
} }

View file

@ -4,6 +4,5 @@
"i18n-ally.localesPaths": ["src/lib/i18n", "src/lib/i18n/locales"], "i18n-ally.localesPaths": ["src/lib/i18n", "src/lib/i18n/locales"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"explorer.sortOrder": "filesFirst", "explorer.sortOrder": "filesFirst",
"explorer.compactFolders": false, "explorer.compactFolders": false
"eslint.validate": ["javascript", "javascriptreact", "svelte"]
} }

View file

@ -1,5 +1,9 @@
FROM docker.io/node:23-slim FROM docker.io/node:23-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY ./Foxnouns.Frontend /app COPY ./Foxnouns.Frontend /app
COPY ./docker/frontend.env /app/.env.local COPY ./docker/frontend.env /app/.env.local
WORKDIR /app WORKDIR /app
@ -7,7 +11,7 @@ WORKDIR /app
ENV PRIVATE_API_HOST=http://rate:5003/api ENV PRIVATE_API_HOST=http://rate:5003/api
ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api
RUN npm ci RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN npm run build RUN pnpm run build
CMD ["node", "build/index.js"] CMD ["pnpm", "node", "build/index.js"]

View file

@ -1,6 +1,6 @@
// This script regenerates the list of icons for the frontend (Foxnouns.Frontend/src/lib/icons.ts) // This script regenerates the list of icons for the frontend (Foxnouns.Frontend/src/lib/icons.ts)
// and the backend (Foxnouns.Backend/Utils/BootstrapIcons.Icons.cs) from the currently installed version of Bootstrap Icons. // and the backend (Foxnouns.Backend/Utils/BootstrapIcons.Icons.cs) from the currently installed version of Bootstrap Icons.
// Run with `node icons.js` in the frontend directory. // Run with `pnpm node icons.js` in the frontend directory.
import { writeFileSync } from "fs"; import { writeFileSync } from "fs";
import icons from "bootstrap-icons/font/bootstrap-icons.json" with { type: "json" }; import icons from "bootstrap-icons/font/bootstrap-icons.json" with { type: "json" };

File diff suppressed because it is too large Load diff

View file

@ -12,42 +12,42 @@
"lint": "prettier --check . && eslint ." "lint": "prettier --check . && eslint ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.10",
"@sveltejs/kit": "^2.20.4", "@sveltejs/kit": "^2.12.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.2",
"@sveltestrap/sveltestrap": "^7.1.0", "@sveltestrap/sveltestrap": "^7.1.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/luxon": "^3.6.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/sanitize-html": "^2.15.0", "@types/sanitize-html": "^2.13.0",
"bootstrap": "^5.3.5", "bootstrap": "^5.3.3",
"dotenv": "^16.4.7", "eslint": "^9.17.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^16.0.0", "globals": "^15.13.0",
"prettier": "^3.5.3", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.2",
"sass": "^1.86.3", "sass": "^1.83.0",
"svelte": "^5.25.7", "svelte": "^5.14.3",
"svelte-bootstrap-icons": "^3.1.2", "svelte-bootstrap-icons": "^3.1.1",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.1",
"svelte-easy-crop": "^4.0.1", "svelte-easy-crop": "^4.0.0",
"sveltekit-i18n": "^2.4.2", "sveltekit-i18n": "^2.4.2",
"typescript": "^5.8.3", "typescript": "^5.7.2",
"typescript-eslint": "^8.29.0", "typescript-eslint": "^8.18.1",
"vite": "^6.2.5" "vite": "^6.0.3"
}, },
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
"dependencies": { "dependencies": {
"@fontsource/firago": "^5.2.5", "@fontsource/firago": "^5.1.0",
"@sentry/sveltekit": "^9.11.0", "@sentry/sveltekit": "^8.52.0",
"base64-arraybuffer": "^1.0.2", "base64-arraybuffer": "^1.0.2",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"luxon": "^3.6.1", "luxon": "^3.5.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"sanitize-html": "^2.15.0", "sanitize-html": "^2.13.1",
"svelte-tippy": "^1.3.2", "svelte-tippy": "^1.3.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tslog": "^4.9.3" "tslog": "^4.9.3"

4207
Foxnouns.Frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,6 @@ export type Meta = {
}; };
members: number; members: number;
limits: Limits; limits: Limits;
notice: { id: string; message: string } | null;
}; };
export type Limits = { export type Limits = {

View file

@ -28,7 +28,6 @@ export type MeUser = UserWithMembers & {
timezone: string; timezone: string;
suspended: boolean; suspended: boolean;
deleted: boolean; deleted: boolean;
settings: UserSettings;
}; };
export type UserWithMembers = User & { members: PartialMember[] | null }; export type UserWithMembers = User & { members: PartialMember[] | null };
@ -41,7 +40,6 @@ export type UserWithHiddenFields = User & {
export type UserSettings = { export type UserSettings = {
dark_mode: boolean | null; dark_mode: boolean | null;
last_read_notice: string | null;
}; };
export type PartialMember = { export type PartialMember = {

View file

@ -5,11 +5,8 @@
currentPage: number; currentPage: number;
pageCount: number; pageCount: number;
center?: boolean; center?: boolean;
listAllPages?: boolean;
}; };
let { currentPage = $bindable(), pageCount, center, listAllPages }: Props = $props(); let { currentPage = $bindable(), pageCount, center }: Props = $props();
let allPages = $derived(listAllPages === undefined || listAllPages);
let prevPage = $derived(currentPage > 0 ? currentPage - 1 : 0); let prevPage = $derived(currentPage > 0 ? currentPage - 1 : 0);
let nextPage = $derived(currentPage < pageCount - 1 ? currentPage + 1 : pageCount - 1); let nextPage = $derived(currentPage < pageCount - 1 ? currentPage + 1 : pageCount - 1);
@ -24,27 +21,11 @@
<PaginationItem> <PaginationItem>
<PaginationLink previous onclick={() => (currentPage = prevPage)} /> <PaginationLink previous onclick={() => (currentPage = prevPage)} />
</PaginationItem> </PaginationItem>
{#if allPages} {#each new Array(pageCount) as _, page}
{#each new Array(pageCount) as _, page} <PaginationItem active={page === currentPage}>
<PaginationItem active={page === currentPage}> <PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink>
<PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink>
</PaginationItem>
{/each}
{:else}
{#if currentPage !== 0}
<PaginationItem onclick={() => (currentPage = prevPage)}>
<PaginationLink>{currentPage}</PaginationLink>
</PaginationItem>
{/if}
<PaginationItem active>
<PaginationLink>{currentPage + 1}</PaginationLink>
</PaginationItem> </PaginationItem>
{#if currentPage !== pageCount - 1} {/each}
<PaginationItem onclick={() => (currentPage = nextPage)}>
<PaginationLink>{currentPage + 2}</PaginationLink>
</PaginationItem>
{/if}
{/if}
<PaginationItem> <PaginationItem>
<PaginationLink next onclick={() => (currentPage = nextPage)} /> <PaginationLink next onclick={() => (currentPage = nextPage)} />
</PaginationItem> </PaginationItem>

View file

@ -1,50 +0,0 @@
<script lang="ts">
import { fastRequest } from "$api";
import type { UserSettings } from "$api/models";
import { idTimestamp } from "$lib";
import { t } from "$lib/i18n";
import log from "$lib/log";
import { renderUnsafeMarkdown } from "$lib/markdown";
import { DateTime } from "luxon";
type Props = { id: string; message: string; settings?: UserSettings; token: string | null };
let { id, message, settings, token }: Props = $props();
let lastReadNotice = $state(settings?.last_read_notice || null);
// Render the notice if:
// - user is not logged in (no settings object)
// - last read notice is null (never marked any notice as read)
// - last read notice ID is smaller than the current one (has not marked the current notice as read)
let renderNotice = $derived(!lastReadNotice || lastReadNotice < id);
let canDismiss = $derived(!!token);
let renderedMessage = $derived(renderUnsafeMarkdown(message));
let dismiss = async () => {
if (!token) return;
try {
await fastRequest("PATCH", "/users/@me/settings", { token, body: { last_read_notice: id } });
lastReadNotice = id;
} catch (e) {
log.error("error updating last read notice ID:", e);
}
};
</script>
{#if renderNotice}
<div class="alert alert-light" role="alert">
<div>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html renderedMessage}
</div>
{#if canDismiss}
<div>
<!-- svelte-ignore a11y_invalid_attribute -->
<a href="#" tabindex="0" role="button" onclick={() => dismiss()} onkeyup={() => dismiss()}>
{$t("notification.mark-as-read")}
</a>
{idTimestamp(id).toLocaleString(DateTime.DATETIME_MED)}
</div>
{/if}
</div>
{/if}

View file

@ -59,7 +59,6 @@
{#if reason} {#if reason}
<details> <details>
<summary>Reason</summary> <summary>Reason</summary>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html reason} {@html reason}
</details> </details>
{:else} {:else}

View file

@ -29,7 +29,6 @@
<h4>Reason</h4> <h4>Reason</h4>
<p> <p>
{#if entry.reason} {#if entry.reason}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html renderMarkdown(entry.reason)} {@html renderMarkdown(entry.reason)}
{:else} {:else}
<em class="text-secondary">(no reason given)</em> <em class="text-secondary">(no reason given)</em>

View file

@ -4,35 +4,23 @@
import Search from "svelte-bootstrap-icons/lib/Search.svelte"; import Search from "svelte-bootstrap-icons/lib/Search.svelte";
import FlagButton from "./FlagButton.svelte"; import FlagButton from "./FlagButton.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import paginate from "$lib/paginate";
import ClientPaginator from "$components/ClientPaginator.svelte";
type Props = { flags: PrideFlag[]; select(flag: PrideFlag): void }; type Props = { flags: PrideFlag[]; select(flag: PrideFlag): void };
let { flags, select }: Props = $props(); let { flags, select }: Props = $props();
const FLAGS_PER_PAGE = 5;
let query = $state(""); let query = $state("");
let filteredFlags = $derived(search(query)); let filteredFlags = $derived(search(query));
let arr: PrideFlag[] = $state([]);
let currentPage = $state(0);
let pageCount = $state(0);
$effect(() => {
const pages = paginate(filteredFlags, currentPage, FLAGS_PER_PAGE);
arr = pages.data;
pageCount = pages.pageCount;
});
function search(q: string) { function search(q: string) {
return flags.filter((f) => f.name.toLowerCase().indexOf(q.toLowerCase()) !== -1); if (!q) return flags.slice(0, 20);
return flags.filter((f) => f.name.toLowerCase().indexOf(q.toLowerCase()) !== -1).slice(0, 20);
} }
</script> </script>
<input class="form-control" placeholder={$t("editor.flag-search-placeholder")} bind:value={query} /> <input class="form-control" placeholder={$t("editor.flag-search-placeholder")} bind:value={query} />
<div class="mt-3"> <div class="mt-3">
{#each arr as flag (flag.id)} {#each filteredFlags as flag (flag.id)}
<FlagButton {flag} onclick={() => select(flag)} padding /> <FlagButton {flag} onclick={() => select(flag)} padding />
{:else} {:else}
<div class="text-secondary text-center"> <div class="text-secondary text-center">
@ -48,7 +36,6 @@
</p> </p>
</div> </div>
{/each} {/each}
<ClientPaginator bind:currentPage {pageCount} listAllPages={false} />
{#if flags.length > 0} {#if flags.length > 0}
<p class="text-secondary mt-2"> <p class="text-secondary mt-2">
<InfoCircleFill aria-hidden /> <InfoCircleFill aria-hidden />

View file

@ -38,6 +38,6 @@
</div> </div>
<div class="card-footer text-body-secondary"> <div class="card-footer text-body-secondary">
{idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)} {idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)}
<a href="/settings/notifications/ack/{notification.id}">{$t("notification.mark-as-read")}</a> <a href="/settings/notifications/ack/{notification.id}">Mark as read</a>
</div> </div>
</div> </div>

View file

@ -296,8 +296,7 @@
"custom-preference-notice": "Want to edit your custom preferences?", "custom-preference-notice": "Want to edit your custom preferences?",
"custom-preference-notice-link": "Go to settings", "custom-preference-notice-link": "Go to settings",
"crop-avatar-header": "Crop avatar", "crop-avatar-header": "Crop avatar",
"crop-avatar-button": "Crop", "crop-avatar-button": "Crop"
"max-custom-preferences": "You have reached the maximum amount of custom preferences ({{current}}/{{max}}), and cannot add new ones."
}, },
"cancel": "Cancel", "cancel": "Cancel",
"report": { "report": {
@ -351,8 +350,6 @@
"notification": { "notification": {
"suspension": "Your account has been suspended for the following reason: {{reason}}", "suspension": "Your account has been suspended for the following reason: {{reason}}",
"warning": "You have been warned for the following reason: {{reason}}", "warning": "You have been warned for the following reason: {{reason}}",
"warning-cleared-fields": "You have been warned for the following reason: {{reason}}\n\nAdditionally, the following fields have been cleared from your profile:\n{{clearedFields}}", "warning-cleared-fields": "You have been warned for the following reason: {{reason}}\n\nAdditionally, the following fields have been cleared from your profile:\n{{clearedFields}}"
"mark-as-read": "Mark as read",
"no-notifications": "You have no notifications."
} }
} }

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import GlobalNotice from "$components/GlobalNotice.svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
type Props = { data: PageData }; type Props = { data: PageData };
@ -11,15 +10,6 @@
</svelte:head> </svelte:head>
<div class="container"> <div class="container">
{#if data.meta.notice}
<GlobalNotice
id={data.meta.notice.id}
message={data.meta.notice.message}
settings={data.meUser?.settings}
token={data.token}
/>
{/if}
<h1>pronouns.cc</h1> <h1>pronouns.cc</h1>
<p> <p>

View file

@ -56,7 +56,6 @@
<h3>Context</h3> <h3>Context</h3>
<p> <p>
{#if report.context} {#if report.context}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html renderMarkdown(report.context)} {@html renderMarkdown(report.context)}
{:else} {:else}
<em>(no context given)</em> <em>(no context given)</em>

View file

@ -18,6 +18,5 @@
</svelte:head> </svelte:head>
<div class="container"> <div class="container">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html md} {@html md}
</div> </div>

View file

@ -3,28 +3,15 @@
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import { Nav, NavLink } from "@sveltestrap/sveltestrap"; import { Nav, NavLink } from "@sveltestrap/sveltestrap";
import { isActive } from "$lib/pageUtils.svelte"; import { isActive } from "$lib/pageUtils.svelte";
import type { LayoutData } from "./$types";
import GlobalNotice from "$components/GlobalNotice.svelte";
type Props = { data: LayoutData; children: Snippet }; type Props = { children: Snippet };
let { data, children }: Props = $props(); let { children }: Props = $props();
</script> </script>
<svelte:head> <svelte:head>
<title>{$t("title.settings")} • pronouns.cc</title> <title>{$t("title.settings")} • pronouns.cc</title>
</svelte:head> </svelte:head>
{#if data.meta.notice}
<div class="container">
<GlobalNotice
id={data.meta.notice.id}
message={data.meta.notice.message}
settings={data.meUser?.settings}
token={data.token}
/>
</div>
{/if}
<div class="container"> <div class="container">
<Nav pills justified fill class="flex-column flex-md-row mb-2"> <Nav pills justified fill class="flex-column flex-md-row mb-2">
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings"> <NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">

View file

@ -13,7 +13,6 @@
import ApiError from "$api/error"; import ApiError from "$api/error";
import log from "$lib/log"; import log from "$lib/log";
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
import Search from "svelte-bootstrap-icons/lib/Search.svelte";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -27,17 +26,13 @@
const FLAGS_PER_PAGE = 50; const FLAGS_PER_PAGE = 50;
$effect(() => { $effect(() => {
const filteredFlags = search const pages = paginate(flags, currentPage, FLAGS_PER_PAGE);
? flags.filter((f) => f.name.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1)
: flags;
const pages = paginate(filteredFlags, currentPage, FLAGS_PER_PAGE);
arr = pages.data; arr = pages.data;
pageCount = pages.pageCount; pageCount = pages.pageCount;
}); });
let lastEditedFlag: string | null = $state(null); let lastEditedFlag: string | null = $state(null);
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
let search: string = $state("");
const update = async ( const update = async (
id: string, id: string,
@ -114,44 +109,20 @@
})} })}
</h4> </h4>
<div class="mb-3">
<input
class="form-control"
type="text"
bind:value={search}
placeholder={$t("editor.flag-search-placeholder")}
/>
</div>
<ClientPaginator center bind:currentPage {pageCount} /> <ClientPaginator center bind:currentPage {pageCount} />
{#if arr.length === 0} <Accordion class="mb-3">
<div class="text-secondary text-center"> {#each arr as flag (flag.id)}
<p> <AccordionItem>
<Search class="no-flags-icon" height={64} width={64} aria-hidden /> <span slot="header">
</p> <EditorFlagImage {flag} />
<p> {flag.name}
{#if search} </span>
{$t("editor.flag-search-no-flags")}
{:else}
{$t("editor.flag-search-no-account-flags")}
{/if}
</p>
</div>
{:else}
<Accordion class="mb-3">
{#each arr as flag (flag.id)}
<AccordionItem>
<span slot="header">
<EditorFlagImage {flag} />
{flag.name}
</span>
{#if lastEditedFlag === flag.id}<FormStatusMarker form={ok} />{/if} {#if lastEditedFlag === flag.id}<FormStatusMarker form={ok} />{/if}
<FlagEditor {flag} {update} {deleteFlag} /> <FlagEditor {flag} {update} {deleteFlag} />
</AccordionItem> </AccordionItem>
{/each} {/each}
</Accordion> </Accordion>
{/if}
<ClientPaginator center bind:currentPage {pageCount} /> <ClientPaginator center bind:currentPage {pageCount} />

View file

@ -2,7 +2,6 @@
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import Notification from "$components/settings/Notification.svelte"; import Notification from "$components/settings/Notification.svelte";
import UrlAlert from "$components/URLAlert.svelte"; import UrlAlert from "$components/URLAlert.svelte";
import { t } from "$lib/i18n";
type Props = { data: PageData }; type Props = { data: PageData };
let { data }: Props = $props(); let { data }: Props = $props();
@ -13,5 +12,5 @@
{#each data.notifications as notification (notification.id)} {#each data.notifications as notification (notification.id)}
<Notification {notification} /> <Notification {notification} />
{:else} {:else}
{$t("notification.no-notifications")} You have no notifications.
{/each} {/each}

View file

@ -7,7 +7,6 @@
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import log from "$lib/log"; import log from "$lib/log";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte";
type Props = { data: PageData }; type Props = { data: PageData };
let { data }: Props = $props(); let { data }: Props = $props();
@ -85,19 +84,8 @@
{$t("settings.custom-preferences-title")} {$t("settings.custom-preferences-title")}
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-primary" onclick={() => save()}>{$t("save-changes")}</button> <button class="btn btn-primary" onclick={() => save()}>{$t("save-changes")}</button>
<button class="btn btn-success" onclick={add} disabled={!canAdd} <button class="btn btn-success" onclick={add}>{$t("editor.add-custom-preference")}</button>
>{$t("editor.add-custom-preference")}</button
>
</div> </div>
{#if !canAdd}
<div>
<InfoCircleFill aria-hidden />
{$t("editor.max-custom-preferences", {
current: customPreferences.length,
max: data.meta.limits.custom_preferences,
})}
</div>
{/if}
</h3> </h3>
<FormStatusMarker form={ok} /> <FormStatusMarker form={ok} />

View file

@ -1,14 +1,5 @@
import adapter from "@sveltejs/adapter-node"; import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import * as path from "node:path";
import { config as dotenv } from "dotenv";
dotenv({
path: [path.resolve(process.cwd(), ".env"), path.resolve(process.cwd(), ".env.local")],
});
console.log(process.env.NODE_ENV);
const isProd = process.env.NODE_ENV === "production";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@ -30,9 +21,6 @@ const config = {
// we only disable it during development, during building NODE_ENV == production // we only disable it during development, during building NODE_ENV == production
checkOrigin: process.env.NODE_ENV !== "development", checkOrigin: process.env.NODE_ENV !== "development",
}, },
paths: {
assets: isProd ? process.env.PRIVATE_ASSETS_PREFIX || "" : "",
},
}, },
}; };

View file

@ -25,12 +25,12 @@ echo "Building Node.js frontend"
cd "$ROOT_DIR/Foxnouns.Frontend" cd "$ROOT_DIR/Foxnouns.Frontend"
[ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build" [ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build"
npm ci pnpm install
npm run build pnpm build
mkdir "$ROOT_DIR/build/fe" mkdir "$ROOT_DIR/build/fe"
cp -r build .env* package.json package-lock.json "$ROOT_DIR/build/fe" cp -r build .env* package.json pnpm-lock.yaml "$ROOT_DIR/build/fe"
cd "$ROOT_DIR/build/fe" cd "$ROOT_DIR/build/fe"
NODE_ENV=production npm ci pnpm install -P
echo "Finished building Foxnouns.NET" echo "Finished building Foxnouns.NET"

View file

@ -1,53 +0,0 @@
services:
backend:
image: code.vulpine.solutions/sam/foxnouns-be:latest
environment:
- "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres"
- "Database:EnablePooling=true"
- "Database:Redis=redis:6379"
- "Host=0.0.0.0"
- "Port=5000"
- "Logging:MetricsPort=5001"
restart: unless-stopped
ports:
- "5001:5000"
- "5002:5001"
volumes:
- ./docker/config.ini:/app/config.ini
- ./docker/static-pages:/app/static-pages
rate:
image: code.vulpine.solutions/sam/foxnouns-rate:latest
environment:
- "PORT=5003"
ports:
- "5003:5003"
restart: unless-stopped
volumes:
- ./docker/proxy-config.json:/app/proxy-config.json
postgres:
image: docker.io/postgres:16
command: [ "postgres",
"-c", "max-connections=1000",
"-c", "timezone=Etc/UTC",
"-c", "max_wal_size=1GB",
"-c", "min_wal_size=80MB",
"-c", "shared_buffers=128MB" ]
environment:
- "POSTGRES_PASSWORD=postgres"
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: registry.redict.io/redict:7
restart: unless-stopped
volumes:
- redict_data:/data
volumes:
caddy_data:
caddy_config:
postgres_data:
redict_data:

View file

@ -1,82 +0,0 @@
// TODO: i'm not even sure if this code works. it's not easy to test either. woops
import postgres from "postgres";
import { config } from "dotenv";
import { Client } from "minio";
import { Logger } from "tslog";
import axios from "axios";
config();
const log = new Logger();
const env = (key) => {
const value = process.env[key];
if (value) return value;
throw `No env variable with key $${key} found`;
};
const oldBaseUrl = env("OLD_BASE_URL");
const bucket = env("MINIO_BUCKET");
const sql = postgres(env("DATABASE_URL"));
const minio = new Client({
endPoint: env("MINIO_ENDPOINT"),
useSSL: true,
accessKey: env("MINIO_ACCESS_KEY"),
secretKey: env("MINIO_SECRET_KEY"),
});
const users =
await sql`select id::text, username, legacy_id, avatar from users where avatar is not null order by id asc`;
log.info("have to migrate %d users", users.length);
const migrate = async (user) => {
log.debug(
"copying /users/%s/%s.webp to /users/%s/avatars/%s.webp",
user.legacy_id,
user.avatar,
user.id,
user.avatar
);
try {
const file = await axios.get(
`${oldBaseUrl}/users/${user.legacy_id}/${user.avatar}.webp`,
{ responseType: "stream" }
);
await minio.putObject(
bucket,
`users/${user.id}/avatars/${user.avatar}.webp`,
file.data,
file.headers["Content-Length"]
);
log.info("copied avatar for user %s", user.id);
await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`;
} catch (e) {
if ("status" in e && e.status === 404) {
log.warn(
"avatar for user %s/%s is not found. marking it as migrated.",
user.id,
user.username
);
await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`;
return;
}
log.error(
"could not migrate avatar for user %s/%s:",
user.id,
user.username,
e
);
}
};
for (let index = 0; index < users.length; index++) {
const user = users[index];
await migrate(user);
}
log.info("all users migrated!");
process.exit();

View file

@ -1,866 +0,0 @@
{
"name": "avatar-migrator",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"axios": "^1.8.4",
"dotenv": "^16.5.0",
"minio": "^8.0.5",
"postgres": "^3.4.5",
"tslog": "^4.9.3"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/block-stream2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
"license": "MIT",
"dependencies": {
"readable-stream": "^3.4.0"
}
},
"node_modules/browser-or-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
"license": "MIT"
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-generator-function": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minio": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz",
"integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.4",
"block-stream2": "^2.1.0",
"browser-or-node": "^2.1.1",
"buffer-crc32": "^1.0.0",
"eventemitter3": "^5.0.1",
"fast-xml-parser": "^4.4.1",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"query-string": "^7.1.3",
"stream-json": "^1.8.0",
"through2": "^4.0.2",
"web-encoding": "^1.1.5",
"xml2js": "^0.5.0 || ^0.6.2"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postgres": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz",
"integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==",
"license": "Unlicense",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
"license": "MIT",
"dependencies": {
"readable-stream": "3"
}
},
"node_modules/tslog": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz",
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/fullstack-build/tslog?sponsor=1"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/web-encoding": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
"license": "MIT",
"dependencies": {
"util": "^0.12.3"
},
"optionalDependencies": {
"@zxing/text-encoding": "0.9.0"
}
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
}
}
}

View file

@ -1,10 +0,0 @@
{
"type": "module",
"dependencies": {
"axios": "^1.8.4",
"dotenv": "^16.5.0",
"minio": "^8.0.5",
"postgres": "^3.4.5",
"tslog": "^4.9.3"
}
}

View file

@ -1,7 +0,0 @@
{
"old_avatar_base": "https://pronounscc-legacy.your-s3-host.com/",
"new_avatar_base": "http://pronounscc.your-s3-host.com/",
"flag_base": "http://pronounscc.your-s3-host.com/",
"port": 6100,
"database": "postgresql://postgres:password@localhost/postgres"
}

View file

@ -1,17 +0,0 @@
module code.vulpine.solutions/sam/Foxnouns.NET/migration-tools/avatar-proxy
go 1.24.2
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/jackc/pgx/v5 v5.7.4
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

View file

@ -1,30 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,103 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Proxy struct {
OldAvatarBase string `json:"old_avatar_base"`
NewAvatarBase string `json:"new_avatar_base"`
FlagBase string `json:"flag_base"`
Port int `json:"port"`
Database string `json:"database"`
db *pgxpool.Pool
}
type EntityWithAvatar struct {
ID uint64
LegacyID string
Avatar string
AvatarMigrated bool
}
func main() {
b, err := os.ReadFile("config.json")
if err != nil {
log.Fatalln("error reading config:", err)
}
p := &Proxy{}
err = json.Unmarshal(b, p)
if err != nil {
log.Fatalln("error parsing config:", err)
}
p.db, err = pgxpool.New(context.Background(), p.Database)
if err != nil {
log.Fatalln("error connecting to database:", err)
}
r := chi.NewRouter()
r.HandleFunc(`/flags/{hash:[\da-f]+}.webp`, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("%s%s", p.FlagBase, r.URL.Path), http.StatusTemporaryRedirect)
})
r.Get(`/members/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("member"))
r.Get(`/users/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("user"))
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("file not found"))
})
log.Printf("serving on port %v", p.Port)
err = http.ListenAndServe(":"+strconv.Itoa(p.Port), r)
if err != nil {
log.Fatalf("listening on port %v: %v", p.Port, err)
}
}
func (p *Proxy) proxyHandler(avatarType string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
hash := chi.URLParam(r, "hash")
var e EntityWithAvatar
// don't do this normally, kids. avatarType can only be "user" or "member" so it's fine here but this is a BAD idea otherwise.
err := p.db.QueryRow(
r.Context(), "SELECT id, legacy_id, avatar, avatar_migrated FROM "+avatarType+"s WHERE id = $1 AND avatar = $2",
id, hash,
).Scan(&e.ID, &e.LegacyID, &e.Avatar, &e.AvatarMigrated)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("avatar not found"))
return
}
log.Printf("error getting avatar for %s %s: %v", avatarType, id, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal server error"))
return
}
if e.AvatarMigrated {
http.Redirect(w, r, fmt.Sprintf("%s/%ss/%d/avatars/%s.webp", p.NewAvatarBase, avatarType, e.ID, e.Avatar), http.StatusTemporaryRedirect)
} else {
http.Redirect(w, r, fmt.Sprintf("%s/%ss/%s/%s.webp", p.OldAvatarBase, avatarType, e.LegacyID, e.Avatar), http.StatusTemporaryRedirect)
}
}
}

View file

@ -4,7 +4,8 @@
}, },
"scripts": { "scripts": {
"watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start", "watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start",
"dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'npm run watch:be' 'cd Foxnouns.Frontend && npm run dev' 'cd rate && go run -v .'", "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'",
"format": "dotnet csharpier . && cd Foxnouns.Frontend && npm run format" "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format"
} },
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
} }