feat: link discord account to existing account
This commit is contained in:
parent
c4cb08cdc1
commit
201c56c3dd
12 changed files with 333 additions and 14 deletions
|
@ -1,5 +1,6 @@
|
|||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
|
@ -50,6 +51,15 @@ public class AuthController(
|
|||
Instant ExpiresAt
|
||||
);
|
||||
|
||||
public record SingleUrlResponse(string Url);
|
||||
|
||||
public record AddOauthAccountResponse(
|
||||
Snowflake Id,
|
||||
AuthType Type,
|
||||
string RemoteId,
|
||||
string? RemoteUsername
|
||||
);
|
||||
|
||||
public record OauthRegisterRequest(string Ticket, string Username);
|
||||
|
||||
public record CallbackRequest(string Code, string State);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
using System.Net;
|
||||
using System.Web;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
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;
|
||||
|
@ -94,6 +98,87 @@ public class DiscordAuthController(
|
|||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddDiscordAccountAsync()
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
var existingAccounts = await db
|
||||
.AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Discord)
|
||||
.CountAsync();
|
||||
if (existingAccounts > AuthUtils.MaxAuthMethodsPerType)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Too many linked Discord accounts, maximum of 3 per account."
|
||||
);
|
||||
}
|
||||
|
||||
var state = HttpUtility.UrlEncode(
|
||||
await keyCacheService.GenerateAddExtraAccountStateAsync(
|
||||
AuthType.Discord,
|
||||
CurrentUser!.Id
|
||||
)
|
||||
);
|
||||
|
||||
var url =
|
||||
$"https://discord.com/oauth2/authorize?response_type=code"
|
||||
+ $"&client_id={config.DiscordAuth.ClientId}&scope=identify"
|
||||
+ $"&prompt=none&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}";
|
||||
|
||||
return Ok(new AuthController.SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync(
|
||||
[FromBody] AuthController.CallbackRequest req
|
||||
)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
var accountState = await keyCacheService.GetAddExtraAccountStateAsync(req.State);
|
||||
if (
|
||||
accountState is not { AuthType: AuthType.Discord }
|
||||
|| accountState.UserId != CurrentUser!.Id
|
||||
)
|
||||
throw new ApiError.BadRequest("Invalid state", "state", req.State);
|
||||
|
||||
var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code);
|
||||
try
|
||||
{
|
||||
var authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Discord,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Discord auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AuthController.AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Discord,
|
||||
authMethod.RemoteId,
|
||||
authMethod.RemoteUsername
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequirements()
|
||||
{
|
||||
if (!config.DiscordAuth.Enabled)
|
||||
|
|
|
@ -147,6 +147,7 @@ public enum ErrorCode
|
|||
GenericApiError,
|
||||
UserNotFound,
|
||||
MemberNotFound,
|
||||
AccountAlreadyLinked,
|
||||
}
|
||||
|
||||
public class ValidationError
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -57,9 +58,39 @@ public static class KeyCacheExtensions
|
|||
delete: true,
|
||||
ct
|
||||
);
|
||||
|
||||
public static async Task<string> GenerateAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
AuthType authType,
|
||||
Snowflake userId,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"add_account:{state}",
|
||||
new AddExtraAccountState(authType, userId),
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<AddExtraAccountState?> GetAddExtraAccountStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await keyCacheService.GetKeyAsync<AddExtraAccountState>(
|
||||
$"add_account:{state}",
|
||||
delete: true,
|
||||
ct
|
||||
);
|
||||
}
|
||||
|
||||
public record RegisterEmailState(
|
||||
string Email,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId
|
||||
);
|
||||
|
||||
public record AddExtraAccountState(AuthType AuthType, Snowflake UserId);
|
||||
|
|
|
@ -72,8 +72,9 @@ public class UserRendererService(
|
|||
a.Id,
|
||||
a.AuthType,
|
||||
a.RemoteId,
|
||||
a.RemoteUsername,
|
||||
a.FediverseApplication?.Domain
|
||||
a.FediverseApplication != null
|
||||
? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}"
|
||||
: a.RemoteUsername
|
||||
))
|
||||
: null,
|
||||
tokenHidden ? user.ListHidden : null,
|
||||
|
@ -130,9 +131,7 @@ public class UserRendererService(
|
|||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||
string RemoteId,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
string? RemoteUsername,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
string? FediverseInstance
|
||||
string? RemoteUsername
|
||||
);
|
||||
|
||||
public record PartialUser(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue