diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 53a38e7..ffd4fe6 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -22,27 +22,19 @@ public class MetaController : ApiControllerBase ), new Limits( MemberCount: MembersController.MaxMemberCount, - BioLength: ValidationUtils.MaxBioLength, - CustomPreferences: UsersController.MaxCustomPreferences)) + BioLength: ValidationUtils.MaxBioLength)) ); } [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); - private record MetaResponse( - string Repository, - string Version, - string Hash, - int Members, - UserInfo Users, - Limits Limits); + private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users, Limits Limits); private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); // All limits that the frontend should know about (for UI purposes) private record Limits( int MemberCount, - int BioLength, - int CustomPreferences); + int BioLength); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 9a182f4..e292fc3 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -177,16 +177,14 @@ public class UsersController( public bool Favourite { get; set; } } - public const int MaxCustomPreferences = 25; - private static List<(string, ValidationError?)> ValidateCustomPreferences( List preferences) { var errors = new List<(string, ValidationError?)>(); - if (preferences.Count > MaxCustomPreferences) + if (preferences.Count > 25) errors.Add(("custom_preferences", - ValidationError.LengthError("Too many custom preferences", 0, MaxCustomPreferences, preferences.Count))); + ValidationError.LengthError("Too many custom preferences", 0, 25, preferences.Count))); if (preferences.Count > 50) return errors; // TODO: validate individual preferences diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index ee64fe1..95d40d3 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -43,7 +43,6 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, flags.Select(f => RenderPrideFlag(f.PrideFlag)), - user.Role, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( @@ -79,8 +78,6 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Fields, Dictionary CustomPreferences, IEnumerable Flags, - [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] - UserRole Role, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 392e5ed..f8f8379 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -7,8 +7,13 @@ namespace Foxnouns.Backend.Utils; /// /// Static methods for validating user input (mostly making sure it's not too short or too long) /// -public static partial class ValidationUtils +public static class ValidationUtils { + private static readonly Regex UsernameRegex = new("^[\\w-.]{2,40}$", RegexOptions.IgnoreCase); + + private static readonly Regex MemberRegex = + new("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$", RegexOptions.IgnoreCase); + private static readonly string[] InvalidUsernames = [ "..", @@ -35,7 +40,7 @@ public static partial class ValidationUtils public static ValidationError? ValidateUsername(string username) { - if (!UsernameRegex().IsMatch(username)) + if (!UsernameRegex.IsMatch(username)) return username.Length switch { < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), @@ -52,7 +57,7 @@ public static partial class ValidationUtils public static ValidationError? ValidateMemberName(string memberName) { - if (!MemberRegex().IsMatch(memberName)) + if (!MemberRegex.IsMatch(memberName)) return memberName.Length switch { < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), @@ -123,7 +128,7 @@ public static partial class ValidationUtils } public const int MaxBioLength = 1024; - + public static ValidationError? ValidateBio(string? bio) { return bio?.Length switch @@ -286,9 +291,4 @@ public static partial class ValidationUtils return errors; } - - [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] - private static partial Regex UsernameRegex(); - [GeneratedRegex("""^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", RegexOptions.IgnoreCase, "en-NL")] - private static partial Regex MemberRegex(); } \ No newline at end of file diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss index 50b0ad0..0d30b1e 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/app/app.scss @@ -27,8 +27,3 @@ max-width: 200px; border-radius: 3px; } - -// This is necessary for line breaks in translation strings to show up. Don't ask me why -.text-has-newline { - white-space: pre-line; -} diff --git a/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx b/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx deleted file mode 100644 index e29ff75..0000000 --- a/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export default function AvatarImage({ - src, - width, - alt, - lazyLoad, -}: { - src: string; - width: number; - alt: string; - lazyLoad?: boolean; -}) { - return ( - {alt} - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx index 2d6171e..a058755 100644 --- a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx +++ b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx @@ -6,7 +6,6 @@ import ProfileLink from "~/components/profile/ProfileLink"; import ProfileField from "~/components/profile/ProfileField"; import { useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; -import AvatarImage from "~/components/profile/AvatarImage"; export type Props = { name: string; @@ -32,16 +31,20 @@ export default function BaseProfile({
{userI18nKeys ? ( - ) : ( - )} {profile.flags && profile.bio && ( diff --git a/Foxnouns.Frontend/app/lib/api/meta.ts b/Foxnouns.Frontend/app/lib/api/meta.ts index 5f2bd11..8f67ada 100644 --- a/Foxnouns.Frontend/app/lib/api/meta.ts +++ b/Foxnouns.Frontend/app/lib/api/meta.ts @@ -8,11 +8,9 @@ export default interface Meta { active_day: number; }; members: number; - limits: Limits; } export type Limits = { member_count: number; bio_length: number; - custom_preferences: number; }; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 4f07301..0b6f375 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -14,14 +14,6 @@ export type User = PartialUser & { pronouns: Pronoun[]; fields: Field[]; flags: PrideFlag[]; - role: "USER" | "MODERATOR" | "ADMIN"; -}; - -export type MeUser = UserWithMembers & { - auth_methods: AuthMethod[]; - member_list_hidden: boolean; - last_active: string; - last_sid_reroll: string; }; export type UserWithMembers = User & { members: PartialMember[] }; @@ -66,14 +58,6 @@ export type PrideFlag = { description: string | null; }; -export type AuthMethod = { - id: string; - type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; - remote_id: string; - remote_username?: string; - fediverse_instance?: string; -}; - export type CustomPreference = { icon: string; tooltip: string; diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx index b112a08..bc6e516 100644 --- a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx +++ b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx @@ -10,7 +10,6 @@ import { defaultAvatarUrl } from "~/lib/utils"; import { useTranslation } from "react-i18next"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { Lock } from "react-bootstrap-icons"; -import AvatarImage from "~/components/profile/AvatarImage"; export default function MemberCard({ user, member }: { user: PartialUser; member: PartialMember }) { const { t } = useTranslation(); @@ -38,11 +37,13 @@ export default function MemberCard({ user, member }: { user: PartialUser; member return (
-

diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index c5200fd..f1e8fb7 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -15,6 +15,7 @@ import { useLoaderData, ShouldRevalidateFunction, useNavigate, + Navigate, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index d575108..de202a3 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -1,94 +1,30 @@ -import { Button, Form, InputGroup, Table } from "react-bootstrap"; +import { Table } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { Form as RemixForm, useActionData, useRouteLoaderData } from "@remix-run/react"; +import { useLoaderData, useRouteLoaderData } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; -import { loader as rootLoader } from "../../root"; -import { DateTime } from "luxon"; -import { defaultAvatarUrl, idTimestamp } from "~/lib/utils"; -import { ExclamationTriangleFill, InfoCircleFill } from "react-bootstrap-icons"; -import AvatarImage from "~/components/profile/AvatarImage"; -import { ActionFunctionArgs, json } from "@remix-run/node"; -import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; +import { LoaderFunctionArgs, json } from "@remix-run/node"; import serverRequest, { getToken } from "~/lib/request.server"; -import { MeUser } from "~/lib/api/user"; -import ErrorAlert from "~/components/ErrorAlert"; +import { PartialMember } from "~/lib/api/user"; +import { limits } from "~/env.server"; +import { DateTime } from "luxon"; +import { idTimestamp } from "~/lib/utils"; -export const action = async ({ request }: ActionFunctionArgs) => { - const data = await request.formData(); - const username = data.get("username") as string | null; +export const loader = async ({ request }: LoaderFunctionArgs) => { const token = getToken(request); - - if (!username) { - return json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username", - } as ApiError, - user: null, - }); - } - - try { - const resp = await serverRequest("PATCH", "/users/@me", { body: { username }, token }); - - return json({ user: resp, error: null }); - } catch (e) { - return json({ error: e as ApiError, user: null }); - } + const members = await serverRequest("GET", "/users/@me/members", { token }); + return json({ members, maxMemberCount: limits.member_count }); }; export default function SettingsIndex() { + const { members, maxMemberCount } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; - const actionData = useActionData(); - const { meta } = useRouteLoaderData("root")!; const { t } = useTranslation(); const createdAt = idTimestamp(user.id); return ( <> -

-
- -
- - {t("settings.general.username")} - - - - - -
-
- -

- {t("settings.general.username-change-hint")} -

- {actionData?.error && } -
-
- -
-
-
-

{t("settings.general.log-out-everywhere")}

-

-
-

{t("settings.general.table-header")}

- +
@@ -103,23 +39,7 @@ export default function SettingsIndex() { - - - - - - - - - - - - @@ -127,18 +47,3 @@ export default function SettingsIndex() { ); } - -function UsernameUpdateError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - const usernameError = firstErrorFor(error, "username"); - if (!usernameError) { - return ; - } - - return ( -

- {" "} - {t("settings.general.username-update-error", { message: usernameError.message })} -

- ); -} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx index 3ef35fd..74123e9 100644 --- a/Foxnouns.Frontend/app/routes/settings/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings/route.tsx @@ -1,7 +1,7 @@ import { LoaderFunctionArgs, json, redirect, MetaFunction } from "@remix-run/node"; import i18n from "~/i18next.server"; import serverRequest, { getToken } from "~/lib/request.server"; -import { MeUser } from "~/lib/api/user"; +import { User } from "~/lib/api/user"; import { Link, Outlet, useLocation } from "@remix-run/react"; import { Nav } from "react-bootstrap"; import { useTranslation } from "react-i18next"; @@ -16,7 +16,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (token) { try { - const user = await serverRequest("GET", "/users/@me", { token }); + const user = await serverRequest("GET", "/users/@me", { token }); return json({ user, meta: { title: t("settings.title") } }); } catch (e) { return redirect("/auth/log-in"); diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 279173b..0baebf2 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -92,18 +92,9 @@ }, "settings": { "general": { - "username": "Username", - "change-username": "Change username", - "username-change-hint": "Changing your username will make any existing links to your or your members' profiles invalid.\nYour username must be unique, be at most 40 characters long, and only contain letters from the basic English alphabet, dashes, underscores, and periods. Your username is used as part of your profile link, you can set a separate display name.", - "log-out-everywhere": "Log out everywhere", - "table-header": "General account information", "id": "Your user ID", "created": "Account created at", - "member-count": "Members", - "member-list-hidden": "Member list hidden?", - "custom-preferences": "Custom preferences", - "role": "Account role", - "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}" + "member-count": "Members" }, "title": "Settings", "nav": { @@ -113,7 +104,5 @@ "authentication": "Authentication", "export": "Export your data" } - }, - "yes": "Yes", - "no": "No" + } }
{t("settings.general.id")}
{t("settings.general.member-count")} - {user.members.length}/{meta.limits.member_count} -
{t("settings.general.member-list-hidden")}{user.member_list_hidden ? t("yes") : t("no")}
{t("settings.general.custom-preferences")} - {Object.keys(user.custom_preferences).length}/{meta.limits.custom_preferences} -
{t("settings.general.role")} - {user.role} + {members.length}/{maxMemberCount}