Compare commits
	
		
			2 commits
		
	
	
		
			8b1d5b2c1b
			...
			de733a0682
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| de733a0682 | |||
| 4780be3019 | 
					 22 changed files with 618 additions and 212 deletions
				
			
		|  | @ -5,6 +5,7 @@ using Foxnouns.Backend.Database.Models; | |||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Newtonsoft.Json; | ||||
|  | @ -56,8 +57,9 @@ public class AuthController( | |||
| 
 | ||||
|     public record AddOauthAccountResponse( | ||||
|         Snowflake Id, | ||||
|         AuthType Type, | ||||
|         [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||
|         string RemoteId, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             string? RemoteUsername | ||||
|     ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,8 @@ public class MetaController : ApiControllerBase | |||
|                 new Limits( | ||||
|                     MemberCount: MembersController.MaxMemberCount, | ||||
|                     BioLength: ValidationUtils.MaxBioLength, | ||||
|                     CustomPreferences: ValidationUtils.MaxCustomPreferences | ||||
|                     CustomPreferences: ValidationUtils.MaxCustomPreferences, | ||||
|                     MaxAuthMethods: AuthUtils.MaxAuthMethodsPerType | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|  | @ -49,5 +50,10 @@ public class MetaController : ApiControllerBase | |||
|     private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); | ||||
| 
 | ||||
|     // All limits that the frontend should know about (for UI purposes) | ||||
|     private record Limits(int MemberCount, int BioLength, int CustomPreferences); | ||||
|     private record Limits( | ||||
|         int MemberCount, | ||||
|         int BioLength, | ||||
|         int CustomPreferences, | ||||
|         int MaxAuthMethods | ||||
|     ); | ||||
| } | ||||
|  |  | |||
|  | @ -71,6 +71,22 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) | |||
|         modelBuilder.Entity<Member>().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); | ||||
|         modelBuilder.Entity<Member>().HasIndex(m => m.Sid).IsUnique(); | ||||
|         modelBuilder.Entity<TemporaryKey>().HasIndex(k => k.Key).IsUnique(); | ||||
|         modelBuilder | ||||
|             .Entity<AuthMethod>() | ||||
|             .HasIndex(m => new | ||||
|             { | ||||
|                 m.AuthType, | ||||
|                 m.RemoteId, | ||||
|                 m.FediverseApplicationId, | ||||
|             }) | ||||
|             .HasFilter("fediverse_application_id IS NOT NULL") | ||||
|             .IsUnique(); | ||||
| 
 | ||||
|         modelBuilder | ||||
|             .Entity<AuthMethod>() | ||||
|             .HasIndex(m => new { m.AuthType, m.RemoteId }) | ||||
|             .HasFilter("fediverse_application_id IS NULL") | ||||
|             .IsUnique(); | ||||
| 
 | ||||
|         modelBuilder.Entity<User>().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()"); | ||||
|         modelBuilder.Entity<User>().Property(u => u.Fields).HasColumnType("jsonb"); | ||||
|  |  | |||
|  | @ -0,0 +1,47 @@ | |||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Database.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     [DbContext(typeof(DatabaseContext))] | ||||
|     [Migration("20241128202508_AddAuthMethodUniqueIndex")] | ||||
|     public partial class AddAuthMethodUniqueIndex : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_auth_methods_auth_type_remote_id", | ||||
|                 table: "auth_methods", | ||||
|                 columns: new[] { "auth_type", "remote_id" }, | ||||
|                 unique: true, | ||||
|                 filter: "fediverse_application_id IS NULL" | ||||
|             ); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id", | ||||
|                 table: "auth_methods", | ||||
|                 columns: new[] { "auth_type", "remote_id", "fediverse_application_id" }, | ||||
|                 unique: true, | ||||
|                 filter: "fediverse_application_id IS NOT NULL" | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropIndex( | ||||
|                 name: "ix_auth_methods_auth_type_remote_id", | ||||
|                 table: "auth_methods" | ||||
|             ); | ||||
| 
 | ||||
|             migrationBuilder.DropIndex( | ||||
|                 name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id", | ||||
|                 table: "auth_methods" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -98,6 +98,16 @@ namespace Foxnouns.Backend.Database.Migrations | |||
|                     b.HasIndex("UserId") | ||||
|                         .HasDatabaseName("ix_auth_methods_user_id"); | ||||
| 
 | ||||
|                     b.HasIndex("AuthType", "RemoteId") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_auth_methods_auth_type_remote_id") | ||||
|                         .HasFilter("fediverse_application_id IS NULL"); | ||||
| 
 | ||||
|                     b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id") | ||||
|                         .HasFilter("fediverse_application_id IS NOT NULL"); | ||||
| 
 | ||||
|                     b.ToTable("auth_methods", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -223,6 +223,15 @@ public class AuthService( | |||
|     { | ||||
|         AssertValidAuthType(authType, null); | ||||
| 
 | ||||
|         // This is already checked when | ||||
|         var currentCount = await db | ||||
|             .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) | ||||
|             .CountAsync(ct); | ||||
|         if (currentCount >= AuthUtils.MaxAuthMethodsPerType) | ||||
|             throw new ApiError.BadRequest( | ||||
|                 "Too many linked accounts of this type, maximum of 3 per account." | ||||
|             ); | ||||
| 
 | ||||
|         var authMethod = new AuthMethod | ||||
|         { | ||||
|             Id = snowflakeGenerator.GenerateSnowflake(), | ||||
|  |  | |||
							
								
								
									
										35
									
								
								Foxnouns.Frontend/src/lib/actions/register.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Foxnouns.Frontend/src/lib/actions/register.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import ApiError, { ErrorCode, type RawApiError } from "$api/error"; | ||||
| import type { AuthResponse } from "$api/models/auth"; | ||||
| import { setToken } from "$lib"; | ||||
| import log from "$lib/log"; | ||||
| import { isRedirect, redirect, type RequestEvent } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export default function createRegisterAction(callbackUrl: string) { | ||||
| 	return async function ({ request, fetch, cookies }: RequestEvent) { | ||||
| 		const data = await request.formData(); | ||||
| 		const username = data.get("username") as string | null; | ||||
| 		const ticket = data.get("ticket") as string | null; | ||||
| 
 | ||||
| 		if (!username || !ticket) | ||||
| 			return { | ||||
| 				error: { message: "Bad request", code: ErrorCode.BadRequest, status: 403 } as RawApiError, | ||||
| 			}; | ||||
| 
 | ||||
| 		try { | ||||
| 			const resp = await apiRequest<AuthResponse>("POST", callbackUrl, { | ||||
| 				body: { username, ticket }, | ||||
| 				isInternal: true, | ||||
| 				fetch, | ||||
| 			}); | ||||
| 
 | ||||
| 			setToken(cookies, resp.token); | ||||
| 			redirect(303, "/auth/welcome"); | ||||
| 		} catch (e) { | ||||
| 			if (isRedirect(e)) throw e; | ||||
| 			log.error("Could not sign up user with username %s:", username, e); | ||||
| 			if (e instanceof ApiError) return { error: e.obj }; | ||||
| 			throw e; | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| import type { User } from "./user"; | ||||
| import type { AuthType, User } from "./user"; | ||||
| 
 | ||||
| export type AuthResponse = { | ||||
| 	user: User; | ||||
|  | @ -21,3 +21,10 @@ export type AuthUrls = { | |||
| 	google?: string; | ||||
| 	tumblr?: string; | ||||
| }; | ||||
| 
 | ||||
| export type AddAccountResponse = { | ||||
| 	id: string; | ||||
| 	type: AuthType; | ||||
| 	remote_id: string; | ||||
| 	remote_username?: string; | ||||
| }; | ||||
|  |  | |||
|  | @ -16,4 +16,5 @@ export type Limits = { | |||
| 	member_count: number; | ||||
| 	bio_length: number; | ||||
| 	custom_preferences: number; | ||||
| 	max_auth_methods: number; | ||||
| }; | ||||
|  |  | |||
|  | @ -71,9 +71,11 @@ export type PrideFlag = { | |||
| 	description: string | null; | ||||
| }; | ||||
| 
 | ||||
| export type AuthType = "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; | ||||
| 
 | ||||
| export type AuthMethod = { | ||||
| 	id: string; | ||||
| 	type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; | ||||
| 	type: AuthType; | ||||
| 	remote_id: string; | ||||
| 	remote_username?: string; | ||||
| }; | ||||
|  |  | |||
|  | @ -0,0 +1,24 @@ | |||
| <script lang="ts"> | ||||
| 	import type { AuthMethod } from "$api/models"; | ||||
| 	import AuthMethodRow from "./AuthMethodRow.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		methods: AuthMethod[]; | ||||
| 		canRemove: boolean; | ||||
| 		max: number; | ||||
| 		buttonLink: string; | ||||
| 		buttonText: string; | ||||
| 	}; | ||||
| 	let { methods, canRemove, max, buttonLink, buttonText }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| {#if methods.length > 0} | ||||
| 	<div class="list-group mb-3"> | ||||
| 		{#each methods as method (method.id)} | ||||
| 			<AuthMethodRow {method} {canRemove} /> | ||||
| 		{/each} | ||||
| 	</div> | ||||
| {/if} | ||||
| {#if methods.length < max} | ||||
| 	<a class="btn btn-primary mb-3" href={buttonLink}>{buttonText}</a> | ||||
| {/if} | ||||
|  | @ -0,0 +1,26 @@ | |||
| <script lang="ts"> | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import type { AuthMethod } from "$api/models"; | ||||
| 
 | ||||
| 	type Props = { method: AuthMethod; canRemove: boolean }; | ||||
| 	let { method, canRemove }: Props = $props(); | ||||
| 
 | ||||
| 	let name = $derived( | ||||
| 		method.type === "EMAIL" ? method.remote_id : (method.remote_username ?? method.remote_id), | ||||
| 	); | ||||
| 	let showId = $derived(method.type !== "FEDIVERSE"); | ||||
| </script> | ||||
| 
 | ||||
| <div class="list-group-item"> | ||||
| 	<div class="row"> | ||||
| 		<div class="col"> | ||||
| 			{name} | ||||
| 			{#if showId}({method.remote_id}){/if} | ||||
| 		</div> | ||||
| 		{#if canRemove} | ||||
| 			<div class="col text-end"> | ||||
| 				<a href="/settings/auth/remove-method/{method.id}">{$t("settings.auth-remove-method")}</a> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
|  | @ -0,0 +1,34 @@ | |||
| <script lang="ts"> | ||||
| 	import type { AuthMethod, PartialUser } from "$api/models"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 
 | ||||
| 	type Props = { method: AuthMethod; user: PartialUser }; | ||||
| 	let { method, user }: Props = $props(); | ||||
| 
 | ||||
| 	let name = $derived( | ||||
| 		method.type === "EMAIL" ? method.remote_id : (method.remote_username ?? method.remote_id), | ||||
| 	); | ||||
| 
 | ||||
| 	let text = $derived.by(() => { | ||||
| 		switch (method.type) { | ||||
| 			case "DISCORD": | ||||
| 				return $t("auth.successful-link-discord"); | ||||
| 			case "GOOGLE": | ||||
| 				return $t("auth.successful-link-google"); | ||||
| 			case "TUMBLR": | ||||
| 				return $t("auth.successful-link-tumblr"); | ||||
| 			case "FEDIVERSE": | ||||
| 				return $t("auth.successful-link-fedi"); | ||||
| 			default: | ||||
| 				return "<you shouldn't see this!>"; | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <h1>{$t("auth.new-auth-method-added")}</h1> | ||||
| 
 | ||||
| <p>{text} <code>{name}</code></p> | ||||
| <p>{$t("auth.successful-link-profile-hint")}</p> | ||||
| <p> | ||||
| 	<a class="btn btn-primary" href="/@{user.username}">{$t("auth.successful-link-profile-link")}</a> | ||||
| </p> | ||||
|  | @ -0,0 +1,35 @@ | |||
| <script lang="ts"> | ||||
| 	import type { RawApiError } from "$api/error"; | ||||
| 	import { enhance } from "$app/forms"; | ||||
| 	import ErrorAlert from "$components/ErrorAlert.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import { Button, Input, Label } from "@sveltestrap/sveltestrap"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		title: string; | ||||
| 		remoteLabel: string; | ||||
| 		remoteUser: string; | ||||
| 		ticket: string; | ||||
| 		error?: RawApiError; | ||||
| 	}; | ||||
| 	let { title, remoteLabel, remoteUser, ticket, error }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <h1>{title}</h1> | ||||
| 
 | ||||
| {#if error} | ||||
| 	<ErrorAlert {error} /> | ||||
| {/if} | ||||
| 
 | ||||
| <form method="POST" use:enhance> | ||||
| 	<div class="mb-3"> | ||||
| 		<Label>{remoteLabel}</Label> | ||||
| 		<Input type="text" readonly value={remoteUser} /> | ||||
| 	</div> | ||||
| 	<div class="mb-3"> | ||||
| 		<Label>{$t("auth.register-username-label")}</Label> | ||||
| 		<Input type="text" name="username" required /> | ||||
| 	</div> | ||||
| 	<input type="hidden" name="ticket" value={ticket} /> | ||||
| 	<Button color="primary" type="submit">{$t("auth.register-button")}</Button> | ||||
| </form> | ||||
|  | @ -18,7 +18,8 @@ | |||
| 	"title": { | ||||
| 		"log-in": "Log in", | ||||
| 		"welcome": "Welcome", | ||||
|     "settings": "Settings" | ||||
| 		"settings": "Settings", | ||||
| 		"an-error-occurred": "An error occurred" | ||||
| 	}, | ||||
| 	"auth": { | ||||
| 		"log-in-form-title": "Log in with email", | ||||
|  | @ -37,7 +38,16 @@ | |||
| 		"register-button": "Register account", | ||||
| 		"register-with-mastodon": "Register with a Fediverse account", | ||||
| 		"log-in-with-fediverse-error-blurb": "Is your instance returning an error?", | ||||
|     "log-in-with-fediverse-force-refresh-button": "Force a refresh on our end" | ||||
| 		"log-in-with-fediverse-force-refresh-button": "Force a refresh on our end", | ||||
| 		"register-with-discord": "Register with a Discord account", | ||||
| 		"new-auth-method-added": "Successfully added authentication method!", | ||||
| 		"successful-link-discord": "Your account has successfully been linked to the following Discord account:", | ||||
| 		"successful-link-google": "Your account has successfully been linked to the following Google account:", | ||||
| 		"successful-link-tumblr": "Your account has successfully been linked to the following Tumblr account:", | ||||
| 		"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" | ||||
| 	}, | ||||
| 	"error": { | ||||
| 		"bad-request-header": "Something was wrong with your input", | ||||
|  | @ -92,7 +102,8 @@ | |||
| 		"avatar": "Avatar", | ||||
| 		"username-update-success": "Successfully changed your username!", | ||||
| 		"create-member-title": "Create a new member", | ||||
|     "create-member-name-label": "Member name" | ||||
| 		"create-member-name-label": "Member name", | ||||
| 		"auth-remove-method": "Remove" | ||||
| 	}, | ||||
| 	"yes": "Yes", | ||||
| 	"no": "No", | ||||
|  |  | |||
|  | @ -0,0 +1,64 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import ApiError, { ErrorCode } from "$api/error"; | ||||
| import type { AddAccountResponse, CallbackResponse } from "$api/models/auth"; | ||||
| import { setToken } from "$lib"; | ||||
| import createRegisterAction from "$lib/actions/register.js"; | ||||
| import log from "$lib/log.js"; | ||||
| import { isRedirect, redirect } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const load = async ({ url, parent, fetch, cookies }) => { | ||||
| 	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/discord/add-account/callback", | ||||
| 				{ | ||||
| 					isInternal: true, | ||||
| 					body: { code, state }, | ||||
| 					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 discord account to user %s:", meUser.id, e); | ||||
| 			throw e; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	try { | ||||
| 		const resp = await apiRequest<CallbackResponse>("POST", "/auth/discord/callback", { | ||||
| 			body: { code, state }, | ||||
| 			isInternal: true, | ||||
| 			fetch, | ||||
| 		}); | ||||
| 
 | ||||
| 		if (resp.has_account) { | ||||
| 			setToken(cookies, resp.token!); | ||||
| 			redirect(303, `/@${resp.user!.username}`); | ||||
| 		} | ||||
| 
 | ||||
| 		return { | ||||
| 			hasAccount: false, | ||||
| 			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 discord callback:", e); | ||||
| 		throw e; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export const actions = { | ||||
| 	default: createRegisterAction("/auth/discord/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-discord")} • pronouns.cc</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| <div class="container"> | ||||
| 	{#if data.error} | ||||
| 		<h1>{$t("auth.register-with-discord")}</h1> | ||||
| 		<Error error={data.error} /> | ||||
| 	{:else if data.isLinkRequest} | ||||
| 		<NewAuthMethod method={data.newAuthMethod!} user={data.meUser!} /> | ||||
| 	{:else} | ||||
| 		<OauthRegistrationForm | ||||
| 			title={$t("auth.register-with-discord")} | ||||
| 			remoteLabel={$t("auth.remote-discord-account-label")} | ||||
| 			remoteUser={data.remoteUser!} | ||||
| 			ticket={data.ticket!} | ||||
| 			error={form?.error} | ||||
| 		/> | ||||
| 	{/if} | ||||
| </div> | ||||
|  | @ -1,9 +1,9 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import ApiError, { ErrorCode, type RawApiError } from "$api/error"; | ||||
| import type { AuthResponse, CallbackResponse } from "$api/models/auth.js"; | ||||
| import ApiError, { ErrorCode } from "$api/error"; | ||||
| import type { CallbackResponse } from "$api/models/auth.js"; | ||||
| import { setToken } from "$lib"; | ||||
| import log from "$lib/log.js"; | ||||
| import { isRedirect, redirect } from "@sveltejs/kit"; | ||||
| import createRegisterAction from "$lib/actions/register.js"; | ||||
| import { redirect } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const load = async ({ parent, params, url, fetch, cookies }) => { | ||||
| 	const { meUser } = await parent(); | ||||
|  | @ -33,30 +33,5 @@ export const load = async ({ parent, params, url, fetch, cookies }) => { | |||
| }; | ||||
| 
 | ||||
| export const actions = { | ||||
| 	default: async ({ request, fetch, cookies }) => { | ||||
| 		const data = await request.formData(); | ||||
| 		const username = data.get("username") as string | null; | ||||
| 		const ticket = data.get("ticket") as string | null; | ||||
| 
 | ||||
| 		if (!username || !ticket) | ||||
| 			return { | ||||
| 				error: { message: "Bad request", code: ErrorCode.BadRequest, status: 403 } as RawApiError, | ||||
| 			}; | ||||
| 
 | ||||
| 		try { | ||||
| 			const resp = await apiRequest<AuthResponse>("POST", "/auth/fediverse/register", { | ||||
| 				body: { username, ticket }, | ||||
| 				isInternal: true, | ||||
| 				fetch, | ||||
| 			}); | ||||
| 
 | ||||
| 			setToken(cookies, resp.token); | ||||
| 			redirect(303, "/auth/welcome"); | ||||
| 		} catch (e) { | ||||
| 			if (isRedirect(e)) throw e; | ||||
| 			log.error("Could not sign up user with username %s:", username, e); | ||||
| 			if (e instanceof ApiError) return { error: e.obj }; | ||||
| 			throw e; | ||||
| 		} | ||||
| 	}, | ||||
| 	default: createRegisterAction("/auth/fediverse/register"), | ||||
| }; | ||||
|  |  | |||
|  | @ -1,9 +1,7 @@ | |||
| <script lang="ts"> | ||||
| 	import { Button, Input, Label } from "@sveltestrap/sveltestrap"; | ||||
| 	import type { ActionData, PageData } from "./$types"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import { enhance } from "$app/forms"; | ||||
| 	import ErrorAlert from "$components/ErrorAlert.svelte"; | ||||
| 	import OauthRegistrationForm from "$components/settings/OauthRegistrationForm.svelte"; | ||||
| 
 | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
|  | @ -14,22 +12,11 @@ | |||
| </svelte:head> | ||||
| 
 | ||||
| <div class="container"> | ||||
| 	<h1>{$t("auth.register-with-mastodon")}</h1> | ||||
| 
 | ||||
| 	{#if form?.error} | ||||
| 		<ErrorAlert error={form?.error} /> | ||||
| 	{/if} | ||||
| 
 | ||||
| 	<form method="POST" use:enhance> | ||||
| 		<div class="mb-3"> | ||||
| 			<Label>{$t("auth.remote-fediverse-account-label")}</Label> | ||||
| 			<Input type="text" readonly value={data.remoteUser} /> | ||||
| 		</div> | ||||
| 		<div class="mb-3"> | ||||
| 			<Label>{$t("auth.register-username-label")}</Label> | ||||
| 			<Input type="text" name="username" required /> | ||||
| 		</div> | ||||
| 		<input type="hidden" name="ticket" value={data.ticket} /> | ||||
| 		<Button color="primary" type="submit">{$t("auth.register-button")}</Button> | ||||
| 	</form> | ||||
| 	<OauthRegistrationForm | ||||
| 		title={$t("auth.register-with-mastodon")} | ||||
| 		remoteLabel={$t("auth.remote-fediverse-account-label")} | ||||
| 		remoteUser={data.remoteUser} | ||||
| 		ticket={data.ticket} | ||||
| 		error={form?.error} | ||||
| 	/> | ||||
| </div> | ||||
|  |  | |||
|  | @ -0,0 +1,7 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import type { AuthUrls } from "$api/models/auth"; | ||||
| 
 | ||||
| export const load = async ({ fetch }) => { | ||||
| 	const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true }); | ||||
| 	return { urls }; | ||||
| }; | ||||
							
								
								
									
										65
									
								
								Foxnouns.Frontend/src/routes/settings/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								Foxnouns.Frontend/src/routes/settings/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| <script lang="ts"> | ||||
| 	import AuthMethodList from "$components/settings/AuthMethodList.svelte"; | ||||
| 	import AuthMethodRow from "$components/settings/AuthMethodRow.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
| 	let { data }: Props = $props(); | ||||
| 
 | ||||
| 	let max = $derived(data.meta.limits.max_auth_methods); | ||||
| 	let canRemove = $derived(data.user.auth_methods.length > 1); | ||||
| 	let emails = $derived(data.user.auth_methods.filter((m) => m.type === "EMAIL")); | ||||
| 	let discordAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "DISCORD")); | ||||
| 	let googleAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "GOOGLE")); | ||||
| 	let tumblrAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "TUMBLR")); | ||||
| 	let fediAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "FEDIVERSE")); | ||||
| </script> | ||||
| 
 | ||||
| {#if data.urls.email_enabled} | ||||
| 	<h3>Email addresses</h3> | ||||
| 	<AuthMethodList | ||||
| 		methods={emails} | ||||
| 		{canRemove} | ||||
| 		{max} | ||||
| 		buttonLink="/settings/auth/add-email" | ||||
| 		buttonText="Add email address" | ||||
| 	/> | ||||
| {/if} | ||||
| {#if data.urls.discord} | ||||
| 	<h3>Discord accounts</h3> | ||||
| 	<AuthMethodList | ||||
| 		methods={discordAccounts} | ||||
| 		{canRemove} | ||||
| 		{max} | ||||
| 		buttonLink="/settings/auth/add-discord" | ||||
| 		buttonText="Link Discord account" | ||||
| 	/> | ||||
| {/if} | ||||
| {#if data.urls.google} | ||||
| 	<h3>Google accounts</h3> | ||||
| 	<AuthMethodList | ||||
| 		methods={googleAccounts} | ||||
| 		{canRemove} | ||||
| 		{max} | ||||
| 		buttonLink="/settings/auth/add-google" | ||||
| 		buttonText="Link Google account" | ||||
| 	/> | ||||
| {/if} | ||||
| {#if data.urls.tumblr} | ||||
| 	<h3>Tumblr accounts</h3> | ||||
| 	<AuthMethodList | ||||
| 		methods={tumblrAccounts} | ||||
| 		{canRemove} | ||||
| 		{max} | ||||
| 		buttonLink="/settings/auth/add-tumblr" | ||||
| 		buttonText="Link Tumblr account" | ||||
| 	/> | ||||
| {/if} | ||||
| <h3>Fediverse accounts</h3> | ||||
| <AuthMethodList | ||||
| 	methods={fediAccounts} | ||||
| 	{canRemove} | ||||
| 	{max} | ||||
| 	buttonLink="/settings/auth/add-fediverse" | ||||
| 	buttonText="Link Fediverse account" | ||||
| /> | ||||
|  | @ -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/discord/add-account", { | ||||
| 		isInternal: true, | ||||
| 		fetch, | ||||
| 		cookies, | ||||
| 	}); | ||||
| 
 | ||||
| 	redirect(303, url); | ||||
| }; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue