Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
b0431ff962 | |||
b07f4b75c0 | |||
22be49976a | |||
3527acb8ba | |||
978b8e100e | |||
f00f5b400e | |||
f5f0416346 | |||
5d452824cd | |||
bba322bd22 | |||
200e648772 | |||
790b39f730 | |||
7d0df67c06 | |||
dd9d35249c | |||
f99d10ecf0 | |||
7759225428 | |||
cd24196cd1 | |||
7d6d4631b8 | |||
a248536789 | |||
218c756a70 | |||
7ea6c62d67 | |||
64ea25e89e | |||
f1f777ff82 | |||
a72c0f41c3 | |||
6fe816404f |
92 changed files with 2639 additions and 810 deletions
|
@ -7,7 +7,7 @@ resharper_not_accessed_positional_property_local_highlighting = none
|
|||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_multiline_binary_expressions_chain = false
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,3 +14,6 @@ docker/proxy-config.json
|
|||
docker/frontend.env
|
||||
|
||||
Foxnouns.DataMigrator/apps.json
|
||||
|
||||
out/
|
||||
build/
|
||||
|
|
27
DOCKER.md
27
DOCKER.md
|
@ -1,10 +1,29 @@
|
|||
# Running with Docker
|
||||
# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)*
|
||||
|
||||
Because SvelteKit is a pain in the ass to build in a container, and processes secrets at build time,
|
||||
there is no pre-built frontend image available.
|
||||
If you don't want to build images on your server, I recommend running the frontend outside of Docker.
|
||||
This is preconfigured in `docker-compose.prebuilt.yml`: the backend, database, and rate limiter will run in Docker,
|
||||
while the frontend is run as a normal, non-containerized service.
|
||||
|
||||
1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking.
|
||||
2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same.
|
||||
3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do th esame.
|
||||
4. Build with `docker compose build`
|
||||
5. Run with `docker compose up`
|
||||
3. Run with `docker compose up -f docker-compose.prebuilt.yml`
|
||||
|
||||
The backend will listen on port 5001 and metrics will be available on port 5002.
|
||||
The rate limiter (which is what should be exposed to the outside) will listen on port 5003.
|
||||
You can use `docker/Caddyfile` as an example for your reverse proxy. If you use nginx, good luck.
|
||||
|
||||
# Running with Docker (local builds)
|
||||
|
||||
In order to run *everything* in Docker, you'll have to build every container yourself.
|
||||
The advantage of this is that it's an all-in-one solution, where you only have to point your reverse proxy at a single container.
|
||||
The disadvantage is that you'll likely have to build the images on the server you'll be running them on.
|
||||
|
||||
1. Configure the backend and rate limiter as in the section above.
|
||||
2. Copy `docker/frontend.example.env` to `docker/frontend.env`, and configure it.
|
||||
3. Build with `docker compose build -f docker-compose.local.yml`
|
||||
4. Run with `docker compose up -f docker-compose.local.yml`
|
||||
|
||||
The Caddy server will listen on `localhost:5004` for the frontend and API,
|
||||
and on `localhost:5005` for the profile URL shortener.
|
||||
|
|
|
@ -26,7 +26,6 @@ public class Config
|
|||
public string MediaBaseUrl { get; init; } = null!;
|
||||
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}";
|
||||
|
||||
public LoggingConfig Logging { get; init; } = new();
|
||||
public DatabaseConfig Database { get; init; } = new();
|
||||
|
@ -55,6 +54,7 @@ public class Config
|
|||
public bool? EnablePooling { get; init; }
|
||||
public int? Timeout { get; init; }
|
||||
public int? MaxPoolSize { get; init; }
|
||||
public string Redis { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public class StorageConfig
|
||||
|
@ -99,6 +99,11 @@ public class Config
|
|||
{
|
||||
public int MaxMemberCount { get; init; } = 1000;
|
||||
|
||||
public int MaxFields { get; init; } = 25;
|
||||
public int MaxFieldNameLength { get; init; } = 100;
|
||||
public int MaxFieldEntryTextLength { get; init; } = 100;
|
||||
public int MaxFieldEntries { get; init; } = 100;
|
||||
|
||||
public int MaxUsernameLength { get; init; } = 40;
|
||||
public int MaxMemberNameLength { get; init; } = 100;
|
||||
public int MaxDisplayNameLength { get; init; } = 100;
|
||||
|
|
|
@ -46,7 +46,7 @@ public class AuthController(
|
|||
config.GoogleAuth.Enabled,
|
||||
config.TumblrAuth.Enabled
|
||||
);
|
||||
string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct));
|
||||
string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());
|
||||
string? discord = null;
|
||||
string? google = null;
|
||||
string? tumblr = null;
|
||||
|
|
|
@ -56,7 +56,7 @@ public class EmailAuthController(
|
|||
if (!req.Email.Contains('@'))
|
||||
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
|
||||
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct);
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null);
|
||||
|
||||
// If there's already a user with that email address, pretend we sent an email but actually ignore it
|
||||
if (
|
||||
|
|
|
@ -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 Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
|
@ -28,13 +27,8 @@ namespace Foxnouns.Backend.Controllers;
|
|||
[Authorize("identify")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ExportsController(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
IClock clock,
|
||||
DatabaseContext db,
|
||||
IQueue queue
|
||||
) : ApiControllerBase
|
||||
public class ExportsController(ILogger logger, Config config, IClock clock, DatabaseContext db)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private static readonly Duration MinimumTimeBetween = Duration.FromDays(1);
|
||||
private readonly ILogger _logger = logger.ForContext<ExportsController>();
|
||||
|
@ -80,10 +74,7 @@ public class ExportsController(
|
|||
throw new ApiError.BadRequest("You can't request a new data export so soon.");
|
||||
}
|
||||
|
||||
queue.QueueInvocableWithPayload<CreateDataExportInvocable, CreateDataExportPayload>(
|
||||
new CreateDataExportPayload(CurrentUser.Id)
|
||||
);
|
||||
|
||||
CreateDataExportJob.Enqueue(CurrentUser.Id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
|
@ -30,8 +29,7 @@ namespace Foxnouns.Backend.Controllers;
|
|||
public class FlagsController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IQueue queue
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
|
@ -74,10 +72,7 @@ public class FlagsController(
|
|||
db.Add(flag);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
queue.QueueInvocableWithPayload<CreateFlagInvocable, CreateFlagPayload>(
|
||||
new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image)
|
||||
);
|
||||
|
||||
CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image));
|
||||
return Accepted(userRenderer.RenderPrideFlag(flag));
|
||||
}
|
||||
|
||||
|
|
|
@ -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 Coravel.Queuing.Interfaces;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
@ -37,7 +36,6 @@ public class MembersController(
|
|||
MemberRendererService memberRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ObjectStorageService objectStorageService,
|
||||
IQueue queue,
|
||||
IClock clock,
|
||||
ValidationService validationService,
|
||||
Config config
|
||||
|
@ -81,13 +79,13 @@ public class MembersController(
|
|||
("display_name", validationService.ValidateDisplayName(req.DisplayName)),
|
||||
("bio", validationService.ValidateBio(req.Bio)),
|
||||
("avatar", validationService.ValidateAvatar(req.Avatar)),
|
||||
.. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||
.. ValidationUtils.ValidateFieldEntries(
|
||||
.. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||
.. validationService.ValidateFieldEntries(
|
||||
req.Names?.ToArray(),
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
),
|
||||
.. ValidationUtils.ValidatePronouns(
|
||||
.. validationService.ValidatePronouns(
|
||||
req.Pronouns?.ToArray(),
|
||||
CurrentUser!.CustomPreferences
|
||||
),
|
||||
|
@ -123,6 +121,9 @@ public class MembersController(
|
|||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -139,9 +140,7 @@ public class MembersController(
|
|||
|
||||
if (req.Avatar != null)
|
||||
{
|
||||
queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(member.Id, req.Avatar)
|
||||
);
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
}
|
||||
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
|
@ -191,7 +190,7 @@ public class MembersController(
|
|||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidateFieldEntries(
|
||||
validationService.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
|
@ -203,7 +202,7 @@ public class MembersController(
|
|||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
member.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
@ -211,7 +210,10 @@ public class MembersController(
|
|||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
);
|
||||
member.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
@ -236,11 +238,12 @@ public class MembersController(
|
|||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(member.Id, req.Avatar)
|
||||
);
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
}
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
|
|
|
@ -13,20 +13,23 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Text.RegularExpressions;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Services.Caching;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/meta")]
|
||||
public partial class MetaController(Config config) : ApiControllerBase
|
||||
public partial class MetaController(Config config, NoticeCacheService noticeCache)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
|
||||
public IActionResult GetMeta() =>
|
||||
public async Task<IActionResult> GetMeta(CancellationToken ct = default) =>
|
||||
Ok(
|
||||
new MetaResponse(
|
||||
Repository,
|
||||
|
@ -45,10 +48,14 @@ public partial class MetaController(Config config) : ApiControllerBase
|
|||
ValidationUtils.MaxCustomPreferences,
|
||||
AuthUtils.MaxAuthMethodsPerType,
|
||||
FlagsController.MaxFlagCount
|
||||
)
|
||||
),
|
||||
Notice: NoticeResponse(await noticeCache.GetAsync(ct))
|
||||
)
|
||||
);
|
||||
|
||||
private static MetaNoticeResponse? NoticeResponse(Notice? notice) =>
|
||||
notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message);
|
||||
|
||||
[HttpGet("page/{page}")]
|
||||
public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default)
|
||||
{
|
||||
|
@ -71,7 +78,7 @@ public partial class MetaController(Config config) : ApiControllerBase
|
|||
|
||||
[HttpGet("/api/v2/coffee")]
|
||||
public IActionResult BrewCoffee() =>
|
||||
Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
|
||||
StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!");
|
||||
|
||||
[GeneratedRegex(@"^[a-z\-_]+$")]
|
||||
private static partial Regex PageRegex();
|
||||
|
|
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/notices")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class NoticesController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetNoticesAsync(CancellationToken ct = default)
|
||||
{
|
||||
List<Notice> notices = await db
|
||||
.Notices.Include(n => n.Author)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.ToListAsync(ct);
|
||||
return Ok(notices.Select(RenderNotice));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateNoticeAsync(CreateNoticeRequest req)
|
||||
{
|
||||
Instant now = clock.GetCurrentInstant();
|
||||
if (req.StartTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Start time cannot be in the past",
|
||||
"start_time",
|
||||
req.StartTime
|
||||
);
|
||||
}
|
||||
|
||||
if (req.EndTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"End time cannot be in the past",
|
||||
"end_time",
|
||||
req.EndTime
|
||||
);
|
||||
}
|
||||
|
||||
var notice = new Notice
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
Message = req.Message,
|
||||
StartTime = req.StartTime ?? clock.GetCurrentInstant(),
|
||||
EndTime = req.EndTime,
|
||||
Author = CurrentUser!,
|
||||
};
|
||||
|
||||
db.Add(notice);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(RenderNotice(notice));
|
||||
}
|
||||
|
||||
private NoticeResponse RenderNotice(Notice notice) =>
|
||||
new(
|
||||
notice.Id,
|
||||
notice.Message,
|
||||
notice.StartTime,
|
||||
notice.EndTime,
|
||||
userRenderer.RenderPartialUser(notice.Author)
|
||||
);
|
||||
}
|
|
@ -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 Coravel.Queuing.Interfaces;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
@ -34,7 +33,6 @@ public class UsersController(
|
|||
ILogger logger,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IQueue queue,
|
||||
IClock clock,
|
||||
ValidationService validationService
|
||||
) : ApiControllerBase
|
||||
|
@ -48,7 +46,15 @@ public class UsersController(
|
|||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct)
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderAuthMethods: true,
|
||||
renderSettings: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -91,7 +97,7 @@ public class UsersController(
|
|||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidateFieldEntries(
|
||||
validationService.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
|
@ -103,7 +109,7 @@ public class UsersController(
|
|||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
user.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
@ -111,7 +117,10 @@ public class UsersController(
|
|||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
);
|
||||
user.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
@ -174,11 +183,11 @@ public class UsersController(
|
|||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)
|
||||
);
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
|
||||
}
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
@ -254,20 +263,12 @@ public class UsersController(
|
|||
}
|
||||
|
||||
user.CustomPreferences = preferences;
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Ok(user.CustomPreferences);
|
||||
}
|
||||
|
||||
[HttpGet("@me/settings")]
|
||||
[Authorize("user.read_hidden")]
|
||||
[ProducesResponseType<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)]
|
||||
|
@ -280,7 +281,10 @@ public class UsersController(
|
|||
|
||||
if (req.HasProperty(nameof(req.DarkMode)))
|
||||
user.Settings.DarkMode = req.DarkMode;
|
||||
if (req.HasProperty(nameof(req.LastReadNotice)))
|
||||
user.Settings.LastReadNotice = req.LastReadNotice;
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
|
|
|
@ -64,7 +64,6 @@ 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!;
|
||||
|
@ -74,6 +73,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
public DbSet<Report> Reports { get; init; } = null!;
|
||||
public DbSet<AuditLogEntry> AuditLog { get; init; } = null!;
|
||||
public DbSet<Notification> Notifications { get; init; } = null!;
|
||||
public DbSet<Notice> Notices { get; init; } = null!;
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
|
@ -87,7 +87,6 @@ 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.
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
|
@ -0,0 +1,915 @@
|
|||
// <auto-generated />
|
||||
using System.Collections.Generic;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250329131053_AddNotices")]
|
||||
partial class AddNotices
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.PrimitiveCollection<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ClearedFields")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("cleared_fields");
|
||||
|
||||
b.Property<long>("ModeratorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("moderator_id");
|
||||
|
||||
b.Property<string>("ModeratorUsername")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("moderator_username");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("ReportId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("report_id");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetMemberName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_member_name");
|
||||
|
||||
b.Property<long?>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.Property<string>("TargetUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_username");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id")
|
||||
.HasFilter("fediverse_application_id IS NULL");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
|
||||
.HasFilter("fediverse_application_id IS NOT NULL");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("filename");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_data_exports");
|
||||
|
||||
b.HasIndex("Filename")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_data_exports_filename");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_data_exports_user_id");
|
||||
|
||||
b.ToTable("data_exports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<bool>("ForceRefresh")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("force_refresh");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_member_sid()");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("MemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("member_id");
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_member_flags");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("ix_member_flags_member_id");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_member_flags_pride_flag_id");
|
||||
|
||||
b.ToTable("member_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("author_id");
|
||||
|
||||
b.Property<Instant>("EndTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("end_time");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Instant>("StartTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("start_time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notices");
|
||||
|
||||
b.HasIndex("AuthorId")
|
||||
.HasDatabaseName("ix_notices_author_id");
|
||||
|
||||
b.ToTable("notices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant?>("AcknowledgedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("acknowledged_at");
|
||||
|
||||
b.Property<string>("LocalizationKey")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("localization_key");
|
||||
|
||||
b.Property<Dictionary<string, string>>("LocalizationParams")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore")
|
||||
.HasColumnName("localization_params");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifications");
|
||||
|
||||
b.HasIndex("TargetId")
|
||||
.HasDatabaseName("ix_notifications_target_id");
|
||||
|
||||
b.ToTable("notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pride_flags");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_pride_flags_legacy_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_pride_flags_user_id");
|
||||
|
||||
b.ToTable("pride_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long>("ReporterId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("reporter_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetSnapshot")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_snapshot");
|
||||
|
||||
b.Property<int>("TargetType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("target_type");
|
||||
|
||||
b.Property<long>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_reports");
|
||||
|
||||
b.HasIndex("ReporterId")
|
||||
.HasDatabaseName("ix_reports_reporter_id");
|
||||
|
||||
b.HasIndex("TargetMemberId")
|
||||
.HasDatabaseName("ix_reports_target_member_id");
|
||||
|
||||
b.HasIndex("TargetUserId")
|
||||
.HasDatabaseName("ix_reports_target_user_id");
|
||||
|
||||
b.ToTable("reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("custom_preferences");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<Instant>("LastSidReroll")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_sid_reroll");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<UserSettings>("Settings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("settings");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_user_sid()");
|
||||
|
||||
b.Property<string>("Timezone")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("timezone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_user_flags");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_user_flags_pride_flag_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_flags_user_id");
|
||||
|
||||
b.ToTable("user_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
|
||||
.WithOne("AuditLogEntry")
|
||||
.HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("DataExports")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_data_exports_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("MemberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_members_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifications_users_target_id");
|
||||
|
||||
b.Navigation("Target");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_pride_flags_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReporterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_reporter_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetMemberId")
|
||||
.HasConstraintName("fk_reports_members_target_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_target_user_id");
|
||||
|
||||
b.Navigation("Reporter");
|
||||
|
||||
b.Navigation("TargetMember");
|
||||
|
||||
b.Navigation("TargetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_users_user_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Navigation("AuditLogEntry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("DataExports");
|
||||
|
||||
b.Navigation("Flags");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotices : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notices",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
message = table.Column<string>(type: "text", nullable: false),
|
||||
start_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
end_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
author_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notices", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notices_users_author_id",
|
||||
column: x => x.author_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notices_author_id",
|
||||
table: "notices",
|
||||
column: "author_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "notices");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
|
@ -343,6 +343,38 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.ToTable("member_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("author_id");
|
||||
|
||||
b.Property<Instant>("EndTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("end_time");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Instant>("StartTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("start_time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notices");
|
||||
|
||||
b.HasIndex("AuthorId")
|
||||
.HasDatabaseName("ix_notices_author_id");
|
||||
|
||||
b.ToTable("notices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
@ -479,39 +511,6 @@ 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")
|
||||
|
@ -783,6 +782,18 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
|
|
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Notice : BaseModel
|
||||
{
|
||||
public required string Message { get; set; }
|
||||
public required Instant StartTime { get; set; }
|
||||
public required Instant EndTime { get; set; }
|
||||
|
||||
public Snowflake AuthorId { get; init; }
|
||||
public User Author { get; init; } = null!;
|
||||
}
|
|
@ -1,25 +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 NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class TemporaryKey
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public required string Value { get; set; }
|
||||
public Instant Expires { get; init; }
|
||||
}
|
|
@ -95,4 +95,5 @@ public enum PreferenceSize
|
|||
public class UserSettings
|
||||
{
|
||||
public bool? DarkMode { get; set; }
|
||||
public Snowflake? LastReadNotice { get; set; }
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
public record MetaResponse(
|
||||
|
@ -22,9 +24,12 @@ public record MetaResponse(
|
|||
string Hash,
|
||||
int Members,
|
||||
UserInfoResponse Users,
|
||||
LimitsResponse Limits
|
||||
LimitsResponse Limits,
|
||||
MetaNoticeResponse? Notice
|
||||
);
|
||||
|
||||
public record MetaNoticeResponse(Snowflake Id, string Message);
|
||||
|
||||
public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
|
||||
|
||||
public record LimitsResponse(
|
||||
|
|
|
@ -122,3 +122,13 @@ public record QueryUserResponse(
|
|||
);
|
||||
|
||||
public record QuerySensitiveUserDataRequest(string Reason);
|
||||
|
||||
public record NoticeResponse(
|
||||
Snowflake Id,
|
||||
string Message,
|
||||
Instant StartTime,
|
||||
Instant EndTime,
|
||||
PartialUser Author
|
||||
);
|
||||
|
||||
public record CreateNoticeRequest(string Message, Instant? StartTime, Instant EndTime);
|
||||
|
|
|
@ -49,7 +49,8 @@ public record UserResponse(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastSidReroll,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Suspended,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] UserSettings? Settings
|
||||
);
|
||||
|
||||
public record CustomPreferenceResponse(
|
||||
|
@ -79,6 +80,7 @@ public record PartialUser(
|
|||
public class UpdateUserSettingsRequest : PatchRequest
|
||||
{
|
||||
public bool? DarkMode { get; init; }
|
||||
public Snowflake? LastReadNotice { get; init; }
|
||||
}
|
||||
|
||||
public class CustomPreferenceUpdateRequest
|
||||
|
|
|
@ -33,24 +33,20 @@ public static class ImageObjectExtensions
|
|||
Snowflake id,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await objectStorageService.RemoveObjectAsync(
|
||||
MemberAvatarUpdateInvocable.Path(id, hash),
|
||||
ct
|
||||
);
|
||||
) => await objectStorageService.RemoveObjectAsync(MemberAvatarUpdateJob.Path(id, hash), ct);
|
||||
|
||||
public static async Task DeleteUserAvatarAsync(
|
||||
this ObjectStorageService objectStorageService,
|
||||
Snowflake id,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct);
|
||||
) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateJob.Path(id, hash), ct);
|
||||
|
||||
public static async Task DeleteFlagAsync(
|
||||
this ObjectStorageService objectStorageService,
|
||||
string hash,
|
||||
CancellationToken ct = default
|
||||
) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct);
|
||||
) => await objectStorageService.RemoveObjectAsync(CreateFlagJob.Path(hash), ct);
|
||||
|
||||
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
|
||||
string uri,
|
||||
|
|
|
@ -23,23 +23,19 @@ namespace Foxnouns.Backend.Extensions;
|
|||
|
||||
public static class KeyCacheExtensions
|
||||
{
|
||||
public static async Task<string> GenerateAuthStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
public static async Task<string> GenerateAuthStateAsync(this KeyCacheService keyCacheService)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct);
|
||||
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10));
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task ValidateAuthStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
string state
|
||||
)
|
||||
{
|
||||
string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", ct: ct);
|
||||
string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}");
|
||||
if (val == null)
|
||||
throw new ApiError.BadRequest("Invalid OAuth state");
|
||||
}
|
||||
|
@ -47,63 +43,55 @@ public static class KeyCacheExtensions
|
|||
public static async Task<string> GenerateRegisterEmailStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string email,
|
||||
Snowflake? userId = null,
|
||||
CancellationToken ct = default
|
||||
Snowflake? userId = null
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"email_state:{state}",
|
||||
new RegisterEmailState(email, userId),
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
Duration.FromDays(1)
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<RegisterEmailState?> GetRegisterEmailStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) => await keyCacheService.GetKeyAsync<RegisterEmailState>($"email_state:{state}", ct: ct);
|
||||
string state
|
||||
) => await keyCacheService.GetKeyAsync<RegisterEmailState>($"email_state:{state}");
|
||||
|
||||
public static async Task<string> GenerateAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
AuthType authType,
|
||||
Snowflake userId,
|
||||
string? instance = null,
|
||||
CancellationToken ct = default
|
||||
string? instance = null
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"add_account:{state}",
|
||||
new AddExtraAccountState(authType, userId, instance),
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
Duration.FromDays(1)
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<AddExtraAccountState?> GetAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) => await keyCacheService.GetKeyAsync<AddExtraAccountState>($"add_account:{state}", true, ct);
|
||||
string state
|
||||
) => await keyCacheService.GetKeyAsync<AddExtraAccountState>($"add_account:{state}", true);
|
||||
|
||||
public static async Task<string> GenerateForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string email,
|
||||
Snowflake userId,
|
||||
CancellationToken ct = default
|
||||
Snowflake userId
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"forgot_password:{state}",
|
||||
new ForgotPasswordState(email, userId),
|
||||
Duration.FromHours(1),
|
||||
ct
|
||||
Duration.FromHours(1)
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
@ -111,14 +99,8 @@ public static class KeyCacheExtensions
|
|||
public static async Task<ForgotPasswordState?> GetForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
bool delete = true,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await keyCacheService.GetKeyAsync<ForgotPasswordState>(
|
||||
$"forgot_password:{state}",
|
||||
delete,
|
||||
ct
|
||||
);
|
||||
bool delete = true
|
||||
) => await keyCacheService.GetKeyAsync<ForgotPasswordState>($"forgot_password:{state}", delete);
|
||||
}
|
||||
|
||||
public record RegisterEmailState(
|
||||
|
|
|
@ -15,14 +15,18 @@
|
|||
using Coravel;
|
||||
using Coravel.Queuing.Interfaces;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Services.Caching;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Http.Resilience;
|
||||
using Minio;
|
||||
using NodaTime;
|
||||
using Polly;
|
||||
using Prometheus;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
@ -51,9 +55,12 @@ 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)
|
||||
|
@ -97,6 +104,40 @@ public static class WebApplicationExtensions
|
|||
builder.Host.ConfigureServices(
|
||||
(ctx, services) =>
|
||||
{
|
||||
// create a single HTTP client for all requests.
|
||||
// it's also configured with a retry mechanism, so that requests aren't immediately lost to the void if they fail
|
||||
services.AddSingleton<HttpClient>(_ =>
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
var retryPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
|
||||
.AddRetry(
|
||||
new HttpRetryStrategyOptions
|
||||
{
|
||||
BackoffType = DelayBackoffType.Linear,
|
||||
MaxRetryAttempts = 3,
|
||||
}
|
||||
)
|
||||
.Build();
|
||||
|
||||
var resilienceHandler = new ResilienceHandler(retryPipeline)
|
||||
{
|
||||
InnerHandler = new SocketsHttpHandler
|
||||
{
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
|
||||
},
|
||||
};
|
||||
|
||||
var client = new HttpClient(resilienceHandler);
|
||||
client.DefaultRequestHeaders.Remove("User-Agent");
|
||||
client.DefaultRequestHeaders.Remove("Accept");
|
||||
client.DefaultRequestHeaders.Add(
|
||||
"User-Agent",
|
||||
$"pronouns.cc/{BuildInfo.Version}"
|
||||
);
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
return client;
|
||||
});
|
||||
|
||||
services
|
||||
.AddQueue()
|
||||
.AddSmtpMailer(ctx.Configuration)
|
||||
|
@ -112,24 +153,25 @@ 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<MemberAvatarUpdateInvocable>()
|
||||
.AddTransient<UserAvatarUpdateInvocable>()
|
||||
.AddTransient<CreateFlagInvocable>()
|
||||
.AddTransient<CreateDataExportInvocable>()
|
||||
.AddTransient<UserAvatarUpdateJob>()
|
||||
.AddTransient<MemberAvatarUpdateJob>()
|
||||
.AddTransient<CreateDataExportJob>()
|
||||
.AddTransient<CreateFlagJob>()
|
||||
// Legacy services
|
||||
.AddScoped<UsersV1Service>()
|
||||
.AddScoped<MembersV1Service>();
|
||||
|
@ -157,9 +199,6 @@ public static class WebApplicationExtensions
|
|||
|
||||
public static async Task Initialize(this WebApplication app, string[] args)
|
||||
{
|
||||
// Read version information from .version in the repository root
|
||||
await BuildInfo.ReadBuildInfo();
|
||||
|
||||
app.Services.ConfigureQueue()
|
||||
.LogQueuedTaskProgress(app.Services.GetRequiredService<ILogger<IQueue>>());
|
||||
|
||||
|
|
|
@ -8,41 +8,46 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Coravel" Version="6.0.0"/>
|
||||
<PackageReference Include="Coravel.Mailer" Version="7.0.0"/>
|
||||
<PackageReference Include="Coravel" Version="6.0.2"/>
|
||||
<PackageReference Include="Coravel.Mailer" Version="7.1.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.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">
|
||||
<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">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<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="Microsoft.Extensions.Caching.Memory" Version="9.0.2"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.2.0"/>
|
||||
<PackageReference Include="MimeKit" Version="4.10.0"/>
|
||||
<PackageReference Include="Minio" Version="6.0.4"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.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="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="prometheus-net" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.13.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="1.2.55"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="4.13.0"/>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.0.26"/>
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="5.3.0"/>
|
||||
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.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="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageReference Include="Yort.Xid.Net" Version="2.0.1"/>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -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,7 +26,8 @@ using NodaTime.Text;
|
|||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateDataExportInvocable(
|
||||
public class CreateDataExportJob(
|
||||
HttpClient client,
|
||||
DatabaseContext db,
|
||||
IClock clock,
|
||||
UserRendererService userRenderer,
|
||||
|
@ -34,37 +35,40 @@ public class CreateDataExportInvocable(
|
|||
ObjectStorageService objectStorageService,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<CreateDataExportPayload>
|
||||
)
|
||||
{
|
||||
private static readonly HttpClient Client = new();
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportInvocable>();
|
||||
public required CreateDataExportPayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(Snowflake userId)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateDataExportJob>(j => j.InvokeAsync(userId));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(Snowflake userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync();
|
||||
await InvokeAsyncInner(userId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", Payload.UserId);
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvokeAsync()
|
||||
private async Task InvokeAsyncInner(Snowflake userId)
|
||||
{
|
||||
User? user = await db
|
||||
.Users.Include(u => u.AuthMethods)
|
||||
.Include(u => u.Flags)
|
||||
.Include(u => u.ProfileFlags)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(u => u.Id == Payload.UserId);
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Received create data export request for user {UserId} but no such user exists, deleted or otherwise. Ignoring request",
|
||||
Payload.UserId
|
||||
userId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -197,7 +201,7 @@ public class CreateDataExportInvocable(
|
|||
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);
|
|
@ -12,49 +12,53 @@
|
|||
//
|
||||
// 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 CreateFlagInvocable(
|
||||
public class CreateFlagJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<CreateFlagPayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagInvocable>();
|
||||
public required CreateFlagPayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(CreateFlagPayload payload)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateFlagJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(CreateFlagPayload payload)
|
||||
{
|
||||
_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
|
||||
);
|
||||
|
@ -68,7 +72,7 @@ public class CreateFlagInvocable(
|
|||
}
|
||||
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,29 +12,33 @@
|
|||
//
|
||||
// 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 MemberAvatarUpdateInvocable(
|
||||
public class MemberAvatarUpdateJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<MemberAvatarUpdateJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateMemberAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
BackgroundJob.Enqueue<MemberAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
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,5 +19,3 @@ 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,29 +12,33 @@
|
|||
//
|
||||
// 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 UserAvatarUpdateInvocable(
|
||||
public class UserAvatarUpdateJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateUserAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
BackgroundJob.Enqueue<UserAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
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)
|
|
@ -19,11 +19,12 @@ 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;
|
||||
|
||||
|
@ -33,6 +34,9 @@ Config config = builder.AddConfiguration();
|
|||
|
||||
builder.AddSerilog();
|
||||
|
||||
// Read version information from .version in the repository root
|
||||
await BuildInfo.ReadBuildInfo();
|
||||
|
||||
builder
|
||||
.WebHost.UseSentry(opts =>
|
||||
{
|
||||
|
@ -46,7 +50,8 @@ 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()
|
||||
|
@ -63,16 +68,27 @@ builder
|
|||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||
};
|
||||
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
// the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine)
|
||||
options.InvalidModelStateResponseFactory = (ActionContext actionContext) =>
|
||||
new BadRequestObjectResult(
|
||||
new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson()
|
||||
);
|
||||
options.InvalidModelStateResponseFactory = 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 =>
|
||||
|
@ -109,16 +125,19 @@ if (config.Logging.SentryTracing)
|
|||
app.UseCors();
|
||||
app.UseCustomMiddleware();
|
||||
app.MapControllers();
|
||||
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}";
|
||||
});
|
||||
app.UseHangfireDashboard();
|
||||
|
||||
app.Urls.Clear();
|
||||
app.Urls.Add(config.Address);
|
||||
// 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";
|
||||
// }
|
||||
// );
|
||||
|
||||
// Make sure metrics are updated whenever Prometheus scrapes them
|
||||
Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct =>
|
||||
|
|
|
@ -253,14 +253,14 @@ public class AuthService(
|
|||
{
|
||||
AssertValidAuthType(authType, app);
|
||||
|
||||
// This is already checked when
|
||||
// This is already checked when generating an add account state, but we check it here too just in case.
|
||||
int currentCount = await db
|
||||
.AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType)
|
||||
.CountAsync(ct);
|
||||
if (currentCount >= AuthUtils.MaxAuthMethodsPerType)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Too many linked accounts of this type, maximum of 3 per account."
|
||||
$"Too many linked accounts of this type, maximum of {AuthUtils.MaxAuthMethodsPerType} per account."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,20 +25,20 @@ namespace Foxnouns.Backend.Services.Auth;
|
|||
public partial class FediverseAuthService
|
||||
{
|
||||
private string MastodonRedirectUri(string instance) =>
|
||||
$"{_config.BaseUrl}/auth/callback/mastodon/{instance}";
|
||||
$"{config.BaseUrl}/auth/callback/mastodon/{instance}";
|
||||
|
||||
private async Task<FediverseApplication> CreateMastodonApplicationAsync(
|
||||
string instance,
|
||||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
$"https://{instance}/api/v1/apps",
|
||||
new CreateMastodonApplicationRequest(
|
||||
$"pronouns.cc (+{_config.BaseUrl})",
|
||||
$"pronouns.cc (+{config.BaseUrl})",
|
||||
MastodonRedirectUri(instance),
|
||||
"read read:accounts",
|
||||
_config.BaseUrl
|
||||
config.BaseUrl
|
||||
)
|
||||
);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
@ -58,19 +58,19 @@ public partial class FediverseAuthService
|
|||
{
|
||||
app = new FediverseApplication
|
||||
{
|
||||
Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
|
||||
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(),
|
||||
ClientId = mastodonApp.ClientId,
|
||||
ClientSecret = mastodonApp.ClientSecret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MastodonApi,
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
db.Add(app);
|
||||
}
|
||||
else
|
||||
{
|
||||
app =
|
||||
await _db.FediverseApplications.FindAsync(existingAppId)
|
||||
await db.FediverseApplications.FindAsync(existingAppId)
|
||||
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
|
||||
|
||||
app.ClientId = mastodonApp.ClientId;
|
||||
|
@ -78,7 +78,7 @@ public partial class FediverseAuthService
|
|||
app.InstanceType = FediverseInstanceType.MastodonApi;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -90,9 +90,9 @@ public partial class FediverseAuthService
|
|||
)
|
||||
{
|
||||
if (state != null)
|
||||
await _keyCacheService.ValidateAuthStateAsync(state);
|
||||
await keyCacheService.ValidateAuthStateAsync(state);
|
||||
|
||||
HttpResponseMessage tokenResp = await _client.PostAsync(
|
||||
HttpResponseMessage tokenResp = await client.PostAsync(
|
||||
MastodonTokenUri(app.Domain),
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -123,7 +123,7 @@ public partial class FediverseAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain));
|
||||
req.Headers.Add("Authorization", $"Bearer {token}");
|
||||
|
||||
HttpResponseMessage currentUserResp = await _client.SendAsync(req);
|
||||
HttpResponseMessage currentUserResp = await client.SendAsync(req);
|
||||
currentUserResp.EnsureSuccessStatusCode();
|
||||
FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
|
||||
if (user == null)
|
||||
|
@ -151,7 +151,7 @@ public partial class FediverseAuthService
|
|||
app = await CreateMastodonApplicationAsync(app.Domain, app.Id);
|
||||
}
|
||||
|
||||
state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
|
||||
state ??= HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());
|
||||
|
||||
return $"https://{app.Domain}/oauth/authorize?response_type=code"
|
||||
+ $"&client_id={app.ClientId}"
|
||||
|
|
|
@ -34,11 +34,11 @@ public partial class FediverseAuthService
|
|||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyAppUri(instance),
|
||||
new CreateMisskeyApplicationRequest(
|
||||
$"pronouns.cc (+{_config.BaseUrl})",
|
||||
$"pronouns.cc on {_config.BaseUrl}",
|
||||
$"pronouns.cc (+{config.BaseUrl})",
|
||||
$"pronouns.cc on {config.BaseUrl}",
|
||||
["read:account"],
|
||||
MastodonRedirectUri(instance)
|
||||
)
|
||||
|
@ -60,19 +60,19 @@ public partial class FediverseAuthService
|
|||
{
|
||||
app = new FediverseApplication
|
||||
{
|
||||
Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
|
||||
Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(),
|
||||
ClientId = misskeyApp.Id,
|
||||
ClientSecret = misskeyApp.Secret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MisskeyApi,
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
db.Add(app);
|
||||
}
|
||||
else
|
||||
{
|
||||
app =
|
||||
await _db.FediverseApplications.FindAsync(existingAppId)
|
||||
await db.FediverseApplications.FindAsync(existingAppId)
|
||||
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
|
||||
|
||||
app.ClientId = misskeyApp.Id;
|
||||
|
@ -80,7 +80,7 @@ public partial class FediverseAuthService
|
|||
app.InstanceType = FediverseInstanceType.MisskeyApi;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ public partial class FediverseAuthService
|
|||
|
||||
private async Task<FediverseUser> GetMisskeyUserAsync(FediverseApplication app, string code)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyTokenUri(app.Domain),
|
||||
new GetMisskeySessionUserKeyRequest(app.ClientSecret, code)
|
||||
);
|
||||
|
@ -130,7 +130,7 @@ public partial class FediverseAuthService
|
|||
app = await CreateMisskeyApplicationAsync(app.Domain, app.Id);
|
||||
}
|
||||
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
HttpResponseMessage resp = await client.PostAsJsonAsync(
|
||||
MisskeyGenerateSessionUri(app.Domain),
|
||||
new CreateMisskeySessionUriRequest(app.ClientSecret)
|
||||
);
|
||||
|
|
|
@ -19,37 +19,17 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService
|
||||
public partial class FediverseAuthService(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
HttpClient client,
|
||||
KeyCacheService keyCacheService,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
)
|
||||
{
|
||||
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Config _config;
|
||||
private readonly DatabaseContext _db;
|
||||
private readonly KeyCacheService _keyCacheService;
|
||||
private readonly ISnowflakeGenerator _snowflakeGenerator;
|
||||
|
||||
public FediverseAuthService(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
)
|
||||
{
|
||||
_logger = logger.ForContext<FediverseAuthService>();
|
||||
_config = config;
|
||||
_db = db;
|
||||
_keyCacheService = keyCacheService;
|
||||
_snowflakeGenerator = snowflakeGenerator;
|
||||
|
||||
_client = new HttpClient();
|
||||
_client.DefaultRequestHeaders.Remove("User-Agent");
|
||||
_client.DefaultRequestHeaders.Remove("Accept");
|
||||
_client.DefaultRequestHeaders.Add("User-Agent", $"pronouns.cc/{BuildInfo.Version}");
|
||||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
}
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthService>();
|
||||
|
||||
public async Task<string> GenerateAuthUrlAsync(
|
||||
string instance,
|
||||
|
@ -70,7 +50,7 @@ public partial class FediverseAuthService
|
|||
|
||||
public async Task<FediverseApplication> GetApplicationAsync(string instance)
|
||||
{
|
||||
FediverseApplication? app = await _db.FediverseApplications.FirstOrDefaultAsync(a =>
|
||||
FediverseApplication? app = await db.FediverseApplications.FirstOrDefaultAsync(a =>
|
||||
a.Domain == instance
|
||||
);
|
||||
if (app != null)
|
||||
|
@ -92,7 +72,7 @@ public partial class FediverseAuthService
|
|||
{
|
||||
_logger.Debug("Requesting software name for fediverse instance {Instance}", instance);
|
||||
|
||||
HttpResponseMessage wellKnownResp = await _client.GetAsync(
|
||||
HttpResponseMessage wellKnownResp = await client.GetAsync(
|
||||
new Uri($"https://{instance}/.well-known/nodeinfo")
|
||||
);
|
||||
wellKnownResp.EnsureSuccessStatusCode();
|
||||
|
@ -107,7 +87,7 @@ public partial class FediverseAuthService
|
|||
);
|
||||
}
|
||||
|
||||
HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl);
|
||||
HttpResponseMessage nodeInfoResp = await client.GetAsync(nodeInfoUrl);
|
||||
nodeInfoResp.EnsureSuccessStatusCode();
|
||||
|
||||
PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>();
|
||||
|
|
|
@ -27,7 +27,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/discord";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_discordTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -59,7 +59,7 @@ public partial class RemoteAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri);
|
||||
req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
||||
|
||||
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await client.SendAsync(req, ct);
|
||||
resp2.EnsureSuccessStatusCode();
|
||||
DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
|
||||
if (user == null)
|
||||
|
|
|
@ -28,7 +28,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/google";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_googleTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
|
|
@ -29,7 +29,7 @@ public partial class RemoteAuthService
|
|||
)
|
||||
{
|
||||
var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr";
|
||||
HttpResponseMessage resp = await _httpClient.PostAsync(
|
||||
HttpResponseMessage resp = await client.PostAsync(
|
||||
_tumblrTokenUri,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
|
@ -62,7 +62,7 @@ public partial class RemoteAuthService
|
|||
var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri);
|
||||
req.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
|
||||
|
||||
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
|
||||
HttpResponseMessage resp2 = await client.SendAsync(req, ct);
|
||||
if (!resp2.IsSuccessStatusCode)
|
||||
{
|
||||
string respBody = await resp2.Content.ReadAsStringAsync(ct);
|
||||
|
|
|
@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class RemoteAuthService(
|
||||
HttpClient client,
|
||||
Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
|
@ -32,7 +33,6 @@ public partial class RemoteAuthService(
|
|||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<RemoteAuthService>();
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
public record RemoteUser(string Id, string Username);
|
||||
|
||||
|
|
39
Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
Normal file
39
Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Caching;
|
||||
|
||||
public class NoticeCacheService(IServiceProvider serviceProvider, IClock clock, ILogger logger)
|
||||
: SingletonCacheService<Notice>(serviceProvider, clock, logger)
|
||||
{
|
||||
public override Duration MaxAge { get; init; } = Duration.FromMinutes(5);
|
||||
|
||||
public override Func<
|
||||
DatabaseContext,
|
||||
CancellationToken,
|
||||
Task<Notice?>
|
||||
> FetchFunc { get; init; } =
|
||||
async (db, ct) =>
|
||||
await db
|
||||
.Notices.Where(n =>
|
||||
n.StartTime < clock.GetCurrentInstant() && n.EndTime > clock.GetCurrentInstant()
|
||||
)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
63
Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
Normal file
63
Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Caching;
|
||||
|
||||
public abstract class SingletonCacheService<T>(
|
||||
IServiceProvider serviceProvider,
|
||||
IClock clock,
|
||||
ILogger logger
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
private T? _item;
|
||||
private Instant _lastUpdated = Instant.MinValue;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private readonly ILogger _logger = logger.ForContext<SingletonCacheService<T>>();
|
||||
|
||||
public virtual Duration MaxAge { get; init; } = Duration.FromMinutes(5);
|
||||
|
||||
public virtual Func<DatabaseContext, CancellationToken, Task<T?>> FetchFunc { get; init; } =
|
||||
(_, __) => Task.FromResult<T?>(null);
|
||||
|
||||
public async Task<T?> GetAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
if (_lastUpdated > clock.GetCurrentInstant() - MaxAge)
|
||||
{
|
||||
return _item;
|
||||
}
|
||||
|
||||
_logger.Debug("Cached item of type {Type} is expired, fetching it.", typeof(T));
|
||||
|
||||
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
|
||||
await using DatabaseContext db =
|
||||
scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
T? item = await FetchFunc(db, ct);
|
||||
_item = item;
|
||||
_lastUpdated = clock.GetCurrentInstant();
|
||||
return item;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,8 +23,11 @@ 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,94 +17,39 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
|
||||
public class KeyCacheService(Config config)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<KeyCacheService>();
|
||||
public ConnectionMultiplexer Multiplexer { get; } =
|
||||
ConnectionMultiplexer.Connect(config.Database.Redis);
|
||||
|
||||
public Task SetKeyAsync(
|
||||
string key,
|
||||
string value,
|
||||
Duration expireAfter,
|
||||
CancellationToken ct = default
|
||||
) => SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct);
|
||||
public async Task SetKeyAsync(string key, string value, Duration expireAfter) =>
|
||||
await Multiplexer
|
||||
.GetDatabase()
|
||||
.StringSetAsync(key, value, expiry: expireAfter.ToTimeSpan());
|
||||
|
||||
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<string?> GetKeyAsync(string key, bool delete = false) =>
|
||||
delete
|
||||
? await Multiplexer.GetDatabase().StringGetDeleteAsync(key)
|
||||
: await Multiplexer.GetDatabase().StringGetAsync(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 DeleteKeyAsync(string key) =>
|
||||
await Multiplexer.GetDatabase().KeyDeleteAsync(key);
|
||||
|
||||
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
|
||||
)
|
||||
public async Task SetKeyAsync<T>(string key, T obj, Duration expiresAt)
|
||||
where T : class
|
||||
{
|
||||
string value = JsonConvert.SerializeObject(obj);
|
||||
await SetKeyAsync(key, value, expires, ct);
|
||||
await SetKeyAsync(key, value, expiresAt);
|
||||
}
|
||||
|
||||
public async Task<T?> GetKeyAsync<T>(
|
||||
string key,
|
||||
bool delete = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
public async Task<T?> GetKeyAsync<T>(string key, bool delete = false)
|
||||
where T : class
|
||||
{
|
||||
string? value = await GetKeyAsync(key, delete, ct);
|
||||
string? value = await GetKeyAsync(key, delete);
|
||||
return value == null ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ public class ModerationService(
|
|||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IQueue queue,
|
||||
IClock clock
|
||||
)
|
||||
{
|
||||
|
@ -181,9 +180,7 @@ public class ModerationService(
|
|||
target.CustomPreferences = [];
|
||||
target.ProfileFlags = [];
|
||||
|
||||
queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(target.Id, null)
|
||||
);
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(target.Id, null));
|
||||
|
||||
// TODO: also clear member profiles?
|
||||
|
||||
|
@ -264,10 +261,9 @@ public class ModerationService(
|
|||
targetMember.DisplayName = null;
|
||||
break;
|
||||
case FieldsToClear.Avatar:
|
||||
queue.QueueInvocableWithPayload<
|
||||
MemberAvatarUpdateInvocable,
|
||||
AvatarUpdatePayload
|
||||
>(new AvatarUpdatePayload(targetMember.Id, null));
|
||||
MemberAvatarUpdateJob.Enqueue(
|
||||
new AvatarUpdatePayload(targetMember.Id, null)
|
||||
);
|
||||
break;
|
||||
case FieldsToClear.Bio:
|
||||
targetMember.Bio = null;
|
||||
|
@ -306,10 +302,7 @@ public class ModerationService(
|
|||
targetUser.DisplayName = null;
|
||||
break;
|
||||
case FieldsToClear.Avatar:
|
||||
queue.QueueInvocableWithPayload<
|
||||
UserAvatarUpdateInvocable,
|
||||
AvatarUpdatePayload
|
||||
>(new AvatarUpdatePayload(targetUser.Id, null));
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(targetUser.Id, null));
|
||||
break;
|
||||
case FieldsToClear.Bio:
|
||||
targetUser.Bio = null;
|
||||
|
|
|
@ -33,11 +33,9 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
|
|||
|
||||
// The type is literally written on the same line, we can just use `var`
|
||||
// ReSharper disable SuggestVarOrType_SimpleTypes
|
||||
var keyCacheService = scope.ServiceProvider.GetRequiredService<KeyCacheService>();
|
||||
var dataCleanupService = scope.ServiceProvider.GetRequiredService<DataCleanupService>();
|
||||
// ReSharper restore SuggestVarOrType_SimpleTypes
|
||||
|
||||
await keyCacheService.DeleteExpiredKeysAsync(ct);
|
||||
await dataCleanupService.InvokeAsync(ct);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await RenderUserInnerAsync(
|
||||
|
@ -42,6 +43,7 @@ public class UserRendererService(
|
|||
renderMembers,
|
||||
renderAuthMethods,
|
||||
overrideSid,
|
||||
renderSettings,
|
||||
ct
|
||||
);
|
||||
|
||||
|
@ -52,6 +54,7 @@ public class UserRendererService(
|
|||
bool renderMembers = true,
|
||||
bool renderAuthMethods = false,
|
||||
string? overrideSid = null,
|
||||
bool renderSettings = false,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
|
@ -62,6 +65,7 @@ public class UserRendererService(
|
|||
|
||||
renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers);
|
||||
renderAuthMethods = renderAuthMethods && tokenPrivileged;
|
||||
renderSettings = renderSettings && tokenHidden;
|
||||
|
||||
IEnumerable<Member> members = renderMembers
|
||||
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
|
||||
|
@ -117,7 +121,8 @@ public class UserRendererService(
|
|||
tokenHidden ? user.LastSidReroll : null,
|
||||
tokenHidden ? user.Timezone ?? "<none>" : null,
|
||||
tokenHidden ? user is { Deleted: true, DeletedBy: not null } : null,
|
||||
tokenHidden ? user.Deleted : null
|
||||
tokenHidden ? user.Deleted : null,
|
||||
renderSettings ? user.Settings : null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public static partial class ValidationUtils
|
||||
public partial class ValidationService
|
||||
{
|
||||
public static readonly string[] DefaultStatusOptions =
|
||||
[
|
||||
|
@ -28,7 +28,7 @@ public static partial class ValidationUtils
|
|||
"avoid",
|
||||
];
|
||||
|
||||
public static IEnumerable<(string, ValidationError?)> ValidateFields(
|
||||
public IEnumerable<(string, ValidationError?)> ValidateFields(
|
||||
List<Field>? fields,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ public static partial class ValidationUtils
|
|||
return [];
|
||||
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
if (fields.Count > 25)
|
||||
if (fields.Count > _limits.MaxFields)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -45,7 +45,7 @@ public static partial class ValidationUtils
|
|||
ValidationError.LengthError(
|
||||
"Too many fields",
|
||||
0,
|
||||
Limits.FieldLimit,
|
||||
_limits.MaxFields,
|
||||
fields.Count
|
||||
)
|
||||
)
|
||||
|
@ -53,39 +53,38 @@ public static partial class ValidationUtils
|
|||
}
|
||||
|
||||
// No overwhelming this function, thank you
|
||||
if (fields.Count > 100)
|
||||
if (fields.Count > _limits.MaxFields + 50)
|
||||
return errors;
|
||||
|
||||
foreach ((Field? field, int index) in fields.Select((field, index) => (field, index)))
|
||||
{
|
||||
switch (field.Name.Length)
|
||||
if (field.Name.Length > _limits.MaxFieldNameLength)
|
||||
{
|
||||
case > Limits.FieldNameLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too long",
|
||||
1,
|
||||
Limits.FieldNameLimit,
|
||||
field.Name.Length
|
||||
)
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too long",
|
||||
1,
|
||||
_limits.MaxFieldNameLength,
|
||||
field.Name.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too short",
|
||||
1,
|
||||
Limits.FieldNameLimit,
|
||||
field.Name.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (field.Name.Length < 1)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"fields.{index}.name",
|
||||
ValidationError.LengthError(
|
||||
"Field name is too short",
|
||||
1,
|
||||
_limits.MaxFieldNameLength,
|
||||
field.Name.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
errors = errors
|
||||
|
@ -102,7 +101,7 @@ public static partial class ValidationUtils
|
|||
return errors;
|
||||
}
|
||||
|
||||
public static IEnumerable<(string, ValidationError?)> ValidateFieldEntries(
|
||||
public IEnumerable<(string, ValidationError?)> ValidateFieldEntries(
|
||||
FieldEntry[]? entries,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences,
|
||||
string errorPrefix = "fields"
|
||||
|
@ -112,7 +111,7 @@ public static partial class ValidationUtils
|
|||
return [];
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (entries.Length > Limits.FieldEntriesLimit)
|
||||
if (entries.Length > _limits.MaxFieldEntries)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -120,7 +119,7 @@ public static partial class ValidationUtils
|
|||
ValidationError.LengthError(
|
||||
"Field has too many entries",
|
||||
0,
|
||||
Limits.FieldEntriesLimit,
|
||||
_limits.MaxFieldEntries,
|
||||
entries.Length
|
||||
)
|
||||
)
|
||||
|
@ -128,7 +127,7 @@ public static partial class ValidationUtils
|
|||
}
|
||||
|
||||
// Same as above, no overwhelming this function with a ridiculous amount of entries
|
||||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
if (entries.Length > _limits.MaxFieldEntries + 50)
|
||||
return errors;
|
||||
|
||||
string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray();
|
||||
|
@ -139,34 +138,33 @@ public static partial class ValidationUtils
|
|||
)
|
||||
)
|
||||
{
|
||||
switch (entry.Value.Length)
|
||||
if (entry.Value.Length > _limits.MaxFieldEntryTextLength)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too long",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too short",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.Value.Length < 1)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Field value is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -186,7 +184,7 @@ public static partial class ValidationUtils
|
|||
return errors;
|
||||
}
|
||||
|
||||
public static IEnumerable<(string, ValidationError?)> ValidatePronouns(
|
||||
public IEnumerable<(string, ValidationError?)> ValidatePronouns(
|
||||
Pronoun[]? entries,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences,
|
||||
string errorPrefix = "pronouns"
|
||||
|
@ -196,7 +194,7 @@ public static partial class ValidationUtils
|
|||
return [];
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (entries.Length > Limits.FieldEntriesLimit)
|
||||
if (entries.Length > _limits.MaxFieldEntries)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
|
@ -204,7 +202,7 @@ public static partial class ValidationUtils
|
|||
ValidationError.LengthError(
|
||||
"Too many pronouns",
|
||||
0,
|
||||
Limits.FieldEntriesLimit,
|
||||
_limits.MaxFieldEntries,
|
||||
entries.Length
|
||||
)
|
||||
)
|
||||
|
@ -212,7 +210,7 @@ public static partial class ValidationUtils
|
|||
}
|
||||
|
||||
// Same as above, no overwhelming this function with a ridiculous amount of entries
|
||||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
if (entries.Length > _limits.MaxFieldEntries + 50)
|
||||
return errors;
|
||||
|
||||
string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray();
|
||||
|
@ -221,66 +219,64 @@ public static partial class ValidationUtils
|
|||
(Pronoun? entry, int entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))
|
||||
)
|
||||
{
|
||||
switch (entry.Value.Length)
|
||||
if (entry.Value.Length > _limits.MaxFieldEntryTextLength)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too long",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too short",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.Value.Length < 1)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.value",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun value is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.DisplayText != null)
|
||||
{
|
||||
switch (entry.DisplayText.Length)
|
||||
if (entry.DisplayText.Length > _limits.MaxFieldEntryTextLength)
|
||||
{
|
||||
case > Limits.FieldEntryTextLimit:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too long",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too long",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too short",
|
||||
1,
|
||||
Limits.FieldEntryTextLimit,
|
||||
entry.Value.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (entry.DisplayText.Length < 1)
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
$"{errorPrefix}.{entryIdx}.display_text",
|
||||
ValidationError.LengthError(
|
||||
"Pronoun display text is too short",
|
||||
1,
|
||||
_limits.MaxFieldEntryTextLength,
|
||||
entry.Value.Length
|
||||
)
|
||||
);
|
||||
break;
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ public partial class ValidationService
|
|||
"settings",
|
||||
"pronouns.cc",
|
||||
"pronounscc",
|
||||
"null",
|
||||
];
|
||||
|
||||
private static readonly string[] InvalidMemberNames =
|
||||
|
@ -38,8 +39,10 @@ public partial class ValidationService
|
|||
// these break routing outright
|
||||
".",
|
||||
"..",
|
||||
// the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
|
||||
// TODO: remove this? i'm not sure if /@[username]/edit will redirect to settings
|
||||
"edit",
|
||||
// this breaks the frontend, somehow
|
||||
"null",
|
||||
];
|
||||
|
||||
public ValidationError? ValidateUsername(string username)
|
||||
|
|
|
@ -1,23 +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/>.
|
||||
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,8 +22,10 @@ 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,
|
||||
|
|
|
@ -20,7 +20,7 @@ public static partial class ValidationUtils
|
|||
|
||||
public static ValidationError? ValidateReportContext(string? context) =>
|
||||
context?.Length > MaximumReportContextLength
|
||||
? ValidationError.GenericValidationError("Avatar is too large", null)
|
||||
? ValidationError.GenericValidationError("Report context is too long", null)
|
||||
: null;
|
||||
|
||||
public const int MinimumPasswordLength = 12;
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
"net9.0": {
|
||||
"Coravel": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "U16V/IxGL2TcpU9sT1gUA3pqoVIlz+WthC4idn8OTPiEtLElTcmNF6sHt+gOx8DRU8TBgN5vjfL4AHetjacOWQ==",
|
||||
"requested": "[6.0.2, )",
|
||||
"resolved": "6.0.2",
|
||||
"contentHash": "/XZiRId4Ilar/OqjGKdxkZWfW97ekeT0wgiWNjGdqf8pPxiK508//Zkc0xrKMDOqchFT7B/oqAoQ+Vrx1txpPQ==",
|
||||
"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.0.0, )",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "mxSlOOBxPjCAZruOpgXtubnZA9lD0DRgutApQmAsts7DoRfe0wTzqWrYjeZTiIzgVJZKZxJglN8duTvbPrw3jQ==",
|
||||
"requested": "[7.1.0, )",
|
||||
"resolved": "7.1.0",
|
||||
"contentHash": "yMbUrwKl5/HbJeX8JkHa8Q3CPTJ3OmPyDSG7sULbXGEhzc2GiYIh7pmVhI1FFeL3VUtFavMDkS8PTwEeCpiwlg==",
|
||||
"dependencies": {
|
||||
"MailKit": "4.3.0",
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.27"
|
||||
"MailKit": "4.8.0",
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.36"
|
||||
}
|
||||
},
|
||||
"EFCore.NamingConventions": {
|
||||
|
@ -46,6 +46,37 @@
|
|||
"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, )",
|
||||
|
@ -60,41 +91,41 @@
|
|||
},
|
||||
"Microsoft.AspNetCore.Mvc.NewtonsoftJson": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "pTFDEmZi3GheCSPrBxzyE63+d5unln2vYldo/nOm1xet/4rpEk2oJYcwpclPQ13E+LZBF9XixkgwYTUwqznlWg==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "cCnaxji6nqIHHLAEhZ6QirXCvwJNi0Q/qCPLkRW5SqMYNuOwoQdGk1KAhW65phBq1VHGt7wLbadpuGPGqfiZuA==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.JsonPatch": "9.0.0",
|
||||
"Microsoft.AspNetCore.JsonPatch": "9.0.2",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Newtonsoft.Json.Bson": "1.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.OpenApi": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "FqUK5j1EOPNuFT7IafltZQ3cakqhSwVzH5ZW1MhZDe4pPXs9sJ2M5jom1Omsu+mwF2tNKKlRAzLRHQTZzbd+6Q==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "JUndpjRNdG8GvzBLH/J4hen4ehWaPcshtiQ6+sUs1Bcj3a7dOsmWpDloDlpPeMOVSlhHwUJ3Xld0ClZjsFLgFQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.OpenApi": "1.6.17"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "wpG+nfnfDAw87R3ovAsUmjr3MZ4tYXf6bFqEPVAIKE6IfPml3DS//iX0DBnf8kWn5ZHSO5oi1m4d/Jf+1LifJQ==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "P90ZuybgcpW32y985eOYxSoZ9IiL0UTYQlY0y1Pt1iHAnpZj/dQHREpSpry1RNvk8YjAeoAkWFdem5conqB9zQ==",
|
||||
"dependencies": {
|
||||
"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.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.Design": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "Pqo8I+yHJ3VQrAoY0hiSncf+5P7gN/RkNilK5e+/K/yKh+yAWxdUAI6t0TG26a9VPlCa9FhyklzyFvRyj3YG9A==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "WWRmTxb/yd05cTW+k32lLvIhffxilgYvwKHDxiqe7GRLKeceyMspuf5BRpW65sFF7S2G+Be9JgjUe1ypGqt9tg==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "2.14.1",
|
||||
"Microsoft.Build.Framework": "17.8.3",
|
||||
|
@ -102,33 +133,45 @@
|
|||
"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.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",
|
||||
"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",
|
||||
"Mono.TextTemplating": "3.0.0",
|
||||
"System.Text.Json": "9.0.0"
|
||||
"System.Text.Json": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "AlEfp0DMz8E1h1Exi8LBrUCNmCYcGDfSM4F/uK1D1cYx/R3w0LVvlmjICqxqXTsy7BEZaCf5leRZY2FuPEiFaw==",
|
||||
"dependencies": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"MimeKit": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.9.0, )",
|
||||
"resolved": "4.9.0",
|
||||
"contentHash": "DZXXMZzmAABDxFhOSMb6SE8KKxcRd/sk1E6aJTUE5ys2FWOQhznYV2Gl3klaaSfqKn27hQ32haqquH1J8Z6kJw==",
|
||||
"requested": "[4.10.0, )",
|
||||
"resolved": "4.10.0",
|
||||
"contentHash": "GQofI17cH55XSh109hJmHaYMtSFqTX/eUek3UcV7hTnYayAIXZ6eHlv345tfdc+bQ/BrEnYOSZVzx9I3wpvvpg==",
|
||||
"dependencies": {
|
||||
"BouncyCastle.Cryptography": "2.5.0",
|
||||
"System.Formats.Asn1": "8.0.1",
|
||||
|
@ -137,11 +180,11 @@
|
|||
},
|
||||
"Minio": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.3, )",
|
||||
"resolved": "6.0.3",
|
||||
"contentHash": "WHlkouclHtiK/pIXPHcjVmbeELHPtElj2qRSopFVpSmsFhZXeM10sPvczrkSPePsmwuvZdFryJ/hJzKu3XeLVg==",
|
||||
"requested": "[6.0.4, )",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "JckRL95hQ/eDHTQZ/BB7jeR0JyF+bOctMW6uriXHY5YPjCX61hiJGsswGjuDSEViKJEPxtPi3e4IwD/1TJ7PIw==",
|
||||
"dependencies": {
|
||||
"CommunityToolkit.HighPerformance": "8.2.2",
|
||||
"CommunityToolkit.HighPerformance": "8.3.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.0",
|
||||
"System.IO.Hashing": "8.0.0",
|
||||
|
@ -156,39 +199,39 @@
|
|||
},
|
||||
"NodaTime": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.2.0, )",
|
||||
"resolved": "3.2.0",
|
||||
"contentHash": "yoRA3jEJn8NM0/rQm78zuDNPA3DonNSZdsorMUj+dltc1D+/Lc5h9YXGqbEEZozMGr37lAoYkcSM/KjTVqD0ow=="
|
||||
"requested": "[3.2.1, )",
|
||||
"resolved": "3.2.1",
|
||||
"contentHash": "D1aHhUfPQUxU2nfDCVuSLahpp0xCYZTmj/KNH3mSK/tStJYcx9HO9aJ0qbOP3hzjGPV/DXOqY2AHe27Nt4xs4g=="
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "cYdOGplIvr9KgsG8nJ8xnzBTImeircbgetlzS1OmepS5dAQW6PuGpVrLOKBNEwEvGYZPsV8037X5vZ/Dmpwz7Q==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "[9.0.0, 10.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[9.0.0, 10.0.0)",
|
||||
"Npgsql": "9.0.2"
|
||||
"Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)",
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "+mfwiRCK+CAKTkeBZCuQuMaOwM/yMX8B65515PS1le9TUjlG8DobuAmb48MSR/Pr/YMvU1tV8FFEFlyQviQzrg==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "QZ80CL3c9xzC83eVMWYWa1RcFZA6HJtpMAKFURlmz+1p0OyysSe8R6f/4sI9vk/nwqF6Fkw3lDgku/xH6HcJYg==",
|
||||
"dependencies": {
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.2",
|
||||
"Npgsql.NodaTime": "9.0.2"
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.4",
|
||||
"Npgsql.NodaTime": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Npgsql.Json.NET": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "E81dvvpNtS4WigxZu16OAFxVvPvbEkXI7vJXZzEp7GQ03MArF5V4HBb7KXDzTaE5ZQ0bhCUFoMTODC6Z8mu27g==",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "lN8p9UKkoXaGUhX3DHg/1W6YeEfbjQiQ7XrJSGREUoDHXOLxDQHJnZ49P/9P2s/pH6HTVgTgT5dijpKoRLN0vQ==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Npgsql": "9.0.2"
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"prometheus-net": {
|
||||
|
@ -212,24 +255,24 @@
|
|||
},
|
||||
"Roslynator.Analyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.12.9, )",
|
||||
"resolved": "4.12.9",
|
||||
"contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w=="
|
||||
"requested": "[4.13.1, )",
|
||||
"resolved": "4.13.1",
|
||||
"contentHash": "KZpLy6ZlCebMk+d/3I5KU2R7AOb4LNJ6tPJqPtvFXmO8bEBHQvCIAvJOnY2tu4C9/aVOROTDYUFADxFqw1gh/g=="
|
||||
},
|
||||
"Scalar.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.55, )",
|
||||
"resolved": "1.2.55",
|
||||
"contentHash": "zArlr6nfPQMRwyia0WFirsyczQby51GhNgWITiEIRkot+CVGZSGQ4oWGqExO11/6x26G+mcQo9Oft1mGpN0/ZQ=="
|
||||
"requested": "[2.0.26, )",
|
||||
"resolved": "2.0.26",
|
||||
"contentHash": "0tKBFM7quBq0ifgRWo7eTTVpiTbnwpf/6ygtb/aYVuo0D2gMsYknAJRqEhH8HFBqzntNiYpzHbQSf2b+VAA8sA=="
|
||||
},
|
||||
"Sentry.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.13.0, )",
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "1cH9hSvjRbTkcpjUejFTrTC3jMIiOrcZ0DIvt16+AYqXhuxPEnI56npR1nhv+7WUGyhyp5cHFIZqrKnyrrGP0w==",
|
||||
"requested": "[5.3.0, )",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zC2yhwQB0laYWGXLYDCsiKSIqleaEK3fUH9Z5t8Bgvfs2nGX0mHmh9oPqNAAbkVGvni56mhgHHCBxN/kpfkawA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Sentry.Extensions.Logging": "4.13.0"
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Sentry.Extensions.Logging": "5.3.0"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
|
@ -264,25 +307,35 @@
|
|||
},
|
||||
"Serilog.Sinks.Seq": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "z5ig56/qzjkX6Fj4U/9m1g8HQaQiYPMZS4Uevtjg1I+WWzoGSf5t/E+6JbMP/jbZYhU63bA5NJN5y0x+qqx2Bw==",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.0.0",
|
||||
"Serilog.Sinks.File": "5.0.0"
|
||||
"Serilog": "4.2.0",
|
||||
"Serilog.Sinks.File": "6.0.0"
|
||||
}
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.6, )",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA=="
|
||||
"requested": "[3.1.7, )",
|
||||
"resolved": "3.1.7",
|
||||
"contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA=="
|
||||
},
|
||||
"StackExchange.Redis": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.8.31, )",
|
||||
"resolved": "2.8.31",
|
||||
"contentHash": "RCHVQa9Zke8k0oBgJn1Yl6BuYy8i6kv+sdMObiH60nOwD6QvWAjxdDwOm+LO78E8WsGiPqgOuItkz98fPS6haQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A=="
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "4TY2Yokh5Xp8XHFhsY9y84yokS7B0rhkaZCXuRiKppIiKwPVH4lVSFD9EEFzRpXdBM5ZeZXD43tc2vB6njEwwQ=="
|
||||
},
|
||||
"System.Text.RegularExpressions": {
|
||||
"type": "Direct",
|
||||
|
@ -306,8 +359,8 @@
|
|||
},
|
||||
"CommunityToolkit.HighPerformance": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.2.2",
|
||||
"contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw=="
|
||||
"resolved": "8.3.0",
|
||||
"contentHash": "2zc0Wfr9OtEbLqm6J1Jycim/nKmYv+v12CytJ3tZGNzw7n3yjh1vNCMX0kIBaFBk3sw8g0pMR86QJGXGlArC+A=="
|
||||
},
|
||||
"EntityFrameworkCore.Exceptions.Common": {
|
||||
"type": "Transitive",
|
||||
|
@ -317,18 +370,46 @@
|
|||
"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.3.0",
|
||||
"contentHash": "jVmB3Nr0JpqhyMiXOGWMin+QvRKpucGpSFBCav9dG6jEJPdBV+yp1RHVpKzxZPfT+0adaBuZlMFdbIciZo1EWA==",
|
||||
"resolved": "4.8.0",
|
||||
"contentHash": "zZ1UoM4FUnSFUJ9fTl5CEEaejR0DNP6+FDt1OfXnjg4igZntcir1tg/8Ufd6WY5vrpmvToAjluYqjVM24A+5lA==",
|
||||
"dependencies": {
|
||||
"MimeKit": "4.3.0"
|
||||
"MimeKit": "4.8.0",
|
||||
"System.Formats.Asn1": "8.0.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.JsonPatch": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "/4UONYoAIeexPoAmbzBPkVGA6KAY7t0BM+1sr0fKss2V1ERCdcM+Llub4X5Ma+LJ60oPp6KzM0e3j+Pp/JHCNw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "bZMRhazEBgw9aZ5EBGYt0017CSd+aecsUCnppVjSa1SzWH6C1ieTSQZRAe+H0DzAVzWAoK7HLwKnQUPioopPrA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CSharp": "4.7.0",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
|
@ -336,27 +417,27 @@
|
|||
},
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "trwJhFrTQuJTImmixMsDnDgRE8zuTzAUAot7WqiUlmjNzlJWLOaXXBpeA/xfNJvZuOsyGjC7RIzEyNyDGhDTLg==",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "KFHRhrGAnd80310lpuWzI7Cf+GidS/h3JaPDFFnSmSGjCxB5vkBv5E+TXclJCJhqPtgNxg+keTC5SF1T9ieG5w==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.27",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.27"
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.36",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.36"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "C6Gh/sAuUACxNtllcH4ZniWtPcGbixJuB1L5RXwoUe1a1wM6rpQ2TVMWpX2+cgeBj8U/izJyWY+nJ4Lz8mmMKA==",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "0OG/wNedsQ9kTMrFuvrUDoJvp6Fxj6BzWgi7AUCluOENxu/0PzbjY9AC5w6mZJ22/AFxn2gFc2m0yOBTfQbiPg==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.27",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.27",
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
"Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.36",
|
||||
"Microsoft.CodeAnalysis.Razor": "6.0.36",
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Razor.Language": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "bI1kIZBgx7oJIB7utPrw4xIgcj7Pdx1jnHMTdsG54U602OcGpBzbfAuKaWs+LVdj+zZVuZsCSoRIZNJKTDP7Hw=="
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
|
@ -410,10 +491,10 @@
|
|||
},
|
||||
"Microsoft.CodeAnalysis.Razor": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.27",
|
||||
"contentHash": "NAUvSjH8QY8gPp/fXjHhi3MnQEGtSJA0iRT/dT3RKO3AdGACPJyGmKEKxLag9+Kf2On51yGHT9DEPPnK3hyezg==",
|
||||
"resolved": "6.0.36",
|
||||
"contentHash": "RTLNJglWezr/1IkiWdtDpPYW7X7lwa4ow8E35cHt+sWdWxOnl+ayQqMy1RfbaLp7CLmRmgXSzMMZZU3D4vZi9Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.27",
|
||||
"Microsoft.AspNetCore.Razor.Language": "6.0.36",
|
||||
"Microsoft.CodeAnalysis.CSharp": "4.0.0",
|
||||
"Microsoft.CodeAnalysis.Common": "4.0.0"
|
||||
}
|
||||
|
@ -449,191 +530,274 @@
|
|||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "fnmifFL8KaA4ZNLCVgfjCWhZUFxkrDInx5hR4qG7Q8IEaSiy/6VOSRFyx55oH7MV4y7wM3J3EE90nSpcVBI44Q=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "oVSjNSIYHsk0N66eqAWgDcyo9etEFbUswbz7SmlYR6nGp05byHrJAYM5N8U2aGWJWJI6WvIC2e4TXJgH6GZ6HQ=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "Qje+DzXJOKiXF72SL0XxNlDtTkvWWvmwknuZtFahY5hIQpRKO59qnGuERIQ3qlzuq5x4bAJ8WMbgU5DLhBgeOQ=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "w4jzX7XI+L3erVGzbHXpx64A3QaLXxqG3f1vPpGYYZGpxOIHkh7e4iLLD7cq4Ng1vjkwzWl5ZJp0Kj/nHsgFYg=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "j+msw6fWgAE9M3Q/5B9Uhv7pdAdAQUvFPJAiBJmoy+OXvehVbfbCE8ftMAa51Uo2ZeiqVnHShhnv4Y4UJJmUzA==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "r7O4N5uaM95InVSGUj7SMOQWN0f1PBF2Y30ow7Jg+pGX5GJCRVd/1fq83lQ50YMyq+EzyHac5o4CDQA2RsjKJQ==",
|
||||
"dependencies": {
|
||||
"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.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.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "a7QhA25n+BzSM5r5d7JznfyluMBGI7z3qyLlFviZ1Eiqv6DdiK27sLZdP/rpYirBM6UYAKxu5TbmfhIy13GN9A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Compliance.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "Te+N4xphDlGIS90lKJMZyezFiMWKLAtYV2/M8gGJG4thH6xyC7LWhMzgz2+tWMehxwZlBUq2D9DvVpjKBZFTPQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.ObjectPool": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "I0O/270E/lUNqbBxlRVjxKOMZyYjP88dpEgQTveml+h2lTzAP4vbawLVwjS9SC7lKaU893bwyyNz0IVJYsm9EA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "WcwfTpl3IcPcaahTVEaJwMUg1eWog1SkIA6jQZZFqMXiMX9/tVkhNB6yzUQmBdGWdlWDDRKpOmK7T7x1Uu05pQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "3ImbcbS68jy9sKr9Z9ToRbEEX0bvIRdb8zyf5ebtL9Av2CUCGHvaO5wsSXfRfAjr60Vrq0tlmNji9IzAxW6EOw=="
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
"Microsoft.Extensions.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "et5JevHsLv1w1O1Zhb6LiUfai/nmDRzIHnbrZJdzLsIbbMCKTZpeHuANYIppAD//n12KvgOne05j4cu0GhG9gw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==",
|
||||
"dependencies": {
|
||||
"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.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "Eeup1LuD5hVk5SsKAuX1D7I9sF380MjrNG10IaaauRLOmrRg8rq2TA8PYTXVBXf3MLkZ6m2xpBqRbZdxf8ygkg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0",
|
||||
"Microsoft.Extensions.Http": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2",
|
||||
"Microsoft.Extensions.Telemetry": "9.2.0",
|
||||
"System.IO.Pipelines": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "loV/0UNpt2bD+6kCDzFALVE63CDtqzPeC0LAetkdhiEr/tTNbvOlQ7CBResH7BQBd3cikrwiBfaHdyHMFUlc2g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "dV9s2Lamc8jSaqhl2BQSPn/AryDIH2sSbQUyLitLXV0ROmsb+SROnn2cH939JFbsNrnf3mIM3GNRKT7P0ldwLg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==",
|
||||
"dependencies": {
|
||||
"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.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Logging": "9.0.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "zr98z+AN8+isdmDmQRuEJ/DAKZGUTHmdv3t0ZzjHvNqvA44nAgkXE9kYtfoN6581iALChhVaSw2Owt+Z2lVbkQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==",
|
||||
"dependencies": {
|
||||
"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.Configuration.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2",
|
||||
"Microsoft.Extensions.Primitives": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA=="
|
||||
},
|
||||
"Microsoft.Extensions.Resilience": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "dyaM+Jeznh/i21bOrrRs3xceFfn0571EOjOq95dRXmL1rHDLC4ExhACJ2xipRBP6g1AgRNqmryi+hMrVWWgmlg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Diagnostics": "9.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": "9.2.0",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2",
|
||||
"Microsoft.Extensions.Telemetry.Abstractions": "9.2.0",
|
||||
"Polly.Extensions": "8.4.2",
|
||||
"Polly.RateLimiting": "8.4.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Telemetry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "4+bw7W4RrAMrND9TxonnSmzJOdXiPxljoda8OPJiReIN607mKCc0t0Mf28sHNsTujO1XQw28wsI0poxeeQxohw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.AmbientMetadata.Application": "9.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "9.0.2",
|
||||
"Microsoft.Extensions.ObjectPool": "9.0.2",
|
||||
"Microsoft.Extensions.Telemetry.Abstractions": "9.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Telemetry.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.2.0",
|
||||
"contentHash": "kEl+5G3RqS20XaEhHh/nOugcjKEK+rgVtMJra1iuwNzdzQXElelf3vu8TugcT7rIZ/T4T76EKW1OX/fmlxz4hw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Compliance.Abstractions": "9.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.2",
|
||||
"Microsoft.Extensions.ObjectPool": "9.0.2",
|
||||
"Microsoft.Extensions.Options": "9.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
|
@ -668,35 +832,67 @@
|
|||
},
|
||||
"Npgsql": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "hCbO8box7i/XXiTFqCJ3GoowyLqx3JXxyrbOJ6om7dr+eAknvBNhhUHeJVGAQo44sySZTfdVffp4BrtPeLZOAA==",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Npgsql.NodaTime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "jURb6VGmmR3pPae2N3HrUixSZ/U5ovqZgg/qo3m5Rq/q0m2fpxbZcsHZo21s5MLa/AfJAx4hcFMY98D4RtLdcg==",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "PMWXCft/iw+5A7eCeMcy6YZXBst6oeisbCkv2JMQVG4SAFa5vQaf6K2voXzUJCqzwOFcCWs+oT42w2uMDFpchw==",
|
||||
"dependencies": {
|
||||
"NodaTime": "3.2.0",
|
||||
"Npgsql": "9.0.2"
|
||||
"Npgsql": "9.0.3"
|
||||
}
|
||||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"Sentry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg=="
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "zlBIP7YmYxySwcgapLMj1gdxPEz9rwdrOa4Yjub/TzcAaMQXusRH9hY4CE6pu0EIibZ7C7Hhjhr6xOTlyK8gFQ=="
|
||||
},
|
||||
"Sentry.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.13.0",
|
||||
"contentHash": "yZ5+TtJKWcss6cG17YjnovImx4X56T8O6Qy6bsMC8tMDttYy8J7HJ2F+WdaZNyjOCo0Rfi6N2gc+Clv/5pf+TQ==",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "DPN6NXvO4LTH21UM2gUFJwSwVa/fuT3B/UZmQyfSfecqViXrZO7WFuKz/h592YUoGNCumyt8x045bxbz6j9btg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.Http": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "8.0.0",
|
||||
"Sentry": "4.13.0"
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
|
||||
"Microsoft.Extensions.Http": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Configuration": "9.0.0",
|
||||
"Sentry": "5.3.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Extensions.Hosting": {
|
||||
|
@ -824,8 +1020,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
|
@ -863,6 +1059,11 @@
|
|||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
|
||||
},
|
||||
"System.Threading.RateLimiting": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.2"/>
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="9.0.2"/>
|
||||
<PackageReference Include="Dapper" Version="2.1.66"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.3"/>
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="9.0.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using Dapper;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.DataMigrator.Models;
|
||||
using NodaTime.Extensions;
|
||||
using Npgsql;
|
||||
|
@ -260,6 +260,6 @@ public class UserMigrator(
|
|||
{
|
||||
if (_preferenceIds.TryGetValue(id, out Snowflake preferenceId))
|
||||
return preferenceId.ToString();
|
||||
return ValidationUtils.DefaultStatusOptions.Contains(id) ? id : "okay";
|
||||
return ValidationService.DefaultStatusOptions.Contains(id) ? id : "okay";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,13 @@
|
|||
"@sveltejs/adapter-node": "^5.2.10",
|
||||
"@sveltejs/kit": "^2.12.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
||||
"@sveltestrap/sveltestrap": "^7.1.0",
|
||||
"@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",
|
||||
|
@ -31,6 +32,7 @@
|
|||
"svelte": "^5.14.3",
|
||||
"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",
|
||||
|
|
25
Foxnouns.Frontend/pnpm-lock.yaml
generated
25
Foxnouns.Frontend/pnpm-lock.yaml
generated
|
@ -55,8 +55,8 @@ importers:
|
|||
specifier: ^5.0.2
|
||||
version: 5.0.2(svelte@5.14.3)(vite@6.0.3(@types/node@22.12.0)(sass@1.83.0))
|
||||
'@sveltestrap/sveltestrap':
|
||||
specifier: ^6.2.7
|
||||
version: 6.2.7(svelte@5.14.3)
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0(svelte@5.14.3)
|
||||
'@types/eslint':
|
||||
specifier: ^9.6.1
|
||||
version: 9.6.1
|
||||
|
@ -72,6 +72,9 @@ importers:
|
|||
bootstrap:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3(@popperjs/core@2.11.8)
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.4.7
|
||||
eslint:
|
||||
specifier: ^9.17.0
|
||||
version: 9.17.0
|
||||
|
@ -102,6 +105,9 @@ importers:
|
|||
svelte-check:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1(picomatch@4.0.2)(svelte@5.14.3)(typescript@5.7.2)
|
||||
svelte-easy-crop:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(svelte@5.14.3)
|
||||
sveltekit-i18n:
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2(svelte@5.14.3)
|
||||
|
@ -1002,8 +1008,8 @@ packages:
|
|||
'@sveltekit-i18n/parser-default@1.1.1':
|
||||
resolution: {integrity: sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==}
|
||||
|
||||
'@sveltestrap/sveltestrap@6.2.7':
|
||||
resolution: {integrity: sha512-WwLLfAFUb42BGuRrf3Vbct30bQMzlEMMipN/MfxhjuLTmLQeW9muVJfPyvjtWS+mY+RjkSCoHvAp/ZobP1NLlQ==}
|
||||
'@sveltestrap/sveltestrap@7.1.0':
|
||||
resolution: {integrity: sha512-TpIx25kqLV+z+VD3yfqYayOI1IaCeWFbT0uqM6NfA4vQgDs9PjFwmjkU4YEAlV/ngs9e7xPmaRWE7lkrg4Miow==}
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0 || ^5.0.0 || ^5.0.0-next.0
|
||||
|
||||
|
@ -1967,6 +1973,11 @@ packages:
|
|||
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
svelte-easy-crop@4.0.0:
|
||||
resolution: {integrity: sha512-/asrrCYypXwCsPqJ07m7s7QArJwrdfEt7D1UN9hC4WF3GgEtuqmGuVi5DGeJVtBpKu5388gYFtCgQz9lA+/Rtg==}
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
|
||||
svelte-eslint-parser@0.43.0:
|
||||
resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -3079,7 +3090,7 @@ snapshots:
|
|||
|
||||
'@sveltekit-i18n/parser-default@1.1.1': {}
|
||||
|
||||
'@sveltestrap/sveltestrap@6.2.7(svelte@5.14.3)':
|
||||
'@sveltestrap/sveltestrap@7.1.0(svelte@5.14.3)':
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
svelte: 5.14.3
|
||||
|
@ -4051,6 +4062,10 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- picomatch
|
||||
|
||||
svelte-easy-crop@4.0.0(svelte@5.14.3):
|
||||
dependencies:
|
||||
svelte: 5.14.3
|
||||
|
||||
svelte-eslint-parser@0.43.0(svelte@5.14.3):
|
||||
dependencies:
|
||||
eslint-scope: 7.2.2
|
||||
|
|
|
@ -10,6 +10,7 @@ export type Meta = {
|
|||
};
|
||||
members: number;
|
||||
limits: Limits;
|
||||
notice: { id: string; message: string } | null;
|
||||
};
|
||||
|
||||
export type Limits = {
|
||||
|
|
|
@ -112,3 +112,12 @@ export enum ClearableField {
|
|||
Flags = "FLAGS",
|
||||
CustomPreferences = "CUSTOM_PREFERENCES",
|
||||
}
|
||||
|
||||
export type Notification = {
|
||||
id: string;
|
||||
type: "NOTICE" | "WARNING" | "SUSPENSION";
|
||||
message?: string;
|
||||
localization_key?: string;
|
||||
localization_params: Record<string, string>;
|
||||
acknowledged: boolean;
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ export type MeUser = UserWithMembers & {
|
|||
timezone: string;
|
||||
suspended: boolean;
|
||||
deleted: boolean;
|
||||
settings: UserSettings;
|
||||
};
|
||||
|
||||
export type UserWithMembers = User & { members: PartialMember[] | null };
|
||||
|
@ -40,6 +41,7 @@ export type UserWithHiddenFields = User & {
|
|||
|
||||
export type UserSettings = {
|
||||
dark_mode: boolean | null;
|
||||
last_read_notice: string | null;
|
||||
};
|
||||
|
||||
export type PartialMember = {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Envelope from "svelte-bootstrap-icons/lib/Envelope.svelte";
|
||||
import CashCoin from "svelte-bootstrap-icons/lib/CashCoin.svelte";
|
||||
import Logo from "./Logo.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = { meta: Meta };
|
||||
let { meta }: Props = $props();
|
||||
|
@ -18,13 +19,13 @@
|
|||
<div class="align-start flex-grow-1">
|
||||
<Logo />
|
||||
<ul class="mt-2 list-unstyled">
|
||||
<li><strong>Version</strong> {meta.version}</li>
|
||||
<li><strong>{$t("footer.version")}</strong> {meta.version}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="align-end">
|
||||
<ul class="list-unstyled">
|
||||
<li>{meta.users.total.toLocaleString()} <strong>users</strong></li>
|
||||
<li>{meta.members.toLocaleString()} <strong>members</strong></li>
|
||||
<li>{meta.users.total.toLocaleString()} <strong>{$t("footer.users")}</strong></li>
|
||||
<li>{meta.members.toLocaleString()} <strong>{$t("footer.members")}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,7 +37,7 @@
|
|||
>
|
||||
<li class="list-inline-item">
|
||||
<Git />
|
||||
Source code
|
||||
{$t("footer.source")}
|
||||
</li>
|
||||
</a>
|
||||
<a
|
||||
|
@ -46,37 +47,37 @@
|
|||
>
|
||||
<li class="list-inline-item">
|
||||
<Reception4 />
|
||||
Status
|
||||
{$t("footer.status")}
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/about">
|
||||
<li class="list-inline-item">
|
||||
<Envelope />
|
||||
About and contact
|
||||
{$t("footer.about-contact")}
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/tos">
|
||||
<li class="list-inline-item">
|
||||
<CardText />
|
||||
Terms of service
|
||||
{$t("footer.terms")}
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/privacy">
|
||||
<li class="list-inline-item">
|
||||
<Shield />
|
||||
Privacy policy
|
||||
{$t("footer.privacy")}
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/changelog">
|
||||
<li class="list-inline-item">
|
||||
<Newspaper />
|
||||
Changelog
|
||||
{$t("footer.changelog")}
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/donate">
|
||||
<li class="list-inline-item">
|
||||
<CashCoin />
|
||||
Donate
|
||||
{$t("footer.donate")}
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
|
|
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { fastRequest } from "$api";
|
||||
import type { UserSettings } from "$api/models";
|
||||
import { idTimestamp } from "$lib";
|
||||
import { t } from "$lib/i18n";
|
||||
import log from "$lib/log";
|
||||
import { renderUnsafeMarkdown } from "$lib/markdown";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
type Props = { id: string; message: string; settings?: UserSettings; token: string | null };
|
||||
let { id, message, settings, token }: Props = $props();
|
||||
|
||||
let lastReadNotice = $state(settings?.last_read_notice || null);
|
||||
|
||||
// Render the notice if:
|
||||
// - user is not logged in (no settings object)
|
||||
// - last read notice is null (never marked any notice as read)
|
||||
// - last read notice ID is smaller than the current one (has not marked the current notice as read)
|
||||
let renderNotice = $derived(!lastReadNotice || lastReadNotice < id);
|
||||
let canDismiss = $derived(!!token);
|
||||
let renderedMessage = $derived(renderUnsafeMarkdown(message));
|
||||
|
||||
let dismiss = async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
await fastRequest("PATCH", "/users/@me/settings", { token, body: { last_read_notice: id } });
|
||||
lastReadNotice = id;
|
||||
} catch (e) {
|
||||
log.error("error updating last read notice ID:", e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if renderNotice}
|
||||
<div class="alert alert-light" role="alert">
|
||||
<div>
|
||||
{@html renderedMessage}
|
||||
</div>
|
||||
{#if canDismiss}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y_invalid_attribute -->
|
||||
<a href="#" tabindex="0" role="button" onclick={() => dismiss()} onkeyup={() => dismiss()}>
|
||||
{$t("notification.mark-as-read")}
|
||||
</a>
|
||||
• {idTimestamp(id).toLocaleString(DateTime.DATETIME_MED)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -13,13 +13,21 @@
|
|||
import Logo from "$components/Logo.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = { user: MeUser | null; meta: Meta };
|
||||
let { user, meta }: Props = $props();
|
||||
type Props = { user: MeUser | null; meta: Meta; unreadNotifications?: boolean };
|
||||
let { user, meta, unreadNotifications }: Props = $props();
|
||||
|
||||
let isOpen = $state(true);
|
||||
const toggleMenu = () => (isOpen = !isOpen);
|
||||
</script>
|
||||
|
||||
{#if user && unreadNotifications}
|
||||
<div class="notification-alert text-center py-3 mb-2 px-2">
|
||||
<strong>{$t("nav.unread-notification-text")}</strong>
|
||||
<br />
|
||||
<a href="/settings/notifications">{$t("nav.unread-notification-link")}</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if user && user.deleted}
|
||||
<div class="deleted-alert text-center py-3 mb-2 px-2">
|
||||
{#if user.suspended}
|
||||
|
@ -87,6 +95,11 @@
|
|||
background-color: var(--bs-danger-bg-subtle);
|
||||
}
|
||||
|
||||
.notification-alert {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
}
|
||||
|
||||
/* These exact values make it look almost identical to the SVG version, which is what we want */
|
||||
#beta-text {
|
||||
font-size: 0.7em;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import Avatar from "$components/Avatar.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import { Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
import { Icon, InputGroup, Modal } from "@sveltestrap/sveltestrap";
|
||||
import Cropper, { type CropArea, type OnCropCompleteEvent } from "svelte-easy-crop";
|
||||
import { encode } from "base64-arraybuffer";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import ShortNoscriptWarning from "./ShortNoscriptWarning.svelte";
|
||||
|
@ -21,6 +22,7 @@
|
|||
let avatar: string = $state("");
|
||||
let avatarExists = $derived(avatar !== "");
|
||||
let avatarTooLarge = $derived(avatar !== "" && avatar.length > MAX_AVATAR_BYTES);
|
||||
let cropperOpen = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
getAvatar(avatarFiles);
|
||||
|
@ -28,7 +30,7 @@
|
|||
|
||||
const getAvatar = async (list: FileList | null) => {
|
||||
if (!list || list.length === 0) {
|
||||
avatar = "";
|
||||
uncroppedAvatar = "";
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -36,7 +38,47 @@
|
|||
const base64 = encode(buffer);
|
||||
|
||||
const uri = `data:${list[0].type};base64,${base64}`;
|
||||
avatar = uri;
|
||||
uncroppedAvatar = uri;
|
||||
cropperOpen = true;
|
||||
};
|
||||
|
||||
let uncroppedAvatar: string = $state("");
|
||||
let crop = $state({ x: 0, y: 0 });
|
||||
let zoom = $state(1);
|
||||
let croppedArea = $state({ x: 0, y: 0, height: 0, width: 0 } satisfies CropArea);
|
||||
|
||||
const onCropComplete = (e: OnCropCompleteEvent) => {
|
||||
croppedArea = e.pixels;
|
||||
};
|
||||
|
||||
const doCrop = () => {
|
||||
cropperOpen = false;
|
||||
if (!uncroppedAvatar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
canvas.width = croppedArea.width;
|
||||
canvas.height = croppedArea.height;
|
||||
|
||||
ctx?.drawImage(
|
||||
img,
|
||||
croppedArea.x,
|
||||
croppedArea.y,
|
||||
croppedArea.width,
|
||||
croppedArea.height,
|
||||
0,
|
||||
0,
|
||||
croppedArea.width,
|
||||
croppedArea.height,
|
||||
);
|
||||
|
||||
avatar = canvas.toDataURL("image/webp", 1);
|
||||
};
|
||||
img.src = uncroppedAvatar;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -44,6 +86,41 @@
|
|||
<Avatar {name} url={avatarExists ? avatar : current} {alt} />
|
||||
</p>
|
||||
|
||||
<Modal
|
||||
isOpen={cropperOpen}
|
||||
autoFocus
|
||||
backdrop
|
||||
fade
|
||||
keyboard
|
||||
returnFocusAfterClose
|
||||
toggle={() => (cropperOpen = !cropperOpen)}
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">{$t("editor.crop-avatar-header")}</h1>
|
||||
</div>
|
||||
<div class="modal-body cropper-wrapper">
|
||||
{#if uncroppedAvatar}
|
||||
<Cropper
|
||||
image={uncroppedAvatar}
|
||||
{crop}
|
||||
{zoom}
|
||||
minZoom={1}
|
||||
maxZoom={4}
|
||||
aspect={1 / 1}
|
||||
oncropcomplete={onCropComplete}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick={() => doCrop()}>
|
||||
{$t("editor.crop-avatar-button")}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick={() => (cropperOpen = false)}>
|
||||
{$t("cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<InputGroup class="mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
|
@ -53,7 +130,7 @@
|
|||
accept="image/png, image/jpeg, image/gif, image/webp"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-primary"
|
||||
disabled={!avatarExists || avatarTooLarge}
|
||||
onclick={() => onclick(avatar)}
|
||||
>
|
||||
|
@ -79,3 +156,9 @@
|
|||
})}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.cropper-wrapper {
|
||||
min-height: 30em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
<div class="alert alert-secondary">
|
||||
{$t("editor.custom-preference-notice")}
|
||||
<a href="/settings/pref" class="alert-link">{$t("editor.custom-preference-notice-link")}</a>
|
||||
<a href="/settings/prefs" class="alert-link">{$t("editor.custom-preference-notice-link")}</a>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import FieldEditor from "./FieldEditor.svelte";
|
||||
import FormStatusMarker from "./FormStatusMarker.svelte";
|
||||
import NoscriptWarning from "./NoscriptWarning.svelte";
|
||||
import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte";
|
||||
|
||||
type Props = {
|
||||
fields: Field[];
|
||||
|
@ -45,6 +46,7 @@
|
|||
|
||||
<NoscriptWarning />
|
||||
<FormStatusMarker form={ok} />
|
||||
<CustomPreferencesNotice />
|
||||
|
||||
<h4>{$t("edit-profile.editing-fields-header")}</h4>
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
icon="chevron-down"
|
||||
color="secondary"
|
||||
tooltip={$t("editor.move-entry-down")}
|
||||
onclick={() => moveValue(index, true)}
|
||||
onclick={() => moveValue(index, false)}
|
||||
/>
|
||||
<input type="text" class="form-control" bind:value={value.value} autocomplete="off" />
|
||||
<ButtonDropdown>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import type { Notification } from "$api/models/moderation";
|
||||
import { t } from "$lib/i18n";
|
||||
import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte";
|
||||
import ExclamationTriangleFill from "svelte-bootstrap-icons/lib/ExclamationTriangleFill.svelte";
|
||||
import XOctagonFill from "svelte-bootstrap-icons/lib/XOctagonFill.svelte";
|
||||
import QuestionCircleFill from "svelte-bootstrap-icons/lib/QuestionCircleFill.svelte";
|
||||
import { idTimestamp } from "$lib";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
type Props = { notification: Notification };
|
||||
let { notification }: Props = $props();
|
||||
|
||||
let Icon = $derived.by(() => {
|
||||
if (notification.type === "NOTICE") return InfoCircleFill;
|
||||
if (notification.type === "WARNING") return ExclamationTriangleFill;
|
||||
if (notification.type === "SUSPENSION") return XOctagonFill;
|
||||
return QuestionCircleFill;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex">
|
||||
<div aria-hidden="true">
|
||||
<Icon width={48} height={48} />
|
||||
</div>
|
||||
<div class="mx-3">
|
||||
<p class="card-text text-has-newline">
|
||||
{#if notification.localization_key}
|
||||
{$t(notification.localization_key, notification.localization_params)}
|
||||
{:else}
|
||||
{notification.message}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
{idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)}
|
||||
• <a href="/settings/notifications/ack/{notification.id}">{$t("notification.mark-as-read")}</a>
|
||||
</div>
|
||||
</div>
|
|
@ -9,7 +9,9 @@
|
|||
"reactivate-account-link": "Reactivate account",
|
||||
"delete-permanently-link": "I want my account deleted permanently",
|
||||
"reactivate-or-delete-link": "I want to reactivate my account or delete all my data",
|
||||
"export-link": "I want to export a copy of my data"
|
||||
"export-link": "I want to export a copy of my data",
|
||||
"unread-notification-text": "You have an unread notification.",
|
||||
"unread-notification-link": "Go to your notifications"
|
||||
},
|
||||
"avatar-tooltip": "Avatar for {{name}}",
|
||||
"profile": {
|
||||
|
@ -86,7 +88,8 @@
|
|||
"unlink-discord-header": "Unlink Discord account",
|
||||
"unlink-confirmation-1": "Are you sure you want to unlink {{username}} from your account?",
|
||||
"unlink-confirmation-2": "You will no longer be able to use this account to log in. Please make sure at least one of your other linked accounts is accessible before continuing.",
|
||||
"unlink-button": "Unlink account"
|
||||
"unlink-button": "Unlink account",
|
||||
"log-in-3rd-party-desc-no-email": "You can use any of the following services to log in. You can add or remove others at any time."
|
||||
},
|
||||
"error": {
|
||||
"bad-request-header": "Something was wrong with your input",
|
||||
|
@ -291,7 +294,9 @@
|
|||
"custom-preference-muted": "Show as muted text",
|
||||
"custom-preference-favourite": "Treat like favourite",
|
||||
"custom-preference-notice": "Want to edit your custom preferences?",
|
||||
"custom-preference-notice-link": "Go to settings"
|
||||
"custom-preference-notice-link": "Go to settings",
|
||||
"crop-avatar-header": "Crop avatar",
|
||||
"crop-avatar-button": "Crop"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"report": {
|
||||
|
@ -326,6 +331,27 @@
|
|||
},
|
||||
"alert": {
|
||||
"auth-method-remove-success": "Successfully unlinked account!",
|
||||
"auth-required": "You must log in to access this page."
|
||||
"auth-required": "You must log in to access this page.",
|
||||
"notif-ack-successful": "Successfully marked notification as read!",
|
||||
"notif-ack-fail": "Could not mark notification as read."
|
||||
},
|
||||
"footer": {
|
||||
"version": "Version",
|
||||
"users": "users",
|
||||
"members": "members",
|
||||
"source": "Source code",
|
||||
"status": "Status",
|
||||
"terms": "Terms of service",
|
||||
"privacy": "Privacy policy",
|
||||
"changelog": "Changelog",
|
||||
"donate": "Donate",
|
||||
"about-contact": "About and contact"
|
||||
},
|
||||
"notification": {
|
||||
"suspension": "Your account has been suspended for the following reason: {{reason}}",
|
||||
"warning": "You have been warned for the following reason: {{reason}}",
|
||||
"warning-cleared-fields": "You have been warned for the following reason: {{reason}}\n\nAdditionally, the following fields have been cleared from your profile:\n{{clearedFields}}",
|
||||
"mark-as-read": "Mark as read",
|
||||
"no-notifications": "You have no notifications."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,24 @@ import { clearToken, TOKEN_COOKIE_NAME } from "$lib";
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { ErrorCode } from "$api/error";
|
||||
import type { Meta, MeUser } from "$api/models";
|
||||
import type { Notification } from "$api/models/moderation";
|
||||
import log from "$lib/log";
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, cookies }) => {
|
||||
let token: string | null = null;
|
||||
let meUser: MeUser | null = null;
|
||||
let unreadNotifications: boolean = false;
|
||||
if (cookies.get(TOKEN_COOKIE_NAME)) {
|
||||
try {
|
||||
meUser = await apiRequest<MeUser>("GET", "/users/@me", { fetch, cookies });
|
||||
token = cookies.get(TOKEN_COOKIE_NAME) || null;
|
||||
|
||||
const notifications = await apiRequest<Notification[]>("GET", "/notifications", {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
unreadNotifications = notifications.filter((n) => !n.acknowledged).length > 0;
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.code === ErrorCode.AuthenticationRequired) clearToken(cookies);
|
||||
else log.error("Could not fetch /users/@me and token has not expired:", e);
|
||||
|
@ -19,5 +27,5 @@ export const load = (async ({ fetch, cookies }) => {
|
|||
}
|
||||
|
||||
const meta = await apiRequest<Meta>("GET", "/meta", { fetch, cookies });
|
||||
return { meta, meUser, token };
|
||||
return { meta, meUser, token, unreadNotifications };
|
||||
}) satisfies LayoutServerLoad;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<div class="flex-grow-1">
|
||||
<Navbar user={data.meUser} meta={data.meta} />
|
||||
<Navbar user={data.meUser} meta={data.meta} unreadNotifications={data.unreadNotifications} />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<Footer meta={data.meta} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData };
|
||||
|
@ -10,6 +11,15 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
{#if data.meta.notice}
|
||||
<GlobalNotice
|
||||
id={data.meta.notice.id}
|
||||
message={data.meta.notice.message}
|
||||
settings={data.meUser?.settings}
|
||||
token={data.token}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<h1>pronouns.cc</h1>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -50,8 +50,13 @@
|
|||
<div class="col-lg-3"></div>
|
||||
{/if}
|
||||
<div class="col-md">
|
||||
<h3>{$t("auth.log-in-3rd-party-header")}</h3>
|
||||
<p>{$t("auth.log-in-3rd-party-desc")}</p>
|
||||
{#if data.urls.email_enabled}
|
||||
<h3>{$t("auth.log-in-3rd-party-header")}</h3>
|
||||
<p>{$t("auth.log-in-3rd-party-desc")}</p>
|
||||
{:else}
|
||||
<h3>{$t("title.log-in")}</h3>
|
||||
<p>{$t("auth.log-in-3rd-party-desc-no-email")}</p>
|
||||
{/if}
|
||||
<form method="POST" action="?/fediToggle" use:enhance>
|
||||
<div class="list-group">
|
||||
{#if data.urls.discord}
|
||||
|
|
|
@ -3,15 +3,28 @@
|
|||
import { t } from "$lib/i18n";
|
||||
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
|
||||
import { isActive } from "$lib/pageUtils.svelte";
|
||||
import type { LayoutData } from "./$types";
|
||||
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||
|
||||
type Props = { children: Snippet };
|
||||
let { children }: Props = $props();
|
||||
type Props = { data: LayoutData; children: Snippet };
|
||||
let { data, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("title.settings")} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if data.meta.notice}
|
||||
<div class="container">
|
||||
<GlobalNotice
|
||||
id={data.meta.notice.id}
|
||||
message={data.meta.notice.message}
|
||||
settings={data.meUser?.settings}
|
||||
token={data.token}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="container">
|
||||
<Nav pills justified fill class="flex-column flex-md-row mb-2">
|
||||
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { type RawApiError } from "$api/error";
|
||||
import { mergePreferences, type User } from "$api/models/user";
|
||||
import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte";
|
||||
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
||||
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
||||
import log from "$lib/log";
|
||||
import ephemeralState from "$lib/state.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -38,7 +36,4 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<NoscriptWarning />
|
||||
<CustomPreferencesNotice />
|
||||
|
||||
<FieldsEditor bind:fields {ok} {allPreferences} {update} />
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { apiRequest } from "$api";
|
||||
import type { Notification } from "$api/models/moderation";
|
||||
import { alertKey } from "$lib";
|
||||
|
||||
export const load = async ({ url, fetch, cookies }) => {
|
||||
const notifications = await apiRequest<Notification[]>("GET", "/notifications", {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
return { notifications, alertKey: alertKey(url) };
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
import Notification from "$components/settings/Notification.svelte";
|
||||
import UrlAlert from "$components/URLAlert.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<UrlAlert {data} />
|
||||
|
||||
{#each data.notifications as notification (notification.id)}
|
||||
<Notification {notification} />
|
||||
{:else}
|
||||
{$t("notification.no-notifications")}
|
||||
{/each}
|
|
@ -0,0 +1,14 @@
|
|||
import { redirect } from "@sveltejs/kit";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { fastRequest } from "$api";
|
||||
import log from "$lib/log";
|
||||
|
||||
export const GET: RequestHandler = async ({ params, fetch, cookies }) => {
|
||||
try {
|
||||
await fastRequest("PUT", `/notifications/${params.id}/ack`, { fetch, cookies });
|
||||
} catch (e) {
|
||||
log.error("error acking notification %s:", params.id, e);
|
||||
redirect(303, "/settings/notifications?alert=notif-ack-fail");
|
||||
}
|
||||
redirect(303, "/settings/notifications?alert=notif-ack-successful");
|
||||
};
|
|
@ -2,9 +2,7 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { type RawApiError } from "$api/error";
|
||||
import { mergePreferences, type User } from "$api/models/user";
|
||||
import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte";
|
||||
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
||||
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
||||
import log from "$lib/log";
|
||||
import ephemeralState from "$lib/state.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -38,7 +36,4 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<NoscriptWarning />
|
||||
<CustomPreferencesNotice />
|
||||
|
||||
<FieldsEditor bind:fields {ok} {allPreferences} {update} />
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import adapter from "@sveltejs/adapter-node";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { config as dotenv } from "dotenv";
|
||||
dotenv({
|
||||
path: [path.resolve(process.cwd(), ".env"), path.resolve(process.cwd(), ".env.local")],
|
||||
});
|
||||
|
||||
console.log(process.env.NODE_ENV);
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
|
@ -21,6 +30,9 @@ const config = {
|
|||
// we only disable it during development, during building NODE_ENV == production
|
||||
checkOrigin: process.env.NODE_ENV !== "development",
|
||||
},
|
||||
paths: {
|
||||
assets: isProd ? process.env.PRIVATE_ASSETS_PREFIX || "" : "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
36
build.sh
Executable file
36
build.sh
Executable file
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
ROOT_DIR=$(pwd)
|
||||
|
||||
echo "Cleaning output directory ($ROOT_DIR/build)"
|
||||
|
||||
[ -d "$ROOT_DIR/build" ] && rm -r "$ROOT_DIR/build"
|
||||
mkdir "$ROOT_DIR/build"
|
||||
|
||||
echo "Building .NET backend"
|
||||
|
||||
cd "$ROOT_DIR/Foxnouns.Backend"
|
||||
[ -d "$ROOT_DIR/Foxnouns.Backend/out" ] && rm -r "$ROOT_DIR/Foxnouns.Backend/out"
|
||||
dotnet publish --artifacts-path "$ROOT_DIR/Foxnouns.Backend/out"
|
||||
mv "$ROOT_DIR/Foxnouns.Backend/out/publish/Foxnouns.Backend/"* "$ROOT_DIR/build/bin"
|
||||
|
||||
echo "Building Go rate limiter"
|
||||
|
||||
cd "$ROOT_DIR/Foxnouns.RateLimiter"
|
||||
go build -o rate -v .
|
||||
mv rate "$ROOT_DIR/build/rate"
|
||||
|
||||
echo "Building Node.js frontend"
|
||||
|
||||
cd "$ROOT_DIR/Foxnouns.Frontend"
|
||||
[ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build"
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
mkdir "$ROOT_DIR/build/fe"
|
||||
cp -r build .env* package.json pnpm-lock.yaml "$ROOT_DIR/build/fe"
|
||||
cd "$ROOT_DIR/build/fe"
|
||||
pnpm install -P
|
||||
|
||||
echo "Finished building Foxnouns.NET"
|
|
@ -7,6 +7,7 @@ services:
|
|||
environment:
|
||||
- "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres"
|
||||
- "Database:EnablePooling=true"
|
||||
- "Database:Redis=redis:6379"
|
||||
- "Host=0.0.0.0"
|
||||
- "Port=5000"
|
||||
- "Logging:MetricsPort=5001"
|
||||
|
@ -31,7 +32,7 @@ services:
|
|||
|
||||
rate:
|
||||
image: rate
|
||||
build: ./rate
|
||||
build: ./Foxnouns.RateLimiter
|
||||
environment:
|
||||
- "PORT=5003"
|
||||
restart: unless-stopped
|
||||
|
@ -52,6 +53,12 @@ services:
|
|||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: registry.redict.io/redict:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redict_data:/data
|
||||
|
||||
caddy:
|
||||
image: docker.io/caddy:2
|
||||
restart: unless-stopped
|
||||
|
@ -67,3 +74,4 @@ volumes:
|
|||
caddy_data:
|
||||
caddy_config:
|
||||
postgres_data:
|
||||
redict_data:
|
53
docker-compose.prebuilt.yml
Normal file
53
docker-compose.prebuilt.yml
Normal file
|
@ -0,0 +1,53 @@
|
|||
services:
|
||||
backend:
|
||||
image: code.vulpine.solutions/sam/foxnouns-be:latest
|
||||
environment:
|
||||
- "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres"
|
||||
- "Database:EnablePooling=true"
|
||||
- "Database:Redis=redis:6379"
|
||||
- "Host=0.0.0.0"
|
||||
- "Port=5000"
|
||||
- "Logging:MetricsPort=5001"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5001:5000"
|
||||
- "5002:5001"
|
||||
volumes:
|
||||
- ./docker/config.ini:/app/config.ini
|
||||
- ./docker/static-pages:/app/static-pages
|
||||
|
||||
rate:
|
||||
image: code.vulpine.solutions/sam/foxnouns-rate:latest
|
||||
environment:
|
||||
- "PORT=5003"
|
||||
ports:
|
||||
- "5003:5003"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker/proxy-config.json:/app/proxy-config.json
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:16
|
||||
command: [ "postgres",
|
||||
"-c", "max-connections=1000",
|
||||
"-c", "timezone=Etc/UTC",
|
||||
"-c", "max_wal_size=1GB",
|
||||
"-c", "min_wal_size=80MB",
|
||||
"-c", "shared_buffers=128MB" ]
|
||||
environment:
|
||||
- "POSTGRES_PASSWORD=postgres"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: registry.redict.io/redict:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redict_data:/data
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
postgres_data:
|
||||
redict_data:
|
Loading…
Add table
Reference in a new issue