feat: allow linking fediverse account to existing user
This commit is contained in:
		
							parent
							
								
									d6bb2f7743
								
							
						
					
					
						commit
						97191933cb
					
				
					 14 changed files with 306 additions and 93 deletions
				
			
		| 
						 | 
				
			
			@ -4,10 +4,11 @@
 | 
			
		|||
 | 
			
		||||
  import { goto } from "$app/navigation";
 | 
			
		||||
  import type { APIError, MeUser } from "$lib/api/entities";
 | 
			
		||||
  import { apiFetch } from "$lib/api/fetch";
 | 
			
		||||
  import { apiFetch, apiFetchClient } from "$lib/api/fetch";
 | 
			
		||||
  import { userStore } from "$lib/store";
 | 
			
		||||
  import type { PageData } from "./$types";
 | 
			
		||||
  import ErrorAlert from "$lib/components/ErrorAlert.svelte";
 | 
			
		||||
  import { addToast } from "$lib/toast";
 | 
			
		||||
 | 
			
		||||
  interface SignupResponse {
 | 
			
		||||
    user: MeUser;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +68,22 @@
 | 
			
		|||
      deleteError = e as APIError;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const linkAccount = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const resp = await apiFetchClient<MeUser>("/auth/mastodon/add-provider", "POST", {
 | 
			
		||||
        instance: data.instance,
 | 
			
		||||
        ticket: data.ticket,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      localStorage.setItem("pronouns-user", JSON.stringify(resp));
 | 
			
		||||
      userStore.set(resp);
 | 
			
		||||
      addToast({ header: "Linked account", body: "Successfully linked account!" });
 | 
			
		||||
      goto("/settings/auth");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      data.error = e as APIError;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +95,32 @@
 | 
			
		|||
{#if data.error}
 | 
			
		||||
  <ErrorAlert error={data.error} />
 | 
			
		||||
{/if}
 | 
			
		||||
{#if data.ticket}
 | 
			
		||||
{#if data.ticket && $userStore}
 | 
			
		||||
  <div>
 | 
			
		||||
    <label for="fediverse">Fediverse username</label>
 | 
			
		||||
    <input
 | 
			
		||||
      id="fediverse"
 | 
			
		||||
      class="form-control"
 | 
			
		||||
      name="fediverse"
 | 
			
		||||
      readonly
 | 
			
		||||
      value="{data.fediverse}@{data.instance}"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
  <div>
 | 
			
		||||
    <label for="fediverse">pronouns.cc username</label>
 | 
			
		||||
    <input
 | 
			
		||||
      id="pronounscc"
 | 
			
		||||
      class="form-control"
 | 
			
		||||
      name="pronounscc"
 | 
			
		||||
      readonly
 | 
			
		||||
      value={$userStore.name}
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
  <div>
 | 
			
		||||
    <Button on:click={linkAccount}>Link account</Button>
 | 
			
		||||
    <Button color="secondary" href="/settings/auth">Cancel</Button>
 | 
			
		||||
  </div>
 | 
			
		||||
{:else if data.ticket}
 | 
			
		||||
  <form on:submit|preventDefault={signupForm}>
 | 
			
		||||
    <div>
 | 
			
		||||
      <label for="fediverse">Fediverse username</label>
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +128,7 @@
 | 
			
		|||
        id="fediverse"
 | 
			
		||||
        class="form-control"
 | 
			
		||||
        name="fediverse"
 | 
			
		||||
        disabled
 | 
			
		||||
        readonly
 | 
			
		||||
        value="{data.fediverse}@{data.instance}"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,14 @@
 | 
			
		|||
        <ListGroupItem tag="a" active={$page.url.pathname === "/settings"} href="/settings">
 | 
			
		||||
          Your profile
 | 
			
		||||
        </ListGroupItem>
 | 
			
		||||
        {#if data.require_invite}
 | 
			
		||||
        <ListGroupItem
 | 
			
		||||
          tag="a"
 | 
			
		||||
          active={$page.url.pathname === "/settings/auth"}
 | 
			
		||||
          href="/settings/auth"
 | 
			
		||||
        >
 | 
			
		||||
          Authentication
 | 
			
		||||
        </ListGroupItem>
 | 
			
		||||
        {#if data.invitesEnabled}
 | 
			
		||||
          <ListGroupItem
 | 
			
		||||
            tag="a"
 | 
			
		||||
            active={$page.url.pathname === "/settings/invites"}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,36 @@
 | 
			
		|||
import {
 | 
			
		||||
  ErrorCode,
 | 
			
		||||
  type APIError,
 | 
			
		||||
  type Invite,
 | 
			
		||||
  type MeUser,
 | 
			
		||||
  type PartialMember,
 | 
			
		||||
} from "$lib/api/entities";
 | 
			
		||||
import { apiFetchClient } from "$lib/api/fetch";
 | 
			
		||||
import type { LayoutLoad } from "./$types";
 | 
			
		||||
 | 
			
		||||
export const ssr = false;
 | 
			
		||||
 | 
			
		||||
export const load = (async ({ parent }) => {
 | 
			
		||||
  const user = await apiFetchClient<MeUser>("/users/@me");
 | 
			
		||||
  const members = await apiFetchClient<PartialMember[]>("/users/@me/members");
 | 
			
		||||
 | 
			
		||||
  let invites: Invite[] = [];
 | 
			
		||||
  let invitesEnabled = true;
 | 
			
		||||
  try {
 | 
			
		||||
    invites = await apiFetchClient<Invite[]>("/auth/invites");
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if ((e as APIError).code === ErrorCode.InvitesDisabled) {
 | 
			
		||||
      invitesEnabled = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const data = await parent();
 | 
			
		||||
  return data;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    user,
 | 
			
		||||
    members,
 | 
			
		||||
    invites,
 | 
			
		||||
    invitesEnabled,
 | 
			
		||||
  };
 | 
			
		||||
}) satisfies LayoutLoad;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  type Invite,
 | 
			
		||||
  type APIError,
 | 
			
		||||
  type MeUser,
 | 
			
		||||
  type PartialMember,
 | 
			
		||||
  ErrorCode,
 | 
			
		||||
} from "$lib/api/entities";
 | 
			
		||||
import { apiFetchClient } from "$lib/api/fetch";
 | 
			
		||||
import { error } from "@sveltejs/kit";
 | 
			
		||||
 | 
			
		||||
export const load = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const user = await apiFetchClient<MeUser>("/users/@me");
 | 
			
		||||
    const members = await apiFetchClient<PartialMember[]>("/users/@me/members");
 | 
			
		||||
 | 
			
		||||
    let invites: Invite[] = [];
 | 
			
		||||
    let invitesEnabled = true;
 | 
			
		||||
    try {
 | 
			
		||||
      invites = await apiFetchClient<Invite[]>("/auth/invites");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if ((e as APIError).code === ErrorCode.InvitesDisabled) {
 | 
			
		||||
        invitesEnabled = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { user, members, invites, invitesEnabled };
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw error(500, (e as APIError).message);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										115
									
								
								frontend/src/routes/settings/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								frontend/src/routes/settings/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,115 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import type { APIError } from "$lib/api/entities";
 | 
			
		||||
  import { apiFetch } from "$lib/api/fetch";
 | 
			
		||||
  import ErrorAlert from "$lib/components/ErrorAlert.svelte";
 | 
			
		||||
  import {
 | 
			
		||||
    Button,
 | 
			
		||||
    Card,
 | 
			
		||||
    CardBody,
 | 
			
		||||
    CardText,
 | 
			
		||||
    CardTitle,
 | 
			
		||||
    Input,
 | 
			
		||||
    Modal,
 | 
			
		||||
    ModalBody,
 | 
			
		||||
    ModalFooter,
 | 
			
		||||
  } from "sveltestrap";
 | 
			
		||||
  import type { PageData } from "./$types";
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
 | 
			
		||||
  let canUnlink = false;
 | 
			
		||||
 | 
			
		||||
  $: canUnlink =
 | 
			
		||||
    [data.user.discord, data.user.fediverse]
 | 
			
		||||
      .map<number>((entry) => (entry === null ? 0 : 1))
 | 
			
		||||
      .reduce((prev, current) => prev + current) >= 2;
 | 
			
		||||
 | 
			
		||||
  let error: APIError | null = null;
 | 
			
		||||
  let instance = "";
 | 
			
		||||
  let fediDisabled = false;
 | 
			
		||||
 | 
			
		||||
  let fediLinkModalOpen = false;
 | 
			
		||||
  let toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen);
 | 
			
		||||
 | 
			
		||||
  const fediLogin = async () => {
 | 
			
		||||
    fediDisabled = true;
 | 
			
		||||
    try {
 | 
			
		||||
      const resp = await apiFetch<{ url: string }>(
 | 
			
		||||
        `/auth/urls/fediverse?instance=${encodeURIComponent(instance)}`,
 | 
			
		||||
        {},
 | 
			
		||||
      );
 | 
			
		||||
      window.location.assign(resp.url);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      error = e as APIError;
 | 
			
		||||
    } finally {
 | 
			
		||||
      fediDisabled = false;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
  <h1>Authentication providers</h1>
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="my-2">
 | 
			
		||||
      <Card>
 | 
			
		||||
        <CardBody>
 | 
			
		||||
          <CardTitle>Fediverse</CardTitle>
 | 
			
		||||
          <CardText>
 | 
			
		||||
            {#if data.user.fediverse}
 | 
			
		||||
              Your currently linked Fediverse account is <b
 | 
			
		||||
                >{data.user.fediverse_username}@{data.user.fediverse_instance}</b
 | 
			
		||||
              >
 | 
			
		||||
              (<code>{data.user.fediverse}</code>).
 | 
			
		||||
            {:else}
 | 
			
		||||
              You do not have a linked Fediverse account.
 | 
			
		||||
            {/if}
 | 
			
		||||
          </CardText>
 | 
			
		||||
          {#if data.user.fediverse}
 | 
			
		||||
            <Button color="danger" disabled={!canUnlink}>Unlink account</Button>
 | 
			
		||||
          {:else}
 | 
			
		||||
            <Button color="secondary" on:click={toggleFediLinkModal}>Link account</Button>
 | 
			
		||||
          {/if}
 | 
			
		||||
        </CardBody>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="my-2">
 | 
			
		||||
      <Card>
 | 
			
		||||
        <CardBody>
 | 
			
		||||
          <CardTitle>Discord</CardTitle>
 | 
			
		||||
          <CardText>
 | 
			
		||||
            {#if data.user.discord}
 | 
			
		||||
              Your currently linked Discord account is <b>{data.user.discord_username}</b>
 | 
			
		||||
              (<code>{data.user.discord}</code>).
 | 
			
		||||
            {:else}
 | 
			
		||||
              You do not have a linked Discord account.
 | 
			
		||||
            {/if}
 | 
			
		||||
          </CardText>
 | 
			
		||||
          {#if data.user.discord}
 | 
			
		||||
            <Button color="danger" disabled={!canUnlink}>Unlink account</Button>
 | 
			
		||||
          {:else}
 | 
			
		||||
            <Button color="secondary" href={data.urls.discord}>Link account</Button>
 | 
			
		||||
          {/if}
 | 
			
		||||
        </CardBody>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Modal header="Pick an instance" isOpen={fediLinkModalOpen} toggle={toggleFediLinkModal}>
 | 
			
		||||
      <ModalBody>
 | 
			
		||||
        <p>
 | 
			
		||||
          <strong>Note:</strong> Misskey (and derivatives) are not supported yet, sorry.
 | 
			
		||||
        </p>
 | 
			
		||||
        <Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} />
 | 
			
		||||
        {#if error}
 | 
			
		||||
          <div class="mt-2">
 | 
			
		||||
            <ErrorAlert {error} />
 | 
			
		||||
          </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
      </ModalBody>
 | 
			
		||||
      <ModalFooter>
 | 
			
		||||
        <Button color="primary" disabled={fediDisabled || instance === ""} on:click={fediLogin}
 | 
			
		||||
          >Log in</Button
 | 
			
		||||
        >
 | 
			
		||||
      </ModalFooter>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										17
									
								
								frontend/src/routes/settings/auth/+page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/routes/settings/auth/+page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
import { PUBLIC_BASE_URL } from "$env/static/public";
 | 
			
		||||
import { apiFetch } from "$lib/api/fetch";
 | 
			
		||||
 | 
			
		||||
export const load = async () => {
 | 
			
		||||
  const resp = await apiFetch<UrlsResponse>("/auth/urls", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: {
 | 
			
		||||
      callback_domain: PUBLIC_BASE_URL,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return { urls: resp };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface UrlsResponse {
 | 
			
		||||
  discord: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
import { ErrorCode, type APIError, type Invite } from "$lib/api/entities";
 | 
			
		||||
import { apiFetchClient } from "$lib/api/fetch";
 | 
			
		||||
import { error } from "@sveltejs/kit";
 | 
			
		||||
import type { PageLoad } from "../$types";
 | 
			
		||||
 | 
			
		||||
export const load = (async () => {
 | 
			
		||||
  const data = {
 | 
			
		||||
    invitesEnabled: true,
 | 
			
		||||
    invites: [] as Invite[],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const invites = await apiFetchClient<Invite[]>("/auth/invites");
 | 
			
		||||
    data.invites = invites;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if ((e as APIError).code === ErrorCode.InvitesDisabled) {
 | 
			
		||||
      data.invitesEnabled = false;
 | 
			
		||||
      data.invites = [];
 | 
			
		||||
    } else {
 | 
			
		||||
      throw error((e as APIError).code, (e as APIError).message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data;
 | 
			
		||||
}) satisfies PageLoad;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue