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.Extensions;
|
||||||
using Foxnouns.Backend.Middleware;
|
using Foxnouns.Backend.Middleware;
|
||||||
using Foxnouns.Backend.Services;
|
using Foxnouns.Backend.Services;
|
||||||
|
using Foxnouns.Backend.Utils;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -56,8 +57,9 @@ public class AuthController(
|
||||||
|
|
||||||
public record AddOauthAccountResponse(
|
public record AddOauthAccountResponse(
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
AuthType Type,
|
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||||
string RemoteId,
|
string RemoteId,
|
||||||
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
string? RemoteUsername
|
string? RemoteUsername
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ public class MetaController : ApiControllerBase
|
||||||
new Limits(
|
new Limits(
|
||||||
MemberCount: MembersController.MaxMemberCount,
|
MemberCount: MembersController.MaxMemberCount,
|
||||||
BioLength: ValidationUtils.MaxBioLength,
|
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);
|
private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
|
||||||
|
|
||||||
// All limits that the frontend should know about (for UI purposes)
|
// 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 => new { m.UserId, m.Name }).IsUnique();
|
||||||
modelBuilder.Entity<Member>().HasIndex(m => m.Sid).IsUnique();
|
modelBuilder.Entity<Member>().HasIndex(m => m.Sid).IsUnique();
|
||||||
modelBuilder.Entity<TemporaryKey>().HasIndex(k => k.Key).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.Sid).HasDefaultValueSql("find_free_user_sid()");
|
||||||
modelBuilder.Entity<User>().Property(u => u.Fields).HasColumnType("jsonb");
|
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")
|
b.HasIndex("UserId")
|
||||||
.HasDatabaseName("ix_auth_methods_user_id");
|
.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);
|
b.ToTable("auth_methods", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -223,6 +223,15 @@ public class AuthService(
|
||||||
{
|
{
|
||||||
AssertValidAuthType(authType, null);
|
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
|
var authMethod = new AuthMethod
|
||||||
{
|
{
|
||||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
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 = {
|
export type AuthResponse = {
|
||||||
user: User;
|
user: User;
|
||||||
|
@ -21,3 +21,10 @@ export type AuthUrls = {
|
||||||
google?: string;
|
google?: string;
|
||||||
tumblr?: 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;
|
member_count: number;
|
||||||
bio_length: number;
|
bio_length: number;
|
||||||
custom_preferences: number;
|
custom_preferences: number;
|
||||||
|
max_auth_methods: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,9 +71,11 @@ export type PrideFlag = {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthType = "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL";
|
||||||
|
|
||||||
export type AuthMethod = {
|
export type AuthMethod = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL";
|
type: AuthType;
|
||||||
remote_id: string;
|
remote_id: string;
|
||||||
remote_username?: 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": {
|
"title": {
|
||||||
"log-in": "Log in",
|
"log-in": "Log in",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"settings": "Settings"
|
"settings": "Settings",
|
||||||
|
"an-error-occurred": "An error occurred"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"log-in-form-title": "Log in with email",
|
"log-in-form-title": "Log in with email",
|
||||||
|
@ -37,7 +38,16 @@
|
||||||
"register-button": "Register account",
|
"register-button": "Register account",
|
||||||
"register-with-mastodon": "Register with a Fediverse 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-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": {
|
"error": {
|
||||||
"bad-request-header": "Something was wrong with your input",
|
"bad-request-header": "Something was wrong with your input",
|
||||||
|
@ -92,7 +102,8 @@
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
"username-update-success": "Successfully changed your username!",
|
"username-update-success": "Successfully changed your username!",
|
||||||
"create-member-title": "Create a new member",
|
"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",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"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 { apiRequest } from "$api";
|
||||||
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
import ApiError, { ErrorCode } from "$api/error";
|
||||||
import type { AuthResponse, CallbackResponse } from "$api/models/auth.js";
|
import type { CallbackResponse } from "$api/models/auth.js";
|
||||||
import { setToken } from "$lib";
|
import { setToken } from "$lib";
|
||||||
import log from "$lib/log.js";
|
import createRegisterAction from "$lib/actions/register.js";
|
||||||
import { isRedirect, redirect } from "@sveltejs/kit";
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load = async ({ parent, params, url, fetch, cookies }) => {
|
export const load = async ({ parent, params, url, fetch, cookies }) => {
|
||||||
const { meUser } = await parent();
|
const { meUser } = await parent();
|
||||||
|
@ -33,30 +33,5 @@ export const load = async ({ parent, params, url, fetch, cookies }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request, fetch, cookies }) => {
|
default: createRegisterAction("/auth/fediverse/register"),
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, Input, Label } from "@sveltestrap/sveltestrap";
|
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { enhance } from "$app/forms";
|
import OauthRegistrationForm from "$components/settings/OauthRegistrationForm.svelte";
|
||||||
import ErrorAlert from "$components/ErrorAlert.svelte";
|
|
||||||
|
|
||||||
type Props = { data: PageData; form: ActionData };
|
type Props = { data: PageData; form: ActionData };
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
@ -14,22 +12,11 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>{$t("auth.register-with-mastodon")}</h1>
|
<OauthRegistrationForm
|
||||||
|
title={$t("auth.register-with-mastodon")}
|
||||||
{#if form?.error}
|
remoteLabel={$t("auth.remote-fediverse-account-label")}
|
||||||
<ErrorAlert error={form?.error} />
|
remoteUser={data.remoteUser}
|
||||||
{/if}
|
ticket={data.ticket}
|
||||||
|
error={form?.error}
|
||||||
<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>
|
|
||||||
</div>
|
</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