feat(frontend): working Discord login + signup

This commit is contained in:
Sam 2023-03-12 04:25:53 +01:00
parent 0e72097346
commit c8b5b7e2c2
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
24 changed files with 287 additions and 119 deletions

View file

@ -0,0 +1,19 @@
import { error } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
import type { APIError } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch";
export const load = (async (event) => {
try {
return await apiFetch<MetaResponse>("/meta", {});
} catch (e) {
throw error(500, (e as APIError).message);
}
}) satisfies LayoutServerLoad;
interface MetaResponse {
git_repository: string;
git_commit: string;
users: number;
members: number;
}

View file

@ -1,9 +1,26 @@
<script>
<script lang="ts">
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import Navigation from "./nav/Navigation.svelte";
import type { LayoutData } from "./$types";
import { version } from "$app/environment";
export let data: LayoutData;
const versionMismatch = data.git_commit !== version && data.git_commit !== "[unknown]";
</script>
<Navigation />
<slot />
<div class="container">
<slot />
<footer>
<hr />
<p>
pronouns.cc <a href={data.git_repository}>{version}</a>
{#if versionMismatch}(backend: {data.git_commit}){/if} &middot;
<a href="/page/terms">Terms of service</a>
- <a href="/page/privacy">Privacy policy</a>
</p>
</footer>
</div>

View file

@ -1,10 +1,21 @@
import { apiFetch } from "$lib/api/fetch";
import type { User } from "$lib/api/entities";
import { ErrorCode, type APIError, type User } from "$lib/api/entities";
import { error } from "@sveltejs/kit";
export const load = async ({ params }) => {
const resp = await apiFetch<User>(`/users/${params.username}`, {
method: "GET",
});
try {
const resp = await apiFetch<User>(`/users/${params.username}`, {
method: "GET",
});
return resp;
return resp;
} catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) {
throw error(404, (e as APIError).message);
}
console.log(e);
throw e;
}
};

View file

@ -48,41 +48,37 @@
</div>
{/if}
</div>
<div class="row">
{#if data.names}
<div class="col-md">
<h4>Names</h4>
<ul class="list-unstyled">
{#each data.names as name}
<li><StatusIcon status={name.status} /> {name.value}</li>
{/each}
</ul>
</div>
{/if}
{#if data.pronouns}
<div class="col-md">
<h4>Pronouns</h4>
<ul class="list-unstyled">
{#each data.pronouns as pronouns}
<li>
<StatusIcon status={pronouns.status} />
<PronounLink {pronouns} />
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
{#if data.fields}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#each data.fields as field}
<div class="col">
<FieldCard {field} />
</div>
{/each}
</div>
{/if}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#if data.names}
<div class="col-md">
<h4>Names</h4>
<ul class="list-unstyled">
{#each data.names as name}
<li><StatusIcon status={name.status} /> {name.value}</li>
{/each}
</ul>
</div>
{/if}
{#if data.pronouns}
<div class="col-md">
<h4>Pronouns</h4>
<ul class="list-unstyled">
{#each data.pronouns as pronouns}
<li>
<StatusIcon status={pronouns.status} />
<PronounLink {pronouns} />
</li>
{/each}
</ul>
</div>
{/if}
{#each data.fields as field}
<div class="col">
<FieldCard {field} />
</div>
{/each}
</div>
{#if data.members}
<div class="row">
<div class="col">
@ -90,7 +86,7 @@
<h2>Members</h2>
</div>
</div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-5 text-center">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center">
{#each data.members as member}
<PartialMemberCard user={data} {member} />
{/each}

View file

@ -49,7 +49,7 @@
</div>
{/if}
</div>
<div class="row">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#if data.names}
<div class="col-md">
<h4>Names</h4>
@ -73,15 +73,11 @@
</ul>
</div>
{/if}
</div>
</div>
{#if data.fields}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#each data.fields as field}
<div class="col">
<FieldCard {field} />
</div>
{/each}
</div>
{/if}
</div>
</div>

View file

@ -1,16 +1,16 @@
import type { MeUser } from "$lib/api/entities";
import type { APIError, MeUser } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch";
import type { PageServerLoad } from "./$types";
import type { PageServerLoad, Actions } from "./$types";
import { PUBLIC_BASE_URL } from "$env/static/public";
export const load = (async (event) => {
export const load = (async ({ locals, url }) => {
try {
const resp = await apiFetch<CallbackResponse>("/auth/discord/callback", {
method: "POST",
body: {
callback_domain: PUBLIC_BASE_URL,
code: event.url.searchParams.get("code"),
state: event.url.searchParams.get("state"),
code: url.searchParams.get("code"),
state: url.searchParams.get("state"),
},
});
@ -18,7 +18,9 @@ export const load = (async (event) => {
...resp,
};
} catch (e) {
return { error: e };
console.log(e);
return { error: e as APIError };
}
}) satisfies PageServerLoad;

View file

@ -0,0 +1,95 @@
<script lang="ts">
import { onMount } from "svelte";
import { Alert } from "sveltestrap";
import { goto } from "$app/navigation";
import type { APIError, MeUser } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch";
import { userStore } from "$lib/store";
import type { PageData } from "./$types";
interface SignupResponse {
user: MeUser;
token: string;
}
export let data: PageData;
onMount(() => {
if (data.token && data.user) {
localStorage.setItem("pronouns-token", data.token);
userStore.set(data.user);
goto("/");
}
});
let username = "";
let invite = "";
const signupForm = async () => {
try {
const resp = await apiFetch<SignupResponse>("/auth/discord/signup", {
method: "POST",
body: {
ticket: data.ticket,
username: username,
invite_code: invite,
},
});
localStorage.setItem("pronouns-token", resp.token);
userStore.set(resp.user);
goto("/");
} catch (e) {
data.error = e as APIError;
}
};
</script>
<svelte:head>
<title>Log in with Discord - pronouns.cc</title>
</svelte:head>
<h1>Log in with Discord</h1>
{#if data.error}
<Alert color="danger">
<h4 class="alert-heading">An error occurred</h4>
<b>{data.error.code}:</b>
{data.error.message}
</Alert>
{/if}
{#if data.ticket}
<form on:submit|preventDefault={signupForm}>
<div>
<label for="discord">Discord username</label>
<input id="discord" class="form-control" name="discord" disabled value={data.discord} />
</div>
<div>
<label for="username">Username</label>
<input id="username" class="form-control" name="username" bind:value={username} />
</div>
{#if data.require_invite}
<div>
<label for="invite">Invite code</label>
<input
id="invite"
class="form-control"
name="invite"
bind:value={invite}
aria-describedby="invite-help"
/>
<div id="invite-help" class="form-text">
You currently need an invite code to sign up. You can get one from an existing user.
</div>
</div>
{/if}
<div class="form-text">
By signing up, you agree to the <a href="/page/tos">terms of service</a> and the
<a href="/page/privacy">privacy policy</a>.
</div>
<button type="submit" class="btn btn-primary">Sign up</button>
</form>
{:else}
Loading...
{/if}

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { userStore } from "$lib/store";
import { onMount } from "svelte";
export const ssr = false;
onMount(() => {
userStore.set(null);
localStorage.removeItem("pronouns-token");
localStorage.removeItem("pronouns-user");
goto("/");
});
</script>
<h1>Log out</h1>

View file

@ -1,17 +0,0 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<svelte:head>
<title>Log in with Discord - pronouns.cc</title>
</svelte:head>
<h1>Log in with Discord</h1>
{#if data.error}
{:else}
{/if}

View file

@ -60,7 +60,7 @@
};
const toggleTheme = () => {
themeStore.set(theme === "dark" ? "light" : "dark")
themeStore.set(theme === "dark" ? "light" : "dark");
};
const toggleMenu = () => {
showMenu = !showMenu;
@ -78,15 +78,21 @@
<NavbarToggler on:click={toggleMenu} />
<Collapse isOpen={showMenu} navbar expand="lg">
<Nav class="ms-auto" navbar>
<NavItem>
{#if currentUser}
{#if currentUser}
<NavItem>
<NavLink href="/@{currentUser.name}">@{currentUser.name}</NavLink>
</NavItem>
<NavItem>
<NavLink href="/settings">Settings</NavLink>
<NavLink href="/logout">Log out</NavLink>
{:else}
<NavLink href="/login">Log in</NavLink>
{/if}
</NavItem>
</NavItem>
<NavItem>
<NavLink href="/auth/logout">Log out</NavLink>
</NavItem>
{:else}
<NavItem>
<NavLink href="/auth/login">Log in</NavLink>
</NavItem>
{/if}
<NavItem>
<NavLink
on:click={() => toggleTheme()}