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 System.Web;
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Database.Models;
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
@ -15,7 +16,7 @@ namespace Foxnouns.Backend.Controllers.Authentication;
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
Config config,
|
Config config,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
KeyCacheService keyCache,
|
KeyCacheService keyCacheService,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : ApiControllerBase
|
) : ApiControllerBase
|
||||||
{
|
{
|
||||||
|
@ -31,7 +32,7 @@ public class AuthController(
|
||||||
config.GoogleAuth.Enabled,
|
config.GoogleAuth.Enabled,
|
||||||
config.TumblrAuth.Enabled
|
config.TumblrAuth.Enabled
|
||||||
);
|
);
|
||||||
var state = HttpUtility.UrlEncode(await keyCache.GenerateAuthStateAsync(ct));
|
var state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct));
|
||||||
string? discord = null;
|
string? discord = null;
|
||||||
if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null })
|
if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null })
|
||||||
discord =
|
discord =
|
||||||
|
@ -75,6 +76,52 @@ public class AuthController(
|
||||||
|
|
||||||
return NoContent();
|
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(
|
public record CallbackResponse(
|
||||||
|
|
|
@ -148,6 +148,7 @@ public enum ErrorCode
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
MemberNotFound,
|
MemberNotFound,
|
||||||
AccountAlreadyLinked,
|
AccountAlreadyLinked,
|
||||||
|
LastAuthMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ValidationError
|
public class ValidationError
|
||||||
|
|
|
@ -67,22 +67,23 @@ public class UserRendererService(
|
||||||
renderMembers
|
renderMembers
|
||||||
? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden))
|
? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden))
|
||||||
: null,
|
: null,
|
||||||
renderAuthMethods
|
renderAuthMethods ? authMethods.Select(RenderAuthMethod) : null,
|
||||||
? authMethods.Select(a => new AuthenticationMethodResponse(
|
|
||||||
a.Id,
|
|
||||||
a.AuthType,
|
|
||||||
a.RemoteId,
|
|
||||||
a.FediverseApplication != null
|
|
||||||
? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}"
|
|
||||||
: a.RemoteUsername
|
|
||||||
))
|
|
||||||
: null,
|
|
||||||
tokenHidden ? user.ListHidden : null,
|
tokenHidden ? user.ListHidden : null,
|
||||||
tokenHidden ? user.LastActive : null,
|
tokenHidden ? user.LastActive : null,
|
||||||
tokenHidden ? user.LastSidReroll : 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) =>
|
public PartialUser RenderPartialUser(User user) =>
|
||||||
new(
|
new(
|
||||||
user.Id,
|
user.Id,
|
||||||
|
@ -118,7 +119,7 @@ public class UserRendererService(
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
IEnumerable<MemberRendererService.PartialMember>? Members,
|
IEnumerable<MemberRendererService.PartialMember>? Members,
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
|
IEnumerable<AuthMethodResponse>? AuthMethods,
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
bool? MemberListHidden,
|
bool? MemberListHidden,
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive,
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive,
|
||||||
|
@ -126,7 +127,7 @@ public class UserRendererService(
|
||||||
Instant? LastSidReroll
|
Instant? LastSidReroll
|
||||||
);
|
);
|
||||||
|
|
||||||
public record AuthenticationMethodResponse(
|
public record AuthMethodResponse(
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||||
string RemoteId,
|
string RemoteId,
|
||||||
|
|
|
@ -142,6 +142,8 @@ export const errorCodeDesc = (t: TFunction, code: ErrorCode) => {
|
||||||
return t("error.errors.user-not-found");
|
return t("error.errors.user-not-found");
|
||||||
case ErrorCode.AccountAlreadyLinked:
|
case ErrorCode.AccountAlreadyLinked:
|
||||||
return t("error.errors.account-already-linked");
|
return t("error.errors.account-already-linked");
|
||||||
|
case ErrorCode.LastAuthMetod:
|
||||||
|
return t("error.errors.last-auth-method");
|
||||||
}
|
}
|
||||||
|
|
||||||
return t("error.errors.generic-error");
|
return t("error.errors.generic-error");
|
||||||
|
|
|
@ -17,6 +17,7 @@ export enum ErrorCode {
|
||||||
UserNotFound = "USER_NOT_FOUND",
|
UserNotFound = "USER_NOT_FOUND",
|
||||||
MemberNotFound = "MEMBER_NOT_FOUND",
|
MemberNotFound = "MEMBER_NOT_FOUND",
|
||||||
AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED",
|
AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED",
|
||||||
|
LastAuthMetod = "LAST_AUTH_METHOD",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidationError = {
|
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.",
|
"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.",
|
"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.",
|
"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",
|
"title": "An error occurred",
|
||||||
"more-info": "Click here for a more detailed error"
|
"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.",
|
"new-email-pending": "Email address added! Click the link in your inbox to confirm.",
|
||||||
"email-link-success": "Email successfully linked",
|
"email-link-success": "Email successfully linked",
|
||||||
"redirect-to-auth-hint": "You will be redirected back to the authentication page in a few seconds.",
|
"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",
|
"remove-auth-method": "Remove",
|
||||||
|
"email-addresses": "Email addresses",
|
||||||
"discord-accounts": "Linked Discord accounts",
|
"discord-accounts": "Linked Discord accounts",
|
||||||
"fediverse-accounts": "Linked Fediverse accounts"
|
"fediverse-accounts": "Linked Fediverse accounts"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue