feat: link fediverse account to existing user
This commit is contained in:
parent
03209e4028
commit
57e1ec09c0
17 changed files with 335 additions and 95 deletions
|
@ -104,21 +104,9 @@ public class DiscordAuthController(
|
|||
{
|
||||
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 state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Discord
|
||||
);
|
||||
|
||||
var url =
|
||||
|
@ -138,12 +126,11 @@ public class DiscordAuthController(
|
|||
{
|
||||
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);
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Discord
|
||||
);
|
||||
|
||||
var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code);
|
||||
try
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
using System.Net;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
|
@ -15,13 +18,14 @@ public class FediverseAuthController(
|
|||
DatabaseContext db,
|
||||
FediverseAuthService fediverseAuthService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService,
|
||||
KeyCacheService keyCacheService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthController>();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<FediverseUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<AuthController.SingleUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFediverseUrlAsync(
|
||||
[FromQuery] string instance,
|
||||
[FromQuery] bool forceRefresh = false
|
||||
|
@ -31,7 +35,7 @@ public class FediverseAuthController(
|
|||
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
|
||||
|
||||
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh);
|
||||
return Ok(new FediverseUrlResponse(url));
|
||||
return Ok(new AuthController.SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("callback")]
|
||||
|
@ -118,9 +122,74 @@ public class FediverseAuthController(
|
|||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
public record CallbackRequest(string Instance, string Code, string State);
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddFediverseAccountAsync(
|
||||
[FromQuery] string instance,
|
||||
[FromQuery] bool forceRefresh = false
|
||||
)
|
||||
{
|
||||
if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.'))
|
||||
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
|
||||
|
||||
private record FediverseUrlResponse(string Url);
|
||||
var state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
instance
|
||||
);
|
||||
|
||||
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state);
|
||||
return Ok(new AuthController.SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
req.Instance
|
||||
);
|
||||
|
||||
var app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
|
||||
try
|
||||
{
|
||||
var authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Fediverse,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username,
|
||||
app
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Fediverse auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AuthController.AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Fediverse,
|
||||
authMethod.RemoteId,
|
||||
$"{authMethod.RemoteUsername}@{app.Domain}"
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record CallbackRequest(string Instance, string Code, string State);
|
||||
|
||||
private record FediverseTicketData(
|
||||
Snowflake ApplicationId,
|
||||
|
|
|
@ -63,13 +63,14 @@ public static class KeyCacheExtensions
|
|||
this KeyCacheService keyCacheService,
|
||||
AuthType authType,
|
||||
Snowflake userId,
|
||||
string? instance = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"add_account:{state}",
|
||||
new AddExtraAccountState(authType, userId),
|
||||
new AddExtraAccountState(authType, userId, instance),
|
||||
Duration.FromDays(1),
|
||||
ct
|
||||
);
|
||||
|
@ -93,4 +94,4 @@ public record RegisterEmailState(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId
|
||||
);
|
||||
|
||||
public record AddExtraAccountState(AuthType AuthType, Snowflake UserId);
|
||||
public record AddExtraAccountState(AuthType AuthType, Snowflake UserId, string? Instance = null);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<PackageReference Include="Coravel.Mailer" Version="5.0.1"/>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2"/>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7"/>
|
||||
|
|
|
@ -218,10 +218,11 @@ public class AuthService(
|
|||
AuthType authType,
|
||||
string remoteId,
|
||||
string? remoteUsername = null,
|
||||
FediverseApplication? app = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
AssertValidAuthType(authType, null);
|
||||
AssertValidAuthType(authType, app);
|
||||
|
||||
// This is already checked when
|
||||
var currentCount = await db
|
||||
|
@ -237,6 +238,7 @@ public class AuthService(
|
|||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
AuthType = authType,
|
||||
RemoteId = remoteId,
|
||||
FediverseApplicationId = app?.Id,
|
||||
RemoteUsername = remoteUsername,
|
||||
UserId = userId,
|
||||
};
|
||||
|
|
|
@ -69,9 +69,10 @@ public partial class FediverseAuthService
|
|||
private async Task<FediverseUser> GetMastodonUserAsync(
|
||||
FediverseApplication app,
|
||||
string code,
|
||||
string state
|
||||
string? state = null
|
||||
)
|
||||
{
|
||||
if (state != null)
|
||||
await _keyCacheService.ValidateAuthStateAsync(state);
|
||||
|
||||
var tokenResp = await _client.PostAsync(
|
||||
|
@ -120,7 +121,8 @@ public partial class FediverseAuthService
|
|||
|
||||
private async Task<string> GenerateMastodonAuthUrlAsync(
|
||||
FediverseApplication app,
|
||||
bool forceRefresh
|
||||
bool forceRefresh,
|
||||
string? state = null
|
||||
)
|
||||
{
|
||||
if (forceRefresh)
|
||||
|
@ -132,7 +134,7 @@ public partial class FediverseAuthService
|
|||
app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id);
|
||||
}
|
||||
|
||||
var state = HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
|
||||
state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
|
||||
|
||||
return $"https://{app.Domain}/oauth/authorize?response_type=code"
|
||||
+ $"&client_id={app.ClientId}"
|
||||
|
|
|
@ -37,10 +37,14 @@ public partial class FediverseAuthService
|
|||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAuthUrlAsync(string instance, bool forceRefresh)
|
||||
public async Task<string> GenerateAuthUrlAsync(
|
||||
string instance,
|
||||
bool forceRefresh,
|
||||
string? state = null
|
||||
)
|
||||
{
|
||||
var app = await GetApplicationAsync(instance);
|
||||
return await GenerateAuthUrlAsync(app, forceRefresh);
|
||||
return await GenerateAuthUrlAsync(app, forceRefresh, state);
|
||||
}
|
||||
|
||||
// thank you, gargron and syuilo, for agreeing on a name for *once* in your lives,
|
||||
|
@ -96,12 +100,17 @@ public partial class FediverseAuthService
|
|||
);
|
||||
}
|
||||
|
||||
private async Task<string> GenerateAuthUrlAsync(FediverseApplication app, bool forceRefresh) =>
|
||||
private async Task<string> GenerateAuthUrlAsync(
|
||||
FediverseApplication app,
|
||||
bool forceRefresh,
|
||||
string? state = null
|
||||
) =>
|
||||
app.InstanceType switch
|
||||
{
|
||||
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(
|
||||
app,
|
||||
forceRefresh
|
||||
forceRefresh,
|
||||
state
|
||||
),
|
||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
|
@ -110,7 +119,7 @@ public partial class FediverseAuthService
|
|||
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
|
||||
FediverseApplication app,
|
||||
string code,
|
||||
string state
|
||||
string? state = null
|
||||
) =>
|
||||
app.InstanceType switch
|
||||
{
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public class RemoteAuthService(Config config, ILogger logger)
|
||||
public class RemoteAuthService(
|
||||
Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<RemoteAuthService>();
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
@ -76,4 +88,56 @@ public class RemoteAuthService(Config config, ILogger logger)
|
|||
private record DiscordUserResponse(string id, string username);
|
||||
|
||||
public record RemoteUser(string Id, string Username);
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether a user can still add a new account of the given AuthType, and throws an error if they can't.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to check.</param>
|
||||
/// <param name="authType">The auth type to check.</param>
|
||||
/// <param name="instance">The optional fediverse instance to generate a state for.</param>
|
||||
/// <returns>A state for the given auth type and user ID.</returns>
|
||||
/// <exception cref="ApiError.BadRequest">The given user can't add another account of this type.
|
||||
/// This exception should not be caught by controller code.</exception>
|
||||
public async Task<string> ValidateAddAccountRequestAsync(
|
||||
Snowflake userId,
|
||||
AuthType authType,
|
||||
string? instance = null
|
||||
)
|
||||
{
|
||||
var existingAccounts = await db
|
||||
.AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType)
|
||||
.CountAsync();
|
||||
if (existingAccounts > AuthUtils.MaxAuthMethodsPerType)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
$"Too many linked {authType.Humanize()} accounts, maximum of {AuthUtils.MaxAuthMethodsPerType} per account."
|
||||
);
|
||||
}
|
||||
|
||||
return HttpUtility.UrlEncode(
|
||||
await keyCacheService.GenerateAddExtraAccountStateAsync(authType, userId, instance)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given state is correct for the given user/auth type combination.
|
||||
/// </summary>
|
||||
/// <exception cref="ApiError.BadRequest">The state doesn't match.
|
||||
/// This exception should not be caught by controller code.</exception>
|
||||
public async Task ValidateAddAccountStateAsync(
|
||||
string state,
|
||||
Snowflake userId,
|
||||
AuthType authType,
|
||||
string? instance = null
|
||||
)
|
||||
{
|
||||
var accountState = await keyCacheService.GetAddExtraAccountStateAsync(state);
|
||||
if (
|
||||
accountState == null
|
||||
|| accountState.AuthType != authType
|
||||
|| accountState.UserId != userId
|
||||
|| (instance != null && accountState.Instance != instance)
|
||||
)
|
||||
throw new ApiError.BadRequest("Invalid state", "state", state);
|
||||
}
|
||||
}
|
|
@ -127,6 +127,9 @@ public static partial class ValidationUtils
|
|||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
return errors;
|
||||
|
||||
var customPreferenceIds =
|
||||
customPreferences?.Keys.Select(id => id.ToString()).ToArray() ?? [];
|
||||
|
||||
foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)))
|
||||
{
|
||||
switch (entry.Value.Length)
|
||||
|
@ -159,8 +162,6 @@ public static partial class ValidationUtils
|
|||
break;
|
||||
}
|
||||
|
||||
var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? [];
|
||||
|
||||
if (
|
||||
!DefaultStatusOptions.Contains(entry.Status)
|
||||
&& !customPreferenceIds.Contains(entry.Status)
|
||||
|
@ -203,6 +204,9 @@ public static partial class ValidationUtils
|
|||
if (entries.Length > Limits.FieldEntriesLimit + 50)
|
||||
return errors;
|
||||
|
||||
var customPreferenceIds =
|
||||
customPreferences?.Keys.Select(id => id.ToString()).ToList() ?? [];
|
||||
|
||||
foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)))
|
||||
{
|
||||
switch (entry.Value.Length)
|
||||
|
@ -268,8 +272,6 @@ public static partial class ValidationUtils
|
|||
}
|
||||
}
|
||||
|
||||
var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? [];
|
||||
|
||||
if (
|
||||
!DefaultStatusOptions.Contains(entry.Status)
|
||||
&& !customPreferenceIds.Contains(entry.Status)
|
||||
|
|
|
@ -45,6 +45,12 @@
|
|||
"Npgsql": "8.0.1"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.14.1, )",
|
||||
"resolved": "2.14.1",
|
||||
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
|
||||
},
|
||||
"JetBrains.Annotations": {
|
||||
"type": "Direct",
|
||||
"requested": "[2024.2.0, )",
|
||||
|
@ -291,11 +297,6 @@
|
|||
"Microsoft.EntityFrameworkCore.Relational": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.14.1",
|
||||
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
|
||||
},
|
||||
"MailKit": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.1",
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
"successful-link-fedi": "Your account has successfully been linked to the following fediverse account:",
|
||||
"successful-link-profile-hint": "You now can close this page, or go back to your profile:",
|
||||
"successful-link-profile-link": "Go to your profile",
|
||||
"remote-discord-account-label": "Your Discord account"
|
||||
"remote-discord-account-label": "Your Discord account",
|
||||
"log-in-with-fediverse-instance-placeholder": "Your instance (i.e. mastodon.social)"
|
||||
},
|
||||
"error": {
|
||||
"bad-request-header": "Something was wrong with your input",
|
||||
|
|
|
@ -1,18 +1,39 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { ErrorCode } from "$api/error";
|
||||
import type { CallbackResponse } from "$api/models/auth.js";
|
||||
import type { AddAccountResponse, CallbackResponse } from "$api/models/auth.js";
|
||||
import { setToken } from "$lib";
|
||||
import createRegisterAction from "$lib/actions/register.js";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import log from "$lib/log";
|
||||
import { isRedirect, redirect } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ parent, params, url, fetch, cookies }) => {
|
||||
const { meUser } = await parent();
|
||||
if (meUser) redirect(303, `/@${meUser.username}`);
|
||||
|
||||
const code = url.searchParams.get("code") as string | null;
|
||||
const state = url.searchParams.get("state") as string | null;
|
||||
if (!code || !state) throw new ApiError(undefined, ErrorCode.BadRequest).obj;
|
||||
|
||||
const { meUser } = await parent();
|
||||
if (meUser) {
|
||||
try {
|
||||
const resp = await apiRequest<AddAccountResponse>(
|
||||
"POST",
|
||||
"/auth/fediverse/add-account/callback",
|
||||
{
|
||||
isInternal: true,
|
||||
body: { code, state, instance: params.instance },
|
||||
fetch,
|
||||
cookies,
|
||||
},
|
||||
);
|
||||
|
||||
return { hasAccount: true, isLinkRequest: true, newAuthMethod: resp };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) return { isLinkRequest: true, error: e.obj };
|
||||
log.error("error linking new fediverse account to user %s:", meUser.id, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await apiRequest<CallbackResponse>("POST", "/auth/fediverse/callback", {
|
||||
body: { code, state, instance: params.instance },
|
||||
isInternal: true,
|
||||
|
@ -26,10 +47,16 @@ export const load = async ({ parent, params, url, fetch, cookies }) => {
|
|||
|
||||
return {
|
||||
hasAccount: false,
|
||||
instance: params.instance,
|
||||
isLinkRequest: false,
|
||||
ticket: resp.ticket!,
|
||||
remoteUser: resp.remote_username!,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isRedirect(e)) throw e;
|
||||
if (e instanceof ApiError) return { isLinkRequest: false, error: e.obj };
|
||||
log.error("error while requesting fediverse callback:", e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import type { ActionData, PageData } from "./$types";
|
||||
import { t } from "$lib/i18n";
|
||||
import Error from "$components/Error.svelte";
|
||||
import OauthRegistrationForm from "$components/settings/OauthRegistrationForm.svelte";
|
||||
import NewAuthMethod from "$components/settings/NewAuthMethod.svelte";
|
||||
|
||||
type Props = { data: PageData; form: ActionData };
|
||||
let { data, form }: Props = $props();
|
||||
|
@ -12,11 +14,18 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
{#if data.error}
|
||||
<h1>{$t("auth.register-with-mastodon")}</h1>
|
||||
<Error error={data.error} />
|
||||
{:else if data.isLinkRequest}
|
||||
<NewAuthMethod method={data.newAuthMethod!} user={data.meUser!} />
|
||||
{:else}
|
||||
<OauthRegistrationForm
|
||||
title={$t("auth.register-with-mastodon")}
|
||||
remoteLabel={$t("auth.remote-fediverse-account-label")}
|
||||
remoteUser={data.remoteUser}
|
||||
ticket={data.ticket}
|
||||
remoteUser={data.remoteUser!}
|
||||
ticket={data.ticket!}
|
||||
error={form?.error}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -72,7 +72,11 @@
|
|||
<h4 class="mt-4">{$t("auth.log-in-with-the-fediverse")}</h4>
|
||||
<form method="POST" action="?/fedi" use:enhance>
|
||||
<InputGroup>
|
||||
<Input name="instance" type="text" placeholder="Your instance (i.e. mastodon.social)" />
|
||||
<Input
|
||||
name="instance"
|
||||
type="text"
|
||||
placeholder={$t("auth.log-in-with-fediverse-instance-placeholder")}
|
||||
/>
|
||||
<Button type="submit" color="secondary">{$t("auth.log-in-button")}</Button>
|
||||
</InputGroup>
|
||||
<p>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { apiRequest } from "$api";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const actions = {
|
||||
add: async ({ request, fetch, cookies }) => {
|
||||
const body = await request.formData();
|
||||
const instance = body.get("instance") as string;
|
||||
|
||||
const { url } = await apiRequest<{ url: string }>(
|
||||
"GET",
|
||||
`/auth/fediverse/add-account?instance=${encodeURIComponent(instance)}`,
|
||||
{
|
||||
isInternal: true,
|
||||
fetch,
|
||||
cookies,
|
||||
},
|
||||
);
|
||||
|
||||
redirect(303, url);
|
||||
},
|
||||
forceRefresh: async ({ request, fetch, cookies }) => {
|
||||
const body = await request.formData();
|
||||
const instance = body.get("instance") as string;
|
||||
|
||||
const { url } = await apiRequest<{ url: string }>(
|
||||
"GET",
|
||||
`/auth/fediverse/add-account?instance=${encodeURIComponent(instance)}&forceRefresh=true`,
|
||||
{
|
||||
isInternal: true,
|
||||
fetch,
|
||||
cookies,
|
||||
},
|
||||
);
|
||||
|
||||
redirect(303, url);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n";
|
||||
import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
</script>
|
||||
|
||||
<h3>Link a new Fediverse account</h3>
|
||||
|
||||
<form method="POST" action="?/add">
|
||||
<InputGroup>
|
||||
<Input
|
||||
name="instance"
|
||||
type="text"
|
||||
placeholder={$t("auth.log-in-with-fediverse-instance-placeholder")}
|
||||
/>
|
||||
<Button type="submit" color="secondary">{$t("auth.log-in-button")}</Button>
|
||||
</InputGroup>
|
||||
<p>
|
||||
{$t("auth.log-in-with-fediverse-error-blurb")}
|
||||
<Button formaction="?/forceRefresh" type="submit" color="link">
|
||||
{$t("auth.log-in-with-fediverse-force-refresh-button")}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
|
@ -12,6 +12,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
|
|
Loading…
Reference in a new issue