Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
b0431ff962 | |||
b07f4b75c0 | |||
22be49976a | |||
3527acb8ba | |||
978b8e100e | |||
f00f5b400e | |||
f5f0416346 | |||
5d452824cd | |||
bba322bd22 | |||
200e648772 |
48 changed files with 1751 additions and 195 deletions
|
@ -7,7 +7,7 @@ resharper_not_accessed_positional_property_local_highlighting = none
|
|||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_multiline_binary_expressions_chain = false
|
||||
|
|
27
DOCKER.md
27
DOCKER.md
|
@ -1,10 +1,29 @@
|
|||
# Running with Docker
|
||||
# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)*
|
||||
|
||||
Because SvelteKit is a pain in the ass to build in a container, and processes secrets at build time,
|
||||
there is no pre-built frontend image available.
|
||||
If you don't want to build images on your server, I recommend running the frontend outside of Docker.
|
||||
This is preconfigured in `docker-compose.prebuilt.yml`: the backend, database, and rate limiter will run in Docker,
|
||||
while the frontend is run as a normal, non-containerized service.
|
||||
|
||||
1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking.
|
||||
2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same.
|
||||
3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do the same.
|
||||
4. Build with `docker compose build`
|
||||
5. Run with `docker compose up`
|
||||
3. Run with `docker compose up -f docker-compose.prebuilt.yml`
|
||||
|
||||
The backend will listen on port 5001 and metrics will be available on port 5002.
|
||||
The rate limiter (which is what should be exposed to the outside) will listen on port 5003.
|
||||
You can use `docker/Caddyfile` as an example for your reverse proxy. If you use nginx, good luck.
|
||||
|
||||
# Running with Docker (local builds)
|
||||
|
||||
In order to run *everything* in Docker, you'll have to build every container yourself.
|
||||
The advantage of this is that it's an all-in-one solution, where you only have to point your reverse proxy at a single container.
|
||||
The disadvantage is that you'll likely have to build the images on the server you'll be running them on.
|
||||
|
||||
1. Configure the backend and rate limiter as in the section above.
|
||||
2. Copy `docker/frontend.example.env` to `docker/frontend.env`, and configure it.
|
||||
3. Build with `docker compose build -f docker-compose.local.yml`
|
||||
4. Run with `docker compose up -f docker-compose.local.yml`
|
||||
|
||||
The Caddy server will listen on `localhost:5004` for the frontend and API,
|
||||
and on `localhost:5005` for the profile URL shortener.
|
||||
|
|
|
@ -26,7 +26,6 @@ public class Config
|
|||
public string MediaBaseUrl { get; init; } = null!;
|
||||
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}";
|
||||
|
||||
public LoggingConfig Logging { get; init; } = new();
|
||||
public DatabaseConfig Database { get; init; } = new();
|
||||
|
|
|
@ -121,6 +121,9 @@ public class MembersController(
|
|||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -238,6 +241,9 @@ public class MembersController(
|
|||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
}
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
|
|
|
@ -13,20 +13,23 @@
|
|||
// 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 System.Text.RegularExpressions;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Services.Caching;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/meta")]
|
||||
public partial class MetaController(Config config) : ApiControllerBase
|
||||
public partial class MetaController(Config config, NoticeCacheService noticeCache)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
|
||||
public IActionResult GetMeta() =>
|
||||
public async Task<IActionResult> GetMeta(CancellationToken ct = default) =>
|
||||
Ok(
|
||||
new MetaResponse(
|
||||
Repository,
|
||||
|
@ -45,10 +48,14 @@ public partial class MetaController(Config config) : ApiControllerBase
|
|||
ValidationUtils.MaxCustomPreferences,
|
||||
AuthUtils.MaxAuthMethodsPerType,
|
||||
FlagsController.MaxFlagCount
|
||||
)
|
||||
),
|
||||
Notice: NoticeResponse(await noticeCache.GetAsync(ct))
|
||||
)
|
||||
);
|
||||
|
||||
private static MetaNoticeResponse? NoticeResponse(Notice? notice) =>
|
||||
notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message);
|
||||
|
||||
[HttpGet("page/{page}")]
|
||||
public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default)
|
||||
{
|
||||
|
@ -71,7 +78,7 @@ public partial class MetaController(Config config) : ApiControllerBase
|
|||
|
||||
[HttpGet("/api/v2/coffee")]
|
||||
public IActionResult BrewCoffee() =>
|
||||
Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
|
||||
StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!");
|
||||
|
||||
[GeneratedRegex(@"^[a-z\-_]+$")]
|
||||
private static partial Regex PageRegex();
|
||||
|
|
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/notices")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class NoticesController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<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)
|
||||
);
|
||||
}
|
|
@ -46,7 +46,15 @@ public class UsersController(
|
|||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct)
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderAuthMethods: true,
|
||||
renderSettings: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -178,6 +186,8 @@ public class UsersController(
|
|||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
|
||||
}
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -253,20 +263,12 @@ public class UsersController(
|
|||
}
|
||||
|
||||
user.CustomPreferences = preferences;
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Ok(user.CustomPreferences);
|
||||
}
|
||||
|
||||
[HttpGet("@me/settings")]
|
||||
[Authorize("user.read_hidden")]
|
||||
[ProducesResponseType<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")]
|
||||
[Authorize("user.read_hidden", "user.update")]
|
||||
[ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)]
|
||||
|
@ -279,7 +281,10 @@ public class UsersController(
|
|||
|
||||
if (req.HasProperty(nameof(req.DarkMode)))
|
||||
user.Settings.DarkMode = req.DarkMode;
|
||||
if (req.HasProperty(nameof(req.LastReadNotice)))
|
||||
user.Settings.LastReadNotice = req.LastReadNotice;
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
public DbSet<Report> Reports { get; init; } = null!;
|
||||
public DbSet<AuditLogEntry> AuditLog { get; init; } = null!;
|
||||
public DbSet<Notification> Notifications { get; init; } = null!;
|
||||
public DbSet<Notice> Notices { get; init; } = null!;
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
|
|
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
|
@ -0,0 +1,915 @@
|
|||
// <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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -343,6 +343,38 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
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")
|
||||
|
@ -750,6 +782,18 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
|
|
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
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!;
|
||||
}
|
|
@ -95,4 +95,5 @@ public enum PreferenceSize
|
|||
public class UserSettings
|
||||
{
|
||||
public bool? DarkMode { get; set; }
|
||||
public Snowflake? LastReadNotice { get; set; }
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
public record MetaResponse(
|
||||
|
@ -22,9 +24,12 @@ public record MetaResponse(
|
|||
string Hash,
|
||||
int Members,
|
||||
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 LimitsResponse(
|
||||
|
|
|
@ -122,3 +122,13 @@ public record QueryUserResponse(
|
|||
);
|
||||
|
||||
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);
|
||||
|
|
|
@ -49,7 +49,8 @@ public record UserResponse(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastSidReroll,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone,
|
||||
[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(
|
||||
|
@ -79,6 +80,7 @@ public record PartialUser(
|
|||
public class UpdateUserSettingsRequest : PatchRequest
|
||||
{
|
||||
public bool? DarkMode { get; init; }
|
||||
public Snowflake? LastReadNotice { get; init; }
|
||||
}
|
||||
|
||||
public class CustomPreferenceUpdateRequest
|
||||
|
|
|
@ -15,14 +15,18 @@
|
|||
using Coravel;
|
||||
using Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Services.Caching;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Http.Resilience;
|
||||
using Minio;
|
||||
using NodaTime;
|
||||
using Polly;
|
||||
using Prometheus;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
@ -100,6 +104,40 @@ public static class WebApplicationExtensions
|
|||
builder.Host.ConfigureServices(
|
||||
(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
|
||||
.AddQueue()
|
||||
.AddSmtpMailer(ctx.Configuration)
|
||||
|
@ -126,6 +164,7 @@ public static class WebApplicationExtensions
|
|||
.AddScoped<ObjectStorageService>()
|
||||
.AddTransient<DataCleanupService>()
|
||||
.AddTransient<ValidationService>()
|
||||
.AddSingleton<NoticeCacheService>()
|
||||
// Background services
|
||||
.AddHostedService<PeriodicTasksService>()
|
||||
// Transient jobs
|
||||
|
@ -160,9 +199,6 @@ public static class WebApplicationExtensions
|
|||
|
||||
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()
|
||||
.LogQueuedTaskProgress(app.Services.GetRequiredService<ILogger<IQueue>>());
|
||||
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<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="Minio" Version="6.0.4"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.1"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.3"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.Json.NET" Version="9.0.3"/>
|
||||
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
|
@ -38,14 +39,14 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.0.18"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="5.2.0"/>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.0.26"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="5.3.0"/>
|
||||
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.31"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.2"/>
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageReference Include="Yort.Xid.Net" Version="2.0.1"/>
|
||||
|
|
|
@ -27,6 +27,7 @@ using NodaTime.Text;
|
|||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateDataExportJob(
|
||||
HttpClient client,
|
||||
DatabaseContext db,
|
||||
IClock clock,
|
||||
UserRendererService userRenderer,
|
||||
|
@ -36,7 +37,6 @@ public class CreateDataExportJob(
|
|||
ILogger logger
|
||||
)
|
||||
{
|
||||
private static readonly HttpClient Client = new();
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>();
|
||||
|
||||
public static void Enqueue(Snowflake userId)
|
||||
|
@ -201,7 +201,7 @@ public class CreateDataExportJob(
|
|||
if (s3Path == null)
|
||||
return;
|
||||
|
||||
HttpResponseMessage resp = await Client.GetAsync(s3Path);
|
||||
HttpResponseMessage resp = await client.GetAsync(s3Path);
|
||||
if (resp.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
_logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path);
|
||||
|
|
|
@ -34,6 +34,9 @@ Config config = builder.AddConfiguration();
|
|||
|
||||
builder.AddSerilog();
|
||||
|
||||
// Read version information from .version in the repository root
|
||||
await BuildInfo.ReadBuildInfo();
|
||||
|
||||
builder
|
||||
.WebHost.UseSentry(opts =>
|
||||
{
|
||||
|
@ -65,12 +68,11 @@ builder
|
|||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||
};
|
||||
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
// the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine)
|
||||
options.InvalidModelStateResponseFactory = (ActionContext actionContext) =>
|
||||
new BadRequestObjectResult(
|
||||
options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult(
|
||||
new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -253,14 +253,14 @@ public class AuthService(
|
|||
{
|
||||
AssertValidAuthType(authType, app);
|
||||
|
||||
// This is already checked when
|
||||
// This is already checked when generating an add account state, but we check it here too just in case.
|
||||
int currentCount = await db
|
||||
.AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType)
|
||||
.CountAsync(ct);
|
||||
if (currentCount >= AuthUtils.MaxAuthMethodsPerType)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Too many linked accounts of this type, maximum of 3 per account."
|
||||
$"Too many linked accounts of this type, maximum of {AuthUtils.MaxAuthMethodsPerType} per account."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,20 +25,20 @@ namespace Foxnouns.Backend.Services.Auth;
|
|||
public partial class FediverseAuthService
|
||||
{
|
||||
private string MastodonRedirectUri(string instance) =>
|
||||
$"{_config.BaseUrl}/auth/callback/mastodon/{instance}";
|
||||
$"{config.BaseUrl}/auth/callback/mastodon/{instance}";
|
||||
|
||||
private async Task<FediverseApplication> CreateMastodonApplicationAsync(
|
||||
string instance,
|
||||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
$"https://{instance}/api/v1/apps",
|
||||
new CreateMastodonApplicationRequest(
|
||||
$"pronouns.cc (+{_config.BaseUrl})",
|
||||
$"pronouns.cc (+{config.BaseUrl})",
|
||||
MastodonRedirectUri(instance),
|
||||
"read read:accounts",
|
||||
_config.BaseUrl
|
||||
config.BaseUrl
|
||||
)
|
||||
);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
@ -58,19 +58,19 @@ public partial class FediverseAuthService
|
|||
{
|
||||
app = new FediverseApplication
|
||||
{
|
||||
Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
|
||||
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(),
|
||||
ClientId = mastodonApp.ClientId,
|
||||
ClientSecret = mastodonApp.ClientSecret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MastodonApi,
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
db.Add(app);
|
||||
}
|
||||
else
|
||||
{
|
||||
app =
|
||||
await _db.FediverseApplications.FindAsync(existingAppId)
|
||||
await db.FediverseApplications.FindAsync(existingAppId)
|
||||
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
|
||||
|
||||
app.ClientId = mastodonApp.ClientId;
|
||||
|
@ -78,7 +78,7 @@ public partial class FediverseAuthService
|
|||
app.InstanceType = FediverseInstanceType.MastodonApi;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -90,9 +90,9 @@ public partial class FediverseAuthService
|
|||
)
|
||||
{
|
||||
if (state != null)
|
||||
await _keyCacheService.ValidateAuthStateAsync(state);
|
||||
await keyCacheService.ValidateAuthStateAsync(state);
|
||||
|
||||
HttpResponseMessage tokenResp = await _client.PostAsync(
|
||||
HttpResponseMessage tokenResp = await client.PostAsync(
|
||||
MastodonTokenUri(app.Domain),
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -123,7 +123,7 @@ public partial class FediverseAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain));
|
||||
req.Headers.Add("Authorization", $"Bearer {token}");
|
||||
|
||||
HttpResponseMessage currentUserResp = await _client.SendAsync(req);
|
||||
HttpResponseMessage currentUserResp = await client.SendAsync(req);
|
||||
currentUserResp.EnsureSuccessStatusCode();
|
||||
FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
|
||||
if (user == null)
|
||||
|
@ -151,7 +151,7 @@ public partial class FediverseAuthService
|
|||
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"
|
||||
+ $"&client_id={app.ClientId}"
|
||||
|
|
|
@ -34,11 +34,11 @@ public partial class FediverseAuthService
|
|||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyAppUri(instance),
|
||||
new CreateMisskeyApplicationRequest(
|
||||
$"pronouns.cc (+{_config.BaseUrl})",
|
||||
$"pronouns.cc on {_config.BaseUrl}",
|
||||
$"pronouns.cc (+{config.BaseUrl})",
|
||||
$"pronouns.cc on {config.BaseUrl}",
|
||||
["read:account"],
|
||||
MastodonRedirectUri(instance)
|
||||
)
|
||||
|
@ -60,19 +60,19 @@ public partial class FediverseAuthService
|
|||
{
|
||||
app = new FediverseApplication
|
||||
{
|
||||
Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
|
||||
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(),
|
||||
ClientId = misskeyApp.Id,
|
||||
ClientSecret = misskeyApp.Secret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MisskeyApi,
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
db.Add(app);
|
||||
}
|
||||
else
|
||||
{
|
||||
app =
|
||||
await _db.FediverseApplications.FindAsync(existingAppId)
|
||||
await db.FediverseApplications.FindAsync(existingAppId)
|
||||
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
|
||||
|
||||
app.ClientId = misskeyApp.Id;
|
||||
|
@ -80,7 +80,7 @@ public partial class FediverseAuthService
|
|||
app.InstanceType = FediverseInstanceType.MisskeyApi;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ public partial class FediverseAuthService
|
|||
|
||||
private async Task<FediverseUser> GetMisskeyUserAsync(FediverseApplication app, string code)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyTokenUri(app.Domain),
|
||||
new GetMisskeySessionUserKeyRequest(app.ClientSecret, code)
|
||||
);
|
||||
|
@ -130,7 +130,7 @@ public partial class FediverseAuthService
|
|||
app = await CreateMisskeyApplicationAsync(app.Domain, app.Id);
|
||||
}
|
||||
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyGenerateSessionUri(app.Domain),
|
||||
new CreateMisskeySessionUriRequest(app.ClientSecret)
|
||||
);
|
||||
|
|
|
@ -19,37 +19,17 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService
|
||||
{
|
||||
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||
|
||||
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(
|
||||
public partial class FediverseAuthService(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
HttpClient client,
|
||||
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");
|
||||
}
|
||||
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthService>();
|
||||
|
||||
public async Task<string> GenerateAuthUrlAsync(
|
||||
string instance,
|
||||
|
@ -70,7 +50,7 @@ public partial class FediverseAuthService
|
|||
|
||||
public async Task<FediverseApplication> GetApplicationAsync(string instance)
|
||||
{
|
||||
FediverseApplication? app = await _db.FediverseApplications.FirstOrDefaultAsync(a =>
|
||||
FediverseApplication? app = await db.FediverseApplications.FirstOrDefaultAsync(a =>
|
||||
a.Domain == instance
|
||||
);
|
||||
if (app != null)
|
||||
|
@ -92,7 +72,7 @@ public partial class FediverseAuthService
|
|||
{
|
||||
_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")
|
||||
);
|
||||
wellKnownResp.EnsureSuccessStatusCode();
|
||||
|
@ -107,7 +87,7 @@ public partial class FediverseAuthService
|
|||
);
|
||||
}
|
||||
|
||||
HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl);
|
||||
HttpResponseMessage nodeInfoResp = await client.GetAsync(nodeInfoUrl);
|
||||
nodeInfoResp.EnsureSuccessStatusCode();
|
||||
|
||||
PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>();
|
||||
|
|
|
@ -27,7 +27,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/discord";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_discordTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -59,7 +59,7 @@ public partial class RemoteAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri);
|
||||
req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
||||
|
||||
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await client.SendAsync(req, ct);
|
||||
resp2.EnsureSuccessStatusCode();
|
||||
DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
|
||||
if (user == null)
|
||||
|
|
|
@ -28,7 +28,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/google";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_googleTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
|
|
@ -29,7 +29,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_tumblrTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -62,7 +62,7 @@ public partial class RemoteAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri);
|
||||
req.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
|
||||
|
||||
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await client.SendAsync(req, ct);
|
||||
if (!resp2.IsSuccessStatusCode)
|
||||
{
|
||||
string respBody = await resp2.Content.ReadAsStringAsync(ct);
|
||||
|
|
|
@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class RemoteAuthService(
|
||||
HttpClient client,
|
||||
Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
|
@ -32,7 +33,6 @@ public partial class RemoteAuthService(
|
|||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<RemoteAuthService>();
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
public record RemoteUser(string Id, string Username);
|
||||
|
||||
|
|
39
Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
Normal file
39
Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// 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);
|
||||
}
|
63
Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
Normal file
63
Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,8 +39,6 @@ public class KeyCacheService(Config config)
|
|||
public async Task DeleteKeyAsync(string key) =>
|
||||
await Multiplexer.GetDatabase().KeyDeleteAsync(key);
|
||||
|
||||
public Task DeleteExpiredKeysAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
public async Task SetKeyAsync<T>(string key, T obj, Duration expiresAt)
|
||||
where T : class
|
||||
{
|
||||
|
|
|
@ -33,11 +33,9 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
|
|||
|
||||
// The type is literally written on the same line, we can just use `var`
|
||||
// ReSharper disable SuggestVarOrType_SimpleTypes
|
||||
var keyCacheService = scope.ServiceProvider.GetRequiredService<KeyCacheService>();
|
||||
var dataCleanupService = scope.ServiceProvider.GetRequiredService<DataCleanupService>();
|
||||
// ReSharper restore SuggestVarOrType_SimpleTypes
|
||||
|
||||
await keyCacheService.DeleteExpiredKeysAsync(ct);
|
||||
await dataCleanupService.InvokeAsync(ct);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await RenderUserInnerAsync(
|
||||
|
@ -42,6 +43,7 @@ public class UserRendererService(
|
|||
renderMembers,
|
||||
renderAuthMethods,
|
||||
overrideSid,
|
||||
renderSettings,
|
||||
ct
|
||||
);
|
||||
|
||||
|
@ -52,6 +54,7 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
|
@ -62,6 +65,7 @@ public class UserRendererService(
|
|||
|
||||
renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers);
|
||||
renderAuthMethods = renderAuthMethods && tokenPrivileged;
|
||||
renderSettings = renderSettings && tokenHidden;
|
||||
|
||||
IEnumerable<Member> members = renderMembers
|
||||
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
|
||||
|
@ -117,7 +121,8 @@ public class UserRendererService(
|
|||
tokenHidden ? user.LastSidReroll : null,
|
||||
tokenHidden ? user.Timezone ?? "<none>" : null,
|
||||
tokenHidden ? user is { Deleted: true, DeletedBy: not null } : null,
|
||||
tokenHidden ? user.Deleted : null
|
||||
tokenHidden ? user.Deleted : null,
|
||||
renderSettings ? user.Settings : null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ public partial class ValidationService
|
|||
"settings",
|
||||
"pronouns.cc",
|
||||
"pronounscc",
|
||||
"null",
|
||||
];
|
||||
|
||||
private static readonly string[] InvalidMemberNames =
|
||||
|
@ -38,8 +39,10 @@ public partial class ValidationService
|
|||
// these break routing outright
|
||||
".",
|
||||
"..",
|
||||
// the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
|
||||
// TODO: remove this? i'm not sure if /@[username]/edit will redirect to settings
|
||||
"edit",
|
||||
// this breaks the frontend, somehow
|
||||
"null",
|
||||
];
|
||||
|
||||
public ValidationError? ValidateUsername(string username)
|
||||
|
|
|
@ -155,6 +155,18 @@
|
|||
"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": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.10.0, )",
|
||||
|
@ -193,23 +205,23 @@
|
|||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "1A6HpMPbzK+quxdtug1aDHI4BSRTgpi7OaDt8WQh7SFJd2sSQ0nNTZ7sYrwyxVf4AdKdN7XJL9tpiiJjRUaa4g==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)",
|
||||
"Npgsql": "9.0.2"
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "Eks1o3NfIbS3EHhrXC0QABrQab7CJ64C2+kF0YJWLwlH/tu3ExrgrSLpLI6INdeMYcLr2PXu71LjJsrQNVciYg==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "QZ80CL3c9xzC83eVMWYWa1RcFZA6HJtpMAKFURlmz+1p0OyysSe8R6f/4sI9vk/nwqF6Fkw3lDgku/xH6HcJYg==",
|
||||
"dependencies": {
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.3",
|
||||
"Npgsql.NodaTime": "9.0.2"
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.4",
|
||||
"Npgsql.NodaTime": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Npgsql.Json.NET": {
|
||||
|
@ -249,18 +261,18 @@
|
|||
},
|
||||
"Scalar.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.18, )",
|
||||
"resolved": "2.0.18",
|
||||
"contentHash": "nS8Sw6wRO1A/dARn3q9R6znIBfddJcmAczI5uMROBGWkO2KG/ad/Ld+UeUePTxGr1+6humJSOxI7An+q4q3oGA=="
|
||||
"requested": "[2.0.26, )",
|
||||
"resolved": "2.0.26",
|
||||
"contentHash": "0tKBFM7quBq0ifgRWo7eTTVpiTbnwpf/6ygtb/aYVuo0D2gMsYknAJRqEhH8HFBqzntNiYpzHbQSf2b+VAA8sA=="
|
||||
},
|
||||
"Sentry.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.2.0, )",
|
||||
"resolved": "5.2.0",
|
||||
"contentHash": "vEKanBDOxCnEQrcMq3j47z8HOblRfiyJotdm9Fyc24cmLrLsTYZnWWprCYstt++M9bGSXYf4jrM2aaWxgJ8aww==",
|
||||
"requested": "[5.3.0, )",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zC2yhwQB0laYWGXLYDCsiKSIqleaEK3fUH9Z5t8Bgvfs2nGX0mHmh9oPqNAAbkVGvni56mhgHHCBxN/kpfkawA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Sentry.Extensions.Logging": "5.2.0"
|
||||
"Sentry.Extensions.Logging": "5.3.0"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
|
@ -305,15 +317,15 @@
|
|||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.6, )",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA=="
|
||||
"requested": "[3.1.7, )",
|
||||
"resolved": "3.1.7",
|
||||
"contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA=="
|
||||
},
|
||||
"StackExchange.Redis": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.8.24, )",
|
||||
"resolved": "2.8.24",
|
||||
"contentHash": "GWllmsFAtLyhm4C47cOCipGxyEi1NQWTFUHXnJ8hiHOsK/bH3T5eLkWPVW+LRL6jDiB3g3izW3YEHgLuPoJSyA==",
|
||||
"requested": "[2.8.31, )",
|
||||
"resolved": "2.8.31",
|
||||
"contentHash": "RCHVQa9Zke8k0oBgJn1Yl6BuYy8i6kv+sdMObiH60nOwD6QvWAjxdDwOm+LO78E8WsGiPqgOuItkz98fPS6haQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||
|
@ -537,6 +549,16 @@
|
|||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
|
@ -545,13 +567,22 @@
|
|||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
|
@ -564,10 +595,10 @@
|
|||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
|
@ -583,6 +614,14 @@
|
|||
"resolved": "9.0.2",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
|
@ -590,54 +629,74 @@
|
|||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "0CF9ZrNw5RAlRfbZuVIvzzhP8QeWqHiUmMBU/2H7Nmit8/vwP3/SbHeEctth7D4Gz2fBnEbokPc1NU8/j/1ZLw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "9.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "DqI4q54U4hH7bIAq9M5a/hl5Odr/KBAoaZ0dcT4OgutD8dook34CbkvAfAIzkMVjYXiL+E5ul9etwwqiX4PHGw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Diagnostics": "9.0.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@ -660,23 +719,23 @@
|
|||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "H05HiqaNmg6GjH34ocYE9Wm1twm3Oz2aXZko8GTwGBzM7op2brpAA8pJ5yyD1OpS1mXUtModBYOlcZ/wXeWsSg==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "9.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
|
@ -689,14 +748,14 @@
|
|||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
|
@ -704,6 +763,42 @@
|
|||
"resolved": "9.0.2",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.1",
|
||||
|
@ -745,11 +840,11 @@
|
|||
},
|
||||
"Npgsql.NodaTime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "jURb6VGmmR3pPae2N3HrUixSZ/U5ovqZgg/qo3m5Rq/q0m2fpxbZcsHZo21s5MLa/AfJAx4hcFMY98D4RtLdcg==",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "PMWXCft/iw+5A7eCeMcy6YZXBst6oeisbCkv2JMQVG4SAFa5vQaf6K2voXzUJCqzwOFcCWs+oT42w2uMDFpchw==",
|
||||
"dependencies": {
|
||||
"NodaTime": "3.2.0",
|
||||
"Npgsql": "9.0.2"
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
|
@ -760,20 +855,44 @@
|
|||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.2.0",
|
||||
"contentHash": "b3aZSOU2CjlIIFRtPRbXParKQ+9PF+JOqkSD7Gxq6PiR07t1rnK+crPtdrWMXfW6PVo/s67trCJ+fuLsgTeADw=="
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zlBIP7YmYxySwcgapLMj1gdxPEz9rwdrOa4Yjub/TzcAaMQXusRH9hY4CE6pu0EIibZ7C7Hhjhr6xOTlyK8gFQ=="
|
||||
},
|
||||
"Sentry.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.2.0",
|
||||
"contentHash": "546bHsERKY7/pG5T4mVIp6WbHnQPMst6VDuxSaeU5DhQHLfh7KhgMmkdZ4Xvdlr95fvWk5/bX2xbipy6qoh/1A==",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "DPN6NXvO4LTH21UM2gUFJwSwVa/fuT3B/UZmQyfSfecqViXrZO7WFuKz/h592YUoGNCumyt8x045bxbz6j9btg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Microsoft.Extensions.Http": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "9.0.0",
|
||||
"Sentry": "5.2.0"
|
||||
"Sentry": "5.3.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Extensions.Hosting": {
|
||||
|
@ -901,8 +1020,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
|
@ -940,6 +1059,11 @@
|
|||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
|
||||
},
|
||||
"System.Threading.RateLimiting": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
|
|
3
Foxnouns.Frontend/pnpm-lock.yaml
generated
3
Foxnouns.Frontend/pnpm-lock.yaml
generated
|
@ -72,6 +72,9 @@ importers:
|
|||
bootstrap:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3(@popperjs/core@2.11.8)
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.4.7
|
||||
eslint:
|
||||
specifier: ^9.17.0
|
||||
version: 9.17.0
|
||||
|
|
|
@ -10,6 +10,7 @@ export type Meta = {
|
|||
};
|
||||
members: number;
|
||||
limits: Limits;
|
||||
notice: { id: string; message: string } | null;
|
||||
};
|
||||
|
||||
export type Limits = {
|
||||
|
|
|
@ -28,6 +28,7 @@ export type MeUser = UserWithMembers & {
|
|||
timezone: string;
|
||||
suspended: boolean;
|
||||
deleted: boolean;
|
||||
settings: UserSettings;
|
||||
};
|
||||
|
||||
export type UserWithMembers = User & { members: PartialMember[] | null };
|
||||
|
@ -40,6 +41,7 @@ export type UserWithHiddenFields = User & {
|
|||
|
||||
export type UserSettings = {
|
||||
dark_mode: boolean | null;
|
||||
last_read_notice: string | null;
|
||||
};
|
||||
|
||||
export type PartialMember = {
|
||||
|
|
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<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>
|
||||
{@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}
|
|
@ -38,6 +38,6 @@
|
|||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
{idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)}
|
||||
• <a href="/settings/notifications/ack/{notification.id}">Mark as read</a>
|
||||
• <a href="/settings/notifications/ack/{notification.id}">{$t("notification.mark-as-read")}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -350,6 +350,8 @@
|
|||
"notification": {
|
||||
"suspension": "Your account has been suspended 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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData };
|
||||
|
@ -10,6 +11,15 @@
|
|||
</svelte:head>
|
||||
|
||||
<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>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -3,15 +3,28 @@
|
|||
import { t } from "$lib/i18n";
|
||||
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
|
||||
import { isActive } from "$lib/pageUtils.svelte";
|
||||
import type { LayoutData } from "./$types";
|
||||
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||
|
||||
type Props = { children: Snippet };
|
||||
let { children }: Props = $props();
|
||||
type Props = { data: LayoutData; children: Snippet };
|
||||
let { data, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("title.settings")} • pronouns.cc</title>
|
||||
</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">
|
||||
<Nav pills justified fill class="flex-column flex-md-row mb-2">
|
||||
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { PageData } from "./$types";
|
||||
import Notification from "$components/settings/Notification.svelte";
|
||||
import UrlAlert from "$components/URLAlert.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
|
@ -12,5 +13,5 @@
|
|||
{#each data.notifications as notification (notification.id)}
|
||||
<Notification {notification} />
|
||||
{:else}
|
||||
You have no notifications.
|
||||
{$t("notification.no-notifications")}
|
||||
{/each}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import adapter from "@sveltejs/adapter-node";
|
||||
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} */
|
||||
const config = {
|
||||
|
@ -21,6 +30,9 @@ const config = {
|
|||
// we only disable it during development, during building NODE_ENV == production
|
||||
checkOrigin: process.env.NODE_ENV !== "development",
|
||||
},
|
||||
paths: {
|
||||
assets: isProd ? process.env.PRIVATE_ASSETS_PREFIX || "" : "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
53
docker-compose.prebuilt.yml
Normal file
53
docker-compose.prebuilt.yml
Normal file
|
@ -0,0 +1,53 @@
|
|||
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:
|
Loading…
Add table
Reference in a new issue