diff --git a/Foxnouns.Backend/Controllers/Moderation/SupportController.cs b/Foxnouns.Backend/Controllers/Moderation/SupportController.cs deleted file mode 100644 index c03e2f6..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/SupportController.cs +++ /dev/null @@ -1,250 +0,0 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; - -namespace Foxnouns.Backend.Controllers.Moderation; - -[Route("/api/v2/support")] -public class SupportController( - ILogger logger, - DatabaseContext db, - SupportRendererService supportRenderer, - ISnowflakeGenerator snowflakeGenerator -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpGet] - [ProducesResponseType>( - statusCode: StatusCodes.Status200OK - )] - [Authorize(":moderator")] - public async Task GetOpenConversationsAsync(CancellationToken ct = default) - { - var conversations = await db - .SupportConversations.Where(c => c.Status == SupportConversationStatus.Open) - .OrderBy(c => c.Id) - .ToListAsync(ct); - - return Ok(conversations.Select(supportRenderer.ToResponse)); - } - - [HttpGet("{id}")] - [ProducesResponseType( - statusCode: StatusCodes.Status200OK - )] - [Authorize("user.support")] - public async Task GetConversationAsync( - Snowflake id, - CancellationToken ct = default - ) - { - var (_, conversation) = await ResolveConversationAsync(id, ct); - - return Ok(supportRenderer.ToResponse(conversation)); - } - - [HttpGet("{id}/messages")] - [ProducesResponseType( - statusCode: StatusCodes.Status200OK - )] - [Authorize("user.support")] - public async Task GetConversationMessagesAsync( - Snowflake id, - CancellationToken ct = default - ) - { - var (isModerator, conversation) = await ResolveConversationAsync(id, ct); - - var messages = await db - .SupportMessages.Where(m => m.ConversationId == conversation.Id) - .OrderBy(m => m.Id) - .ToListAsync(ct); - - return Ok(messages.Select(m => supportRenderer.ToResponse(m, modView: isModerator))); - } - - private const int MaxOpenConversations = 5; - private const int MinTitleLength = 5; - private const int MaxTitleLength = 512; - private const int MaxMessageLength = 4096; - - [HttpPost] - [ProducesResponseType( - statusCode: StatusCodes.Status200OK - )] - [Authorize("user.support")] - public async Task CreateConversationAsync( - [FromBody] CreateConversationRequest req - ) - { - var openCount = await db - .SupportConversations.Where(c => - c.UserId == CurrentUser!.Id && c.Status == SupportConversationStatus.Open - ) - .CountAsync(); - if (openCount >= MaxOpenConversations) - throw new ApiError.BadRequest("Too many open conversations."); - - req.Title.ValidateStringLength("title", MinTitleLength, MaxTitleLength); - req.InitialMessage.Content?.ValidateStringLength( - "initial_message.content", - 0, - MaxMessageLength - ); - - var conversation = new SupportConversation - { - Id = snowflakeGenerator.GenerateSnowflake(), - User = CurrentUser!, - Title = req.Title, - }; - - var message = new SupportMessage - { - Id = snowflakeGenerator.GenerateSnowflake(), - ConversationId = conversation.Id, - UserId = CurrentUser!.Id, - IsAnonymous = false, - Content = req.InitialMessage.Content, - }; - - db.Add(conversation); - db.Add(message); - - await db.SaveChangesAsync(); - - return Ok(supportRenderer.ToResponse(conversation)); - } - - [HttpPost("{id}/messages")] - public async Task CreateMessageAsync( - Snowflake id, - [FromBody] CreateMessageRequest req - ) - { - var (isModerator, conversation) = await ResolveConversationAsync(id); - - req.Content?.ValidateStringLength("content", 0, MaxMessageLength); - if (req.IsAnonymous && !isModerator) - { - throw new ApiError.BadRequest( - "Only moderators can mark a message as anonymous.", - "is_anonymous", - true - ); - } - - var message = new SupportMessage - { - Id = snowflakeGenerator.GenerateSnowflake(), - ConversationId = conversation.Id, - UserId = CurrentUser!.Id, - IsAnonymous = req.IsAnonymous, - Content = req.Content, - }; - - db.Add(message); - await db.SaveChangesAsync(); - - return Ok(supportRenderer.ToResponse(message, modView: isModerator)); - } - - private async Task<( - bool IsModerator, - SupportConversation Conversation - )> ResolveConversationAsync(Snowflake id, CancellationToken ct = default) - { - var isModerator = CurrentUser!.Role is UserRole.Moderator or UserRole.Admin; - - var conversation = await db.SupportConversations.FindAsync([id], ct); - if (conversation == null) - throw new ApiError.NotFound("Conversation not found"); - if (!isModerator && conversation.UserId != CurrentUser.Id) - throw new ApiError.NotFound("Conversation not found"); - - return (isModerator, conversation); - } - - [HttpPatch("{id}")] - [ProducesResponseType( - statusCode: StatusCodes.Status200OK - )] - [Authorize("user.support")] - public async Task UpdateConversationAsync( - Snowflake id, - [FromBody] UpdateConversationRequest req - ) - { - var isModerator = CurrentUser!.Role is UserRole.Moderator or UserRole.Admin; - - var conversation = await db.SupportConversations.FindAsync(id); - if (conversation == null) - throw new ApiError.NotFound("Conversation not found"); - if (!isModerator && conversation.UserId != CurrentUser.Id) - throw new ApiError.NotFound("Conversation not found"); - - if ( - !isModerator - && ( - req.AssignedTo != null - || (req.Status != null && req.Status != SupportConversationStatus.Closed) - ) - ) - { - throw new ApiError.Forbidden("You can't change those options."); - } - - if (req.Title != null) - { - req.Title.ValidateStringLength("title", MinTitleLength, MaxTitleLength); - conversation.Title = req.Title; - } - - if (req.HasProperty(nameof(req.AssignedTo))) - { - if (req.AssignedTo == null) - conversation.AssignedToId = null; - else - { - var assignedToUser = await db.ResolveUserAsync(req.AssignedTo.Value); - if (assignedToUser.Role is not (UserRole.Admin or UserRole.Moderator)) - { - throw new ApiError.BadRequest( - "That user is not a moderator.", - "assigned_to", - req.AssignedTo - ); - } - - conversation.AssignedToId = assignedToUser.Id; - } - } - - if (req.Status != null) - conversation.Status = req.Status.Value; - - db.Update(conversation); - await db.SaveChangesAsync(); - - return Ok(supportRenderer.ToResponse(conversation)); - } - - public record CreateConversationRequest(string Title, CreateMessageRequest InitialMessage); - - public record CreateMessageRequest(string? Content, bool IsAnonymous = false); - - public class UpdateConversationRequest : PatchRequest - { - public string? Title { get; init; } - public Snowflake? AssignedTo { get; init; } - - [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] - public SupportConversationStatus? Status { get; init; } - } -} diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index ce2a423..95e0317 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -26,9 +26,6 @@ public class DatabaseContext : DbContext public DbSet UserFlags { get; set; } public DbSet MemberFlags { get; set; } - public DbSet SupportConversations { get; set; } - public DbSet SupportMessages { get; set; } - public DatabaseContext(Config config, ILoggerFactory? loggerFactory) { var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) @@ -91,9 +88,6 @@ public class DatabaseContext : DbContext modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); - modelBuilder.Entity().Property(m => m.Attachments).HasColumnType("jsonb"); - modelBuilder.Entity().Property(m => m.History).HasColumnType("jsonb"); - modelBuilder .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!) .HasName("find_free_user_sid"); diff --git a/Foxnouns.Backend/Database/Migrations/20241003150352_AddSupportConversations.cs b/Foxnouns.Backend/Database/Migrations/20241003150352_AddSupportConversations.cs deleted file mode 100644 index ee903a2..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241003150352_AddSupportConversations.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Collections.Generic; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241003150352_AddSupportConversations")] - public partial class AddSupportConversations : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "support_conversations", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), - title = table.Column(type: "text", nullable: false), - assigned_to_id = table.Column(type: "bigint", nullable: true), - status = table.Column(type: "integer", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_support_conversations", x => x.id); - table.ForeignKey( - name: "fk_support_conversations_users_assigned_to_id", - column: x => x.assigned_to_id, - principalTable: "users", - principalColumn: "id" - ); - table.ForeignKey( - name: "fk_support_conversations_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateTable( - name: "support_messages", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - conversation_id = table.Column(type: "bigint", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), - is_anonymous = table.Column(type: "boolean", nullable: false), - content = table.Column(type: "text", nullable: true), - attachments = table.Column>( - type: "jsonb", - nullable: false - ), - history = table.Column>( - type: "jsonb", - nullable: false - ), - }, - constraints: table => - { - table.PrimaryKey("pk_support_messages", x => x.id); - table.ForeignKey( - name: "fk_support_messages_support_conversations_conversation_id", - column: x => x.conversation_id, - principalTable: "support_conversations", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - table.ForeignKey( - name: "fk_support_messages_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_support_conversations_assigned_to_id", - table: "support_conversations", - column: "assigned_to_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_support_conversations_user_id", - table: "support_conversations", - column: "user_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_support_messages_conversation_id", - table: "support_messages", - column: "conversation_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_support_messages_user_id", - table: "support_messages", - column: "user_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "support_messages"); - - migrationBuilder.DropTable(name: "support_conversations"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 8809a15..e1e05c2 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -266,85 +266,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("pride_flags", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.SupportConversation", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AssignedToId") - .HasColumnType("bigint") - .HasColumnName("assigned_to_id"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text") - .HasColumnName("title"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_support_conversations"); - - b.HasIndex("AssignedToId") - .HasDatabaseName("ix_support_conversations_assigned_to_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_support_conversations_user_id"); - - b.ToTable("support_conversations", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.SupportMessage", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property>("Attachments") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("attachments"); - - b.Property("Content") - .HasColumnType("text") - .HasColumnName("content"); - - b.Property("ConversationId") - .HasColumnType("bigint") - .HasColumnName("conversation_id"); - - b.Property>("History") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("history"); - - b.Property("IsAnonymous") - .HasColumnType("boolean") - .HasColumnName("is_anonymous"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_support_messages"); - - b.HasIndex("ConversationId") - .HasDatabaseName("ix_support_messages_conversation_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_support_messages_user_id"); - - b.ToTable("support_messages", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => { b.Property("Id") @@ -621,46 +542,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_pride_flags_users_user_id"); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.SupportConversation", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "AssignedTo") - .WithMany() - .HasForeignKey("AssignedToId") - .HasConstraintName("fk_support_conversations_users_assigned_to_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_support_conversations_users_user_id"); - - b.Navigation("AssignedTo"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.SupportMessage", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.SupportConversation", "Conversation") - .WithMany() - .HasForeignKey("ConversationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_support_messages_support_conversations_conversation_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_support_messages_users_user_id"); - - b.Navigation("Conversation"); - - b.Navigation("User"); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => { b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") diff --git a/Foxnouns.Backend/Database/Models/SupportConversation.cs b/Foxnouns.Backend/Database/Models/SupportConversation.cs deleted file mode 100644 index b9929bb..0000000 --- a/Foxnouns.Backend/Database/Models/SupportConversation.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Foxnouns.Backend.Database.Models; - -public class SupportConversation : BaseModel -{ - public Snowflake UserId { get; init; } - public User User { get; init; } = null!; - public required string Title { get; set; } - public Snowflake? AssignedToId { get; set; } - public User? AssignedTo { get; set; } - - public SupportConversationStatus Status { get; set; } = SupportConversationStatus.Open; -} - -public enum SupportConversationStatus -{ - Open, - Resolved, - Closed, -} diff --git a/Foxnouns.Backend/Database/Models/SupportMessage.cs b/Foxnouns.Backend/Database/Models/SupportMessage.cs deleted file mode 100644 index 82ba2bd..0000000 --- a/Foxnouns.Backend/Database/Models/SupportMessage.cs +++ /dev/null @@ -1,30 +0,0 @@ -using NodaTime; - -namespace Foxnouns.Backend.Database.Models; - -public class SupportMessage : BaseModel -{ - public Snowflake ConversationId { get; init; } - public SupportConversation Conversation { get; init; } = null!; - public Snowflake UserId { get; init; } - public User User { get; init; } = null!; - public bool IsAnonymous { get; init; } - - public string? Content { get; set; } - - public List Attachments { get; set; } = []; - public List History { get; set; } = []; - - public class Attachment - { - public required Snowflake Id { get; init; } - public required string Hash { get; init; } - public required string ContentType { get; init; } - } - - public class HistoryEntry - { - public required Instant Timestamp { get; init; } - public required string? Content { get; init; } - } -} diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 3e02516..fdd0b5d 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -51,12 +51,6 @@ public class ApiError( } ) { } - public BadRequest(string field, ValidationError error) - : this( - "Error validating input", - new Dictionary> { { field, [error] } } - ) { } - public JObject ToJson() { var o = new JObject diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index a6d2a97..3e6926c 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -98,7 +98,6 @@ public static class WebApplicationExtensions .AddSingleton() .AddScoped() .AddScoped() - .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/Foxnouns.Backend/Services/SupportRendererService.cs b/Foxnouns.Backend/Services/SupportRendererService.cs deleted file mode 100644 index 77ae0f1..0000000 --- a/Foxnouns.Backend/Services/SupportRendererService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; - -namespace Foxnouns.Backend.Services; - -public class SupportRendererService(UserRendererService userRenderer) -{ - public ConversationResponse ToResponse(SupportConversation c) => - new( - c.Id, - userRenderer.RenderPartialUser(c.User), - c.Title, - userRenderer.TryRenderPartialUser(c.AssignedTo), - c.Status - ); - - public MessageResponse ToResponse(SupportMessage m, bool modView = false) => - new( - m.Id, - m.IsAnonymous - ? modView - ? userRenderer.RenderPartialUser(m.User) - : null - : userRenderer.RenderPartialUser(m.User), - m.IsAnonymous, - m.Content - ); - - public record ConversationResponse( - Snowflake Id, - UserRendererService.PartialUser User, - string Title, - UserRendererService.PartialUser? AssignedTo, - [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] - SupportConversationStatus Status - ); - - public record MessageResponse( - Snowflake Id, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - UserRendererService.PartialUser? User, - bool IsAnonymous, - string? Content - ); -} diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index aef0d6c..b47832d 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -92,9 +92,6 @@ public class UserRendererService( user.CustomPreferences ); - public PartialUser? TryRenderPartialUser(User? user) => - user != null ? RenderPartialUser(user) : null; - private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index b477855..aaf0c08 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -7,7 +7,6 @@ public static class AuthUtils { public const string ClientCredentials = "client_credentials"; public const string AuthorizationCode = "authorization_code"; - private static readonly string[] ForbiddenSchemes = [ "javascript", @@ -22,7 +21,6 @@ public static class AuthUtils "user.read_hidden", "user.read_privileged", "user.update", - "user.support", ]; public static readonly string[] MemberScopes = diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 855d3d2..0969a47 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -9,25 +9,6 @@ namespace Foxnouns.Backend.Utils; /// public static partial class ValidationUtils { - public static void ValidateStringLength( - this string s, - string fieldName, - int minLength, - int maxLength - ) - { - if (s.Length < minLength) - throw new ApiError.BadRequest( - fieldName, - ValidationError.LengthError("Field is too short", minLength, maxLength, s.Length) - ); - if (s.Length > maxLength) - throw new ApiError.BadRequest( - fieldName, - ValidationError.LengthError("Field is too long", minLength, maxLength, s.Length) - ); - } - private static readonly string[] InvalidUsernames = [ "..", diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 8dab154..7da3e84 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -1,5 +1,5 @@ import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { API_BASE, INTERNAL_API_BASE } from "~/env.server"; +import { INTERNAL_API_BASE } from "~/env.server"; import { ApiError, ErrorCode } from "./api/error"; import { tokenCookieName } from "~/lib/utils"; @@ -11,17 +11,12 @@ export type RequestParams = { isInternal?: boolean; }; -export type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE"; - export async function baseRequest( - method: RequestMethod, + method: string, path: string, params: RequestParams = {}, ): Promise { - // Internal requests, unauthenticated requests, and GET requests bypass the rate limiting proxy. - // All other requests go through the proxy, and are rate limited. - let base = params.isInternal || !params.token || method === "GET" ? INTERNAL_API_BASE : API_BASE; - base += params.isInternal ? "/internal" : "/v2"; + const base = params.isInternal ? INTERNAL_API_BASE + "/internal" : INTERNAL_API_BASE + "/v2"; const url = `${base}${path}`; const resp = await fetch(url, { @@ -48,12 +43,12 @@ export async function baseRequest( return resp; } -export async function fastRequest(method: RequestMethod, path: string, params: RequestParams = {}) { +export async function fastRequest(method: string, path: string, params: RequestParams = {}) { await baseRequest(method, path, params); } export default async function serverRequest( - method: RequestMethod, + method: string, path: string, params: RequestParams = {}, ) { diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index 2829098..88655fc 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -5,6 +5,7 @@ import { Link, Outlet, useActionData, + useFetcher, useRouteLoaderData, } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route";