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…
	
	Add table
		Add a link
		
	
		Reference in a new issue