Compare commits
No commits in common. "main" and "v2024.12.0" have entirely different histories.
main
...
v2024.12.0
211 changed files with 1508 additions and 8566 deletions
.config
.dockerignore.editorconfig.gitignore.husky
DOCKER.mdFoxnouns.Backend
Config.cs
Controllers
Authentication
DeleteUserController.csExportsController.csFlagsController.csInternalController.csMembersController.csMetaController.csModeration
NotificationsController.csUsersController.csV1
Database
DatabaseContext.cs
Migrations
20241218195457_AddContextToReports.cs20241218201855_MakeAuditLogReportsNullable.cs20241225155818_AddLegacyIds.cs20250304155708_RemoveTemporaryKeys.cs20250329131053_AddNotices.Designer.cs20250329131053_AddNotices.csDatabaseContextModelSnapshot.cs
Models
Snowflake.csDto
ExpectedError.csExtensions
Foxnouns.Backend.csprojJobs
CreateDataExportInvocable.csCreateFlagInvocable.csMemberAvatarUpdateInvocable.csPayloads.csUserAvatarUpdateInvocable.cs
Middleware
Program.csServices
Auth
AuthService.csFediverseAuthService.Mastodon.csFediverseAuthService.Misskey.csFediverseAuthService.csRemoteAuthService.Discord.csRemoteAuthService.Google.csRemoteAuthService.Tumblr.csRemoteAuthService.cs
Caching
DataCleanupService.csEmailRateLimiter.csKeyCacheService.csModerationRendererService.csModerationService.csPeriodicTasksService.csUserRendererService.csV1
ValidationService.Strings.csValidationService.csUtils
config.example.inipackages.lock.jsonstatic-pages
Foxnouns.DataMigrator
Foxnouns.Frontend
|
@ -3,14 +3,14 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"husky": {
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.1",
|
||||
"commands": [
|
||||
"husky"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"csharpier": {
|
||||
"version": "0.30.6",
|
||||
"version": "0.29.2",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
],
|
||||
|
|
|
@ -21,4 +21,3 @@
|
|||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
static-pages/*
|
||||
|
|
|
@ -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, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_multiline_binary_expressions_chain = false
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -6,14 +6,9 @@ config.ini
|
|||
*.DotSettings.user
|
||||
proxy-config.json
|
||||
.DS_Store
|
||||
.idea/.idea.Foxnouns.NET/.idea/dataSources.xml
|
||||
.idea/.idea.Foxnouns.NET/.idea/sqldialects.xml
|
||||
|
||||
docker/config.ini
|
||||
docker/proxy-config.json
|
||||
docker/frontend.env
|
||||
|
||||
Foxnouns.DataMigrator/apps.json
|
||||
|
||||
out/
|
||||
build/
|
||||
|
|
|
@ -4,31 +4,14 @@
|
|||
{
|
||||
"name": "run-prettier",
|
||||
"command": "pnpm",
|
||||
"args": [
|
||||
"prettier",
|
||||
"-w",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"Foxnouns.Frontend/**/*.ts",
|
||||
"Foxnouns.Frontend/**/*.json",
|
||||
"Foxnouns.Frontend/**/*.scss",
|
||||
"Foxnouns.Frontend/**/*.js",
|
||||
"Foxnouns.Frontend/**/*.svelte"
|
||||
],
|
||||
"cwd": "Foxnouns.Frontend/",
|
||||
"args": ["format"],
|
||||
"pathMode": "absolute"
|
||||
},
|
||||
{
|
||||
"name": "run-csharpier",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"**/*.cs"
|
||||
]
|
||||
"args": [ "csharpier", "${staged}" ],
|
||||
"include": [ "**/*.cs" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
27
DOCKER.md
27
DOCKER.md
|
@ -1,29 +1,10 @@
|
|||
# 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.
|
||||
# Running with Docker
|
||||
|
||||
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. 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`
|
||||
3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do th esame.
|
||||
4. Build with `docker compose build`
|
||||
5. Run with `docker compose up`
|
||||
|
||||
The Caddy server will listen on `localhost:5004` for the frontend and API,
|
||||
and on `localhost:5005` for the profile URL shortener.
|
||||
|
|
|
@ -26,11 +26,11 @@ 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();
|
||||
public StorageConfig Storage { get; init; } = new();
|
||||
public LimitsConfig Limits { get; init; } = new();
|
||||
public EmailAuthConfig EmailAuth { get; init; } = new();
|
||||
public DiscordAuthConfig DiscordAuth { get; init; } = new();
|
||||
public GoogleAuthConfig GoogleAuth { get; init; } = new();
|
||||
|
@ -54,7 +54,6 @@ public class Config
|
|||
public bool? EnablePooling { get; init; }
|
||||
public int? Timeout { get; init; }
|
||||
public int? MaxPoolSize { get; init; }
|
||||
public string Redis { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public class StorageConfig
|
||||
|
@ -94,22 +93,4 @@ public class Config
|
|||
public string? ClientId { get; init; }
|
||||
public string? ClientSecret { get; init; }
|
||||
}
|
||||
|
||||
public class LimitsConfig
|
||||
{
|
||||
public int MaxMemberCount { get; init; } = 1000;
|
||||
|
||||
public int MaxFields { get; init; } = 25;
|
||||
public int MaxFieldNameLength { get; init; } = 100;
|
||||
public int MaxFieldEntryTextLength { get; init; } = 100;
|
||||
public int MaxFieldEntries { get; init; } = 100;
|
||||
|
||||
public int MaxUsernameLength { get; init; } = 40;
|
||||
public int MaxMemberNameLength { get; init; } = 100;
|
||||
public int MaxDisplayNameLength { get; init; } = 100;
|
||||
public int MaxLinks { get; init; } = 25;
|
||||
public int MaxLinkLength { get; init; } = 256;
|
||||
public int MaxBioLength { get; init; } = 1024;
|
||||
public int MaxAvatarLength { get; init; } = 1_500_000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public class AuthController(
|
|||
config.GoogleAuth.Enabled,
|
||||
config.TumblrAuth.Enabled
|
||||
);
|
||||
string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());
|
||||
string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct));
|
||||
string? discord = null;
|
||||
string? google = null;
|
||||
string? tumblr = null;
|
||||
|
|
|
@ -56,7 +56,7 @@ public class EmailAuthController(
|
|||
if (!req.Email.Contains('@'))
|
||||
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
|
||||
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null);
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct);
|
||||
|
||||
// If there's already a user with that email address, pretend we sent an email but actually ignore it
|
||||
if (
|
||||
|
|
|
@ -94,7 +94,8 @@ public class FediverseAuthController(
|
|||
public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
|
||||
{
|
||||
FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
||||
$"fediverse:{req.Ticket}"
|
||||
$"fediverse:{req.Ticket}",
|
||||
true
|
||||
);
|
||||
if (ticketData == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/internal/self-delete")]
|
||||
[Authorize("*")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DeleteUserController(DatabaseContext db, IClock clock, ILogger logger)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<DeleteUserController>();
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> DeleteSelfAsync()
|
||||
{
|
||||
_logger.Information(
|
||||
"User {UserId} has requested their account to be deleted",
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = true;
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant();
|
||||
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("force")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> ForceDeleteAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested an early full delete of their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
// This is the easiest way to force delete a user, don't judge me
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant() - Duration.FromDays(365);
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("undelete")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> UndeleteSelfAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
if (CurrentUser!.DeletedBy != null)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Your account has been suspended and can't be reactivated by yourself."
|
||||
);
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested to undelete their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = false;
|
||||
CurrentUser.DeletedAt = null;
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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 Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
|
@ -25,10 +26,14 @@ namespace Foxnouns.Backend.Controllers;
|
|||
|
||||
[Route("/api/internal/data-exports")]
|
||||
[Authorize("identify")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ExportsController(ILogger logger, Config config, IClock clock, DatabaseContext db)
|
||||
: ApiControllerBase
|
||||
public class ExportsController(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
IClock clock,
|
||||
DatabaseContext db,
|
||||
IQueue queue
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private static readonly Duration MinimumTimeBetween = Duration.FromDays(1);
|
||||
private readonly ILogger _logger = logger.ForContext<ExportsController>();
|
||||
|
@ -52,7 +57,7 @@ public class ExportsController(ILogger logger, Config config, IClock clock, Data
|
|||
}
|
||||
|
||||
private string ExportUrl(Snowflake userId, string filename) =>
|
||||
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}/data-export.zip";
|
||||
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip";
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> QueueDataExportAsync()
|
||||
|
@ -74,7 +79,10 @@ public class ExportsController(ILogger logger, Config config, IClock clock, Data
|
|||
throw new ApiError.BadRequest("You can't request a new data export so soon.");
|
||||
}
|
||||
|
||||
CreateDataExportJob.Enqueue(CurrentUser.Id);
|
||||
queue.QueueInvocableWithPayload<CreateDataExportInvocable, CreateDataExportPayload>(
|
||||
new CreateDataExportPayload(CurrentUser.Id)
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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 Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
|
@ -21,7 +22,6 @@ using Foxnouns.Backend.Services;
|
|||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
|
@ -29,11 +29,12 @@ namespace Foxnouns.Backend.Controllers;
|
|||
public class FlagsController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IQueue queue
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Authorize("user.read_flags")]
|
||||
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
|
||||
|
@ -63,7 +64,6 @@ public class FlagsController(
|
|||
var flag = new PrideFlag
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
UserId = CurrentUser!.Id,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
|
@ -72,7 +72,10 @@ public class FlagsController(
|
|||
db.Add(flag);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image));
|
||||
queue.QueueInvocableWithPayload<CreateFlagInvocable, CreateFlagPayload>(
|
||||
new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image)
|
||||
);
|
||||
|
||||
return Accepted(userRenderer.RenderPrideFlag(flag));
|
||||
}
|
||||
|
||||
|
|
|
@ -38,8 +38,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
|
|||
{
|
||||
if (template.StartsWith("api/v2"))
|
||||
template = template["api/v2".Length..];
|
||||
else if (template.StartsWith("api/v1"))
|
||||
template = template["api/v1".Length..];
|
||||
template = PathVarRegex()
|
||||
.Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}`
|
||||
.Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}`
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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 Coravel.Queuing.Interfaces;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
@ -25,7 +26,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using NodaTime;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
|
@ -36,16 +36,15 @@ public class MembersController(
|
|||
MemberRendererService memberRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ObjectStorageService objectStorageService,
|
||||
IClock clock,
|
||||
ValidationService validationService,
|
||||
Config config
|
||||
IQueue queue,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<MembersController>();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
|
@ -54,7 +53,7 @@ public class MembersController(
|
|||
|
||||
[HttpGet("{memberRef}")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
|
@ -65,6 +64,8 @@ public class MembersController(
|
|||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
}
|
||||
|
||||
public const int MaxMemberCount = 500;
|
||||
|
||||
[HttpPost("/api/v2/users/@me/members")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Authorize("member.create")]
|
||||
|
@ -75,32 +76,31 @@ public class MembersController(
|
|||
{
|
||||
ValidationUtils.Validate(
|
||||
[
|
||||
("name", validationService.ValidateMemberName(req.Name)),
|
||||
("display_name", validationService.ValidateDisplayName(req.DisplayName)),
|
||||
("bio", validationService.ValidateBio(req.Bio)),
|
||||
("avatar", validationService.ValidateAvatar(req.Avatar)),
|
||||
.. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||
.. validationService.ValidateFieldEntries(
|
||||
("name", ValidationUtils.ValidateMemberName(req.Name)),
|
||||
("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)),
|
||||
("bio", ValidationUtils.ValidateBio(req.Bio)),
|
||||
("avatar", ValidationUtils.ValidateAvatar(req.Avatar)),
|
||||
.. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||
.. ValidationUtils.ValidateFieldEntries(
|
||||
req.Names?.ToArray(),
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
),
|
||||
.. validationService.ValidatePronouns(
|
||||
.. ValidationUtils.ValidatePronouns(
|
||||
req.Pronouns?.ToArray(),
|
||||
CurrentUser!.CustomPreferences
|
||||
),
|
||||
.. validationService.ValidateLinks(req.Links),
|
||||
.. ValidationUtils.ValidateLinks(req.Links),
|
||||
]
|
||||
);
|
||||
|
||||
int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct);
|
||||
if (memberCount >= config.Limits.MaxMemberCount)
|
||||
if (memberCount >= MaxMemberCount)
|
||||
throw new ApiError.BadRequest("Maximum number of members reached");
|
||||
|
||||
var member = new Member
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
User = CurrentUser!,
|
||||
Name = req.Name,
|
||||
DisplayName = req.DisplayName,
|
||||
|
@ -121,9 +121,6 @@ public class MembersController(
|
|||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -140,7 +137,9 @@ public class MembersController(
|
|||
|
||||
if (req.Avatar != null)
|
||||
{
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(member.Id, req.Avatar)
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
|
@ -162,25 +161,25 @@ public class MembersController(
|
|||
// These should only take effect when a member's name is changed, not on other changes.
|
||||
if (req.Name != null && req.Name != member.Name)
|
||||
{
|
||||
errors.Add(("name", validationService.ValidateMemberName(req.Name)));
|
||||
errors.Add(("name", ValidationUtils.ValidateMemberName(req.Name)));
|
||||
member.Name = req.Name;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.DisplayName)))
|
||||
{
|
||||
errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName)));
|
||||
errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)));
|
||||
member.DisplayName = req.DisplayName;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Bio)))
|
||||
{
|
||||
errors.Add(("bio", validationService.ValidateBio(req.Bio)));
|
||||
errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio)));
|
||||
member.Bio = req.Bio;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Links)))
|
||||
{
|
||||
errors.AddRange(validationService.ValidateLinks(req.Links));
|
||||
errors.AddRange(ValidationUtils.ValidateLinks(req.Links));
|
||||
member.Links = req.Links ?? [];
|
||||
}
|
||||
|
||||
|
@ -190,7 +189,7 @@ public class MembersController(
|
|||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFieldEntries(
|
||||
ValidationUtils.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
|
@ -202,7 +201,7 @@ public class MembersController(
|
|||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
member.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
@ -210,10 +209,7 @@ public class MembersController(
|
|||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
|
||||
);
|
||||
member.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
@ -230,7 +226,7 @@ public class MembersController(
|
|||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar)));
|
||||
errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
|
||||
|
||||
ValidationUtils.Validate(errors);
|
||||
// This is fired off regardless of whether the transaction is committed
|
||||
|
@ -238,12 +234,11 @@ public class MembersController(
|
|||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(member.Id, req.Avatar)
|
||||
);
|
||||
}
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
|
|
|
@ -12,24 +12,20 @@
|
|||
//
|
||||
// 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, NoticeCacheService noticeCache)
|
||||
: ApiControllerBase
|
||||
public class MetaController : ApiControllerBase
|
||||
{
|
||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetMeta(CancellationToken ct = default) =>
|
||||
public IActionResult GetMeta() =>
|
||||
Ok(
|
||||
new MetaResponse(
|
||||
Repository,
|
||||
|
@ -43,43 +39,16 @@ public partial class MetaController(Config config, NoticeCacheService noticeCach
|
|||
(int)FoxnounsMetrics.UsersActiveDayCount.Value
|
||||
),
|
||||
new LimitsResponse(
|
||||
config.Limits.MaxMemberCount,
|
||||
config.Limits.MaxBioLength,
|
||||
MembersController.MaxMemberCount,
|
||||
ValidationUtils.MaxBioLength,
|
||||
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)
|
||||
{
|
||||
if (!PageRegex().IsMatch(page))
|
||||
{
|
||||
throw new ApiError.BadRequest("Invalid page name");
|
||||
}
|
||||
|
||||
string path = Path.Join(Directory.GetCurrentDirectory(), "static-pages", $"{page}.md");
|
||||
try
|
||||
{
|
||||
string text = await System.IO.File.ReadAllTextAsync(path, ct);
|
||||
return Ok(text);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new ApiError.NotFound("Page not found", code: ErrorCode.PageNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/api/v2/coffee")]
|
||||
public IActionResult BrewCoffee() =>
|
||||
StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!");
|
||||
|
||||
[GeneratedRegex(@"^[a-z\-_]+$")]
|
||||
private static partial Regex PageRegex();
|
||||
Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
|
||||
}
|
||||
|
|
|
@ -30,9 +30,7 @@ public class AuditLogController(DatabaseContext db, ModerationRendererService mo
|
|||
public async Task<IActionResult> GetAuditLogAsync(
|
||||
[FromQuery] AuditLogEntryType? type = null,
|
||||
[FromQuery] int? limit = null,
|
||||
[FromQuery] Snowflake? before = null,
|
||||
[FromQuery] Snowflake? after = null,
|
||||
[FromQuery(Name = "by-moderator")] Snowflake? byModerator = null
|
||||
[FromQuery] Snowflake? before = null
|
||||
)
|
||||
{
|
||||
limit = limit switch
|
||||
|
@ -43,36 +41,15 @@ public class AuditLogController(DatabaseContext db, ModerationRendererService mo
|
|||
_ => limit,
|
||||
};
|
||||
|
||||
IQueryable<AuditLogEntry> query = db
|
||||
.AuditLog.Include(e => e.Report)
|
||||
.OrderByDescending(e => e.Id);
|
||||
IQueryable<AuditLogEntry> query = db.AuditLog.OrderByDescending(e => e.Id);
|
||||
|
||||
if (before != null)
|
||||
query = query.Where(e => e.Id < before.Value);
|
||||
else if (after != null)
|
||||
query = query.Where(e => e.Id > after.Value);
|
||||
|
||||
if (type != null)
|
||||
query = query.Where(e => e.Type == type);
|
||||
if (byModerator != null)
|
||||
query = query.Where(e => e.ModeratorId == byModerator.Value);
|
||||
|
||||
List<AuditLogEntry> entries = await query.Take(limit!.Value).ToListAsync();
|
||||
|
||||
return Ok(entries.Select(moderationRenderer.RenderAuditLogEntry));
|
||||
}
|
||||
|
||||
[HttpGet("moderators")]
|
||||
public async Task<IActionResult> GetModeratorsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var moderators = await db
|
||||
.Users.Where(u =>
|
||||
!u.Deleted && (u.Role == UserRole.Admin || u.Role == UserRole.Moderator)
|
||||
)
|
||||
.Select(u => new { u.Id, u.Username })
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Ok(moderators);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/moderation/lookup")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class LookupController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ModerationService moderationService,
|
||||
ModerationRendererService moderationRenderer
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> QueryUsersAsync(
|
||||
[FromBody] QueryUsersRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var query = db.Users.Select(u => new { u.Id, u.Username });
|
||||
query = req.Fuzzy
|
||||
? query.Where(u => u.Username.Contains(req.Query))
|
||||
: query.Where(u => u.Username == req.Query);
|
||||
|
||||
var users = await query.OrderBy(u => u.Id).Take(100).ToListAsync(ct);
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> QueryUserAsync(Snowflake id, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id, ct);
|
||||
|
||||
bool showSensitiveData = await moderationService.ShowSensitiveDataAsync(
|
||||
CurrentUser!,
|
||||
user,
|
||||
ct
|
||||
);
|
||||
|
||||
List<AuthMethod> authMethods = showSensitiveData
|
||||
? await db
|
||||
.AuthMethods.Where(a => a.UserId == user.Id)
|
||||
.Include(a => a.FediverseApplication)
|
||||
.ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
return Ok(
|
||||
new QueryUserResponse(
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
renderMembers: false,
|
||||
renderAuthMethods: false,
|
||||
ct: ct
|
||||
),
|
||||
MemberListHidden: user.ListHidden,
|
||||
LastActive: user.LastActive,
|
||||
LastSidReroll: user.LastSidReroll,
|
||||
Suspended: user is { Deleted: true, DeletedBy: not null },
|
||||
Deleted: user.Deleted,
|
||||
ShowSensitiveData: showSensitiveData,
|
||||
AuthMethods: showSensitiveData
|
||||
? authMethods.Select(UserRendererService.RenderAuthMethod)
|
||||
: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/sensitive")]
|
||||
public async Task<IActionResult> QuerySensitiveUserDataAsync(
|
||||
Snowflake id,
|
||||
[FromBody] QuerySensitiveUserDataRequest req
|
||||
)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id);
|
||||
|
||||
// Don't let mods accidentally spam the audit log
|
||||
bool alreadyAuthorized = await moderationService.ShowSensitiveDataAsync(CurrentUser!, user);
|
||||
if (alreadyAuthorized)
|
||||
return NoContent();
|
||||
|
||||
AuditLogEntry entry = await moderationService.QuerySensitiveDataAsync(
|
||||
CurrentUser!,
|
||||
user,
|
||||
req.Reason
|
||||
);
|
||||
|
||||
return Ok(moderationRenderer.RenderAuditLogEntry(entry));
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/notices")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class NoticesController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetNoticesAsync(CancellationToken ct = default)
|
||||
{
|
||||
List<Notice> notices = await db
|
||||
.Notices.Include(n => n.Author)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.ToListAsync(ct);
|
||||
return Ok(notices.Select(RenderNotice));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateNoticeAsync(CreateNoticeRequest req)
|
||||
{
|
||||
Instant now = clock.GetCurrentInstant();
|
||||
if (req.StartTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Start time cannot be in the past",
|
||||
"start_time",
|
||||
req.StartTime
|
||||
);
|
||||
}
|
||||
|
||||
if (req.EndTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"End time cannot be in the past",
|
||||
"end_time",
|
||||
req.EndTime
|
||||
);
|
||||
}
|
||||
|
||||
var notice = new Notice
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
Message = req.Message,
|
||||
StartTime = req.StartTime ?? clock.GetCurrentInstant(),
|
||||
EndTime = req.EndTime,
|
||||
Author = CurrentUser!,
|
||||
};
|
||||
|
||||
db.Add(notice);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(RenderNotice(notice));
|
||||
}
|
||||
|
||||
private NoticeResponse RenderNotice(Notice notice) =>
|
||||
new(
|
||||
notice.Id,
|
||||
notice.Message,
|
||||
notice.StartTime,
|
||||
notice.EndTime,
|
||||
userRenderer.RenderPartialUser(notice.Author)
|
||||
);
|
||||
}
|
|
@ -18,7 +18,6 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -50,8 +49,6 @@ public class ReportsController(
|
|||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
User target = await db.ResolveUserAsync(id);
|
||||
|
||||
if (target.Id == CurrentUser!.Id)
|
||||
|
@ -99,7 +96,6 @@ public class ReportsController(
|
|||
TargetUserId = target.Id,
|
||||
TargetMemberId = null,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.User,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
@ -116,8 +112,6 @@ public class ReportsController(
|
|||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
Member target = await db.ResolveMemberAsync(id);
|
||||
|
||||
if (target.User.Id == CurrentUser!.Id)
|
||||
|
@ -164,7 +158,6 @@ public class ReportsController(
|
|||
TargetUserId = target.User.Id,
|
||||
TargetMemberId = target.Id,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.Member,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
@ -220,40 +213,7 @@ public class ReportsController(
|
|||
return Ok(reports.Select(moderationRenderer.RenderReport));
|
||||
}
|
||||
|
||||
[HttpGet("reports/{id}")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public async Task<IActionResult> GetReportAsync(Snowflake id, CancellationToken ct = default)
|
||||
{
|
||||
Report? report = await db
|
||||
.Reports.Include(r => r.Reporter)
|
||||
.Include(r => r.TargetUser)
|
||||
.Include(r => r.TargetMember)
|
||||
.Include(r => r.AuditLogEntry)
|
||||
.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
if (report == null)
|
||||
throw new ApiError.NotFound("No report with that ID found.");
|
||||
|
||||
return Ok(
|
||||
new ReportDetailResponse(
|
||||
Report: moderationRenderer.RenderReport(report),
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
report.TargetUser,
|
||||
renderMembers: false,
|
||||
ct: ct
|
||||
),
|
||||
Member: report.TargetMember != null
|
||||
? memberRenderer.RenderMember(report.TargetMember)
|
||||
: null,
|
||||
AuditLogEntry: report.AuditLogEntry != null
|
||||
? moderationRenderer.RenderAuditLogEntry(report.AuditLogEntry)
|
||||
: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("reports/{id}/ignore")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public async Task<IActionResult> IgnoreReportAsync(
|
||||
Snowflake id,
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
// 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 Foxnouns.Backend.Middleware;
|
||||
|
@ -31,7 +17,7 @@ public class NotificationsController(
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetNotificationsAsync([FromQuery] bool all = false)
|
||||
{
|
||||
IQueryable<Notification> query = db.Notifications.Where(n => n.TargetId == CurrentUser!.Id);
|
||||
|
@ -45,7 +31,7 @@ public class NotificationsController(
|
|||
|
||||
[HttpPut("{id}/ack")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> AcknowledgeNotificationAsync(Snowflake id)
|
||||
{
|
||||
Notification? notification = await db.Notifications.FirstOrDefaultAsync(n =>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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 Coravel.Queuing.Interfaces;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
@ -33,28 +34,20 @@ public class UsersController(
|
|||
ILogger logger,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock,
|
||||
ValidationService validationService
|
||||
IQueue queue,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UsersController>();
|
||||
|
||||
[HttpGet("{userRef}")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderAuthMethods: true,
|
||||
renderSettings: true,
|
||||
ct: ct
|
||||
)
|
||||
await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -72,32 +65,32 @@ public class UsersController(
|
|||
|
||||
if (req.Username != null && req.Username != user.Username)
|
||||
{
|
||||
errors.Add(("username", validationService.ValidateUsername(req.Username)));
|
||||
errors.Add(("username", ValidationUtils.ValidateUsername(req.Username)));
|
||||
user.Username = req.Username;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.DisplayName)))
|
||||
{
|
||||
errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName)));
|
||||
errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)));
|
||||
user.DisplayName = req.DisplayName;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Bio)))
|
||||
{
|
||||
errors.Add(("bio", validationService.ValidateBio(req.Bio)));
|
||||
errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio)));
|
||||
user.Bio = req.Bio;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Links)))
|
||||
{
|
||||
errors.AddRange(validationService.ValidateLinks(req.Links));
|
||||
errors.AddRange(ValidationUtils.ValidateLinks(req.Links));
|
||||
user.Links = req.Links ?? [];
|
||||
}
|
||||
|
||||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFieldEntries(
|
||||
ValidationUtils.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
|
@ -109,7 +102,7 @@ public class UsersController(
|
|||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
user.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
@ -117,10 +110,7 @@ public class UsersController(
|
|||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
|
||||
);
|
||||
user.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
@ -133,7 +123,7 @@ public class UsersController(
|
|||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar)));
|
||||
errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
|
||||
|
||||
if (req.HasProperty(nameof(req.MemberTitle)))
|
||||
{
|
||||
|
@ -143,9 +133,7 @@ public class UsersController(
|
|||
}
|
||||
else
|
||||
{
|
||||
errors.Add(
|
||||
("member_title", validationService.ValidateDisplayName(req.MemberTitle))
|
||||
);
|
||||
errors.Add(("member_title", ValidationUtils.ValidateDisplayName(req.MemberTitle)));
|
||||
user.MemberTitle = req.MemberTitle;
|
||||
}
|
||||
}
|
||||
|
@ -183,11 +171,11 @@ public class UsersController(
|
|||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
|
||||
queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)
|
||||
);
|
||||
}
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -234,7 +222,7 @@ public class UsersController(
|
|||
.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key))
|
||||
.ToDictionary();
|
||||
|
||||
foreach (CustomPreferenceUpdateRequest r in req)
|
||||
foreach (CustomPreferenceUpdateRequest? r in req)
|
||||
{
|
||||
if (r.Id != null && preferences.ContainsKey(r.Id.Value))
|
||||
{
|
||||
|
@ -245,7 +233,6 @@ public class UsersController(
|
|||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = preferences[r.Id.Value].LegacyId,
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -257,18 +244,25 @@ public class UsersController(
|
|||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = Guid.NewGuid(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
|
@ -281,10 +275,7 @@ 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);
|
||||
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.V1;
|
||||
|
||||
[Route("/api/v1")]
|
||||
public class V1ReadController(
|
||||
UsersV1Service usersV1Service,
|
||||
MembersV1Service membersV1Service,
|
||||
DatabaseContext db
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("users/@me")]
|
||||
[Authorize("identify")]
|
||||
public async Task<IActionResult> GetMeAsync(CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync("@me", CurrentToken, ct);
|
||||
return Ok(await usersV1Service.RenderCurrentUserAsync(user, ct));
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}")]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await usersV1Service.RenderUserAsync(
|
||||
user,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("members/{id}")]
|
||||
public async Task<IActionResult> GetMemberAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(id, ct);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members")]
|
||||
public async Task<IActionResult> GetUserMembersAsync(
|
||||
string userRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
List<Member> members = await db
|
||||
.Members.Where(m => m.UserId == user.Id)
|
||||
.OrderBy(m => m.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<MemberResponse> responses = [];
|
||||
foreach (Member member in members)
|
||||
{
|
||||
responses.Add(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
user,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(responses);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members/{memberRef}")]
|
||||
public async Task<IActionResult> GetUserMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(
|
||||
userRef,
|
||||
memberRef,
|
||||
CurrentToken,
|
||||
ct
|
||||
);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
public DbSet<FediverseApplication> FediverseApplications { get; init; } = null!;
|
||||
public DbSet<Token> Tokens { get; init; } = null!;
|
||||
public DbSet<Application> Applications { get; init; } = null!;
|
||||
public DbSet<TemporaryKey> TemporaryKeys { get; init; } = null!;
|
||||
public DbSet<DataExport> DataExports { get; init; } = null!;
|
||||
|
||||
public DbSet<PrideFlag> PrideFlags { get; init; } = null!;
|
||||
|
@ -73,7 +74,6 @@ 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)
|
||||
{
|
||||
|
@ -87,6 +87,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
modelBuilder.Entity<User>().HasIndex(u => u.Sid).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => new { m.UserId, m.Name }).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => m.Sid).IsUnique();
|
||||
modelBuilder.Entity<TemporaryKey>().HasIndex(k => k.Key).IsUnique();
|
||||
modelBuilder.Entity<DataExport>().HasIndex(d => d.Filename).IsUnique();
|
||||
|
||||
// Two indexes on auth_methods, one for fediverse auth and one for all other types.
|
||||
|
@ -107,12 +108,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
.HasFilter("fediverse_application_id IS NULL")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<AuditLogEntry>()
|
||||
.HasOne(e => e.Report)
|
||||
.WithOne(e => e.AuditLogEntry)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<User>().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()");
|
||||
modelBuilder.Entity<User>().Property(u => u.Fields).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.Names).HasColumnType("jsonb");
|
||||
|
@ -138,26 +133,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!)
|
||||
.HasName("find_free_member_sid");
|
||||
|
||||
// Indexes for legacy IDs for APIv1
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => m.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<PrideFlag>().HasIndex(f => f.LegacyId).IsUnique();
|
||||
|
||||
// a UUID is not an xid, but this should always be set by the application anyway.
|
||||
// we're just setting it here to shut EFCore up because squashing migrations is for nerds
|
||||
modelBuilder
|
||||
.Entity<User>()
|
||||
.Property(u => u.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<Member>()
|
||||
.Property(m => m.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<PrideFlag>()
|
||||
.Property(f => f.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218195457_AddContextToReports")]
|
||||
public partial class AddContextToReports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "context",
|
||||
table: "reports",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "context", table: "reports");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218201855_MakeAuditLogReportsNullable")]
|
||||
public partial class MakeAuditLogReportsNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id"
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241225155818_AddLegacyIds")]
|
||||
public partial class AddLegacyIds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "pride_flags",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_legacy_id",
|
||||
table: "users",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_pride_flags_legacy_id",
|
||||
table: "pride_flags",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_members_legacy_id",
|
||||
table: "members",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(name: "ix_users_legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_pride_flags_legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_members_legacy_id", table: "members");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "members");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250304155708_RemoveTemporaryKeys")]
|
||||
public partial class RemoveTemporaryKeys : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "temporary_keys");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "temporary_keys",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
expires = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
key = table.Column<string>(type: "text", nullable: false),
|
||||
value = table.Column<string>(type: "text", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_temporary_keys", x => x.id);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_temporary_keys_key",
|
||||
table: "temporary_keys",
|
||||
column: "key",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,915 +0,0 @@
|
|||
// <auto-generated />
|
||||
using System.Collections.Generic;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250329131053_AddNotices")]
|
||||
partial class AddNotices
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.PrimitiveCollection<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ClearedFields")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("cleared_fields");
|
||||
|
||||
b.Property<long>("ModeratorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("moderator_id");
|
||||
|
||||
b.Property<string>("ModeratorUsername")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("moderator_username");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("ReportId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("report_id");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetMemberName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_member_name");
|
||||
|
||||
b.Property<long?>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.Property<string>("TargetUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_username");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id")
|
||||
.HasFilter("fediverse_application_id IS NULL");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
|
||||
.HasFilter("fediverse_application_id IS NOT NULL");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("filename");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_data_exports");
|
||||
|
||||
b.HasIndex("Filename")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_data_exports_filename");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_data_exports_user_id");
|
||||
|
||||
b.ToTable("data_exports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<bool>("ForceRefresh")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("force_refresh");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_member_sid()");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("MemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("member_id");
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_member_flags");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("ix_member_flags_member_id");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_member_flags_pride_flag_id");
|
||||
|
||||
b.ToTable("member_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("author_id");
|
||||
|
||||
b.Property<Instant>("EndTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("end_time");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Instant>("StartTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("start_time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notices");
|
||||
|
||||
b.HasIndex("AuthorId")
|
||||
.HasDatabaseName("ix_notices_author_id");
|
||||
|
||||
b.ToTable("notices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant?>("AcknowledgedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("acknowledged_at");
|
||||
|
||||
b.Property<string>("LocalizationKey")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("localization_key");
|
||||
|
||||
b.Property<Dictionary<string, string>>("LocalizationParams")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore")
|
||||
.HasColumnName("localization_params");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifications");
|
||||
|
||||
b.HasIndex("TargetId")
|
||||
.HasDatabaseName("ix_notifications_target_id");
|
||||
|
||||
b.ToTable("notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pride_flags");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_pride_flags_legacy_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_pride_flags_user_id");
|
||||
|
||||
b.ToTable("pride_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long>("ReporterId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("reporter_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetSnapshot")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_snapshot");
|
||||
|
||||
b.Property<int>("TargetType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("target_type");
|
||||
|
||||
b.Property<long>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_reports");
|
||||
|
||||
b.HasIndex("ReporterId")
|
||||
.HasDatabaseName("ix_reports_reporter_id");
|
||||
|
||||
b.HasIndex("TargetMemberId")
|
||||
.HasDatabaseName("ix_reports_target_member_id");
|
||||
|
||||
b.HasIndex("TargetUserId")
|
||||
.HasDatabaseName("ix_reports_target_user_id");
|
||||
|
||||
b.ToTable("reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("custom_preferences");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<Instant>("LastSidReroll")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_sid_reroll");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<UserSettings>("Settings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("settings");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_user_sid()");
|
||||
|
||||
b.Property<string>("Timezone")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("timezone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_user_flags");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_user_flags_pride_flag_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_flags_user_id");
|
||||
|
||||
b.ToTable("user_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
|
||||
.WithOne("AuditLogEntry")
|
||||
.HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("DataExports")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_data_exports_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("MemberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_members_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifications_users_target_id");
|
||||
|
||||
b.Navigation("Target");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_pride_flags_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReporterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_reporter_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetMemberId")
|
||||
.HasConstraintName("fk_reports_members_target_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_target_user_id");
|
||||
|
||||
b.Navigation("Reporter");
|
||||
|
||||
b.Navigation("TargetMember");
|
||||
|
||||
b.Navigation("TargetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_users_user_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Navigation("AuditLogEntry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("DataExports");
|
||||
|
||||
b.Navigation("Flags");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotices : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notices",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
message = table.Column<string>(type: "text", nullable: false),
|
||||
start_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
end_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
author_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notices", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notices_users_author_id",
|
||||
column: x => x.author_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notices_author_id",
|
||||
table: "notices",
|
||||
column: "author_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "notices");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
|
@ -113,7 +113,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
|
@ -254,13 +253,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.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[]")
|
||||
|
@ -299,10 +291,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
@ -343,38 +331,6 @@ 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")
|
||||
|
@ -429,13 +385,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.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")
|
||||
|
@ -448,10 +397,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
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");
|
||||
|
||||
|
@ -464,10 +409,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
@ -511,6 +452,39 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.ToTable("reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<Instant>("Expires")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("value");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_temporary_keys");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_temporary_keys_key");
|
||||
|
||||
b.ToTable("temporary_keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
@ -603,13 +577,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.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[]")
|
||||
|
@ -665,10 +632,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
@ -712,9 +675,8 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
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)
|
||||
.WithMany()
|
||||
.HasForeignKey("ReportId")
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
|
@ -782,18 +744,6 @@ 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")
|
||||
|
@ -889,11 +839,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
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");
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
//
|
||||
// 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.ComponentModel.DataAnnotations.Schema;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
@ -41,5 +40,4 @@ public enum AuditLogEntryType
|
|||
WarnUser,
|
||||
WarnUserAndClearProfile,
|
||||
SuspendUser,
|
||||
QuerySensitiveUserData,
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ public class Member : BaseModel
|
|||
{
|
||||
public required string Name { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Notice : BaseModel
|
||||
{
|
||||
public required string Message { get; set; }
|
||||
public required Instant StartTime { get; set; }
|
||||
public required Instant EndTime { get; set; }
|
||||
|
||||
public Snowflake AuthorId { get; init; }
|
||||
public User Author { get; init; } = null!;
|
||||
}
|
|
@ -17,7 +17,6 @@ namespace Foxnouns.Backend.Database.Models;
|
|||
public class PrideFlag : BaseModel
|
||||
{
|
||||
public required Snowflake UserId { get; init; }
|
||||
public required string LegacyId { get; init; }
|
||||
|
||||
// A null hash means the flag hasn't been processed yet.
|
||||
public string? Hash { get; set; }
|
||||
|
|
|
@ -29,12 +29,9 @@ public class Report : BaseModel
|
|||
|
||||
public ReportStatus Status { get; set; }
|
||||
public ReportReason Reason { get; init; }
|
||||
public string? Context { get; init; }
|
||||
|
||||
public ReportTargetType TargetType { get; init; }
|
||||
public string? TargetSnapshot { get; init; }
|
||||
|
||||
public AuditLogEntry? AuditLogEntry { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
|
|
|
@ -12,23 +12,14 @@
|
|||
//
|
||||
// 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 NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public static class V1Utils
|
||||
public class TemporaryKey
|
||||
{
|
||||
public static string TranslateStatus(
|
||||
string status,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
)
|
||||
{
|
||||
if (!Snowflake.TryParse(status, out Snowflake? sf))
|
||||
return status;
|
||||
|
||||
return customPreferences.TryGetValue(sf.Value, out User.CustomPreference? cf)
|
||||
? cf.LegacyId.ToString()
|
||||
: "unknown";
|
||||
}
|
||||
public long Id { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public required string Value { get; set; }
|
||||
public Instant Expires { get; init; }
|
||||
}
|
|
@ -25,7 +25,6 @@ public class User : BaseModel
|
|||
{
|
||||
public required string Username { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? MemberTitle { get; set; }
|
||||
|
@ -70,8 +69,6 @@ public class User : BaseModel
|
|||
// This type is generally serialized directly, so the converter is applied here.
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public PreferenceSize Size { get; set; }
|
||||
|
||||
public Guid LegacyId { get; init; } = Guid.NewGuid();
|
||||
}
|
||||
|
||||
public static readonly Duration DeleteAfter = Duration.FromDays(30);
|
||||
|
@ -95,5 +92,4 @@ public enum PreferenceSize
|
|||
public class UserSettings
|
||||
{
|
||||
public bool? DarkMode { get; set; }
|
||||
public Snowflake? LastReadNotice { get; set; }
|
||||
}
|
||||
|
|
|
@ -113,30 +113,24 @@ public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
|
|||
) => writer.WriteStringValue(value.Value.ToString());
|
||||
}
|
||||
|
||||
private class JsonConverter : JsonConverter<Snowflake?>
|
||||
private class JsonConverter : JsonConverter<Snowflake>
|
||||
{
|
||||
public override void WriteJson(
|
||||
JsonWriter writer,
|
||||
Snowflake? value,
|
||||
Snowflake value,
|
||||
JsonSerializer serializer
|
||||
)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(value.Value.ToString());
|
||||
else
|
||||
writer.WriteNull();
|
||||
}
|
||||
|
||||
public override Snowflake? ReadJson(
|
||||
public override Snowflake ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Snowflake? existingValue,
|
||||
Snowflake existingValue,
|
||||
bool hasExistingValue,
|
||||
JsonSerializer serializer
|
||||
) =>
|
||||
reader.TokenType is not (JsonToken.None or JsonToken.Null)
|
||||
? ulong.Parse((string)reader.Value!)
|
||||
: null;
|
||||
) => ulong.Parse((string)reader.Value!);
|
||||
}
|
||||
|
||||
private class TypeConverter : System.ComponentModel.TypeConverter
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
// 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(
|
||||
|
@ -24,12 +22,9 @@ public record MetaResponse(
|
|||
string Hash,
|
||||
int Members,
|
||||
UserInfoResponse Users,
|
||||
LimitsResponse Limits,
|
||||
MetaNoticeResponse? Notice
|
||||
LimitsResponse Limits
|
||||
);
|
||||
|
||||
public record MetaNoticeResponse(Snowflake Id, string Message);
|
||||
|
||||
public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
|
||||
|
||||
public record LimitsResponse(
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
|
@ -31,19 +29,10 @@ public record ReportResponse(
|
|||
PartialMember? TargetMember,
|
||||
ReportStatus Status,
|
||||
ReportReason Reason,
|
||||
string? Context,
|
||||
ReportTargetType TargetType,
|
||||
JObject? Snapshot
|
||||
);
|
||||
|
||||
public record ReportDetailResponse(
|
||||
ReportResponse Report,
|
||||
UserResponse User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] MemberResponse? Member,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
AuditLogResponse? AuditLogEntry
|
||||
);
|
||||
|
||||
public record AuditLogResponse(
|
||||
Snowflake Id,
|
||||
AuditLogEntity Moderator,
|
||||
|
@ -51,23 +40,12 @@ public record AuditLogResponse(
|
|||
AuditLogEntity? TargetUser,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
AuditLogEntity? TargetMember,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] PartialReport? Report,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ReportId,
|
||||
AuditLogEntryType Type,
|
||||
string? Reason,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string[]? ClearedFields
|
||||
);
|
||||
|
||||
public record PartialReport(
|
||||
Snowflake Id,
|
||||
Snowflake ReporterId,
|
||||
Snowflake TargetUserId,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
Snowflake? TargetMemberId,
|
||||
ReportReason Reason,
|
||||
string? Context,
|
||||
ReportTargetType TargetType
|
||||
);
|
||||
|
||||
public record NotificationResponse(
|
||||
Snowflake Id,
|
||||
NotificationType Type,
|
||||
|
@ -79,21 +57,19 @@ public record NotificationResponse(
|
|||
|
||||
public record AuditLogEntity(Snowflake Id, string Username);
|
||||
|
||||
public record CreateReportRequest(ReportReason Reason, string? Context = null);
|
||||
public record CreateReportRequest(ReportReason Reason);
|
||||
|
||||
public record IgnoreReportRequest(string? Reason = null);
|
||||
|
||||
public class WarnUserRequest
|
||||
{
|
||||
public required string Reason { get; init; }
|
||||
public FieldsToClear[]? ClearFields { get; init; }
|
||||
public Snowflake? MemberId { get; init; }
|
||||
public Snowflake? ReportId { get; init; }
|
||||
}
|
||||
public record WarnUserRequest(
|
||||
string Reason,
|
||||
FieldsToClear[]? ClearFields = null,
|
||||
Snowflake? MemberId = null,
|
||||
Snowflake? ReportId = null
|
||||
);
|
||||
|
||||
public record SuspendUserRequest(string Reason, bool ClearProfile, Snowflake? ReportId = null);
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum FieldsToClear
|
||||
{
|
||||
DisplayName,
|
||||
|
@ -106,29 +82,3 @@ public enum FieldsToClear
|
|||
Flags,
|
||||
CustomPreferences,
|
||||
}
|
||||
|
||||
public record QueryUsersRequest(string Query, bool Fuzzy);
|
||||
|
||||
public record QueryUserResponse(
|
||||
UserResponse User,
|
||||
bool MemberListHidden,
|
||||
Instant LastActive,
|
||||
Instant LastSidReroll,
|
||||
bool Suspended,
|
||||
bool Deleted,
|
||||
bool ShowSensitiveData,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
IEnumerable<AuthMethodResponse>? AuthMethods
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
|
@ -36,7 +36,7 @@ public record UserResponse(
|
|||
IEnumerable<FieldEntry> Names,
|
||||
IEnumerable<Pronoun> Pronouns,
|
||||
IEnumerable<Field> Fields,
|
||||
Dictionary<Snowflake, CustomPreferenceResponse> CustomPreferences,
|
||||
Dictionary<Snowflake, User.CustomPreference> CustomPreferences,
|
||||
IEnumerable<PrideFlagResponse> Flags,
|
||||
int? UtcOffset,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role,
|
||||
|
@ -49,16 +49,7 @@ 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)] UserSettings? Settings
|
||||
);
|
||||
|
||||
public record CustomPreferenceResponse(
|
||||
string Icon,
|
||||
string Tooltip,
|
||||
bool Muted,
|
||||
bool Favourite,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] PreferenceSize Size
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted
|
||||
);
|
||||
|
||||
public record AuthMethodResponse(
|
||||
|
@ -80,7 +71,6 @@ public record PartialUser(
|
|||
public class UpdateUserSettingsRequest : PatchRequest
|
||||
{
|
||||
public bool? DarkMode { get; init; }
|
||||
public Snowflake? LastReadNotice { get; init; }
|
||||
}
|
||||
|
||||
public class CustomPreferenceUpdateRequest
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Foxnouns.Backend.Dto.V1;
|
||||
|
||||
public record PartialMember(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns
|
||||
);
|
||||
|
||||
public record MemberResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialUser User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted
|
||||
);
|
||||
|
||||
public record PartialUser(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Avatar,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences
|
||||
);
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Dto.V1;
|
||||
|
||||
public record UserResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? MemberTitle,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialMember[] Members,
|
||||
int? UtcOffset,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences
|
||||
);
|
||||
|
||||
public record CurrentUserResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? MemberTitle,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialMember[] Members,
|
||||
int? UtcOffset,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences,
|
||||
Instant CreatedAt,
|
||||
string? Timezone,
|
||||
bool IsAdmin,
|
||||
bool ListPrivate,
|
||||
Instant LastSidReroll,
|
||||
string? Discord,
|
||||
string? DiscordUsername,
|
||||
string? Google,
|
||||
string? GoogleUsername,
|
||||
string? Tumblr,
|
||||
string? TumblrUsername,
|
||||
string? Fediverse,
|
||||
string? FediverseUsername,
|
||||
string? FediverseInstance
|
||||
);
|
||||
|
||||
public record CustomPreference(
|
||||
string Icon,
|
||||
string Tooltip,
|
||||
[property: JsonConverter(typeof(StringEnumConverter), typeof(SnakeCaseNamingStrategy))]
|
||||
PreferenceSize Size,
|
||||
bool Muted,
|
||||
bool Favourite
|
||||
);
|
||||
|
||||
public record ProfileField(string Name, FieldEntry[] Entries)
|
||||
{
|
||||
public static ProfileField FromField(
|
||||
Field field,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) => new(field.Name, FieldEntry.FromEntries(field.Entries, customPreferences));
|
||||
|
||||
public static ProfileField[] FromFields(
|
||||
IEnumerable<Field> fields,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) => fields.Select(f => FromField(f, customPreferences)).ToArray();
|
||||
}
|
||||
|
||||
public record FieldEntry(string Value, string Status)
|
||||
{
|
||||
public static FieldEntry[] FromEntries(
|
||||
IEnumerable<Foxnouns.Backend.Database.Models.FieldEntry> entries,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
entries
|
||||
.Select(e => new FieldEntry(
|
||||
e.Value,
|
||||
V1Utils.TranslateStatus(e.Status, customPreferences)
|
||||
))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public record PronounEntry(string Pronouns, string? DisplayText, string Status)
|
||||
{
|
||||
public static PronounEntry[] FromPronouns(
|
||||
IEnumerable<Pronoun> pronouns,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
pronouns
|
||||
.Select(p => new PronounEntry(
|
||||
p.Value,
|
||||
p.DisplayText,
|
||||
V1Utils.TranslateStatus(p.Status, customPreferences)
|
||||
))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description);
|
|
@ -164,7 +164,6 @@ public enum ErrorCode
|
|||
GenericApiError,
|
||||
UserNotFound,
|
||||
MemberNotFound,
|
||||
PageNotFound,
|
||||
AccountAlreadyLinked,
|
||||
LastAuthMethod,
|
||||
InvalidReportTarget,
|
||||
|
|
|
@ -33,20 +33,24 @@ public static class ImageObjectExtensions
|
|||
Snowflake id,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) => await objectStorageService.RemoveObjectAsync(MemberAvatarUpdateJob.Path(id, hash), ct);
|
||||
) =>
|
||||
await objectStorageService.RemoveObjectAsync(
|
||||
MemberAvatarUpdateInvocable.Path(id, hash),
|
||||
ct
|
||||
);
|
||||
|
||||
public static async Task DeleteUserAvatarAsync(
|
||||
this ObjectStorageService objectStorageService,
|
||||
Snowflake id,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateJob.Path(id, hash), ct);
|
||||
) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct);
|
||||
|
||||
public static async Task DeleteFlagAsync(
|
||||
this ObjectStorageService objectStorageService,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) => await objectStorageService.RemoveObjectAsync(CreateFlagJob.Path(hash), ct);
|
||||
) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct);
|
||||
|
||||
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
|
||||
string uri,
|
||||
|
|
|
@ -23,19 +23,23 @@ namespace Foxnouns.Backend.Extensions;
|
|||
|
||||
public static class KeyCacheExtensions
|
||||
{
|
||||
public static async Task<string> GenerateAuthStateAsync(this KeyCacheService keyCacheService)
|
||||
public static async Task<string> GenerateAuthStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10));
|
||||
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task ValidateAuthStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}");
|
||||
string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", ct: ct);
|
||||
if (val == null)
|
||||
throw new ApiError.BadRequest("Invalid OAuth state");
|
||||
}
|
||||
|
@ -43,55 +47,63 @@ public static class KeyCacheExtensions
|
|||
public static async Task<string> GenerateRegisterEmailStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string email,
|
||||
Snowflake? userId = null
|
||||
Snowflake? userId = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"email_state:{state}",
|
||||
new RegisterEmailState(email, userId),
|
||||
Duration.FromDays(1)
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<RegisterEmailState?> GetRegisterEmailStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state
|
||||
) => await keyCacheService.GetKeyAsync<RegisterEmailState>($"email_state:{state}");
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) => await keyCacheService.GetKeyAsync<RegisterEmailState>($"email_state:{state}", ct: ct);
|
||||
|
||||
public static async Task<string> GenerateAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
AuthType authType,
|
||||
Snowflake userId,
|
||||
string? instance = null
|
||||
string? instance = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"add_account:{state}",
|
||||
new AddExtraAccountState(authType, userId, instance),
|
||||
Duration.FromDays(1)
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<AddExtraAccountState?> GetAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state
|
||||
) => await keyCacheService.GetKeyAsync<AddExtraAccountState>($"add_account:{state}", true);
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) => await keyCacheService.GetKeyAsync<AddExtraAccountState>($"add_account:{state}", true, ct);
|
||||
|
||||
public static async Task<string> GenerateForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string email,
|
||||
Snowflake userId
|
||||
Snowflake userId,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"forgot_password:{state}",
|
||||
new ForgotPasswordState(email, userId),
|
||||
Duration.FromHours(1)
|
||||
Duration.FromHours(1),
|
||||
ct
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
@ -99,8 +111,14 @@ public static class KeyCacheExtensions
|
|||
public static async Task<ForgotPasswordState?> GetForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
bool delete = true
|
||||
) => await keyCacheService.GetKeyAsync<ForgotPasswordState>($"forgot_password:{state}", delete);
|
||||
bool delete = true,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await keyCacheService.GetKeyAsync<ForgotPasswordState>(
|
||||
$"forgot_password:{state}",
|
||||
delete,
|
||||
ct
|
||||
);
|
||||
}
|
||||
|
||||
public record RegisterEmailState(
|
||||
|
|
|
@ -15,18 +15,13 @@
|
|||
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;
|
||||
|
@ -55,12 +50,9 @@ public static class WebApplicationExtensions
|
|||
"Microsoft.EntityFrameworkCore.Database.Command",
|
||||
config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal
|
||||
)
|
||||
// These spam the output even on INF level
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
// Hangfire's debug-level logs are extremely spammy for no reason
|
||||
.MinimumLevel.Override("Hangfire", LogEventLevel.Information)
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen);
|
||||
|
||||
if (config.Logging.SeqLogUrl != null)
|
||||
|
@ -104,40 +96,6 @@ 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)
|
||||
|
@ -153,28 +111,23 @@ public static class WebApplicationExtensions
|
|||
.AddSnowflakeGenerator()
|
||||
.AddSingleton<MailService>()
|
||||
.AddSingleton<EmailRateLimiter>()
|
||||
.AddSingleton<KeyCacheService>()
|
||||
.AddScoped<UserRendererService>()
|
||||
.AddScoped<MemberRendererService>()
|
||||
.AddScoped<ModerationRendererService>()
|
||||
.AddScoped<ModerationService>()
|
||||
.AddScoped<AuthService>()
|
||||
.AddScoped<KeyCacheService>()
|
||||
.AddScoped<RemoteAuthService>()
|
||||
.AddScoped<FediverseAuthService>()
|
||||
.AddScoped<ObjectStorageService>()
|
||||
.AddTransient<DataCleanupService>()
|
||||
.AddTransient<ValidationService>()
|
||||
.AddSingleton<NoticeCacheService>()
|
||||
// Background services
|
||||
.AddHostedService<PeriodicTasksService>()
|
||||
// Transient jobs
|
||||
.AddTransient<UserAvatarUpdateJob>()
|
||||
.AddTransient<MemberAvatarUpdateJob>()
|
||||
.AddTransient<CreateDataExportJob>()
|
||||
.AddTransient<CreateFlagJob>()
|
||||
// Legacy services
|
||||
.AddScoped<UsersV1Service>()
|
||||
.AddScoped<MembersV1Service>();
|
||||
.AddTransient<MemberAvatarUpdateInvocable>()
|
||||
.AddTransient<UserAvatarUpdateInvocable>()
|
||||
.AddTransient<CreateFlagInvocable>()
|
||||
.AddTransient<CreateDataExportInvocable>();
|
||||
|
||||
if (!config.Logging.EnableMetrics)
|
||||
services.AddHostedService<BackgroundMetricsCollectionService>();
|
||||
|
@ -199,6 +152,9 @@ 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>>());
|
||||
|
||||
|
|
|
@ -8,48 +8,42 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Coravel" Version="6.0.2"/>
|
||||
<PackageReference Include="Coravel.Mailer" Version="7.1.0"/>
|
||||
<PackageReference Include="Coravel" Version="6.0.0"/>
|
||||
<PackageReference Include="Coravel.Mailer" Version="7.0.0"/>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3"/>
|
||||
<PackageReference Include="Hangfire" Version="1.8.18"/>
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.18"/>
|
||||
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.4"/>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.2"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<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="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
|
||||
<PackageReference Include="MimeKit" Version="4.9.0"/>
|
||||
<PackageReference Include="Minio" Version="6.0.3"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.1"/>
|
||||
<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="NodaTime" Version="3.2.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.2"/>
|
||||
<PackageReference Include="Npgsql.Json.NET" Version="9.0.2"/>
|
||||
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.13.1">
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.0.26"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="5.3.0"/>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="1.2.55"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="4.13.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.7"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.31"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.2"/>
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0"/>
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageReference Include="Yort.Xid.Net" Version="2.0.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
@ -26,8 +26,7 @@ using NodaTime.Text;
|
|||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateDataExportJob(
|
||||
HttpClient client,
|
||||
public class CreateDataExportInvocable(
|
||||
DatabaseContext db,
|
||||
IClock clock,
|
||||
UserRendererService userRenderer,
|
||||
|
@ -35,40 +34,37 @@ public class CreateDataExportJob(
|
|||
ObjectStorageService objectStorageService,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ILogger logger
|
||||
)
|
||||
) : IInvocable, IInvocableWithPayload<CreateDataExportPayload>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>();
|
||||
private static readonly HttpClient Client = new();
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportInvocable>();
|
||||
public required CreateDataExportPayload Payload { get; set; }
|
||||
|
||||
public static void Enqueue(Snowflake userId)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateDataExportJob>(j => j.InvokeAsync(userId));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(Snowflake userId)
|
||||
public async Task Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsyncInner(userId);
|
||||
await InvokeAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", userId);
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", Payload.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvokeAsyncInner(Snowflake userId)
|
||||
private async Task InvokeAsync()
|
||||
{
|
||||
User? user = await db
|
||||
.Users.Include(u => u.AuthMethods)
|
||||
.Include(u => u.Flags)
|
||||
.Include(u => u.ProfileFlags)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
.FirstOrDefaultAsync(u => u.Id == Payload.UserId);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Received create data export request for user {UserId} but no such user exists, deleted or otherwise. Ignoring request",
|
||||
userId
|
||||
Payload.UserId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -201,7 +197,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);
|
||||
|
@ -224,5 +220,5 @@ public class CreateDataExportJob(
|
|||
}
|
||||
|
||||
private static string ExportPath(Snowflake userId, string b64) =>
|
||||
$"data-exports/{userId}/{b64}/data-export.zip";
|
||||
$"data-exports/{userId}/{b64}.zip";
|
||||
}
|
|
@ -12,53 +12,49 @@
|
|||
//
|
||||
// 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 Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateFlagJob(
|
||||
public class CreateFlagInvocable(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
)
|
||||
) : IInvocable, IInvocableWithPayload<CreateFlagPayload>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagJob>();
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagInvocable>();
|
||||
public required CreateFlagPayload Payload { get; set; }
|
||||
|
||||
public static void Enqueue(CreateFlagPayload payload)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateFlagJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(CreateFlagPayload payload)
|
||||
public async Task Invoke()
|
||||
{
|
||||
_logger.Information(
|
||||
"Creating flag {FlagId} for user {UserId} with image data length {DataLength}",
|
||||
payload.Id,
|
||||
payload.UserId,
|
||||
payload.ImageData.Length
|
||||
Payload.Id,
|
||||
Payload.UserId,
|
||||
Payload.ImageData.Length
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f =>
|
||||
f.Id == payload.Id && f.UserId == payload.UserId
|
||||
f.Id == Payload.Id && f.UserId == Payload.UserId
|
||||
);
|
||||
if (flag == null)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Got a flag create job for {FlagId} but it doesn't exist, aborting",
|
||||
payload.Id
|
||||
Payload.Id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
(string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||
payload.ImageData,
|
||||
Payload.ImageData,
|
||||
256,
|
||||
false
|
||||
);
|
||||
|
@ -72,7 +68,7 @@ public class CreateFlagJob(
|
|||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
_logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", payload.Id, ae.Message);
|
||||
_logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", Payload.Id, ae.Message);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
|
|
|
@ -12,33 +12,29 @@
|
|||
//
|
||||
// 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 Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class MemberAvatarUpdateJob(
|
||||
public class MemberAvatarUpdateInvocable(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
)
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<MemberAvatarUpdateJob>();
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
public async Task Invoke()
|
||||
{
|
||||
BackgroundJob.Enqueue<MemberAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (payload.NewAvatar != null)
|
||||
await UpdateMemberAvatarAsync(payload.Id, payload.NewAvatar);
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateMemberAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
else
|
||||
await ClearMemberAvatarAsync(payload.Id);
|
||||
await ClearMemberAvatarAsync(Payload.Id);
|
||||
}
|
||||
|
||||
private async Task UpdateMemberAvatarAsync(Snowflake id, string newAvatar)
|
|
@ -19,3 +19,5 @@ namespace Foxnouns.Backend.Jobs;
|
|||
public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar);
|
||||
|
||||
public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string ImageData);
|
||||
|
||||
public record CreateDataExportPayload(Snowflake UserId);
|
||||
|
|
|
@ -12,33 +12,29 @@
|
|||
//
|
||||
// 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 Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class UserAvatarUpdateJob(
|
||||
public class UserAvatarUpdateInvocable(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
)
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateJob>();
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
public async Task Invoke()
|
||||
{
|
||||
BackgroundJob.Enqueue<UserAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (payload.NewAvatar != null)
|
||||
await UpdateUserAvatarAsync(payload.Id, payload.NewAvatar);
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateUserAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
else
|
||||
await ClearUserAvatarAsync(payload.Id);
|
||||
await ClearUserAvatarAsync(Payload.Id);
|
||||
}
|
||||
|
||||
private async Task UpdateUserAvatarAsync(Snowflake id, string newAvatar)
|
|
@ -41,7 +41,7 @@ public class LimitMiddleware : IMiddleware
|
|||
return;
|
||||
}
|
||||
|
||||
if (token?.User.Deleted == true && !attribute.UsableByDeletedUsers)
|
||||
if (token?.User.Deleted == true && !attribute.UsableBySuspendedUsers)
|
||||
throw new ApiError.Forbidden("Deleted users cannot access this endpoint.");
|
||||
|
||||
if (attribute.RequireAdmin && token?.User.Role != UserRole.Admin)
|
||||
|
@ -62,7 +62,7 @@ public class LimitMiddleware : IMiddleware
|
|||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class LimitAttribute : Attribute
|
||||
{
|
||||
public bool UsableByDeletedUsers { get; init; }
|
||||
public bool UsableBySuspendedUsers { get; init; }
|
||||
public bool RequireAdmin { get; init; }
|
||||
public bool RequireModerator { get; init; }
|
||||
}
|
||||
|
|
|
@ -19,12 +19,11 @@ using Foxnouns.Backend.Extensions;
|
|||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Foxnouns.Backend.Utils.OpenApi;
|
||||
using Hangfire;
|
||||
using Hangfire.Redis.StackExchange;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Prometheus;
|
||||
using Scalar.AspNetCore;
|
||||
using Sentry.Extensibility;
|
||||
using Serilog;
|
||||
|
||||
|
@ -34,9 +33,6 @@ Config config = builder.AddConfiguration();
|
|||
|
||||
builder.AddSerilog();
|
||||
|
||||
// Read version information from .version in the repository root
|
||||
await BuildInfo.ReadBuildInfo();
|
||||
|
||||
builder
|
||||
.WebHost.UseSentry(opts =>
|
||||
{
|
||||
|
@ -50,8 +46,7 @@ builder
|
|||
// No valid request body will ever come close to this limit,
|
||||
// but the limit is slightly higher to prevent valid requests from being rejected.
|
||||
opts.Limits.MaxRequestBodySize = 2 * 1024 * 1024;
|
||||
})
|
||||
.UseUrls(config.Address);
|
||||
});
|
||||
|
||||
builder
|
||||
.Services.AddControllers()
|
||||
|
@ -68,27 +63,16 @@ builder
|
|||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||
};
|
||||
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult(
|
||||
// the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine)
|
||||
options.InvalidModelStateResponseFactory = (ActionContext actionContext) =>
|
||||
new BadRequestObjectResult(
|
||||
new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson()
|
||||
);
|
||||
});
|
||||
|
||||
builder
|
||||
.Services.AddHangfire(
|
||||
(services, c) =>
|
||||
{
|
||||
c.UseRedisStorage(
|
||||
services.GetRequiredService<KeyCacheService>().Multiplexer,
|
||||
new RedisStorageOptions { Prefix = "foxnouns_net:" }
|
||||
);
|
||||
}
|
||||
)
|
||||
.AddHangfireServer();
|
||||
|
||||
builder.Services.AddOpenApi(
|
||||
"v2",
|
||||
options =>
|
||||
|
@ -125,19 +109,16 @@ if (config.Logging.SentryTracing)
|
|||
app.UseCors();
|
||||
app.UseCustomMiddleware();
|
||||
app.MapControllers();
|
||||
app.UseHangfireDashboard();
|
||||
app.MapOpenApi("/api-docs/openapi/{documentName}.json");
|
||||
app.MapScalarApiReference(options =>
|
||||
{
|
||||
options.Title = "pronouns.cc API";
|
||||
options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json";
|
||||
options.EndpointPathPrefix = "/api-docs/{documentName}";
|
||||
});
|
||||
|
||||
// TODO: I can't figure out why this doesn't work yet
|
||||
// TODO: Manually write API docs in the meantime
|
||||
// app.MapOpenApi("/api-docs/openapi/{documentName}.json");
|
||||
// app.MapScalarApiReference(
|
||||
// "/api-docs/",
|
||||
// options =>
|
||||
// {
|
||||
// options.Title = "pronouns.cc API";
|
||||
// options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json";
|
||||
// }
|
||||
// );
|
||||
app.Urls.Clear();
|
||||
app.Urls.Add(config.Address);
|
||||
|
||||
// Make sure metrics are updated whenever Prometheus scrapes them
|
||||
Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct =>
|
||||
|
|
|
@ -20,7 +20,6 @@ using Foxnouns.Backend.Utils;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
|
@ -29,8 +28,7 @@ public class AuthService(
|
|||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
UserRendererService userRenderer,
|
||||
ValidationService validationService
|
||||
UserRendererService userRenderer
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<AuthService>();
|
||||
|
@ -50,7 +48,7 @@ public class AuthService(
|
|||
// Validate username and whether it's not taken
|
||||
ValidationUtils.Validate(
|
||||
[
|
||||
("username", validationService.ValidateUsername(username)),
|
||||
("username", ValidationUtils.ValidateUsername(username)),
|
||||
("password", ValidationUtils.ValidatePassword(password)),
|
||||
]
|
||||
);
|
||||
|
@ -72,7 +70,6 @@ public class AuthService(
|
|||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
@ -98,7 +95,7 @@ public class AuthService(
|
|||
AssertValidAuthType(authType, instance);
|
||||
|
||||
// Validate username and whether it's not taken
|
||||
ValidationUtils.Validate([("username", validationService.ValidateUsername(username))]);
|
||||
ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(username))]);
|
||||
if (await db.Users.AnyAsync(u => u.Username == username, ct))
|
||||
throw new ApiError.BadRequest("Username is already taken", "username", username);
|
||||
|
||||
|
@ -119,7 +116,6 @@ public class AuthService(
|
|||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
@ -253,14 +249,14 @@ public class AuthService(
|
|||
{
|
||||
AssertValidAuthType(authType, app);
|
||||
|
||||
// This is already checked when generating an add account state, but we check it here too just in case.
|
||||
// This is already checked when
|
||||
int currentCount = await db
|
||||
.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 {AuthUtils.MaxAuthMethodsPerType} per account."
|
||||
"Too many linked accounts of this type, maximum of 3 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,17 +19,37 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService(
|
||||
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(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
HttpClient client,
|
||||
KeyCacheService keyCacheService,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
)
|
||||
{
|
||||
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthService>();
|
||||
_logger = logger.ForContext<FediverseAuthService>();
|
||||
_config = config;
|
||||
_db = db;
|
||||
_keyCacheService = keyCacheService;
|
||||
_snowflakeGenerator = snowflakeGenerator;
|
||||
|
||||
_client = new HttpClient();
|
||||
_client.DefaultRequestHeaders.Remove("User-Agent");
|
||||
_client.DefaultRequestHeaders.Remove("Accept");
|
||||
_client.DefaultRequestHeaders.Add("User-Agent", $"pronouns.cc/{BuildInfo.Version}");
|
||||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAuthUrlAsync(
|
||||
string instance,
|
||||
|
@ -50,7 +70,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)
|
||||
|
@ -72,7 +92,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();
|
||||
|
@ -87,7 +107,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 client.PostAsync(
|
||||
HttpResponseMessage resp = await _httpClient.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 client.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await _httpClient.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 client.PostAsync(
|
||||
HttpResponseMessage resp = await _httpClient.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 client.PostAsync(
|
||||
HttpResponseMessage resp = await _httpClient.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 client.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
|
||||
if (!resp2.IsSuccessStatusCode)
|
||||
{
|
||||
string respBody = await resp2.Content.ReadAsStringAsync(ct);
|
||||
|
|
|
@ -25,7 +25,6 @@ using Microsoft.EntityFrameworkCore;
|
|||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class RemoteAuthService(
|
||||
HttpClient client,
|
||||
Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
|
@ -33,6 +32,7 @@ public partial class RemoteAuthService(
|
|||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<RemoteAuthService>();
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
public record RemoteUser(string Id, string Username);
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Caching;
|
||||
|
||||
public class NoticeCacheService(IServiceProvider serviceProvider, IClock clock, ILogger logger)
|
||||
: SingletonCacheService<Notice>(serviceProvider, clock, logger)
|
||||
{
|
||||
public override Duration MaxAge { get; init; } = Duration.FromMinutes(5);
|
||||
|
||||
public override Func<
|
||||
DatabaseContext,
|
||||
CancellationToken,
|
||||
Task<Notice?>
|
||||
> FetchFunc { get; init; } =
|
||||
async (db, ct) =>
|
||||
await db
|
||||
.Notices.Where(n =>
|
||||
n.StartTime < clock.GetCurrentInstant() && n.EndTime > clock.GetCurrentInstant()
|
||||
)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Caching;
|
||||
|
||||
public abstract class SingletonCacheService<T>(
|
||||
IServiceProvider serviceProvider,
|
||||
IClock clock,
|
||||
ILogger logger
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
private T? _item;
|
||||
private Instant _lastUpdated = Instant.MinValue;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private readonly ILogger _logger = logger.ForContext<SingletonCacheService<T>>();
|
||||
|
||||
public virtual Duration MaxAge { get; init; } = Duration.FromMinutes(5);
|
||||
|
||||
public virtual Func<DatabaseContext, CancellationToken, Task<T?>> FetchFunc { get; init; } =
|
||||
(_, __) => Task.FromResult<T?>(null);
|
||||
|
||||
public async Task<T?> GetAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
if (_lastUpdated > clock.GetCurrentInstant() - MaxAge)
|
||||
{
|
||||
return _item;
|
||||
}
|
||||
|
||||
_logger.Debug("Cached item of type {Type} is expired, fetching it.", typeof(T));
|
||||
|
||||
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
|
||||
await using DatabaseContext db =
|
||||
scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
T? item = await FetchFunc(db, ct);
|
||||
_item = item;
|
||||
_lastUpdated = clock.GetCurrentInstant();
|
||||
return item;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -128,5 +128,5 @@ public class DataCleanupService(
|
|||
}
|
||||
|
||||
private static string ExportPath(Snowflake userId, string b64) =>
|
||||
$"data-exports/{userId}/{b64}/data-export.zip";
|
||||
$"data-exports/{userId}/{b64}.zip";
|
||||
}
|
||||
|
|
|
@ -23,11 +23,8 @@ public class EmailRateLimiter
|
|||
{
|
||||
private readonly ConcurrentDictionary<string, RateLimiter> _limiters = new();
|
||||
|
||||
private readonly FixedWindowRateLimiterOptions _limiterOptions = new()
|
||||
{
|
||||
Window = TimeSpan.FromHours(2),
|
||||
PermitLimit = 3,
|
||||
};
|
||||
private readonly FixedWindowRateLimiterOptions _limiterOptions =
|
||||
new() { Window = TimeSpan.FromHours(2), PermitLimit = 3 };
|
||||
|
||||
private RateLimiter GetLimiter(string bucket) =>
|
||||
_limiters.GetOrAdd(bucket, _ => new FixedWindowRateLimiter(_limiterOptions));
|
||||
|
|
|
@ -17,39 +17,94 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public class KeyCacheService(Config config)
|
||||
public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
|
||||
{
|
||||
public ConnectionMultiplexer Multiplexer { get; } =
|
||||
ConnectionMultiplexer.Connect(config.Database.Redis);
|
||||
private readonly ILogger _logger = logger.ForContext<KeyCacheService>();
|
||||
|
||||
public async Task SetKeyAsync(string key, string value, Duration expireAfter) =>
|
||||
await Multiplexer
|
||||
.GetDatabase()
|
||||
.StringSetAsync(key, value, expiry: expireAfter.ToTimeSpan());
|
||||
public Task SetKeyAsync(
|
||||
string key,
|
||||
string value,
|
||||
Duration expireAfter,
|
||||
CancellationToken ct = default
|
||||
) => SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct);
|
||||
|
||||
public async Task<string?> GetKeyAsync(string key, bool delete = false) =>
|
||||
delete
|
||||
? await Multiplexer.GetDatabase().StringGetDeleteAsync(key)
|
||||
: await Multiplexer.GetDatabase().StringGetAsync(key);
|
||||
public async Task SetKeyAsync(
|
||||
string key,
|
||||
string value,
|
||||
Instant expires,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
db.TemporaryKeys.Add(
|
||||
new TemporaryKey
|
||||
{
|
||||
Expires = expires,
|
||||
Key = key,
|
||||
Value = value,
|
||||
}
|
||||
);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteKeyAsync(string key) =>
|
||||
await Multiplexer.GetDatabase().KeyDeleteAsync(key);
|
||||
public async Task<string?> GetKeyAsync(
|
||||
string key,
|
||||
bool delete = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
TemporaryKey? value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct);
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
public async Task SetKeyAsync<T>(string key, T obj, Duration expiresAt)
|
||||
if (delete)
|
||||
await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct);
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public async Task DeleteKeyAsync(string key, CancellationToken ct = default) =>
|
||||
await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct);
|
||||
|
||||
public async Task DeleteExpiredKeysAsync(CancellationToken ct)
|
||||
{
|
||||
int count = await db
|
||||
.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant())
|
||||
.ExecuteDeleteAsync(ct);
|
||||
if (count != 0)
|
||||
_logger.Information("Removed {Count} expired keys from the database", count);
|
||||
}
|
||||
|
||||
public Task SetKeyAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
Duration expiresAt,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
where T : class => SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt, ct);
|
||||
|
||||
public async Task SetKeyAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
Instant expires,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
string value = JsonConvert.SerializeObject(obj);
|
||||
await SetKeyAsync(key, value, expiresAt);
|
||||
await SetKeyAsync(key, value, expires, ct);
|
||||
}
|
||||
|
||||
public async Task<T?> GetKeyAsync<T>(string key, bool delete = false)
|
||||
public async Task<T?> GetKeyAsync<T>(
|
||||
string key,
|
||||
bool delete = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
string? value = await GetKeyAsync(key, delete);
|
||||
string? value = await GetKeyAsync(key, delete, ct);
|
||||
return value == null ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ public class ModerationRendererService(
|
|||
: null,
|
||||
report.Status,
|
||||
report.Reason,
|
||||
report.Context,
|
||||
report.TargetType,
|
||||
report.TargetSnapshot != null
|
||||
? JsonConvert.DeserializeObject<JObject>(report.TargetSnapshot)
|
||||
|
@ -46,26 +45,12 @@ public class ModerationRendererService(
|
|||
|
||||
public AuditLogResponse RenderAuditLogEntry(AuditLogEntry entry)
|
||||
{
|
||||
PartialReport? report = null;
|
||||
if (entry.Report != null)
|
||||
{
|
||||
report = new PartialReport(
|
||||
entry.Report.Id,
|
||||
entry.Report.ReporterId,
|
||||
entry.Report.TargetUserId,
|
||||
entry.Report.TargetMemberId,
|
||||
entry.Report.Reason,
|
||||
entry.Report.Context,
|
||||
entry.Report.TargetType
|
||||
);
|
||||
}
|
||||
|
||||
return new AuditLogResponse(
|
||||
Id: entry.Id,
|
||||
Moderator: ToEntity(entry.ModeratorId, entry.ModeratorUsername)!,
|
||||
TargetUser: ToEntity(entry.TargetUserId, entry.TargetUsername),
|
||||
TargetMember: ToEntity(entry.TargetMemberId, entry.TargetMemberName),
|
||||
Report: report,
|
||||
ReportId: entry.ReportId,
|
||||
Type: entry.Type,
|
||||
Reason: entry.Reason,
|
||||
ClearedFields: entry.ClearedFields
|
||||
|
|
|
@ -18,7 +18,6 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Humanizer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
@ -27,6 +26,7 @@ public class ModerationService(
|
|||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IQueue queue,
|
||||
IClock clock
|
||||
)
|
||||
{
|
||||
|
@ -63,54 +63,6 @@ public class ModerationService(
|
|||
return entry;
|
||||
}
|
||||
|
||||
public async Task<AuditLogEntry> QuerySensitiveDataAsync(
|
||||
User moderator,
|
||||
User target,
|
||||
string reason
|
||||
)
|
||||
{
|
||||
_logger.Information(
|
||||
"Moderator {ModeratorId} is querying sensitive data for {TargetId}",
|
||||
moderator.Id,
|
||||
target.Id
|
||||
);
|
||||
|
||||
var entry = new AuditLogEntry
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
ModeratorId = moderator.Id,
|
||||
ModeratorUsername = moderator.Username,
|
||||
TargetUserId = target.Id,
|
||||
TargetUsername = target.Username,
|
||||
Type = AuditLogEntryType.QuerySensitiveUserData,
|
||||
Reason = reason,
|
||||
};
|
||||
db.AuditLog.Add(entry);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async Task<bool> ShowSensitiveDataAsync(
|
||||
User moderator,
|
||||
User target,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Snowflake cutoff = snowflakeGenerator.GenerateSnowflake(
|
||||
clock.GetCurrentInstant() - Duration.FromDays(1)
|
||||
);
|
||||
|
||||
return await db.AuditLog.AnyAsync(
|
||||
e =>
|
||||
e.ModeratorId == moderator.Id
|
||||
&& e.TargetUserId == target.Id
|
||||
&& e.Type == AuditLogEntryType.QuerySensitiveUserData
|
||||
&& e.Id > cutoff,
|
||||
ct
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<AuditLogEntry> ExecuteSuspensionAsync(
|
||||
User moderator,
|
||||
User target,
|
||||
|
@ -153,12 +105,6 @@ public class ModerationService(
|
|||
target.DeletedAt = clock.GetCurrentInstant();
|
||||
target.DeletedBy = moderator.Id;
|
||||
|
||||
if (report != null)
|
||||
{
|
||||
report.Status = ReportStatus.Closed;
|
||||
db.Update(report);
|
||||
}
|
||||
|
||||
if (!clearProfile)
|
||||
{
|
||||
db.Update(target);
|
||||
|
@ -180,7 +126,9 @@ public class ModerationService(
|
|||
target.CustomPreferences = [];
|
||||
target.ProfileFlags = [];
|
||||
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(target.Id, null));
|
||||
queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(target.Id, null)
|
||||
);
|
||||
|
||||
// TODO: also clear member profiles?
|
||||
|
||||
|
@ -261,9 +209,10 @@ public class ModerationService(
|
|||
targetMember.DisplayName = null;
|
||||
break;
|
||||
case FieldsToClear.Avatar:
|
||||
MemberAvatarUpdateJob.Enqueue(
|
||||
new AvatarUpdatePayload(targetMember.Id, null)
|
||||
);
|
||||
queue.QueueInvocableWithPayload<
|
||||
MemberAvatarUpdateInvocable,
|
||||
AvatarUpdatePayload
|
||||
>(new AvatarUpdatePayload(targetMember.Id, null));
|
||||
break;
|
||||
case FieldsToClear.Bio:
|
||||
targetMember.Bio = null;
|
||||
|
@ -302,7 +251,10 @@ public class ModerationService(
|
|||
targetUser.DisplayName = null;
|
||||
break;
|
||||
case FieldsToClear.Avatar:
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(targetUser.Id, null));
|
||||
queue.QueueInvocableWithPayload<
|
||||
UserAvatarUpdateInvocable,
|
||||
AvatarUpdatePayload
|
||||
>(new AvatarUpdatePayload(targetUser.Id, null));
|
||||
break;
|
||||
case FieldsToClear.Bio:
|
||||
targetUser.Bio = null;
|
||||
|
@ -333,12 +285,6 @@ public class ModerationService(
|
|||
db.Update(targetUser);
|
||||
}
|
||||
|
||||
if (report != null)
|
||||
{
|
||||
report.Status = ReportStatus.Closed;
|
||||
db.Update(report);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return entry;
|
||||
|
|
|
@ -33,9 +33,11 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
|
|||
|
||||
// The type is literally written on the same line, we can just use `var`
|
||||
// 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,7 +33,6 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await RenderUserInnerAsync(
|
||||
|
@ -43,7 +42,6 @@ public class UserRendererService(
|
|||
renderMembers,
|
||||
renderAuthMethods,
|
||||
overrideSid,
|
||||
renderSettings,
|
||||
ct
|
||||
);
|
||||
|
||||
|
@ -54,7 +52,6 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
|
@ -65,7 +62,6 @@ 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)
|
||||
|
@ -107,8 +103,7 @@ public class UserRendererService(
|
|||
user.Names,
|
||||
user.Pronouns,
|
||||
user.Fields,
|
||||
user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value)))
|
||||
.ToDictionary(),
|
||||
user.CustomPreferences,
|
||||
flags.Select(f => RenderPrideFlag(f.PrideFlag)),
|
||||
utcOffset,
|
||||
user.Role,
|
||||
|
@ -121,8 +116,7 @@ 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,
|
||||
renderSettings ? user.Settings : null
|
||||
tokenHidden ? user.Deleted : null
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -136,14 +130,6 @@ public class UserRendererService(
|
|||
: a.RemoteUsername
|
||||
);
|
||||
|
||||
public static CustomPreferenceResponse RenderCustomPreference(User.CustomPreference pref) =>
|
||||
new(pref.Icon, pref.Tooltip, pref.Muted, pref.Favourite, pref.Size);
|
||||
|
||||
public static Dictionary<Snowflake, CustomPreferenceResponse> RenderCustomPreferences(
|
||||
User user
|
||||
) =>
|
||||
user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))).ToDictionary();
|
||||
|
||||
public PartialUser RenderPartialUser(User user) =>
|
||||
new(
|
||||
user.Id,
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
|
||||
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
|
||||
public class MembersV1Service(DatabaseContext db, UsersV1Service usersV1Service)
|
||||
{
|
||||
public async Task<Member> ResolveMemberAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
Member? member;
|
||||
if (Snowflake.TryParse(id, out Snowflake? sf))
|
||||
{
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == sf && !m.User.Deleted, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
}
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.LegacyId == id && !m.User.Deleted, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound);
|
||||
}
|
||||
|
||||
public async Task<Member> ResolveMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, token, ct);
|
||||
|
||||
Member? member;
|
||||
if (Snowflake.TryParse(memberRef, out Snowflake? sf))
|
||||
{
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == sf && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
}
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.LegacyId == memberRef && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
throw new ApiError.NotFound(
|
||||
"No member with that ID or name found.",
|
||||
ErrorCode.MemberNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<MemberResponse> RenderMemberAsync(
|
||||
Member m,
|
||||
Token? token = default,
|
||||
User? user = null,
|
||||
bool renderFlags = true,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
user ??= m.User;
|
||||
bool renderUnlisted = m.UserId == token?.UserId;
|
||||
|
||||
List<MemberFlag> flags = renderFlags
|
||||
? await db.MemberFlags.Where(f => f.MemberId == m.Id).OrderBy(f => f.Id).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
return new MemberResponse(
|
||||
m.LegacyId,
|
||||
m.Id,
|
||||
m.Sid,
|
||||
m.Name,
|
||||
m.DisplayName,
|
||||
m.Bio,
|
||||
m.Avatar,
|
||||
m.Links,
|
||||
Names: FieldEntry.FromEntries(m.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(m.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(m.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
User: UsersV1Service.RenderPartialUser(user),
|
||||
Unlisted: renderUnlisted ? m.Unlisted : null
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
|
||||
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
|
||||
public class UsersV1Service(DatabaseContext db)
|
||||
{
|
||||
public async Task<User> ResolveUserAsync(
|
||||
string userRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
if (userRef == "@me")
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
throw new ApiError.Unauthorized(
|
||||
"This endpoint requires an authenticated user.",
|
||||
ErrorCode.AuthenticationRequired
|
||||
);
|
||||
}
|
||||
|
||||
return await db.Users.FirstAsync(u => u.Id == token.UserId, ct);
|
||||
}
|
||||
|
||||
User? user;
|
||||
if (Snowflake.TryParse(userRef, out Snowflake? sf))
|
||||
{
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.Id == sf && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
}
|
||||
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.LegacyId == userRef && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.Username == userRef && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
|
||||
throw new ApiError.NotFound(
|
||||
"No user with that ID or username found.",
|
||||
ErrorCode.UserNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<UserResponse> RenderUserAsync(
|
||||
User user,
|
||||
Token? token = null,
|
||||
bool renderMembers = true,
|
||||
bool renderFlags = true,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
bool isSelfUser = user.Id == token?.UserId;
|
||||
renderMembers = renderMembers && (isSelfUser || !user.ListHidden);
|
||||
|
||||
// Only fetch members if we're rendering members (duh)
|
||||
List<Member> members = renderMembers
|
||||
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
List<UserFlag> flags = renderFlags
|
||||
? await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
int? utcOffset = null;
|
||||
if (
|
||||
user.Timezone != null
|
||||
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz)
|
||||
)
|
||||
{
|
||||
utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds;
|
||||
}
|
||||
|
||||
return new UserResponse(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Sid,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Bio,
|
||||
user.MemberTitle,
|
||||
user.Avatar,
|
||||
user.Links,
|
||||
Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(),
|
||||
utcOffset,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<CurrentUserResponse> RenderCurrentUserAsync(
|
||||
User user,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
List<Member> members = await db
|
||||
.Members.Where(m => m.UserId == user.Id)
|
||||
.OrderBy(m => m.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<UserFlag> flags = await db
|
||||
.UserFlags.Where(f => f.UserId == user.Id)
|
||||
.OrderBy(f => f.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
int? utcOffset = null;
|
||||
if (
|
||||
user.Timezone != null
|
||||
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz)
|
||||
)
|
||||
{
|
||||
utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds;
|
||||
}
|
||||
|
||||
List<AuthMethod> authMethods = await db
|
||||
.AuthMethods.Include(a => a.FediverseApplication)
|
||||
.Where(a => a.UserId == user.Id)
|
||||
.OrderBy(a => a.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
AuthMethod? discord = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Discord);
|
||||
AuthMethod? google = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Google);
|
||||
AuthMethod? tumblr = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Tumblr);
|
||||
AuthMethod? fediverse = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Fediverse);
|
||||
|
||||
return new CurrentUserResponse(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Sid,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Bio,
|
||||
user.MemberTitle,
|
||||
user.Avatar,
|
||||
user.Links,
|
||||
Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(),
|
||||
utcOffset,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences),
|
||||
user.Id.Time,
|
||||
user.Timezone,
|
||||
user.Role is UserRole.Admin,
|
||||
user.ListHidden,
|
||||
user.LastSidReroll,
|
||||
discord?.RemoteId,
|
||||
discord?.RemoteUsername,
|
||||
google?.RemoteId,
|
||||
google?.RemoteUsername,
|
||||
tumblr?.RemoteId,
|
||||
tumblr?.RemoteUsername,
|
||||
fediverse?.RemoteId,
|
||||
fediverse?.RemoteUsername,
|
||||
fediverse?.FediverseApplication?.Domain
|
||||
);
|
||||
}
|
||||
|
||||
private static Dictionary<Guid, CustomPreference> RenderCustomPreferences(
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
customPreferences
|
||||
.Select(x =>
|
||||
(
|
||||
x.Value.LegacyId,
|
||||
new CustomPreference(
|
||||
x.Value.Icon,
|
||||
x.Value.Tooltip,
|
||||
x.Value.Size,
|
||||
x.Value.Muted,
|
||||
x.Value.Favourite
|
||||
)
|
||||
)
|
||||
)
|
||||
.ToDictionary();
|
||||
|
||||
private static PartialMember RenderPartialMember(
|
||||
Member m,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
new(
|
||||
m.LegacyId,
|
||||
m.Id,
|
||||
m.Sid,
|
||||
m.Name,
|
||||
m.DisplayName,
|
||||
m.Bio,
|
||||
m.Avatar,
|
||||
m.Links,
|
||||
Names: FieldEntry.FromEntries(m.Names, customPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(m.Pronouns, customPreferences)
|
||||
);
|
||||
|
||||
public static PartialUser RenderPartialUser(User user) =>
|
||||
new(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
|
||||
);
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public partial class ValidationService
|
||||
{
|
||||
private static readonly string[] InvalidUsernames =
|
||||
[
|
||||
"..",
|
||||
"admin",
|
||||
"administrator",
|
||||
"mod",
|
||||
"moderator",
|
||||
"api",
|
||||
"page",
|
||||
"pronouns",
|
||||
"settings",
|
||||
"pronouns.cc",
|
||||
"pronounscc",
|
||||
"null",
|
||||
];
|
||||
|
||||
private static readonly string[] InvalidMemberNames =
|
||||
[
|
||||
// these break routing outright
|
||||
".",
|
||||
"..",
|
||||
// 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)
|
||||
{
|
||||
if (!UsernameRegex().IsMatch(username))
|
||||
{
|
||||
if (username.Length < 2)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Username is too short",
|
||||
2,
|
||||
_limits.MaxUsernameLength,
|
||||
username.Length
|
||||
);
|
||||
}
|
||||
|
||||
if (username.Length > _limits.MaxUsernameLength)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Username is too long",
|
||||
2,
|
||||
_limits.MaxUsernameLength,
|
||||
username.Length
|
||||
);
|
||||
}
|
||||
|
||||
return ValidationError.GenericValidationError(
|
||||
"Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods",
|
||||
username
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
InvalidUsernames.Any(u =>
|
||||
string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Username is not allowed", username);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ValidationError? ValidateMemberName(string memberName)
|
||||
{
|
||||
if (!MemberRegex().IsMatch(memberName))
|
||||
{
|
||||
if (memberName.Length < 1)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Name is too short",
|
||||
1,
|
||||
_limits.MaxMemberNameLength,
|
||||
memberName.Length
|
||||
);
|
||||
}
|
||||
|
||||
if (memberName.Length > _limits.MaxMemberNameLength)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Name is too long",
|
||||
1,
|
||||
_limits.MaxMemberNameLength,
|
||||
memberName.Length
|
||||
);
|
||||
}
|
||||
|
||||
return ValidationError.GenericValidationError(
|
||||
"Member name cannot contain any of the following: "
|
||||
+ " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , "
|
||||
+ "and cannot be one or two periods",
|
||||
memberName
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
InvalidMemberNames.Any(u =>
|
||||
string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Name is not allowed", memberName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ValidationError? ValidateDisplayName(string? displayName)
|
||||
{
|
||||
if (displayName?.Length == 0)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Display name is too short",
|
||||
1,
|
||||
_limits.MaxDisplayNameLength,
|
||||
displayName.Length
|
||||
);
|
||||
}
|
||||
|
||||
if (displayName?.Length > _limits.MaxDisplayNameLength)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Display name is too long",
|
||||
1,
|
||||
_limits.MaxDisplayNameLength,
|
||||
displayName.Length
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links)
|
||||
{
|
||||
if (links == null)
|
||||
return [];
|
||||
if (links.Length > _limits.MaxLinks)
|
||||
{
|
||||
return
|
||||
[
|
||||
(
|
||||
"links",
|
||||
ValidationError.LengthError("Too many links", 0, _limits.MaxLinks, links.Length)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
foreach ((string link, int idx) in links.Select((l, i) => (l, i)))
|
||||
{
|
||||
if (link.Length == 0)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"links.{idx}",
|
||||
ValidationError.LengthError(
|
||||
"Link cannot be empty",
|
||||
1,
|
||||
_limits.MaxLinkLength,
|
||||
0
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (link.Length > _limits.MaxLinkLength)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"links.{idx}",
|
||||
ValidationError.LengthError(
|
||||
"Link is too long",
|
||||
1,
|
||||
_limits.MaxLinkLength,
|
||||
link.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public ValidationError? ValidateBio(string? bio)
|
||||
{
|
||||
if (bio?.Length == 0)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Bio is too short",
|
||||
1,
|
||||
_limits.MaxBioLength,
|
||||
bio.Length
|
||||
);
|
||||
}
|
||||
|
||||
if (bio?.Length > _limits.MaxBioLength)
|
||||
{
|
||||
return ValidationError.LengthError(
|
||||
"Bio is too long",
|
||||
1,
|
||||
_limits.MaxBioLength,
|
||||
bio.Length
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ValidationError? ValidateAvatar(string? avatar)
|
||||
{
|
||||
if (avatar?.Length == 0)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Avatar cannot be empty", null);
|
||||
}
|
||||
|
||||
if (avatar?.Length > _limits.MaxAvatarLength)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Avatar is too large", null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-US")]
|
||||
private static partial Regex UsernameRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
"""^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""",
|
||||
RegexOptions.IgnoreCase,
|
||||
"en-US"
|
||||
)]
|
||||
private static partial Regex MemberRegex();
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public partial class ValidationService(Config config)
|
||||
{
|
||||
private readonly Config.LimitsConfig _limits = config.Limits;
|
||||
}
|
|
@ -135,7 +135,7 @@ public static class AuthUtils
|
|||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('=');
|
||||
|
||||
public static string RandomToken(int bytes = 48) =>
|
||||
RandomUrlUnsafeToken(bytes)
|
||||
RandomUrlUnsafeToken()
|
||||
// Make the token URL-safe
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
|
|
23
Foxnouns.Backend/Utils/Limits.cs
Normal file
23
Foxnouns.Backend/Utils/Limits.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
// 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/>.
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
public static class Limits
|
||||
{
|
||||
public const int FieldLimit = 25;
|
||||
public const int FieldNameLimit = 100;
|
||||
public const int FieldEntryTextLimit = 100;
|
||||
public const int FieldEntriesLimit = 100;
|
||||
}
|
|
@ -22,10 +22,8 @@ namespace Foxnouns.Backend.Utils.OpenApi;
|
|||
|
||||
public class PropertyKeySchemaTransformer : IOpenApiSchemaTransformer
|
||||
{
|
||||
private static readonly DefaultContractResolver SnakeCaseConverter = new()
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||
};
|
||||
private static readonly DefaultContractResolver SnakeCaseConverter =
|
||||
new() { NamingStrategy = new SnakeCaseNamingStrategy() };
|
||||
|
||||
public Task TransformAsync(
|
||||
OpenApiSchema schema,
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
public partial class ValidationService
|
||||
public static partial class ValidationUtils
|
||||
{
|
||||
public static readonly string[] DefaultStatusOptions =
|
||||
[
|
||||
|
@ -28,7 +28,7 @@ public partial class ValidationService
|
|||
"avoid",
|
||||
];
|
||||
|
||||
public IEnumerable<(string, ValidationError?)> ValidateFields(
|
||||
public static IEnumerable<(string, ValidationError?)> ValidateFields(
|
||||
List<Field>? fields,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ public partial class ValidationService
|
|||
return [];
|
||||
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
if (fields.Count > _limits.MaxFields)
|
||||
if (fields.Count > 25)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -45,7 +45,7 @@ public partial class ValidationService
|
|||
ValidationError.LengthError(
|
||||
"Too many fields",
|
||||
0,
|
||||
_limits.MaxFields,
|
||||
Limits.FieldLimit,
|
||||
fields.Count
|
||||
)
|
||||
)
|
||||
|
@ -53,38 +53,39 @@ public partial class ValidationService
|
|||
}
|
||||
|
||||
// No overwhelming this function, thank you
|
||||
if (fields.Count > _limits.MaxFields + 50)
|
||||
if (fields.Count > 100)
|
||||
return errors;
|
||||
|
||||
foreach ((Field? field, int index) in fields.Select((field, index) => (field, index)))
|
||||
{
|
||||
if (field.Name.Length > _limits.MaxFieldNameLength)
|
||||
switch (field.Name.Length)
|
||||
{
|
||||
case > Limits.FieldNameLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too long",
|
||||
1,
|
||||
_limits.MaxFieldNameLength,
|
||||
Limits.FieldNameLimit,
|
||||
field.Name.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (field.Name.Length < 1)
|
||||
{
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too short",
|
||||
1,
|
||||
_limits.MaxFieldNameLength,
|
||||
Limits.FieldNameLimit,
|
||||
field.Name.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
errors = errors
|
||||
|
@ -101,7 +102,7 @@ public partial class ValidationService
|
|||
return errors;
|
||||
}
|
||||
|
||||
public IEnumerable<(string, ValidationError?)> ValidateFieldEntries(
|
||||
public static IEnumerable<(string, ValidationError?)> ValidateFieldEntries(
|
||||
FieldEntry[]? entries,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences,
|
||||
string errorPrefix = "fields"
|
||||
|
@ -111,7 +112,7 @@ public partial class ValidationService
|
|||
return [];
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (entries.Length > _limits.MaxFieldEntries)
|
||||
if (entries.Length > Limits.FieldEntriesLimit)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -119,7 +120,7 @@ public partial class ValidationService
|
|||
ValidationError.LengthError(
|
||||
"Field has too many entries",
|
||||
0,
|
||||
_limits.MaxFieldEntries,
|
||||
Limits.FieldEntriesLimit,
|
||||
entries.Length
|
||||
)
|
||||
)
|
||||
|
@ -127,7 +128,7 @@ public partial class ValidationService
|
|||
}
|
||||
|
||||
// Same as above, no overwhelming this function with a ridiculous amount of entries
|
||||
if (entries.Length > _limits.MaxFieldEntries + 50)
|
||||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
return errors;
|
||||
|
||||
string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray();
|
||||
|
@ -138,33 +139,34 @@ public partial class ValidationService
|
|||
)
|
||||
)
|
||||
{
|
||||
if (entry.Value.Length > _limits.MaxFieldEntryTextLength)
|
||||
switch (entry.Value.Length)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.Value.Length < 1)
|
||||
{
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -184,7 +186,7 @@ public partial class ValidationService
|
|||
return errors;
|
||||
}
|
||||
|
||||
public IEnumerable<(string, ValidationError?)> ValidatePronouns(
|
||||
public static IEnumerable<(string, ValidationError?)> ValidatePronouns(
|
||||
Pronoun[]? entries,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences,
|
||||
string errorPrefix = "pronouns"
|
||||
|
@ -194,7 +196,7 @@ public partial class ValidationService
|
|||
return [];
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (entries.Length > _limits.MaxFieldEntries)
|
||||
if (entries.Length > Limits.FieldEntriesLimit)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -202,7 +204,7 @@ public partial class ValidationService
|
|||
ValidationError.LengthError(
|
||||
"Too many pronouns",
|
||||
0,
|
||||
_limits.MaxFieldEntries,
|
||||
Limits.FieldEntriesLimit,
|
||||
entries.Length
|
||||
)
|
||||
)
|
||||
|
@ -210,7 +212,7 @@ public partial class ValidationService
|
|||
}
|
||||
|
||||
// Same as above, no overwhelming this function with a ridiculous amount of entries
|
||||
if (entries.Length > _limits.MaxFieldEntries + 50)
|
||||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
return errors;
|
||||
|
||||
string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray();
|
||||
|
@ -219,64 +221,66 @@ public partial class ValidationService
|
|||
(Pronoun? entry, int entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))
|
||||
)
|
||||
{
|
||||
if (entry.Value.Length > _limits.MaxFieldEntryTextLength)
|
||||
switch (entry.Value.Length)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.Value.Length < 1)
|
||||
{
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.DisplayText != null)
|
||||
{
|
||||
if (entry.DisplayText.Length > _limits.MaxFieldEntryTextLength)
|
||||
switch (entry.DisplayText.Length)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.DisplayText.Length < 1)
|
||||
{
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,16 +12,189 @@
|
|||
//
|
||||
// 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;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
public static partial class ValidationUtils
|
||||
{
|
||||
public const int MaximumReportContextLength = 512;
|
||||
private static readonly string[] InvalidUsernames =
|
||||
[
|
||||
"..",
|
||||
"admin",
|
||||
"administrator",
|
||||
"mod",
|
||||
"moderator",
|
||||
"api",
|
||||
"page",
|
||||
"pronouns",
|
||||
"settings",
|
||||
"pronouns.cc",
|
||||
"pronounscc",
|
||||
];
|
||||
|
||||
public static ValidationError? ValidateReportContext(string? context) =>
|
||||
context?.Length > MaximumReportContextLength
|
||||
? ValidationError.GenericValidationError("Report context is too long", null)
|
||||
: null;
|
||||
private static readonly string[] InvalidMemberNames =
|
||||
[
|
||||
// these break routing outright
|
||||
".",
|
||||
"..",
|
||||
// the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
|
||||
"edit",
|
||||
];
|
||||
|
||||
public static ValidationError? ValidateUsername(string username)
|
||||
{
|
||||
if (!UsernameRegex().IsMatch(username))
|
||||
{
|
||||
return username.Length switch
|
||||
{
|
||||
< 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length),
|
||||
> 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length),
|
||||
_ => ValidationError.GenericValidationError(
|
||||
"Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods",
|
||||
username
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
InvalidUsernames.Any(u =>
|
||||
string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Username is not allowed", username);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ValidationError? ValidateMemberName(string memberName)
|
||||
{
|
||||
if (!MemberRegex().IsMatch(memberName))
|
||||
{
|
||||
return memberName.Length switch
|
||||
{
|
||||
< 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length),
|
||||
> 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length),
|
||||
_ => ValidationError.GenericValidationError(
|
||||
"Member name cannot contain any of the following: "
|
||||
+ " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , "
|
||||
+ "and cannot be one or two periods",
|
||||
memberName
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
InvalidMemberNames.Any(u =>
|
||||
string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
return ValidationError.GenericValidationError("Name is not allowed", memberName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ValidationError? ValidateDisplayName(string? displayName)
|
||||
{
|
||||
return displayName?.Length switch
|
||||
{
|
||||
0 => ValidationError.LengthError(
|
||||
"Display name is too short",
|
||||
1,
|
||||
100,
|
||||
displayName.Length
|
||||
),
|
||||
> 100 => ValidationError.LengthError(
|
||||
"Display name is too long",
|
||||
1,
|
||||
100,
|
||||
displayName.Length
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private const int MaxLinks = 25;
|
||||
private const int MaxLinkLength = 256;
|
||||
|
||||
public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links)
|
||||
{
|
||||
if (links == null)
|
||||
return [];
|
||||
if (links.Length > MaxLinks)
|
||||
{
|
||||
return
|
||||
[
|
||||
("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)),
|
||||
];
|
||||
}
|
||||
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
foreach ((string link, int idx) in links.Select((l, i) => (l, i)))
|
||||
{
|
||||
switch (link.Length)
|
||||
{
|
||||
case 0:
|
||||
errors.Add(
|
||||
(
|
||||
$"links.{idx}",
|
||||
ValidationError.LengthError("Link cannot be empty", 1, 256, 0)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case > MaxLinkLength:
|
||||
errors.Add(
|
||||
(
|
||||
$"links.{idx}",
|
||||
ValidationError.LengthError(
|
||||
"Link is too long",
|
||||
1,
|
||||
MaxLinkLength,
|
||||
link.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public const int MaxBioLength = 1024;
|
||||
public const int MaxAvatarLength = 1_500_000;
|
||||
|
||||
public static ValidationError? ValidateBio(string? bio)
|
||||
{
|
||||
return bio?.Length switch
|
||||
{
|
||||
0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length),
|
||||
> MaxBioLength => ValidationError.LengthError(
|
||||
"Bio is too long",
|
||||
1,
|
||||
MaxBioLength,
|
||||
bio.Length
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public static ValidationError? ValidateAvatar(string? avatar)
|
||||
{
|
||||
return avatar?.Length switch
|
||||
{
|
||||
0 => ValidationError.GenericValidationError("Avatar cannot be empty", null),
|
||||
> MaxAvatarLength => ValidationError.GenericValidationError(
|
||||
"Avatar is too large",
|
||||
null
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public const int MinimumPasswordLength = 12;
|
||||
public const int MaximumPasswordLength = 1024;
|
||||
|
@ -43,4 +216,14 @@ public static partial class ValidationUtils
|
|||
),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
[GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")]
|
||||
private static partial Regex UsernameRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
"""^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""",
|
||||
RegexOptions.IgnoreCase,
|
||||
"en-NL"
|
||||
)]
|
||||
private static partial Regex MemberRegex();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
; The host the server will listen on
|
||||
Host = localhost
|
||||
; The port the server will listen on
|
||||
Port = 6000
|
||||
Port = 5000
|
||||
; The base *external* URL
|
||||
BaseUrl = https://pronouns.localhost
|
||||
; The base URL for media, without a trailing slash. This must be publicly accessible.
|
||||
|
@ -43,9 +43,6 @@ AccessKey = <s3AccessKey>
|
|||
SecretKey = <s3SecretKey>
|
||||
Bucket = pronounscc
|
||||
|
||||
[Limits]
|
||||
MaxMemberCount = 5000
|
||||
|
||||
[EmailAuth]
|
||||
; The address that emails will be sent from. If not set, email auth is disabled.
|
||||
From = noreply@accounts.pronouns.cc
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
"net9.0": {
|
||||
"Coravel": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.2, )",
|
||||
"resolved": "6.0.2",
|
||||
"contentHash": "/XZiRId4Ilar/OqjGKdxkZWfW97ekeT0wgiWNjGdqf8pPxiK508//Zkc0xrKMDOqchFT7B/oqAoQ+Vrx1txpPQ==",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "U16V/IxGL2TcpU9sT1gUA3pqoVIlz+WthC4idn8OTPiEtLElTcmNF6sHt+gOx8DRU8TBgN5vjfL4AHetjacOWQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Memory": "3.1.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "6.0.0",
|
||||
|
@ -17,12 +17,12 @@
|
|||
},
|
||||
"Coravel.Mailer": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.1.0, )",
|
||||
"resolved": "7.1.0",
|
||||
"contentHash": "yMbUrwKl5/HbJeX8JkHa8Q3CPTJ3OmPyDSG7sULbXGEhzc2GiYIh7pmVhI1FFeL3VUtFavMDkS8PTwEeCpiwlg==",
|
||||
"requested": "[7.0.0, )",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "mxSlOOBxPjCAZruOpgXtubnZA9lD0DRgutApQmAsts7DoRfe0wTzqWrYjeZTiIzgVJZKZxJglN8duTvbPrw3jQ==",
|
||||
"dependencies": {
|
||||
"MailKit": "4.8.0",
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.36"
|
||||
"MailKit": "4.3.0",
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.27"
|
||||
}
|
||||
},
|
||||
"EFCore.NamingConventions": {
|
||||
|
@ -46,37 +46,6 @@
|
|||
"Npgsql": "8.0.3"
|
||||
}
|
||||
},
|
||||
"Hangfire": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.8.18, )",
|
||||
"resolved": "1.8.18",
|
||||
"contentHash": "EY+UqMHTOQAtdjeJf3jlnj8MpENyDPTpA6OHMncucVlkaongZjrx+gCN4bgma7vD3BNHqfQ7irYrfE5p1DOBEQ==",
|
||||
"dependencies": {
|
||||
"Hangfire.AspNetCore": "[1.8.18]",
|
||||
"Hangfire.Core": "[1.8.18]",
|
||||
"Hangfire.SqlServer": "[1.8.18]"
|
||||
}
|
||||
},
|
||||
"Hangfire.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.8.18, )",
|
||||
"resolved": "1.8.18",
|
||||
"contentHash": "oNAkV8QQoYg5+vM2M024NBk49EhTO2BmKDLuQaKNew23RpH9OUGtKDl1KldBdDJrD8TMFzjhWCArol3igd2i2w==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.1"
|
||||
}
|
||||
},
|
||||
"Hangfire.Redis.StackExchange": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.9.4, )",
|
||||
"resolved": "1.9.4",
|
||||
"contentHash": "rB4eGf4+hFhdnrN3//2O39JGuy1ThIKL3oTdVI2F3HqmSaSD9Cixl2xmMAqGJMld39Ke7eoP9sxbxnpVnYW66g==",
|
||||
"dependencies": {
|
||||
"Hangfire.Core": "1.8.7",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"StackExchange.Redis": "2.7.10"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.14.1, )",
|
||||
|
@ -91,41 +60,41 @@
|
|||
},
|
||||
"Microsoft.AspNetCore.Mvc.NewtonsoftJson": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "cCnaxji6nqIHHLAEhZ6QirXCvwJNi0Q/qCPLkRW5SqMYNuOwoQdGk1KAhW65phBq1VHGt7wLbadpuGPGqfiZuA==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "pTFDEmZi3GheCSPrBxzyE63+d5unln2vYldo/nOm1xet/4rpEk2oJYcwpclPQ13E+LZBF9XixkgwYTUwqznlWg==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.JsonPatch": "9.0.2",
|
||||
"Microsoft.AspNetCore.JsonPatch": "9.0.0",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Newtonsoft.Json.Bson": "1.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.OpenApi": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "JUndpjRNdG8GvzBLH/J4hen4ehWaPcshtiQ6+sUs1Bcj3a7dOsmWpDloDlpPeMOVSlhHwUJ3Xld0ClZjsFLgFQ==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "FqUK5j1EOPNuFT7IafltZQ3cakqhSwVzH5ZW1MhZDe4pPXs9sJ2M5jom1Omsu+mwF2tNKKlRAzLRHQTZzbd+6Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.OpenApi": "1.6.17"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "P90ZuybgcpW32y985eOYxSoZ9IiL0UTYQlY0y1Pt1iHAnpZj/dQHREpSpry1RNvk8YjAeoAkWFdem5conqB9zQ==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "wpG+nfnfDAw87R3ovAsUmjr3MZ4tYXf6bFqEPVAIKE6IfPml3DS//iX0DBnf8kWn5ZHSO5oi1m4d/Jf+1LifJQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.2",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.2",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2"
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.0",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Design": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "WWRmTxb/yd05cTW+k32lLvIhffxilgYvwKHDxiqe7GRLKeceyMspuf5BRpW65sFF7S2G+Be9JgjUe1ypGqt9tg==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "Pqo8I+yHJ3VQrAoY0hiSncf+5P7gN/RkNilK5e+/K/yKh+yAWxdUAI6t0TG26a9VPlCa9FhyklzyFvRyj3YG9A==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "2.14.1",
|
||||
"Microsoft.Build.Framework": "17.8.3",
|
||||
|
@ -133,45 +102,33 @@
|
|||
"Microsoft.CodeAnalysis.CSharp": "4.8.0",
|
||||
"Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0",
|
||||
"Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "9.0.2",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "9.0.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.0",
|
||||
"Mono.TextTemplating": "3.0.0",
|
||||
"System.Text.Json": "9.0.2"
|
||||
"System.Text.Json": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "AlEfp0DMz8E1h1Exi8LBrUCNmCYcGDfSM4F/uK1D1cYx/R3w0LVvlmjICqxqXTsy7BEZaCf5leRZY2FuPEiFaw==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http.Resilience": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.2.0, )",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "Km+YyCuk1IaeOsAzPDygtgsUOh3Fi89hpA18si0tFJmpSBf9aKzP9ffV5j7YOoVDvRWirpumXAPQzk1inBsvKw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
|
||||
"Microsoft.Extensions.Http.Diagnostics": "9.2.0",
|
||||
"Microsoft.Extensions.ObjectPool": "9.0.2",
|
||||
"Microsoft.Extensions.Resilience": "9.2.0"
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"MimeKit": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.10.0, )",
|
||||
"resolved": "4.10.0",
|
||||
"contentHash": "GQofI17cH55XSh109hJmHaYMtSFqTX/eUek3UcV7hTnYayAIXZ6eHlv345tfdc+bQ/BrEnYOSZVzx9I3wpvvpg==",
|
||||
"requested": "[4.9.0, )",
|
||||
"resolved": "4.9.0",
|
||||
"contentHash": "DZXXMZzmAABDxFhOSMb6SE8KKxcRd/sk1E6aJTUE5ys2FWOQhznYV2Gl3klaaSfqKn27hQ32haqquH1J8Z6kJw==",
|
||||
"dependencies": {
|
||||
"BouncyCastle.Cryptography": "2.5.0",
|
||||
"System.Formats.Asn1": "8.0.1",
|
||||
|
@ -180,11 +137,11 @@
|
|||
},
|
||||
"Minio": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.4, )",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "JckRL95hQ/eDHTQZ/BB7jeR0JyF+bOctMW6uriXHY5YPjCX61hiJGsswGjuDSEViKJEPxtPi3e4IwD/1TJ7PIw==",
|
||||
"requested": "[6.0.3, )",
|
||||
"resolved": "6.0.3",
|
||||
"contentHash": "WHlkouclHtiK/pIXPHcjVmbeELHPtElj2qRSopFVpSmsFhZXeM10sPvczrkSPePsmwuvZdFryJ/hJzKu3XeLVg==",
|
||||
"dependencies": {
|
||||
"CommunityToolkit.HighPerformance": "8.3.0",
|
||||
"CommunityToolkit.HighPerformance": "8.2.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.0",
|
||||
"System.IO.Hashing": "8.0.0",
|
||||
|
@ -199,39 +156,39 @@
|
|||
},
|
||||
"NodaTime": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.2.1, )",
|
||||
"resolved": "3.2.1",
|
||||
"contentHash": "D1aHhUfPQUxU2nfDCVuSLahpp0xCYZTmj/KNH3mSK/tStJYcx9HO9aJ0qbOP3hzjGPV/DXOqY2AHe27Nt4xs4g=="
|
||||
"requested": "[3.2.0, )",
|
||||
"resolved": "3.2.0",
|
||||
"contentHash": "yoRA3jEJn8NM0/rQm78zuDNPA3DonNSZdsorMUj+dltc1D+/Lc5h9YXGqbEEZozMGr37lAoYkcSM/KjTVqD0ow=="
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "cYdOGplIvr9KgsG8nJ8xnzBTImeircbgetlzS1OmepS5dAQW6PuGpVrLOKBNEwEvGYZPsV8037X5vZ/Dmpwz7Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)",
|
||||
"Npgsql": "9.0.3"
|
||||
"Microsoft.EntityFrameworkCore": "[9.0.0, 10.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[9.0.0, 10.0.0)",
|
||||
"Npgsql": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "QZ80CL3c9xzC83eVMWYWa1RcFZA6HJtpMAKFURlmz+1p0OyysSe8R6f/4sI9vk/nwqF6Fkw3lDgku/xH6HcJYg==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "+mfwiRCK+CAKTkeBZCuQuMaOwM/yMX8B65515PS1le9TUjlG8DobuAmb48MSR/Pr/YMvU1tV8FFEFlyQviQzrg==",
|
||||
"dependencies": {
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.4",
|
||||
"Npgsql.NodaTime": "9.0.3"
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.2",
|
||||
"Npgsql.NodaTime": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Npgsql.Json.NET": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "lN8p9UKkoXaGUhX3DHg/1W6YeEfbjQiQ7XrJSGREUoDHXOLxDQHJnZ49P/9P2s/pH6HTVgTgT5dijpKoRLN0vQ==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "E81dvvpNtS4WigxZu16OAFxVvPvbEkXI7vJXZzEp7GQ03MArF5V4HBb7KXDzTaE5ZQ0bhCUFoMTODC6Z8mu27g==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Npgsql": "9.0.3"
|
||||
"Npgsql": "9.0.2"
|
||||
}
|
||||
},
|
||||
"prometheus-net": {
|
||||
|
@ -255,24 +212,24 @@
|
|||
},
|
||||
"Roslynator.Analyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.13.1, )",
|
||||
"resolved": "4.13.1",
|
||||
"contentHash": "KZpLy6ZlCebMk+d/3I5KU2R7AOb4LNJ6tPJqPtvFXmO8bEBHQvCIAvJOnY2tu4C9/aVOROTDYUFADxFqw1gh/g=="
|
||||
"requested": "[4.12.9, )",
|
||||
"resolved": "4.12.9",
|
||||
"contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w=="
|
||||
},
|
||||
"Scalar.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.26, )",
|
||||
"resolved": "2.0.26",
|
||||
"contentHash": "0tKBFM7quBq0ifgRWo7eTTVpiTbnwpf/6ygtb/aYVuo0D2gMsYknAJRqEhH8HFBqzntNiYpzHbQSf2b+VAA8sA=="
|
||||
"requested": "[1.2.55, )",
|
||||
"resolved": "1.2.55",
|
||||
"contentHash": "zArlr6nfPQMRwyia0WFirsyczQby51GhNgWITiEIRkot+CVGZSGQ4oWGqExO11/6x26G+mcQo9Oft1mGpN0/ZQ=="
|
||||
},
|
||||
"Sentry.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.3.0, )",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zC2yhwQB0laYWGXLYDCsiKSIqleaEK3fUH9Z5t8Bgvfs2nGX0mHmh9oPqNAAbkVGvni56mhgHHCBxN/kpfkawA==",
|
||||
"requested": "[4.13.0, )",
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "1cH9hSvjRbTkcpjUejFTrTC3jMIiOrcZ0DIvt16+AYqXhuxPEnI56npR1nhv+7WUGyhyp5cHFIZqrKnyrrGP0w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Sentry.Extensions.Logging": "5.3.0"
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Sentry.Extensions.Logging": "4.13.0"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
|
@ -307,35 +264,25 @@
|
|||
},
|
||||
"Serilog.Sinks.Seq": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "z5ig56/qzjkX6Fj4U/9m1g8HQaQiYPMZS4Uevtjg1I+WWzoGSf5t/E+6JbMP/jbZYhU63bA5NJN5y0x+qqx2Bw==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.2.0",
|
||||
"Serilog.Sinks.File": "6.0.0"
|
||||
"Serilog": "4.0.0",
|
||||
"Serilog.Sinks.File": "5.0.0"
|
||||
}
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.7, )",
|
||||
"resolved": "3.1.7",
|
||||
"contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA=="
|
||||
},
|
||||
"StackExchange.Redis": {
|
||||
"type": "Direct",
|
||||
"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"
|
||||
}
|
||||
"requested": "[3.1.6, )",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA=="
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "4TY2Yokh5Xp8XHFhsY9y84yokS7B0rhkaZCXuRiKppIiKwPVH4lVSFD9EEFzRpXdBM5ZeZXD43tc2vB6njEwwQ=="
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A=="
|
||||
},
|
||||
"System.Text.RegularExpressions": {
|
||||
"type": "Direct",
|
||||
|
@ -346,12 +293,6 @@
|
|||
"System.Runtime": "4.3.1"
|
||||
}
|
||||
},
|
||||
"Yort.Xid.Net": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.1, )",
|
||||
"resolved": "2.0.1",
|
||||
"contentHash": "+3sNX7/RKSKheVuMz9jtWLazD+R4PXpx8va2d9SdDgvKOhETbEb0VYis8K/fD1qm/qOQT57LadToSpzReGMZlw=="
|
||||
},
|
||||
"BouncyCastle.Cryptography": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.0",
|
||||
|
@ -359,8 +300,8 @@
|
|||
},
|
||||
"CommunityToolkit.HighPerformance": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.0",
|
||||
"contentHash": "2zc0Wfr9OtEbLqm6J1Jycim/nKmYv+v12CytJ3tZGNzw7n3yjh1vNCMX0kIBaFBk3sw8g0pMR86QJGXGlArC+A=="
|
||||
"resolved": "8.2.2",
|
||||
"contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw=="
|
||||
},
|
||||
"EntityFrameworkCore.Exceptions.Common": {
|
||||
"type": "Transitive",
|
||||
|
@ -370,46 +311,18 @@
|
|||
"Microsoft.EntityFrameworkCore.Relational": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Hangfire.AspNetCore": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.8.18",
|
||||
"contentHash": "5D6Do0qgoAnakvh4KnKwhIoUzFU84Z0sCYMB+Sit+ygkpL1P6JGYDcd/9vDBcfr5K3JqBxD4Zh2IK2LOXuuiaw==",
|
||||
"dependencies": {
|
||||
"Hangfire.NetCore": "[1.8.18]"
|
||||
}
|
||||
},
|
||||
"Hangfire.NetCore": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.8.18",
|
||||
"contentHash": "3KAV9AZ1nqQHC54qR4buNEEKRmQJfq+lODtZxUk5cdi68lV8+9K2f4H1/mIfDlPpgjPFjEfCobNoi2+TIpKySw==",
|
||||
"dependencies": {
|
||||
"Hangfire.Core": "[1.8.18]",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "3.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "3.0.0"
|
||||
}
|
||||
},
|
||||
"Hangfire.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.8.18",
|
||||
"contentHash": "yBfI2ygYfN/31rOrahfOFHee1mwTrG0ppsmK9awCS0mAr2GEaB9eyYqg/lURgZy8AA8UVJVs5nLHa2hc1pDAVQ==",
|
||||
"dependencies": {
|
||||
"Hangfire.Core": "[1.8.18]"
|
||||
}
|
||||
},
|
||||
"MailKit": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.8.0",
|
||||
"contentHash": "zZ1UoM4FUnSFUJ9fTl5CEEaejR0DNP6+FDt1OfXnjg4igZntcir1tg/8Ufd6WY5vrpmvToAjluYqjVM24A+5lA==",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "jVmB3Nr0JpqhyMiXOGWMin+QvRKpucGpSFBCav9dG6jEJPdBV+yp1RHVpKzxZPfT+0adaBuZlMFdbIciZo1EWA==",
|
||||
"dependencies": {
|
||||
"MimeKit": "4.8.0",
|
||||
"System.Formats.Asn1": "8.0.1"
|
||||
"MimeKit": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.JsonPatch": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "bZMRhazEBgw9aZ5EBGYt0017CSd+aecsUCnppVjSa1SzWH6C1ieTSQZRAe+H0DzAVzWAoK7HLwKnQUPioopPrA==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "/4UONYoAIeexPoAmbzBPkVGA6KAY7t0BM+1sr0fKss2V1ERCdcM+Llub4X5Ma+LJ60oPp6KzM0e3j+Pp/JHCNw==",
|
||||
"dependencies": {
|
||||
"Microsoft.CSharp": "4.7.0",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
|
@ -417,27 +330,27 @@
|
|||
},
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "KFHRhrGAnd80310lpuWzI7Cf+GidS/h3JaPDFFnSmSGjCxB5vkBv5E+TXclJCJhqPtgNxg+keTC5SF1T9ieG5w==",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "trwJhFrTQuJTImmixMsDnDgRE8zuTzAUAot7WqiUlmjNzlJWLOaXXBpeA/xfNJvZuOsyGjC7RIzEyNyDGhDTLg==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.36",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.36"
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.27",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.27"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "0OG/wNedsQ9kTMrFuvrUDoJvp6Fxj6BzWgi7AUCluOENxu/0PzbjY9AC5w6mZJ22/AFxn2gFc2m0yOBTfQbiPg==",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "C6Gh/sAuUACxNtllcH4ZniWtPcGbixJuB1L5RXwoUe1a1wM6rpQ2TVMWpX2+cgeBj8U/izJyWY+nJ4Lz8mmMKA==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.36",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.36",
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.2"
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.27",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.27",
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Razor.Language": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA=="
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "bI1kIZBgx7oJIB7utPrw4xIgcj7Pdx1jnHMTdsG54U602OcGpBzbfAuKaWs+LVdj+zZVuZsCSoRIZNJKTDP7Hw=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
|
@ -491,10 +404,10 @@
|
|||
},
|
||||
"Microsoft.CodeAnalysis.Razor": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "RTLNJglWezr/1IkiWdtDpPYW7X7lwa4ow8E35cHt+sWdWxOnl+ayQqMy1RfbaLp7CLmRmgXSzMMZZU3D4vZi9Q==",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "NAUvSjH8QY8gPp/fXjHhi3MnQEGtSJA0iRT/dT3RKO3AdGACPJyGmKEKxLag9+Kf2On51yGHT9DEPPnK3hyezg==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.36",
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.27",
|
||||
"Microsoft.CodeAnalysis.CSharp": "4.0.0",
|
||||
"Microsoft.CodeAnalysis.Common": "4.0.0"
|
||||
}
|
||||
|
@ -530,274 +443,191 @@
|
|||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "oVSjNSIYHsk0N66eqAWgDcyo9etEFbUswbz7SmlYR6nGp05byHrJAYM5N8U2aGWJWJI6WvIC2e4TXJgH6GZ6HQ=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "fnmifFL8KaA4ZNLCVgfjCWhZUFxkrDInx5hR4qG7Q8IEaSiy/6VOSRFyx55oH7MV4y7wM3J3EE90nSpcVBI44Q=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "w4jzX7XI+L3erVGzbHXpx64A3QaLXxqG3f1vPpGYYZGpxOIHkh7e4iLLD7cq4Ng1vjkwzWl5ZJp0Kj/nHsgFYg=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "Qje+DzXJOKiXF72SL0XxNlDtTkvWWvmwknuZtFahY5hIQpRKO59qnGuERIQ3qlzuq5x4bAJ8WMbgU5DLhBgeOQ=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "r7O4N5uaM95InVSGUj7SMOQWN0f1PBF2Y30ow7Jg+pGX5GJCRVd/1fq83lQ50YMyq+EzyHac5o4CDQA2RsjKJQ==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "j+msw6fWgAE9M3Q/5B9Uhv7pdAdAQUvFPJAiBJmoy+OXvehVbfbCE8ftMAa51Uo2ZeiqVnHShhnv4Y4UJJmUzA==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "9.0.2",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.AmbientMetadata.Application": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "GMCX3zybUB22aAADjYPXrWhhd1HNMkcY5EcFAJnXy/4k5pPpJ6TS4VRl37xfrtosNyzbpO2SI7pd2Q5PvggSdg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
"Microsoft.EntityFrameworkCore": "9.0.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "a7QhA25n+BzSM5r5d7JznfyluMBGI7z3qyLlFviZ1Eiqv6DdiK27sLZdP/rpYirBM6UYAKxu5TbmfhIy13GN9A==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==",
|
||||
"dependencies": {
|
||||
"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.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "I0O/270E/lUNqbBxlRVjxKOMZyYjP88dpEgQTveml+h2lTzAP4vbawLVwjS9SC7lKaU893bwyyNz0IVJYsm9EA==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"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"
|
||||
}
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "3ImbcbS68jy9sKr9Z9ToRbEEX0bvIRdb8zyf5ebtL9Av2CUCGHvaO5wsSXfRfAjr60Vrq0tlmNji9IzAxW6EOw=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA=="
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
|
||||
"dependencies": {
|
||||
"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.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==",
|
||||
"dependencies": {
|
||||
"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.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.Http": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==",
|
||||
"dependencies": {
|
||||
"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.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics": "8.0.0",
|
||||
"Microsoft.Extensions.Logging": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "loV/0UNpt2bD+6kCDzFALVE63CDtqzPeC0LAetkdhiEr/tTNbvOlQ7CBResH7BQBd3cikrwiBfaHdyHMFUlc2g==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "dV9s2Lamc8jSaqhl2BQSPn/AryDIH2sSbQUyLitLXV0ROmsb+SROnn2cH939JFbsNrnf3mIM3GNRKT7P0ldwLg==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==",
|
||||
"dependencies": {
|
||||
"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.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Logging": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw=="
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "zr98z+AN8+isdmDmQRuEJ/DAKZGUTHmdv3t0ZzjHvNqvA44nAgkXE9kYtfoN6581iALChhVaSw2Owt+Z2lVbkQ==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
||||
"dependencies": {
|
||||
"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.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"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"
|
||||
}
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
|
@ -832,67 +662,35 @@
|
|||
},
|
||||
"Npgsql": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "hCbO8box7i/XXiTFqCJ3GoowyLqx3JXxyrbOJ6om7dr+eAknvBNhhUHeJVGAQo44sySZTfdVffp4BrtPeLZOAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Npgsql.NodaTime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "PMWXCft/iw+5A7eCeMcy6YZXBst6oeisbCkv2JMQVG4SAFa5vQaf6K2voXzUJCqzwOFcCWs+oT42w2uMDFpchw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "jURb6VGmmR3pPae2N3HrUixSZ/U5ovqZgg/qo3m5Rq/q0m2fpxbZcsHZo21s5MLa/AfJAx4hcFMY98D4RtLdcg==",
|
||||
"dependencies": {
|
||||
"NodaTime": "3.2.0",
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.8",
|
||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
||||
"dependencies": {
|
||||
"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"
|
||||
"Npgsql": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Sentry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zlBIP7YmYxySwcgapLMj1gdxPEz9rwdrOa4Yjub/TzcAaMQXusRH9hY4CE6pu0EIibZ7C7Hhjhr6xOTlyK8gFQ=="
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg=="
|
||||
},
|
||||
"Sentry.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "DPN6NXvO4LTH21UM2gUFJwSwVa/fuT3B/UZmQyfSfecqViXrZO7WFuKz/h592YUoGNCumyt8x045bxbz6j9btg==",
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "yZ5+TtJKWcss6cG17YjnovImx4X56T8O6Qy6bsMC8tMDttYy8J7HJ2F+WdaZNyjOCo0Rfi6N2gc+Clv/5pf+TQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Microsoft.Extensions.Http": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "9.0.0",
|
||||
"Sentry": "5.3.0"
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.Http": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "8.0.0",
|
||||
"Sentry": "4.13.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Extensions.Hosting": {
|
||||
|
@ -1020,8 +818,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ=="
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
|
@ -1059,11 +857,6 @@
|
|||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
Foxnouns.Backend/static-pages/.gitignore
vendored
2
Foxnouns.Backend/static-pages/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -12,9 +12,9 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.3"/>
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="9.0.3"/>
|
||||
<PackageReference Include="Dapper" Version="2.1.35"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.2"/>
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="9.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using Dapper;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Foxnouns.DataMigrator.Models;
|
||||
using NodaTime.Extensions;
|
||||
using Npgsql;
|
||||
|
@ -39,7 +39,6 @@ public class UserMigrator(
|
|||
_user = new User
|
||||
{
|
||||
Id = goUser.SnowflakeId,
|
||||
LegacyId = goUser.Id,
|
||||
Username = goUser.Username,
|
||||
DisplayName = goUser.DisplayName,
|
||||
Bio = goUser.Bio,
|
||||
|
@ -140,7 +139,6 @@ public class UserMigrator(
|
|||
new PrideFlag
|
||||
{
|
||||
Id = flag.SnowflakeId,
|
||||
LegacyId = flag.Id,
|
||||
UserId = _user!.Id,
|
||||
Hash = flag.Hash,
|
||||
Name = flag.Name,
|
||||
|
@ -192,7 +190,6 @@ public class UserMigrator(
|
|||
UserId = _user!.Id,
|
||||
Name = goMember.Name,
|
||||
Sid = goMember.Sid,
|
||||
LegacyId = goMember.Id,
|
||||
DisplayName = goMember.DisplayName,
|
||||
Bio = goMember.Bio,
|
||||
Avatar = goMember.Avatar,
|
||||
|
@ -238,7 +235,6 @@ public class UserMigrator(
|
|||
"small" => PreferenceSize.Small,
|
||||
_ => PreferenceSize.Normal,
|
||||
},
|
||||
LegacyId = new Guid(id),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -260,6 +256,6 @@ public class UserMigrator(
|
|||
{
|
||||
if (_preferenceIds.TryGetValue(id, out Snowflake preferenceId))
|
||||
return preferenceId.ToString();
|
||||
return ValidationService.DefaultStatusOptions.Contains(id) ? id : "okay";
|
||||
return ValidationUtils.DefaultStatusOptions.Contains(id) ? id : "okay";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,7 @@
|
|||
# Example .env file--DO NOT EDIT, copy to .env or .env.local then edit
|
||||
|
||||
# The language the frontend will use. Valid languages are listed in src/lib/i18n/index.ts.
|
||||
# Example .env file--DO NOT EDIT
|
||||
PUBLIC_LANGUAGE=en
|
||||
# The public base URL, i.e. the one users will see. Used for building links.
|
||||
PUBLIC_BASE_URL=https://pronouns.cc
|
||||
# The base URL for the URL shortener service. Used for building short links.
|
||||
PUBLIC_SHORT_URL=https://prns.cc
|
||||
# The base public URL for the API. This is (almost) always the public base URL + /api.
|
||||
PUBLIC_API_BASE=https://pronouns.cc/api
|
||||
# The base *private* URL for the API's rate limiter proxy. The frontend will rewrite API URLs to use this.
|
||||
# In development, you can set this to the same value as $PRIVATE_INTERNAL_API_HOST, but be aware that this will disable rate limiting.
|
||||
PRIVATE_API_HOST=http://localhost:5003/api
|
||||
# The base private URL for the API, which bypasses the rate limiter. Used for /api/internal paths and unauthenticated GET requests.
|
||||
PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api
|
||||
|
||||
# The Sentry URL to use. Optional.
|
||||
PRIVATE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM docker.io/node:23-slim
|
||||
FROM docker.io/node:22-slim
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
|
|
@ -13,15 +13,14 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.10",
|
||||
"@sveltejs/kit": "^2.12.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@sveltestrap/sveltestrap": "^7.1.0",
|
||||
"@sveltejs/kit": "^2.11.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@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",
|
||||
|
@ -29,19 +28,17 @@
|
|||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"sass": "^1.83.0",
|
||||
"svelte": "^5.14.3",
|
||||
"svelte": "^5.13.0",
|
||||
"svelte-bootstrap-icons": "^3.1.1",
|
||||
"svelte-check": "^4.1.1",
|
||||
"svelte-easy-crop": "^4.0.0",
|
||||
"sveltekit-i18n": "^2.4.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"vite": "^6.0.3"
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
|
||||
"dependencies": {
|
||||
"@fontsource/firago": "^5.1.0",
|
||||
"@sentry/sveltekit": "^8.52.0",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"luxon": "^3.5.0",
|
||||
|
|
2068
Foxnouns.Frontend/pnpm-lock.yaml
generated
2068
Foxnouns.Frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
10
Foxnouns.Frontend/src/app.d.ts
vendored
10
Foxnouns.Frontend/src/app.d.ts
vendored
|
@ -1,17 +1,7 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
|
||||
import type { ErrorCode } from "$api/error";
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Error {
|
||||
message: string;
|
||||
status: number;
|
||||
code: ErrorCode;
|
||||
errors?: Array<{ key: string; errors: ValidationError[] }>;
|
||||
error_id?: string;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
|
|
|
@ -64,11 +64,3 @@
|
|||
max-width: 200px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.big-footer {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: bootstrap.shade-color(bootstrap.$dark, 20%);
|
||||
}
|
||||
|
||||
background-color: bootstrap.shade-color(bootstrap.$light, 5%);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import ApiError, { ErrorCode } from "$api/error";
|
||||
import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { PUBLIC_API_BASE } from "$env/static/public";
|
||||
import log from "$lib/log";
|
||||
import type { HandleFetch, HandleServerError } from "@sveltejs/kit";
|
||||
import * as Sentry from "@sentry/sveltekit";
|
||||
import type { HandleFetch } from "@sveltejs/kit";
|
||||
|
||||
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
||||
if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) {
|
||||
|
@ -15,33 +11,3 @@ export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
|||
|
||||
return await fetch(request);
|
||||
};
|
||||
|
||||
Sentry.init({
|
||||
dsn: env.PRIVATE_SENTRY_DSN,
|
||||
});
|
||||
|
||||
export const handleError: HandleServerError = async ({ error, status, message }) => {
|
||||
if (error instanceof ApiError) {
|
||||
return {
|
||||
status: error.raw?.status || status,
|
||||
message: error.raw?.message || "Unknown error",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 400 && status <= 499) {
|
||||
return { status, message, code: ErrorCode.GenericApiError };
|
||||
}
|
||||
|
||||
// client errors and backend API errors just clog up sentry, so we don't send those.
|
||||
const id = Sentry.captureException(error, {
|
||||
mechanism: {
|
||||
type: "sveltekit",
|
||||
handled: false,
|
||||
},
|
||||
});
|
||||
|
||||
log.error("[%s] error in handler:", id, error);
|
||||
|
||||
return { error_id: id, status, message, code: ErrorCode.InternalServerError };
|
||||
};
|
||||
|
|
|
@ -4,12 +4,10 @@ import type { AddAccountResponse, CallbackResponse } from "$api/models";
|
|||
import { setToken } from "$lib";
|
||||
import log from "$lib/log";
|
||||
import { isRedirect, redirect, type ServerLoadEvent } from "@sveltejs/kit";
|
||||
import type { TicketData } from "../../routes/auth/callback/register/[ticket]/+page.server";
|
||||
|
||||
export default function createCallbackLoader(
|
||||
callbackType: string,
|
||||
bodyFn?: (event: ServerLoadEvent) => Promise<unknown>,
|
||||
returnData?: boolean,
|
||||
) {
|
||||
return async (event: ServerLoadEvent) => {
|
||||
const { parent, fetch, cookies } = event;
|
||||
|
@ -55,23 +53,12 @@ export default function createCallbackLoader(
|
|||
redirect(303, `/@${resp.user!.username}`);
|
||||
}
|
||||
|
||||
if (returnData)
|
||||
return {
|
||||
hasAccount: false,
|
||||
isLinkRequest: false,
|
||||
ticket: resp.ticket!,
|
||||
remoteUser: resp.remote_username!,
|
||||
};
|
||||
|
||||
const ticket = btoa(
|
||||
JSON.stringify({
|
||||
type: callbackType,
|
||||
ticket: resp.ticket!,
|
||||
remoteUsername: resp.remote_username!,
|
||||
} satisfies TicketData),
|
||||
)
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_");
|
||||
|
||||
redirect(303, "/auth/callback/register/" + ticket);
|
||||
} catch (e) {
|
||||
if (isRedirect(e)) throw e;
|
||||
if (e instanceof ApiError) return { isLinkRequest: false, error: e.obj };
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||
import type { AuditLogEntry, ClearableField } from "$api/models/moderation";
|
||||
import log from "$lib/log";
|
||||
import { type RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
type ModactionResponse = { ok: boolean; resp: AuditLogEntry | null; error: RawApiError | null };
|
||||
type ModactionFunction = (evt: RequestEvent) => Promise<ModactionResponse>;
|
||||
|
||||
export default function createModactionAction(
|
||||
type: "ignore" | "warn" | "suspend",
|
||||
requireReason: boolean,
|
||||
): ModactionFunction {
|
||||
return async function ({ request, fetch, cookies }) {
|
||||
const body = await request.formData();
|
||||
const userId = body.get("user") as string;
|
||||
const memberId = body.get("member") as string | null;
|
||||
const reportId = body.get("report") as string | null;
|
||||
const reason = body.get("reason") as string | null;
|
||||
|
||||
if (!reportId && type === "ignore") {
|
||||
return {
|
||||
ok: false,
|
||||
resp: null,
|
||||
error: {
|
||||
status: 400,
|
||||
message: "Bad request",
|
||||
code: ErrorCode.BadRequest,
|
||||
errors: [
|
||||
{ key: "report", errors: [{ message: "Ignoring a report requires a report ID" }] },
|
||||
],
|
||||
} satisfies RawApiError,
|
||||
};
|
||||
}
|
||||
|
||||
if (!reason && requireReason) {
|
||||
return {
|
||||
ok: false,
|
||||
resp: null,
|
||||
error: {
|
||||
status: 400,
|
||||
message: "Bad request",
|
||||
code: ErrorCode.BadRequest,
|
||||
errors: [{ key: "reason", errors: [{ message: "You must give a reason" }] }],
|
||||
} satisfies RawApiError,
|
||||
};
|
||||
}
|
||||
|
||||
let clearFields: ClearableField[] | undefined = undefined;
|
||||
if (type === "warn") {
|
||||
clearFields = body.getAll("clear-fields") as ClearableField[];
|
||||
}
|
||||
|
||||
let path: string;
|
||||
if (type === "warn") path = `/moderation/warnings/${userId}`;
|
||||
else if (type === "suspend") path = `/moderation/suspensions/${userId}`;
|
||||
else path = `/moderation/reports/${reportId}/ignore`;
|
||||
|
||||
try {
|
||||
const resp = await apiRequest<AuditLogEntry>("POST", path, {
|
||||
fetch,
|
||||
cookies,
|
||||
body: {
|
||||
reason: reason,
|
||||
// These are ignored by POST /reports/{id}/ignore
|
||||
member_id: memberId,
|
||||
report_id: reportId,
|
||||
// This is ignored by everything but POST /warnings/{id}
|
||||
clear_fields: clearFields,
|
||||
// This is ignored by everything but POST /suspensions/{id}
|
||||
clear_profile: !!body.get("clear-profile"),
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true, resp, error: null };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) return { ok: false, error: e.obj, resp: null };
|
||||
log.error("could not take action on %s:", path, e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createModactions() {
|
||||
return {
|
||||
ignore: createModactionAction("ignore", false),
|
||||
warn: createModactionAction("warn", true),
|
||||
suspend: createModactionAction("suspend", true),
|
||||
};
|
||||
}
|
|
@ -3,23 +3,10 @@ import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
|||
import type { AuthResponse } from "$api/models/auth";
|
||||
import { setToken } from "$lib";
|
||||
import log from "$lib/log";
|
||||
import { isRedirect, redirect } from "@sveltejs/kit";
|
||||
|
||||
export type TicketData = {
|
||||
type: string;
|
||||
ticket: string;
|
||||
remoteUsername: string;
|
||||
};
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const data = JSON.parse(atob(params.ticket)) as TicketData;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, fetch, cookies, params }) => {
|
||||
const type = (JSON.parse(atob(params.ticket)) as TicketData).type;
|
||||
import { isRedirect, redirect, type RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
export default function createRegisterAction(callbackUrl: string) {
|
||||
return async function ({ request, fetch, cookies }: RequestEvent) {
|
||||
const data = await request.formData();
|
||||
const username = data.get("username") as string | null;
|
||||
const ticket = data.get("ticket") as string | null;
|
||||
|
@ -30,7 +17,7 @@ export const actions = {
|
|||
};
|
||||
|
||||
try {
|
||||
const resp = await apiRequest<AuthResponse>("POST", `/auth/${type}/register`, {
|
||||
const resp = await apiRequest<AuthResponse>("POST", callbackUrl, {
|
||||
body: { username, ticket },
|
||||
isInternal: true,
|
||||
fetch,
|
||||
|
@ -44,5 +31,5 @@ export const actions = {
|
|||
if (e instanceof ApiError) return { error: e.obj };
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -14,7 +14,6 @@ export default class ApiError {
|
|||
|
||||
toObject(): RawApiError {
|
||||
return {
|
||||
error_id: this.raw?.error_id,
|
||||
status: this.raw?.status || 500,
|
||||
code: this.code,
|
||||
message: this.raw?.message || "Internal server error",
|
||||
|
@ -24,7 +23,6 @@ export default class ApiError {
|
|||
}
|
||||
|
||||
export type RawApiError = {
|
||||
error_id?: string;
|
||||
status: number;
|
||||
message: string;
|
||||
code: ErrorCode;
|
||||
|
@ -43,7 +41,6 @@ export enum ErrorCode {
|
|||
MemberNotFound = "MEMBER_NOT_FOUND",
|
||||
AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED",
|
||||
LastAuthMethod = "LAST_AUTH_METHOD",
|
||||
PageNotFound = "PAGE_NOT_FOUND",
|
||||
// This code isn't actually returned by the API
|
||||
Non204Response = "(non 204 response)",
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|||
/**
|
||||
* Optional arguments for a request. `load` and `action` functions should always pass `fetch` and `cookies`.
|
||||
*/
|
||||
export type RequestArgs<T> = {
|
||||
export type RequestArgs = {
|
||||
/**
|
||||
* The token for this request. Where possible, `cookies` should be passed instead.
|
||||
* Will override `cookies` if both are passed.
|
||||
|
@ -23,7 +23,7 @@ export type RequestArgs<T> = {
|
|||
/**
|
||||
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
|
||||
*/
|
||||
body?: T;
|
||||
body?: unknown;
|
||||
/**
|
||||
* The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests.
|
||||
*/
|
||||
|
@ -41,10 +41,10 @@ export type RequestArgs<T> = {
|
|||
* @param args Optional arguments to the request function.
|
||||
* @returns A Response object.
|
||||
*/
|
||||
export async function baseRequest<T = unknown>(
|
||||
export async function baseRequest(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<T> = {},
|
||||
args: RequestArgs = {},
|
||||
): Promise<Response> {
|
||||
const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME);
|
||||
|
||||
|
@ -72,20 +72,19 @@ export async function baseRequest<T = unknown>(
|
|||
* @param args Optional arguments to the request function.
|
||||
* @returns The response deserialized as `T`.
|
||||
*/
|
||||
export async function apiRequest<TResponse, TRequest = unknown>(
|
||||
export async function apiRequest<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<TRequest> = {},
|
||||
): Promise<TResponse> {
|
||||
args: RequestArgs = {},
|
||||
): Promise<T> {
|
||||
const resp = await baseRequest(method, path, args);
|
||||
|
||||
if (resp.status < 200 || resp.status > 299) {
|
||||
const err = await resp.json();
|
||||
log.error("Received error for request to %s %s:", method, path, err);
|
||||
if ("code" in err) throw new ApiError(err);
|
||||
else throw new ApiError();
|
||||
}
|
||||
return (await resp.json()) as TResponse;
|
||||
return (await resp.json()) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,10 +94,10 @@ export async function apiRequest<TResponse, TRequest = unknown>(
|
|||
* @param args Optional arguments to the request function.
|
||||
* @param enforce204 Whether to throw an error on a non-204 status code.
|
||||
*/
|
||||
export async function fastRequest<T = unknown>(
|
||||
export async function fastRequest(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<T> = {},
|
||||
args: RequestArgs = {},
|
||||
enforce204: boolean = false,
|
||||
): Promise<void> {
|
||||
const resp = await baseRequest(method, path, args);
|
||||
|
|
|
@ -10,7 +10,6 @@ export type Meta = {
|
|||
};
|
||||
members: number;
|
||||
limits: Limits;
|
||||
notice: { id: string; message: string } | null;
|
||||
};
|
||||
|
||||
export type Limits = {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue