feat: log in with google
This commit is contained in:
		
							parent
							
								
									bb2fa55cd5
								
							
						
					
					
						commit
						8a8b4caa18
					
				
					 11 changed files with 403 additions and 74 deletions
				
			
		|  | @ -33,6 +33,7 @@ public class AuthController( | |||
|         ); | ||||
|         string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); | ||||
|         string? discord = null; | ||||
|         string? google = null; | ||||
|         if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) | ||||
|         { | ||||
|             discord = | ||||
|  | @ -42,7 +43,17 @@ public class AuthController( | |||
|                 + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; | ||||
|         } | ||||
| 
 | ||||
|         return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); | ||||
|         if (config.GoogleAuth is { ClientId: not null, ClientSecret: not null }) | ||||
|         { | ||||
|             google = | ||||
|                 "https://accounts.google.com/o/oauth2/auth?response_type=code" | ||||
|                 + $"&client_id={config.GoogleAuth.ClientId}" | ||||
|                 + $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}" | ||||
|                 + $"&prompt=select_account&state={state}" | ||||
|                 + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}"; | ||||
|         } | ||||
| 
 | ||||
|         return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, google, null)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("force-log-out")] | ||||
|  |  | |||
|  | @ -0,0 +1,164 @@ | |||
| using System.Net; | ||||
| using System.Web; | ||||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Services.Auth; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using JetBrains.Annotations; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers.Authentication; | ||||
| 
 | ||||
| [Route("/api/internal/auth/google")] | ||||
| public class GoogleAuthController( | ||||
|     [UsedImplicitly] Config config, | ||||
|     ILogger logger, | ||||
|     DatabaseContext db, | ||||
|     KeyCacheService keyCacheService, | ||||
|     AuthService authService, | ||||
|     RemoteAuthService remoteAuthService | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<GoogleAuthController>(); | ||||
| 
 | ||||
|     [HttpPost("callback")] | ||||
|     [ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
|         await keyCacheService.ValidateAuthStateAsync(req.State); | ||||
| 
 | ||||
|         RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync( | ||||
|             req.Code | ||||
|         ); | ||||
|         User? user = await authService.AuthenticateUserAsync(AuthType.Google, remoteUser.Id); | ||||
|         if (user != null) | ||||
|             return Ok(await authService.GenerateUserTokenAsync(user)); | ||||
| 
 | ||||
|         _logger.Debug( | ||||
|             "Google user {Username} ({Id}) authenticated with no local account", | ||||
|             remoteUser.Username, | ||||
|             remoteUser.Id | ||||
|         ); | ||||
| 
 | ||||
|         string ticket = AuthUtils.RandomToken(); | ||||
|         await keyCacheService.SetKeyAsync($"google:{ticket}", remoteUser, Duration.FromMinutes(20)); | ||||
| 
 | ||||
|         return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("register")] | ||||
|     [ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req) | ||||
|     { | ||||
|         RemoteAuthService.RemoteUser? remoteUser = | ||||
|             await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>($"google:{req.Ticket}"); | ||||
|         if (remoteUser == null) | ||||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
|         if ( | ||||
|             await db.AuthMethods.AnyAsync(a => | ||||
|                 a.AuthType == AuthType.Google && a.RemoteId == remoteUser.Id | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             _logger.Error( | ||||
|                 "Google user {Id} has valid ticket but is already linked to an existing account", | ||||
|                 remoteUser.Id | ||||
|             ); | ||||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
|         } | ||||
| 
 | ||||
|         User user = await authService.CreateUserWithRemoteAuthAsync( | ||||
|             req.Username, | ||||
|             AuthType.Google, | ||||
|             remoteUser.Id, | ||||
|             remoteUser.Username | ||||
|         ); | ||||
| 
 | ||||
|         return Ok(await authService.GenerateUserTokenAsync(user)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("add-account")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> AddGoogleAccountAsync() | ||||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         string state = await remoteAuthService.ValidateAddAccountRequestAsync( | ||||
|             CurrentUser!.Id, | ||||
|             AuthType.Google | ||||
|         ); | ||||
| 
 | ||||
|         string url = | ||||
|             "https://accounts.google.com/o/oauth2/auth?response_type=code" | ||||
|             + $"&client_id={config.GoogleAuth.ClientId}" | ||||
|             + $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}" | ||||
|             + $"&prompt=select_account&state={state}" | ||||
|             + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}"; | ||||
| 
 | ||||
|         return Ok(new SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("add-account/callback")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         await remoteAuthService.ValidateAddAccountStateAsync( | ||||
|             req.State, | ||||
|             CurrentUser!.Id, | ||||
|             AuthType.Google | ||||
|         ); | ||||
| 
 | ||||
|         RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync( | ||||
|             req.Code | ||||
|         ); | ||||
|         try | ||||
|         { | ||||
|             AuthMethod authMethod = await authService.AddAuthMethodAsync( | ||||
|                 CurrentUser.Id, | ||||
|                 AuthType.Google, | ||||
|                 remoteUser.Id, | ||||
|                 remoteUser.Username | ||||
|             ); | ||||
|             _logger.Debug( | ||||
|                 "Added new Google auth method {AuthMethodId} to user {UserId}", | ||||
|                 authMethod.Id, | ||||
|                 CurrentUser.Id | ||||
|             ); | ||||
| 
 | ||||
|             return Ok( | ||||
|                 new AddOauthAccountResponse( | ||||
|                     authMethod.Id, | ||||
|                     AuthType.Google, | ||||
|                     authMethod.RemoteId, | ||||
|                     authMethod.RemoteUsername | ||||
|                 ) | ||||
|             ); | ||||
|         } | ||||
|         catch (UniqueConstraintException) | ||||
|         { | ||||
|             throw new ApiError( | ||||
|                 "That account is already linked.", | ||||
|                 HttpStatusCode.BadRequest, | ||||
|                 ErrorCode.AccountAlreadyLinked | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void CheckRequirements() | ||||
|     { | ||||
|         if (!config.GoogleAuth.Enabled) | ||||
|         { | ||||
|             throw new ApiError.BadRequest("Google authentication is not enabled on this instance."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										90
									
								
								Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| // Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published | ||||
| // by the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using JetBrains.Annotations; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services.Auth; | ||||
| 
 | ||||
| public partial class RemoteAuthService | ||||
| { | ||||
|     private readonly Uri _discordTokenUri = new("https://discord.com/api/oauth2/token"); | ||||
|     private readonly Uri _discordUserUri = new("https://discord.com/api/v10/users/@me"); | ||||
| 
 | ||||
|     public async Task<RemoteUser> RequestDiscordTokenAsync( | ||||
|         string code, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; | ||||
|         HttpResponseMessage resp = await _httpClient.PostAsync( | ||||
|             _discordTokenUri, | ||||
|             new FormUrlEncodedContent( | ||||
|                 new Dictionary<string, string> | ||||
|                 { | ||||
|                     { "client_id", config.DiscordAuth.ClientId! }, | ||||
|                     { "client_secret", config.DiscordAuth.ClientSecret! }, | ||||
|                     { "grant_type", "authorization_code" }, | ||||
|                     { "code", code }, | ||||
|                     { "redirect_uri", redirectUri }, | ||||
|                 } | ||||
|             ), | ||||
|             ct | ||||
|         ); | ||||
|         if (!resp.IsSuccessStatusCode) | ||||
|         { | ||||
|             string respBody = await resp.Content.ReadAsStringAsync(ct); | ||||
|             _logger.Error( | ||||
|                 "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", | ||||
|                 (int)resp.StatusCode, | ||||
|                 respBody | ||||
|             ); | ||||
|             throw new FoxnounsError("Invalid Discord OAuth response"); | ||||
|         } | ||||
| 
 | ||||
|         DiscordTokenResponse? token = await resp.Content.ReadFromJsonAsync<DiscordTokenResponse>( | ||||
|             ct | ||||
|         ); | ||||
|         if (token == null) | ||||
|             throw new FoxnounsError("Discord token response was null"); | ||||
| 
 | ||||
|         var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); | ||||
|         req.Headers.Add("Authorization", $"{token.token_type} {token.access_token}"); | ||||
| 
 | ||||
|         HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); | ||||
|         resp2.EnsureSuccessStatusCode(); | ||||
|         DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct); | ||||
|         if (user == null) | ||||
|             throw new FoxnounsError("Discord user response was null"); | ||||
| 
 | ||||
|         return new RemoteUser(user.id, user.username); | ||||
|     } | ||||
| 
 | ||||
|     [SuppressMessage( | ||||
|         "ReSharper", | ||||
|         "InconsistentNaming", | ||||
|         Justification = "Easier to use snake_case here, rather than passing in JSON converter options" | ||||
|     )] | ||||
|     [UsedImplicitly] | ||||
|     private record DiscordTokenResponse(string access_token, string token_type); | ||||
| 
 | ||||
|     [SuppressMessage( | ||||
|         "ReSharper", | ||||
|         "InconsistentNaming", | ||||
|         Justification = "Easier to use snake_case here, rather than passing in JSON converter options" | ||||
|     )] | ||||
|     [UsedImplicitly] | ||||
|     private record DiscordUserResponse(string id, string username); | ||||
| } | ||||
							
								
								
									
										80
									
								
								Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | |||
| // Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published | ||||
| // by the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services.Auth; | ||||
| 
 | ||||
| public partial class RemoteAuthService | ||||
| { | ||||
|     private readonly Uri _googleTokenUri = new("https://oauth2.googleapis.com/token"); | ||||
| 
 | ||||
|     public async Task<RemoteUser> RequestGoogleTokenAsync( | ||||
|         string code, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var redirectUri = $"{config.BaseUrl}/auth/callback/google"; | ||||
|         HttpResponseMessage resp = await _httpClient.PostAsync( | ||||
|             _googleTokenUri, | ||||
|             new FormUrlEncodedContent( | ||||
|                 new Dictionary<string, string> | ||||
|                 { | ||||
|                     { "client_id", config.GoogleAuth.ClientId! }, | ||||
|                     { "client_secret", config.GoogleAuth.ClientSecret! }, | ||||
|                     { "grant_type", "authorization_code" }, | ||||
|                     { "scope", "openid https://www.googleapis.com/auth/userinfo.email" }, | ||||
|                     { "code", code }, | ||||
|                     { "redirect_uri", redirectUri }, | ||||
|                 } | ||||
|             ), | ||||
|             ct | ||||
|         ); | ||||
|         if (!resp.IsSuccessStatusCode) | ||||
|         { | ||||
|             string respBody = await resp.Content.ReadAsStringAsync(ct); | ||||
|             _logger.Error( | ||||
|                 "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", | ||||
|                 (int)resp.StatusCode, | ||||
|                 respBody | ||||
|             ); | ||||
|             throw new FoxnounsError("Invalid Google OAuth response"); | ||||
|         } | ||||
| 
 | ||||
|         GoogleTokenResponse? token = await resp.Content.ReadFromJsonAsync<GoogleTokenResponse>(ct); | ||||
|         if (token == null) | ||||
|             throw new FoxnounsError("Google token response was null"); | ||||
| 
 | ||||
|         byte[] rawIdToken = Convert.FromBase64String(token.IdToken.Split(".")[1]); | ||||
|         GoogleUser? user = JsonSerializer.Deserialize<GoogleUser>( | ||||
|             Encoding.UTF8.GetString(rawIdToken) | ||||
|         ); | ||||
|         if (user == null) | ||||
|             throw new FoxnounsError("Google user was null"); | ||||
| 
 | ||||
|         return new RemoteUser(user.Id, user.Email); | ||||
|     } | ||||
| 
 | ||||
|     [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] | ||||
|     private record GoogleTokenResponse([property: JsonPropertyName("id_token")] string IdToken); | ||||
| 
 | ||||
|     private record GoogleUser( | ||||
|         [property: JsonPropertyName("sub")] string Id, | ||||
|         [property: JsonPropertyName("email")] string Email | ||||
|     ); | ||||
| } | ||||
|  | @ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore; | |||
| 
 | ||||
| namespace Foxnouns.Backend.Services.Auth; | ||||
| 
 | ||||
| public class RemoteAuthService( | ||||
| public partial class RemoteAuthService( | ||||
|     Config config, | ||||
|     ILogger logger, | ||||
|     DatabaseContext db, | ||||
|  | @ -20,75 +20,6 @@ public class RemoteAuthService( | |||
|     private readonly ILogger _logger = logger.ForContext<RemoteAuthService>(); | ||||
|     private readonly HttpClient _httpClient = new(); | ||||
| 
 | ||||
|     private readonly Uri _discordTokenUri = new("https://discord.com/api/oauth2/token"); | ||||
|     private readonly Uri _discordUserUri = new("https://discord.com/api/v10/users/@me"); | ||||
| 
 | ||||
|     public async Task<RemoteUser> RequestDiscordTokenAsync( | ||||
|         string code, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; | ||||
|         HttpResponseMessage resp = await _httpClient.PostAsync( | ||||
|             _discordTokenUri, | ||||
|             new FormUrlEncodedContent( | ||||
|                 new Dictionary<string, string> | ||||
|                 { | ||||
|                     { "client_id", config.DiscordAuth.ClientId! }, | ||||
|                     { "client_secret", config.DiscordAuth.ClientSecret! }, | ||||
|                     { "grant_type", "authorization_code" }, | ||||
|                     { "code", code }, | ||||
|                     { "redirect_uri", redirectUri }, | ||||
|                 } | ||||
|             ), | ||||
|             ct | ||||
|         ); | ||||
|         if (!resp.IsSuccessStatusCode) | ||||
|         { | ||||
|             string respBody = await resp.Content.ReadAsStringAsync(ct); | ||||
|             _logger.Error( | ||||
|                 "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", | ||||
|                 (int)resp.StatusCode, | ||||
|                 respBody | ||||
|             ); | ||||
|             throw new FoxnounsError("Invalid Discord OAuth response"); | ||||
|         } | ||||
| 
 | ||||
|         resp.EnsureSuccessStatusCode(); | ||||
|         DiscordTokenResponse? token = await resp.Content.ReadFromJsonAsync<DiscordTokenResponse>( | ||||
|             ct | ||||
|         ); | ||||
|         if (token == null) | ||||
|             throw new FoxnounsError("Discord token response was null"); | ||||
| 
 | ||||
|         var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); | ||||
|         req.Headers.Add("Authorization", $"{token.token_type} {token.access_token}"); | ||||
| 
 | ||||
|         HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); | ||||
|         resp2.EnsureSuccessStatusCode(); | ||||
|         DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct); | ||||
|         if (user == null) | ||||
|             throw new FoxnounsError("Discord user response was null"); | ||||
| 
 | ||||
|         return new RemoteUser(user.id, user.username); | ||||
|     } | ||||
| 
 | ||||
|     [SuppressMessage( | ||||
|         "ReSharper", | ||||
|         "InconsistentNaming", | ||||
|         Justification = "Easier to use snake_case here, rather than passing in JSON converter options" | ||||
|     )] | ||||
|     [UsedImplicitly] | ||||
|     private record DiscordTokenResponse(string access_token, string token_type); | ||||
| 
 | ||||
|     [SuppressMessage( | ||||
|         "ReSharper", | ||||
|         "InconsistentNaming", | ||||
|         Justification = "Easier to use snake_case here, rather than passing in JSON converter options" | ||||
|     )] | ||||
|     [UsedImplicitly] | ||||
|     private record DiscordUserResponse(string id, string username); | ||||
| 
 | ||||
|     public record RemoteUser(string Id, string Username); | ||||
| 
 | ||||
|     /// <summary> | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { PUBLIC_API_BASE } from "$env/static/public"; | |||
| import type { HandleFetch } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const handleFetch: HandleFetch = async ({ request, fetch }) => { | ||||
| 	console.log(PUBLIC_API_BASE, PRIVATE_INTERNAL_API_HOST, PRIVATE_API_HOST); | ||||
| 	if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) { | ||||
| 		request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_INTERNAL_API_HOST), request); | ||||
| 	} else if (request.url.startsWith(PUBLIC_API_BASE)) { | ||||
|  |  | |||
|  | @ -52,7 +52,9 @@ | |||
| 		"register-with-email": "Register with an email address", | ||||
| 		"email-label": "Your email address", | ||||
| 		"confirm-password-label": "Confirm password", | ||||
| 		"register-with-email-init-success": "Success! An email has been sent to your inbox, please press the link there to continue." | ||||
| 		"register-with-email-init-success": "Success! An email has been sent to your inbox, please press the link there to continue.", | ||||
| 		"register-with-google": "Register with a Google account", | ||||
| 		"remote-google-account-label": "Your Google account" | ||||
| 	}, | ||||
| 	"error": { | ||||
| 		"bad-request-header": "Something was wrong with your input", | ||||
|  |  | |||
|  | @ -3,7 +3,8 @@ | |||
| import type { Cookies } from "@sveltejs/kit"; | ||||
| import { DateTime } from "luxon"; | ||||
| 
 | ||||
| export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; | ||||
| // export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token";
 | ||||
| export const TOKEN_COOKIE_NAME = "pronounscc-token"; | ||||
| 
 | ||||
| export const setToken = (cookies: Cookies, token: string) => | ||||
| 	cookies.set(TOKEN_COOKIE_NAME, token, { path: "/" }); | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| import createCallbackLoader from "$lib/actions/callback"; | ||||
| import createRegisterAction from "$lib/actions/register"; | ||||
| 
 | ||||
| export const load = createCallbackLoader("google"); | ||||
| 
 | ||||
| export const actions = { | ||||
| 	default: createRegisterAction("/auth/google/register"), | ||||
| }; | ||||
|  | @ -0,0 +1,31 @@ | |||
| <script lang="ts"> | ||||
| 	import Error from "$components/Error.svelte"; | ||||
| 	import NewAuthMethod from "$components/settings/NewAuthMethod.svelte"; | ||||
| 	import OauthRegistrationForm from "$components/settings/OauthRegistrationForm.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import type { ActionData, PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <svelte:head> | ||||
| 	<title>{$t("auth.register-with-google")} • pronouns.cc</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| <div class="container"> | ||||
| 	{#if data.error} | ||||
| 		<h1>{$t("auth.register-with-google")}</h1> | ||||
| 		<Error error={data.error} /> | ||||
| 	{:else if data.isLinkRequest} | ||||
| 		<NewAuthMethod method={data.newAuthMethod!} user={data.meUser!} /> | ||||
| 	{:else} | ||||
| 		<OauthRegistrationForm | ||||
| 			title={$t("auth.register-with-google")} | ||||
| 			remoteLabel={$t("auth.remote-google-account-label")} | ||||
| 			remoteUser={data.remoteUser!} | ||||
| 			ticket={data.ticket!} | ||||
| 			error={form?.error} | ||||
| 		/> | ||||
| 	{/if} | ||||
| </div> | ||||
|  | @ -0,0 +1,12 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import { redirect } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const load = async ({ fetch, cookies }) => { | ||||
| 	const { url } = await apiRequest<{ url: string }>("GET", "/auth/google/add-account", { | ||||
| 		isInternal: true, | ||||
| 		fetch, | ||||
| 		cookies, | ||||
| 	}); | ||||
| 
 | ||||
| 	redirect(303, url); | ||||
| }; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue