feat: remove auth method
This commit is contained in:
parent
201c56c3dd
commit
9160281ea2
7 changed files with 144 additions and 16 deletions
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
@ -15,7 +16,7 @@ namespace Foxnouns.Backend.Controllers.Authentication;
|
|||
public class AuthController(
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCache,
|
||||
KeyCacheService keyCacheService,
|
||||
ILogger logger
|
||||
) : ApiControllerBase
|
||||
{
|
||||
|
@ -31,7 +32,7 @@ public class AuthController(
|
|||
config.GoogleAuth.Enabled,
|
||||
config.TumblrAuth.Enabled
|
||||
);
|
||||
var state = HttpUtility.UrlEncode(await keyCache.GenerateAuthStateAsync(ct));
|
||||
var state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct));
|
||||
string? discord = null;
|
||||
if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null })
|
||||
discord =
|
||||
|
@ -75,6 +76,52 @@ public class AuthController(
|
|||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("methods/{id}")]
|
||||
[Authorize("*")]
|
||||
[ProducesResponseType<UserRendererService.AuthMethodResponse>(
|
||||
statusCode: StatusCodes.Status200OK
|
||||
)]
|
||||
public async Task<IActionResult> GetAuthMethodAsync(Snowflake id)
|
||||
{
|
||||
var authMethod = await db
|
||||
.AuthMethods.Include(a => a.FediverseApplication)
|
||||
.FirstOrDefaultAsync(a => a.UserId == CurrentUser!.Id && a.Id == id);
|
||||
if (authMethod == null)
|
||||
throw new ApiError.NotFound("No authentication method with that ID found.");
|
||||
|
||||
return Ok(UserRendererService.RenderAuthMethod(authMethod));
|
||||
}
|
||||
|
||||
[HttpDelete("methods/{id}")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> DeleteAuthMethodAsync(Snowflake id)
|
||||
{
|
||||
var authMethods = await db
|
||||
.AuthMethods.Where(a => a.UserId == CurrentUser!.Id)
|
||||
.ToListAsync();
|
||||
if (authMethods.Count < 2)
|
||||
throw new ApiError(
|
||||
"You cannot remove your last authentication method.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.LastAuthMethod
|
||||
);
|
||||
|
||||
var authMethod = authMethods.FirstOrDefault(a => a.Id == id);
|
||||
if (authMethod == null)
|
||||
throw new ApiError.NotFound("No authentication method with that ID found.");
|
||||
|
||||
_logger.Debug(
|
||||
"Deleting auth method {AuthMethodId} for user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
db.Remove(authMethod);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public record CallbackResponse(
|
||||
|
|
|
@ -148,6 +148,7 @@ public enum ErrorCode
|
|||
UserNotFound,
|
||||
MemberNotFound,
|
||||
AccountAlreadyLinked,
|
||||
LastAuthMethod,
|
||||
}
|
||||
|
||||
public class ValidationError
|
||||
|
|
|
@ -67,22 +67,23 @@ public class UserRendererService(
|
|||
renderMembers
|
||||
? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden))
|
||||
: null,
|
||||
renderAuthMethods
|
||||
? authMethods.Select(a => new AuthenticationMethodResponse(
|
||||
a.Id,
|
||||
a.AuthType,
|
||||
a.RemoteId,
|
||||
a.FediverseApplication != null
|
||||
? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}"
|
||||
: a.RemoteUsername
|
||||
))
|
||||
: null,
|
||||
renderAuthMethods ? authMethods.Select(RenderAuthMethod) : null,
|
||||
tokenHidden ? user.ListHidden : null,
|
||||
tokenHidden ? user.LastActive : null,
|
||||
tokenHidden ? user.LastSidReroll : null
|
||||
);
|
||||
}
|
||||
|
||||
public static AuthMethodResponse RenderAuthMethod(AuthMethod a) =>
|
||||
new(
|
||||
a.Id,
|
||||
a.AuthType,
|
||||
a.RemoteId,
|
||||
a.FediverseApplication != null
|
||||
? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}"
|
||||
: a.RemoteUsername
|
||||
);
|
||||
|
||||
public PartialUser RenderPartialUser(User user) =>
|
||||
new(
|
||||
user.Id,
|
||||
|
@ -118,7 +119,7 @@ public class UserRendererService(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
IEnumerable<MemberRendererService.PartialMember>? Members,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
|
||||
IEnumerable<AuthMethodResponse>? AuthMethods,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
bool? MemberListHidden,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive,
|
||||
|
@ -126,7 +127,7 @@ public class UserRendererService(
|
|||
Instant? LastSidReroll
|
||||
);
|
||||
|
||||
public record AuthenticationMethodResponse(
|
||||
public record AuthMethodResponse(
|
||||
Snowflake Id,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||
string RemoteId,
|
||||
|
|
|
@ -142,6 +142,8 @@ export const errorCodeDesc = (t: TFunction, code: ErrorCode) => {
|
|||
return t("error.errors.user-not-found");
|
||||
case ErrorCode.AccountAlreadyLinked:
|
||||
return t("error.errors.account-already-linked");
|
||||
case ErrorCode.LastAuthMetod:
|
||||
return t("error.errors.last-auth-method");
|
||||
}
|
||||
|
||||
return t("error.errors.generic-error");
|
||||
|
|
|
@ -17,6 +17,7 @@ export enum ErrorCode {
|
|||
UserNotFound = "USER_NOT_FOUND",
|
||||
MemberNotFound = "MEMBER_NOT_FOUND",
|
||||
AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED",
|
||||
LastAuthMetod = "LAST_AUTH_METHOD",
|
||||
}
|
||||
|
||||
export type ValidationError = {
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { ActionFunctionArgs, json, LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import i18n from "~/i18next.server";
|
||||
import serverRequest, { fastRequest, getToken } from "~/lib/request.server";
|
||||
import { AuthMethod } from "~/lib/api/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData, Form } from "@remix-run/react";
|
||||
import { Button } from "react-bootstrap";
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const data = await request.formData();
|
||||
const token = getToken(request);
|
||||
|
||||
const id = data.get("remove-id") as string;
|
||||
|
||||
await fastRequest("DELETE", `/auth/methods/${id}`, { token, isInternal: true });
|
||||
|
||||
return redirect("/settings/auth", 303);
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const t = await i18n.getFixedT(request);
|
||||
const token = getToken(request);
|
||||
|
||||
const method = await serverRequest<AuthMethod>("GET", `/auth/methods/${params.id}`, {
|
||||
token,
|
||||
isInternal: true,
|
||||
});
|
||||
return json({ method, meta: { title: t("settings.auth.remove-auth-method-title") } });
|
||||
};
|
||||
|
||||
export default function RemoveAuthMethodPage() {
|
||||
const { t } = useTranslation();
|
||||
const { method } = useLoaderData<typeof loader>();
|
||||
|
||||
let methodName;
|
||||
switch (method.type) {
|
||||
case "EMAIL":
|
||||
methodName = "email";
|
||||
break;
|
||||
case "DISCORD":
|
||||
methodName = "Discord";
|
||||
break;
|
||||
case "FEDIVERSE":
|
||||
methodName = "Fediverse";
|
||||
break;
|
||||
case "GOOGLE":
|
||||
methodName = "Google";
|
||||
break;
|
||||
case "TUMBLR":
|
||||
methodName = "Tumblr";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>{t("settings.auth.remove-auth-method-title")}</h3>
|
||||
<p>
|
||||
{t("settings.auth.remove-auth-method-hint", {
|
||||
username: method.remote_username || method.remote_id,
|
||||
methodName,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<Form method="POST">
|
||||
<input type="hidden" name="remove-id" value={method.id} />
|
||||
<Button type="submit" color="primary">
|
||||
{t("settings.auth.remove-auth-method")}
|
||||
</Button>
|
||||
</Form>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -17,7 +17,8 @@
|
|||
"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.",
|
||||
"account-already-linked": "This account is already linked with a pronouns.cc account."
|
||||
"account-already-linked": "This account is already linked with a pronouns.cc account.",
|
||||
"last-auth-method": "You cannot remove your last authentication method."
|
||||
},
|
||||
"title": "An error occurred",
|
||||
"more-info": "Click here for a more detailed error"
|
||||
|
@ -141,8 +142,10 @@
|
|||
"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-title": "Remove authentication method",
|
||||
"remove-auth-method-hint": "Are you sure you want to remove {{username}} ({{methodName}}) from your account? You will no longer be able to log in using it.",
|
||||
"remove-auth-method": "Remove",
|
||||
"email-addresses": "Email addresses",
|
||||
"discord-accounts": "Linked Discord accounts",
|
||||
"fediverse-accounts": "Linked Fediverse accounts"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue