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