diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 6b31a40..0781443 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Serilog.Events; namespace Foxnouns.Backend; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index b9570c0..4c83ce4 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -45,7 +45,7 @@ public class AuthController(Config config, KeyCacheService keyCache, ILogger log ); public record CallbackResponse( - bool HasAccount, + bool HasAccount, // If true, user has an account, but it's deleted [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index a1c3eed..20840ad 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -3,7 +3,6 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; -using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -12,7 +11,7 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/discord")] public class DiscordAuthController( - [UsedImplicitly] Config config, + Config config, ILogger logger, IClock clock, DatabaseContext db, @@ -27,15 +26,14 @@ public class DiscordAuthController( // TODO: duplicating attribute doesn't work, find another way to mark both as possible response // leaving it here for documentation purposes [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, - CancellationToken ct = default) + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, CancellationToken ct = default) { CheckRequirements(); await keyCacheService.ValidateAuthStateAsync(req.State, ct); var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code, req.State, ct); var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); - if (user != null) return Ok(await GenerateUserTokenAsync(user, ct)); + if (user != null) return Ok(await GenerateUserTokenAsync(user,ct)); _logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); @@ -55,25 +53,24 @@ public class DiscordAuthController( [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) + public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req, CancellationToken ct = default) { - var remoteUser = await keyCacheService.GetKeyAsync($"discord:{req.Ticket}"); + var remoteUser = await keyCacheService.GetKeyAsync($"discord:{req.Ticket}",ct:ct); if (remoteUser == null) throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - if (await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Discord && a.RemoteId == remoteUser.Id)) + if (await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Discord && a.RemoteId == remoteUser.Id, ct)) { _logger.Error("Discord user {Id} has valid ticket but is already linked to an existing account", remoteUser.Id); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); + throw new FoxnounsError("Discord ticket was issued for user with existing link"); } var user = await authService.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, - remoteUser.Username); + remoteUser.Username, ct: ct); - return Ok(await GenerateUserTokenAsync(user)); + return Ok(await GenerateUserTokenAsync(user, ct)); } - private async Task GenerateUserTokenAsync(User user, - CancellationToken ct = default) + private async Task GenerateUserTokenAsync(User user, CancellationToken ct = default) { var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with Discord", user.Id); diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 1649948..b7e8ff4 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -3,7 +3,6 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; -using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -12,8 +11,8 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/email")] public class EmailAuthController( - [UsedImplicitly] Config config, DatabaseContext db, + Config config, AuthService authService, MailService mailService, KeyCacheService keyCacheService, diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index e63b579..265cf3d 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -17,7 +17,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase private static string GetCleanedTemplate(string template) { - if (template.StartsWith("api/v2")) template = template["api/v2".Length..]; + if (template.StartsWith("api/v2")) template = template.Substring("api/v2".Length); template = PathVarRegex() .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` @@ -50,7 +50,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase Snowflake? UserId, string Template); - private static RouteEndpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) + private static Endpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) { var endpointDataSource = httpContext.RequestServices.GetService(); if (endpointDataSource == null) return null; @@ -60,7 +60,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase { if (endpoint.RoutePattern.RawText == null) continue; - var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new RouteValueDictionary()); + var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new()); if (!templateMatcher.TryMatch(url, new())) continue; var httpMethodAttribute = endpoint.Metadata.GetMetadata(); if (httpMethodAttribute != null && diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index a92f947..f051ca1 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -88,17 +88,19 @@ public class MembersController( [HttpDelete("/api/v2/users/@me/members/{memberRef}")] [Authorize("member.update")] - public async Task DeleteMemberAsync(string memberRef) + public async Task DeleteMemberAsync(string memberRef, CancellationToken ct = default) { - var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef, ct); var deleteCount = await db.Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) - .ExecuteDeleteAsync(); + .ExecuteDeleteAsync(ct); if (deleteCount == 0) { _logger.Warning("Successfully resolved member {Id} but could not delete them", member.Id); return NoContent(); } + await db.SaveChangesAsync(ct); + if (member.Avatar != null) await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar); return NoContent(); } diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 60d4499..f8a544c 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -94,7 +95,7 @@ public static class DatabaseQueryExtensions { Id = new Snowflake(0), ClientId = RandomNumberGenerator.GetHexString(32, true), - ClientSecret = AuthUtils.RandomToken(), + ClientSecret = AuthUtils.RandomToken(48), Name = "pronouns.cc", Scopes = ["*"], RedirectUris = [], diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index 49b711e..f64bfc9 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -9,7 +9,7 @@ public class Application : BaseModel public required string ClientSecret { get; init; } public required string Name { get; init; } public required string[] Scopes { get; init; } - public required string[] RedirectUris { get; init; } + public required string[] RedirectUris { get; set; } public static Application Create(ISnowflakeGenerator snowflakeGenerator, string name, string[] scopes, string[] redirectUrls) diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 3c1c355..f277265 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -1,4 +1,6 @@ +using System.Collections.ObjectModel; using System.Net; +using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -49,7 +51,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, - { "code", "BAD_REQUEST" } + { "code", ErrorCode.BadRequest.ToString() } }; if (errors == null) return o; @@ -82,7 +84,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, - { "code", "BAD_REQUEST" } + { "code", ErrorCode.BadRequest.ToString() } }; if (modelState == null) return o; diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 6c4ea28..92abc6a 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -10,7 +10,6 @@ - diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index 6b6da6d..c52c3f0 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,6 +1,7 @@ using System.Net; using Foxnouns.Backend.Utils; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index f1e907b..f69441a 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -74,7 +74,6 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// /// The user's email address /// The user's password, in plain text - /// Cancellation token /// A tuple of the authenticated user and whether multi-factor authentication is required /// Thrown if the email address is not associated with any user /// or if the password is incorrect diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 78ea3ae..33b595f 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -1,5 +1,6 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using NodaTime; diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index fefaf16..b08b0a5 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -1,5 +1,5 @@ using System.Diagnostics.CodeAnalysis; -using JetBrains.Annotations; +using System.Web; namespace Foxnouns.Backend.Services; @@ -27,11 +27,10 @@ public class RemoteAuthService(Config config, ILogger logger) if (!resp.IsSuccessStatusCode) { var respBody = await resp.Content.ReadAsStringAsync(ct); - _logger.Error("Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", - (int)resp.StatusCode, respBody); + _logger.Error("Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", (int)resp.StatusCode, respBody); throw new FoxnounsError("Invalid Discord OAuth response"); } - + resp.EnsureSuccessStatusCode(); var token = await resp.Content.ReadFromJsonAsync(ct); if (token == null) throw new FoxnounsError("Discord token response was null"); @@ -47,14 +46,10 @@ public class RemoteAuthService(Config config, ILogger logger) return new RemoteUser(user.id, user.username); } - [SuppressMessage("ReSharper", "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options")] - [UsedImplicitly] + [SuppressMessage("ReSharper", "InconsistentNaming")] private record DiscordTokenResponse(string access_token, string token_type); - [SuppressMessage("ReSharper", "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options")] - [UsedImplicitly] + [SuppressMessage("ReSharper", "InconsistentNaming")] private record DiscordUserResponse(string id, string username); public record RemoteUser(string Id, string Username); diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index cedb69a..e30c516 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -1,13 +1,7 @@ import { TFunction } from "i18next"; import Alert from "react-bootstrap/Alert"; -import { Trans, useTranslation } from "react-i18next"; -import { - ApiError, - ErrorCode, - ValidationError, - validationErrorType, - ValidationErrorType, -} from "~/lib/api/error"; +import { useTranslation } from "react-i18next"; +import { ApiError, ErrorCode } from "~/lib/api/error"; export default function ErrorAlert({ error }: { error: ApiError }) { const { t } = useTranslation(); @@ -16,112 +10,10 @@ export default function ErrorAlert({ error }: { error: ApiError }) { {t("error.heading")} {errorCodeDesc(t, error.code)} - {error.errors && ( -
    - {error.errors.map((e, i) => ( - - ))} -
- )}
); } -function ValidationErrors({ errorKey, errors }: { errorKey: string; errors: ValidationError[] }) { - return ( -
  • - - {errorKey} - - : -
      - {errors.map((e, i) => ( -
    • - -
    • - ))} -
    -
  • - ); -} - -function ValidationErrorEntry({ error }: { error: ValidationError }) { - const { t } = useTranslation(); - - const { - min_length: minLength, - max_length: maxLength, - actual_length: actualLength, - message: reason, - actual_value: actualValue, - allowed_values: allowedValues, - } = error; - - switch (validationErrorType(error)) { - case ValidationErrorType.LengthError: - if (error.actual_length! > error.max_length!) { - return ( - - Value is too long, maximum length is {{ maxLength }}, current length is{" "} - {{ actualLength }}. - - ); - } - - if (error.actual_length! < error.min_length!) { - return ( - - Value is too short, minimum length is {{ minLength }}, current length is{" "} - {{ actualLength }}. - - ); - } - - break; - - case ValidationErrorType.DisallowedValueError: - return ( - v.toString()).join(", "), - }} - > - {/* @ts-expect-error i18next handles interpolation */} - The value {{ actualValue }} is not allowed here. Allowed values are:{" "} - {/* @ts-expect-error i18next handles interpolation */} - {{ allowedValues }} - - ); - - default: - if (error.actual_value) { - return ( - - {/* @ts-expect-error i18next handles interpolation */} - The value {{ actualValue }} is not allowed here. Reason: {{ reason }} - - ); - } - - return <>{t("error.validation.generic-no-value", { reason: error.message })}; - } -} - export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { switch (code) { case ErrorCode.AuthenticationError: diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts index 0a3d9b9..02e871c 100644 --- a/Foxnouns.Frontend/app/lib/api/error.ts +++ b/Foxnouns.Frontend/app/lib/api/error.ts @@ -3,7 +3,7 @@ export type ApiError = { status: number; message: string; code: ErrorCode; - errors?: Array<{ key: string; errors: ValidationError[] }>; + errors?: ValidationError[]; }; export enum ErrorCode { @@ -26,31 +26,3 @@ export type ValidationError = { allowed_values?: any[]; actual_value?: any; }; - -/** - * Returns the first error for the value `key` in `error`. - * @param error The error object to traverse. - * @param key The JSON key to find. - */ -export const firstErrorFor = (error: ApiError, key: string): ValidationError | undefined => { - if (!error.errors) return undefined; - const field = error.errors.find((e) => e.key == key); - if (!field?.errors) return undefined; - return field.errors.length != 0 ? field.errors[0] : undefined; -}; - -export enum ValidationErrorType { - LengthError = 0, - DisallowedValueError = 1, - GenericValidationError = 2, -} - -export const validationErrorType = (error: ValidationError) => { - if (error.min_length && error.max_length && error.actual_length) { - return ValidationErrorType.LengthError; - } - if (error.allowed_values && error.actual_value) { - return ValidationErrorType.DisallowedValueError; - } - return ValidationErrorType.GenericValidationError; -}; diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 31d4d64..f946ee0 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -1,23 +1,11 @@ -import { ActionFunctionArgs, json, redirect, LoaderFunctionArgs } from "@remix-run/node"; -import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; +import { json, LoaderFunctionArgs } from "@remix-run/node"; +import { type ApiError, ErrorCode } from "~/lib/api/error"; import serverRequest, { writeCookie } from "~/lib/request.server"; -import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; -import { - Form as RemixForm, - Link, - useActionData, - useLoaderData, - ShouldRevalidateFunction, -} from "@remix-run/react"; +import { CallbackResponse } from "~/lib/api/auth"; +import { Form as RemixForm, Link, useLoaderData } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; -import ErrorAlert from "~/components/ErrorAlert"; -import Alert from "react-bootstrap/Alert"; - -export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { - return !actionResult; -}; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); @@ -29,7 +17,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; const resp = await serverRequest("POST", "/auth/discord/callback", { - body: { code, state }, + body: { code, state } }); if (resp.has_account) { @@ -37,9 +25,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { { hasAccount: true, user: resp.user!, ticket: null, remoteUser: null }, { headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token!), - }, - }, + "Set-Cookie": writeCookie("pronounscc-token", resp.token!) + } + } ); } @@ -47,62 +35,26 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { hasAccount: false, user: null, ticket: resp.ticket!, - remoteUser: resp.remote_username!, + remoteUser: resp.remote_username! }); }; -export const action = async ({ request }: ActionFunctionArgs) => { - const data = await request.formData(); - const username = data.get("username") as string | null; - const ticket = data.get("ticket") as string | null; - - if (!username || !ticket) - return json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username or ticket", - } as ApiError, - user: null, - }); - - try { - const resp = await serverRequest("POST", "/auth/discord/register", { - body: { username, ticket }, - }); - - return redirect("/auth/welcome", { - headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token), - }, - status: 303, - }); - } catch (e) { - JSON.stringify(e); - - return json({ error: e as ApiError }); - } -}; +// TODO: action function export default function DiscordCallbackPage() { const { t } = useTranslation(); const data = useLoaderData(); - const actionData = useActionData(); if (data.hasAccount) { const username = data.user!.username; - + return ( <>

    {t("log-in.callback.success")}

    - + {/* @ts-expect-error react-i18next handles interpolation here */} - Welcome back, @{{ username }}! + Welcome back, @{{username}}!
    {t("log-in.callback.redirect-hint")} @@ -114,7 +66,6 @@ export default function DiscordCallbackPage() { return (

    - {actionData?.error && } {t("log-in.callback.remote-username.discord")} @@ -131,34 +82,3 @@ export default function DiscordCallbackPage() { ); } - -function RegisterError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - - // TODO: maybe turn these messages into their own error codes? - const ticketMessage = firstErrorFor(error, "ticket")?.message; - const usernameMessage = firstErrorFor(error, "username")?.message; - - if (ticketMessage === "Invalid ticket") { - return ( - - {t("error.heading")} - - Invalid ticket (it might have been too long since you logged in with Discord), please{" "} - try again. - - - ); - } - - if (usernameMessage === "Username is already taken") { - return ( - - {t("log-in.callback.invalid-username")} - {t("log-in.callback.username-taken")} - - ); - } - - return ; -} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index f2dfbf2..83455f7 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,62 +1,52 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "Error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up", - "theme": "Theme", - "theme-auto": "Automatic", - "theme-dark": "Dark", - "theme-light": "Light" - }, - "log-in": { - "callback": { - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - } + "error": { + "heading": "An error occurred", + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "Error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up", + "theme": "Theme", + "theme-auto": "Automatic", + "theme-dark": "Dark", + "theme-light": "Light" + }, + "log-in": { + "callback": { + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up" + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + } }