diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 30bcbe9..1a737eb 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -50,6 +50,17 @@ public class AuthController( Instant ExpiresAt ); + public record CallbackResponse( + bool HasAccount, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? RemoteUsername, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + UserRendererService.UserResponse? User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt + ); + public record OauthRegisterRequest(string Ticket, string Username); public record CallbackRequest(string Code, string State); @@ -66,13 +77,3 @@ public class AuthController( return NoContent(); } } - -public record CallbackResponse( - bool HasAccount, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - UserRendererService.UserResponse? User, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt -); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index aad683f..344d8ff 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -2,7 +2,6 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; -using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Utils; using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; @@ -15,25 +14,32 @@ namespace Foxnouns.Backend.Controllers.Authentication; public class DiscordAuthController( [UsedImplicitly] Config config, ILogger logger, + IClock clock, DatabaseContext db, KeyCacheService keyCacheService, AuthService authService, - RemoteAuthService remoteAuthService + RemoteAuthService remoteAuthService, + UserRendererService userRenderer ) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); [HttpPost("callback")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) + // 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 + ) { CheckRequirements(); - await keyCacheService.ValidateAuthStateAsync(req.State); + await keyCacheService.ValidateAuthStateAsync(req.State, ct); - var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); - var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); + 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 authService.GenerateUserTokenAsync(user)); + return Ok(await GenerateUserTokenAsync(user, ct)); _logger.Debug( "Discord user {Username} ({Id}) authenticated with no local account", @@ -45,11 +51,12 @@ public class DiscordAuthController( await keyCacheService.SetKeyAsync( $"discord:{ticket}", remoteUser, - Duration.FromMinutes(20) + Duration.FromMinutes(20), + ct ); return Ok( - new CallbackResponse( + new AuthController.CallbackResponse( HasAccount: false, Ticket: ticket, RemoteUsername: remoteUser.Username, @@ -91,7 +98,42 @@ public class DiscordAuthController( remoteUser.Username ); - return Ok(await authService.GenerateUserTokenAsync(user)); + return Ok(await GenerateUserTokenAsync(user)); + } + + 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); + + var (tokenStr, token) = authService.GenerateToken( + user, + frontendApp, + ["*"], + clock.GetCurrentInstant() + Duration.FromDays(365) + ); + db.Add(token); + + _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); + + await db.SaveChangesAsync(ct); + + return new AuthController.CallbackResponse( + HasAccount: true, + Ticket: null, + RemoteUsername: null, + User: await userRenderer.RenderUserAsync( + user, + selfUser: user, + renderMembers: false, + ct: ct + ), + Token: tokenStr, + ExpiresAt: token.ExpiresAt + ); } private void CheckRequirements() diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 6aadf65..7e3706e 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.Middleware; using Foxnouns.Backend.Services; -using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Utils; using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; @@ -85,7 +84,7 @@ public class EmailAuthController( await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); return Ok( - new CallbackResponse( + new AuthController.CallbackResponse( HasAccount: false, Ticket: ticket, RemoteUsername: state.Email, diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 43a2955..fdd10b7 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -1,26 +1,11 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Services; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; -using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/fediverse")] -public class FediverseAuthController( - ILogger logger, - DatabaseContext db, - FediverseAuthService fediverseAuthService, - AuthService authService, - KeyCacheService keyCacheService -) : ApiControllerBase +public class FediverseAuthController(FediverseAuthService fediverseAuthService) : ApiControllerBase { - private readonly ILogger _logger = logger.ForContext(); - [HttpGet] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetFediverseUrlAsync([FromQuery] string instance) @@ -29,88 +14,12 @@ public class FediverseAuthController( return Ok(new FediverseUrlResponse(url)); } - [HttpPost("callback")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { - var app = await fediverseAuthService.GetApplicationAsync(req.Instance); - var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); - - var user = await authService.AuthenticateUserAsync( - AuthType.Fediverse, - remoteUser.Id, - instance: app - ); - if (user != null) - return Ok(await authService.GenerateUserTokenAsync(user)); - - var ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"fediverse:{ticket}", - new FediverseTicketData(app.Id, remoteUser), - Duration.FromMinutes(20) - ); - - return Ok( - new CallbackResponse( - HasAccount: false, - Ticket: ticket, - RemoteUsername: $"@{remoteUser.Username}@{app.Domain}", - User: null, - Token: null, - ExpiresAt: null - ) - ); - } - - [HttpPost("register")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task RegisterAsync( - [FromBody] AuthController.OauthRegisterRequest req - ) - { - var ticketData = await keyCacheService.GetKeyAsync( - $"fediverse:{req.Ticket}" - ); - if (ticketData == null) - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - - var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId); - if ( - await db.AuthMethods.AnyAsync(a => - a.AuthType == AuthType.Fediverse - && a.RemoteId == ticketData.User.Id - && a.FediverseApplicationId == app.Id - ) - ) - { - _logger.Error( - "Fediverse user {Id}/{ApplicationId} ({Username} on {Domain}) has valid ticket but is already linked to an existing account", - ticketData.User.Id, - ticketData.ApplicationId, - ticketData.User.Username, - app.Domain - ); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - } - - var user = await authService.CreateUserWithRemoteAuthAsync( - req.Username, - AuthType.Fediverse, - ticketData.User.Id, - ticketData.User.Username, - instance: app - ); - - return Ok(await authService.GenerateUserTokenAsync(user)); + throw new NotImplementedException(); } public record CallbackRequest(string Instance, string Code); private record FediverseUrlResponse(string Url); - - private record FediverseTicketData( - Snowflake ApplicationId, - FediverseAuthService.FediverseUser User - ); } diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index c505f4d..ce2f59b 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -4,14 +4,12 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; -using Foxnouns.Backend.Services.Auth; using Microsoft.EntityFrameworkCore; using Minio; using NodaTime; using Prometheus; using Serilog; using Serilog.Events; -using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; using IClock = NodaTime.IClock; namespace Foxnouns.Backend.Extensions; diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs similarity index 85% rename from Foxnouns.Backend/Services/Auth/AuthService.cs rename to Foxnouns.Backend/Services/AuthService.cs index 9675f22..d03496c 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -1,5 +1,4 @@ using System.Security.Cryptography; -using Foxnouns.Backend.Controllers.Authentication; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; @@ -7,17 +6,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Foxnouns.Backend.Services.Auth; +namespace Foxnouns.Backend.Services; -public class AuthService( - IClock clock, - ILogger logger, - DatabaseContext db, - ISnowflakeGenerator snowflakeGenerator, - UserRendererService userRenderer -) +public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) { - private readonly ILogger _logger = logger.ForContext(); private readonly PasswordHasher _passwordHasher = new(); /// @@ -264,45 +256,6 @@ public class AuthService( ); } - /// - /// Generates a token for the given user and adds it to the database, returning a fully formed auth response for the user. - /// This method is always called at the end of an endpoint method, so the resulting token - /// (and user, if this is a registration request) is also saved to the database. - /// - public async Task GenerateUserTokenAsync( - User user, - CancellationToken ct = default - ) - { - var frontendApp = await db.GetFrontendApplicationAsync(ct); - - var (tokenStr, token) = GenerateToken( - user, - frontendApp, - ["*"], - clock.GetCurrentInstant() + Duration.FromDays(365) - ); - db.Add(token); - - _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); - - await db.SaveChangesAsync(ct); - - return new CallbackResponse( - HasAccount: true, - Ticket: null, - RemoteUsername: null, - User: await userRenderer.RenderUserAsync( - user, - selfUser: user, - renderMembers: false, - ct: ct - ), - Token: tokenStr, - ExpiresAt: token.ExpiresAt - ); - } - private static (string, byte[]) GenerateToken() { var token = AuthUtils.RandomToken(); diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs similarity index 98% rename from Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs rename to Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs index 139830b..e0e5e98 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs @@ -5,12 +5,12 @@ using Foxnouns.Backend.Database.Models; using Duration = NodaTime.Duration; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; -namespace Foxnouns.Backend.Services.Auth; +namespace Foxnouns.Backend.Services; public partial class FediverseAuthService { private string MastodonRedirectUri(string instance) => - $"{_config.BaseUrl}/auth/callback/mastodon/{instance}"; + $"{_config.BaseUrl}/auth/login/mastodon/{instance}"; private async Task CreateMastodonApplicationAsync( string instance, diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/FediverseAuthService.cs similarity index 93% rename from Foxnouns.Backend/Services/Auth/FediverseAuthService.cs rename to Foxnouns.Backend/Services/FediverseAuthService.cs index fc54017..ff39e88 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/FediverseAuthService.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; -namespace Foxnouns.Backend.Services.Auth; +namespace Foxnouns.Backend.Services; public partial class FediverseAuthService { @@ -43,6 +43,12 @@ public partial class FediverseAuthService return await GenerateAuthUrlAsync(app); } + public async Task GetRemoteFediverseUserAsync(string instance, string code) + { + var app = await GetApplicationAsync(instance); + return await GetRemoteUserAsync(app, code); + } + // thank you, gargron and syuilo, for agreeing on a name for *once* in your lives, // and having both mastodon and misskey use "username" in the self user response public record FediverseUser( @@ -50,7 +56,7 @@ public partial class FediverseAuthService [property: J("username")] string Username ); - public async Task GetApplicationAsync(string instance) + private async Task GetApplicationAsync(string instance) { var app = await _db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance); if (app != null) @@ -104,10 +110,7 @@ public partial class FediverseAuthService _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; - public async Task GetRemoteFediverseUserAsync( - FediverseApplication app, - string code - ) => + private async Task GetRemoteUserAsync(FediverseApplication app, string code) => app.InstanceType switch { FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code), diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index 91a2dc5..505029d 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -13,6 +13,7 @@ public class RemoteAuthService(Config config, ILogger logger) public async Task RequestDiscordTokenAsync( string code, + string state, CancellationToken ct = default ) { diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index b560ba6..b47832d 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -97,7 +97,7 @@ public class UserRendererService( ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; public record UserResponse( Snowflake Id, diff --git a/Foxnouns.Frontend/app/components/RegisterError.tsx b/Foxnouns.Frontend/app/components/RegisterError.tsx deleted file mode 100644 index 1ecbbdb..0000000 --- a/Foxnouns.Frontend/app/components/RegisterError.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ApiError, firstErrorFor } from "~/lib/api/error"; -import { Trans, useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; -import { Link } from "@remix-run/react"; -import ErrorAlert from "~/components/ErrorAlert"; - -export default 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), 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/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 49f2d20..8dab154 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -30,7 +30,7 @@ export async function baseRequest( headers: { ...params.headers, ...(params.token ? { Authorization: params.token } : {}), - ...(params.body ? { "Content-Type": "application/json" } : {}), + "Content-Type": "application/json", }, }); diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 3a1fd1a..5fb246c 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -22,7 +22,6 @@ import ErrorAlert from "~/components/ErrorAlert"; import i18n from "~/i18next.server"; import { tokenCookieName } from "~/lib/utils"; import { useEffect } from "react"; -import RegisterError from "~/components/RegisterError"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -164,3 +163,34 @@ 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/app/routes/auth.callback.mastodon.$instance/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx deleted file mode 100644 index 8444bb5..0000000 --- a/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - ActionFunctionArgs, - json, - LoaderFunctionArgs, - MetaFunction, - redirect, -} from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { type ApiError, ErrorCode } from "~/lib/api/error"; -import serverRequest, { writeCookie } from "~/lib/request.server"; -import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; -import { tokenCookieName } from "~/lib/utils"; -import { - Link, - ShouldRevalidateFunction, - useActionData, - useLoaderData, - useNavigate, -} from "@remix-run/react"; -import { Trans, useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { Form as RemixForm } from "@remix-run/react/dist/components"; -import { Button, Form } from "react-bootstrap"; -import RegisterError from "~/components/RegisterError"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; -}; - -export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { - return !actionResult; -}; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const url = new URL(request.url); - - const code = url.searchParams.get("code"); - if (!code) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code" } as ApiError; - - const resp = await serverRequest("POST", "/auth/fediverse/callback", { - body: { code, instance: params.instance! }, - isInternal: true, - }); - - if (resp.has_account) { - return json( - { - meta: { title: t("log-in.callback.title.fediverse-success") }, - hasAccount: true, - user: resp.user!, - ticket: null, - remoteUser: null, - }, - { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token!), - }, - }, - ); - } - - return json({ - meta: { title: t("log-in.callback.title.fediverse-register") }, - hasAccount: false, - user: null, - instance: params.instance!, - ticket: resp.ticket!, - 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/fediverse/register", { - body: { username, ticket }, - isInternal: true, - }); - - return redirect("/auth/welcome", { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token), - }, - status: 303, - }); - } catch (e) { - JSON.stringify(e); - - return json({ error: e as ApiError }); - } -}; - -export default function FediverseCallbackPage() { - const { t } = useTranslation(); - const data = useLoaderData(); - const actionData = useActionData(); - const navigate = useNavigate(); - - useEffect(() => { - setTimeout(() => { - if (data.hasAccount) { - navigate(`/@${data.user!.username}`); - } - }, 2000); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - 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 }}! - -
- {t("log-in.callback.redirect-hint")} -

- - ); - } - - return ( - -
- {actionData?.error && } - - {t("log-in.callback.remote-username.fediverse")} - - - - {t("log-in.callback.username")} - - - - - -
- ); -} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index dc3af98..aaffd80 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -126,9 +126,6 @@ export default function LoginPage() { {t("log-in.3rd-party.tumblr")} )} - - {t("log-in.3rd-party.fediverse")} - {!urls.email_enabled &&
} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx deleted file mode 100644 index a7304f5..0000000 --- a/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - LoaderFunctionArgs, - json, - MetaFunction, - ActionFunctionArgs, - redirect, -} from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { useTranslation } from "react-i18next"; -import { Form as RemixForm, useActionData } from "@remix-run/react"; -import { Button, Form } from "react-bootstrap"; -import serverRequest from "~/lib/request.server"; -import { ApiError, ErrorCode } from "~/lib/api/error"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in with a Fediverse account"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - - return json({ meta: { title: t("log-in.fediverse.choose-title") } }); -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - const body = await request.formData(); - const instance = body.get("instance") as string | null; - if (!instance) - return json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid instance name", - } as ApiError, - }); - - try { - const resp = await serverRequest<{ url: string }>( - "GET", - `/auth/fediverse?instance=${encodeURIComponent(instance)}`, - { - isInternal: true, - }, - ); - - return redirect(resp.url); - } catch (e) { - return json({ error: e as ApiError }); - } -}; - -export default function AuthFediversePage() { - const { t } = useTranslation(); - - const data = useActionData(); - - return ( - <> -

{t("log-in.fediverse.choose-form-title")}

- {data?.error && } - -
- - {t("log-in.fediverse-instance-label")} - - - -
-
- - ); -} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 78e2bf3..c834e95 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -47,31 +47,22 @@ }, "log-in": { "callback": { - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else.", "title": { "discord-success": "Log in with Discord", - "discord-register": "Register with Discord", - "fediverse-success": "Log in with a Fediverse account", - "fediverse-register": "Register with a Fediverse account" + "discord-register": "Register with Discord" }, "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", - "fediverse": "Your Fediverse account" + "discord": "Your discord username" }, "username": "Username", - "sign-up-button": "Sign up" + "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." }, - "fediverse": { - "choose-title": "Log in with a Fediverse account", - "choose-form-title": "Choose a Fediverse instance" - }, - "fediverse-instance-label": "Your Fediverse instance", - "fediverse-log-in-button": "Log in", "title": "Log in", "form-title": "Log in with email", "email": "Email address", @@ -83,8 +74,7 @@ "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", - "fediverse": "Log in with the Fediverse" + "tumblr": "Log in with Tumblr" }, "invalid-credentials": "Invalid email address or password, please check your spelling and try again." }, diff --git a/Foxnouns.NET.sln.DotSettings b/Foxnouns.NET.sln.DotSettings index e9d37e2..69e6273 100644 --- a/Foxnouns.NET.sln.DotSettings +++ b/Foxnouns.NET.sln.DotSettings @@ -1,18 +1,3 @@  - 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/>. - True True True \ No newline at end of file