diff --git a/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml b/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml deleted file mode 100644 index 5e24061..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 937ab3a..41eab25 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -183,48 +183,10 @@ public class EmailAuthController( [HttpPost("add")] [Authorize("*")] - public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) + public async Task AddEmailAddressAsync() { - var emails = await db - .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) - .ToListAsync(); - if (emails.Count > AuthUtils.MaxAuthMethodsPerType) - { - throw new ApiError.BadRequest( - "Too many email addresses, maximum of 3 per account.", - "email", - null - ); - } + _logger.Information("beep"); - if (emails.Count != 0) - { - var validPassword = await authService.ValidatePasswordAsync(CurrentUser!, req.Password); - if (!validPassword) - { - throw new ApiError.Forbidden("Invalid password"); - } - } - else - { - await authService.SetUserPasswordAsync(CurrentUser!, req.Password); - await db.SaveChangesAsync(); - } - - var state = await keyCacheService.GenerateRegisterEmailStateAsync( - req.Email, - userId: CurrentUser!.Id - ); - - var emailExists = await db - .AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email) - .AnyAsync(); - if (emailExists) - { - return NoContent(); - } - - mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username); return NoContent(); } diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index fdd0b5d..0630892 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -42,7 +42,7 @@ public class ApiError( IReadOnlyDictionary>? errors = null ) : ApiError(message, statusCode: HttpStatusCode.BadRequest) { - public BadRequest(string message, string field, object? actualValue) + public BadRequest(string message, string field, object actualValue) : this( "Error validating input", new Dictionary> diff --git a/Foxnouns.Backend/Mailables/AddEmailMailable.cs b/Foxnouns.Backend/Mailables/AddEmailMailable.cs deleted file mode 100644 index ee5792d..0000000 --- a/Foxnouns.Backend/Mailables/AddEmailMailable.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Coravel.Mailer.Mail; - -namespace Foxnouns.Backend.Mailables; - -public class AddEmailMailable(Config config, AddEmailMailableView view) - : Mailable -{ - public override void Build() - { - To(view.To).From(config.EmailAuth.From!).View("~/Views/Mail/AddEmail.cshtml", view); - } -} - -public class AddEmailMailableView : BaseView -{ - public required string Code { get; init; } - public required string Username { get; init; } -} diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index d03496c..1aaa5e4 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -135,43 +135,6 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s MfaRequired, } - /// - /// Validates a user's password outside an authentication context, for when a password is required for changing - /// a setting, such as adding a new email address or changing passwords. - /// - public async Task ValidatePasswordAsync( - User user, - string password, - CancellationToken ct = default - ) - { - if (user.Password == null) - { - throw new FoxnounsError("Password for user supplied to ValidatePasswordAsync was null"); - } - - var pwResult = await Task.Run( - () => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), - ct - ); - return pwResult - is PasswordVerificationResult.SuccessRehashNeeded - or PasswordVerificationResult.Success; - } - - /// - /// Sets or updates a password for the given user. This method does not save the updated password automatically. - /// - public async Task SetUserPasswordAsync( - User user, - string password, - CancellationToken ct = default - ) - { - user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); - db.Update(user); - } - /// /// Authenticates a user with a remote authentication provider. /// diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index 888f5fb..c605866 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -33,31 +33,4 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co } }); } - - public void QueueAddEmailAddressEmail(string to, string code, string username) - { - _logger.Debug("Sending add email address email to {ToEmail}", to); - queue.QueueAsyncTask(async () => - { - try - { - await mailer.SendAsync( - new AddEmailMailable( - config, - new AddEmailMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - Username = username, - } - ) - ); - } - catch (Exception exc) - { - _logger.Error(exc, "Sending add email address email"); - } - }); - } } diff --git a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml deleted file mode 100644 index dabef6c..0000000 --- a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml +++ /dev/null @@ -1,12 +0,0 @@ -@model Foxnouns.Backend.Mailables.AddEmailMailableView - -

- Hello @@@Model.Username, please confirm adding this email address to your account by using the following link: -
- Confirm your email address -
- Note that this link will expire in one hour. -

-

- If you didn't mean to link this email address to @@@Model.Username, feel free to ignore this email. -

\ No newline at end of file diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 562666d..c92f67d 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -11,7 +11,7 @@ export type RequestParams = { isInternal?: boolean; }; -export async function baseRequest( +async function requestInternal( method: string, path: string, params: RequestParams = {}, @@ -44,7 +44,7 @@ export async function baseRequest( } export async function fastRequest(method: string, path: string, params: RequestParams = {}) { - await baseRequest(method, path, params); + await requestInternal(method, path, params); } export default async function serverRequest( @@ -52,7 +52,7 @@ export default async function serverRequest( path: string, params: RequestParams = {}, ) { - const resp = await baseRequest(method, path, params); + const resp = await requestInternal(method, path, params); return (await resp.json()) as T; } diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx deleted file mode 100644 index 22d2fcd..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import i18n from "~/i18next.server"; -import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, useRouteLoaderData } from "@remix-run/react"; -import { Button, ListGroup } from "react-bootstrap"; -import { loader as settingsLoader } from "~/routes/settings/route"; -import { useTranslation } from "react-i18next"; -import { AuthMethod, MeUser } from "~/lib/api/user"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - return { meta: { title: t("settings.auth.title") } }; -}; - -export default function AuthSettings() { - const { user } = useRouteLoaderData("routes/settings")!; - - return ( -
- -
- ); -} - -function EmailSettings({ user }: { user: MeUser }) { - const { t } = useTranslation(); - const oneAuthMethod = user.auth_methods.length === 1; - const emails = user.auth_methods.filter((m) => m.type === "EMAIL"); - - return ( - <> -

{t("settings.auth.email-addresses")}

- {emails.length > 0 && ( - <> - - {emails.map((e) => ( - - ))} - - - )} - {emails.length < 3 && ( -

- {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} - -

- )} - - ); -} - -function EmailRow({ email, disabled }: { email: AuthMethod; disabled: boolean }) { - const { t } = useTranslation(); - - return ( - -
-
{email.remote_id}
- {!disabled && ( -
- - {t("settings.auth.remove-auth-method")} - -
- )} -
-
- ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx deleted file mode 100644 index 40eed7f..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { json, useActionData, useNavigate, useRouteLoaderData } from "@remix-run/react"; -import { loader as settingsLoader } from "~/routes/settings/route"; -import { useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { Button, Card, Form } from "react-bootstrap"; -import { Form as RemixForm } from "@remix-run/react/dist/components"; -import { ApiError, ErrorCode } from "~/lib/api/error"; -import { fastRequest, getToken } from "~/lib/request.server"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - return { meta: { title: t("settings.auth.title") } }; -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - const token = getToken(request)!; - const body = await request.formData(); - const email = body.get("email") as string | null; - const password = body.get("password-1") as string | null; - const password2 = body.get("password-2") as string | null; - - if (!email || !password || !password2) { - return json({ - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "One or more required fields are missing.", - } as ApiError, - ok: false, - }); - } - - if (password !== password2) { - return json({ - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "Passwords do not match.", - } as ApiError, - ok: false, - }); - } - - await fastRequest("POST", "/auth/email/add", { - body: { email, password }, - token, - isInternal: true, - }); - - return json({ error: null, ok: true }); -}; - -export default function AddEmailPage() { - const { t } = useTranslation(); - const { user } = useRouteLoaderData("routes/settings")!; - const actionData = useActionData(); - const navigate = useNavigate(); - const emails = user.auth_methods.filter((m) => m.type === "EMAIL"); - - useEffect(() => { - if (emails.length >= 3) { - navigate("/settings/auth"); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - - - {emails.length === 0 - ? t("settings.auth.form.add-first-email") - : t("settings.auth.form.add-extra-email")} - - {emails.length === 0 && !actionData?.ok &&

{t("settings.auth.no-email")}

} - {actionData?.ok &&

{t("settings.auth.new-email-pending")}

} - {actionData?.error && } -
- - {t("settings.auth.form.email-address")} - - - - {t("settings.auth.form.password-1")} - - - - {t("settings.auth.form.password-2")} - - - -
-
-
- ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx deleted file mode 100644 index d7fa86e..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { LoaderFunctionArgs, json } from "@remix-run/node"; -import { baseRequest } from "~/lib/request.server"; -import { useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { useNavigate } from "@remix-run/react"; - -export const loader = async ({ params }: LoaderFunctionArgs) => { - const state = params.code!; - - const resp = await baseRequest("POST", "/auth/email/callback", { - body: { state }, - isInternal: true, - }); - if (resp.status !== 204) { - // TODO: handle non-204 status (this indicates that the email was not linked to an account) - } - - return json({ ok: true }); -}; - -export default function ConfirmEmailPage() { - const { t } = useTranslation(); - const navigate = useNavigate(); - - useEffect(() => { - setTimeout(() => { - navigate("/settings/auth"); - }, 2000); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> -

{t("settings.auth.email-link-success")}

-

{t("settings.auth.redirect-to-auth-hint")}

- - ); -} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 5a25098..c35a1d7 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -107,23 +107,6 @@ "role": "Account role", "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}" }, - "auth": { - "title": "Authentication", - "form": { - "add-first-email": "Set an email address", - "add-extra-email": "Add another email address", - "email-address": "Email address", - "password-1": "Password", - "password-2": "Confirm password", - "add-email-button": "Add email address" - }, - "no-email": "You haven't linked any email addresses yet. You can add one using this form.", - "new-email-pending": "Email address added! Click the link in your inbox to confirm.", - "email-link-success": "Email successfully linked", - "redirect-to-auth-hint": "You will be redirected back to the authentication page in a few seconds.", - "email-addresses": "Email addresses", - "remove-auth-method": "Remove" - }, "title": "Settings", "nav": { "general-information": "General information",