feat: add invites page

This commit is contained in:
Sam 2023-03-14 00:16:19 +01:00
parent fb10f29e2b
commit 1647ec16a4
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
8 changed files with 141 additions and 7 deletions

View file

@ -11,7 +11,7 @@ import (
) )
type inviteResponse struct { type inviteResponse struct {
Code string `json:"string"` Code string `json:"code"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Used bool `json:"used"` Used bool `json:"used"`
} }

View file

@ -2,6 +2,7 @@ package meta
import ( import (
"net/http" "net/http"
"os"
"codeberg.org/u1f320/pronouns.cc/backend/server" "codeberg.org/u1f320/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
@ -24,6 +25,7 @@ type MetaResponse struct {
GitCommit string `json:"git_commit"` GitCommit string `json:"git_commit"`
Users int64 `json:"users"` Users int64 `json:"users"`
Members int64 `json:"members"` Members int64 `json:"members"`
RequireInvite bool `json:"require_invite"`
} }
func (s *Server) meta(w http.ResponseWriter, r *http.Request) error { func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
@ -45,6 +47,7 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
GitCommit: server.Revision, GitCommit: server.Revision,
Users: numUsers, Users: numUsers,
Members: numMembers, Members: numMembers,
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
}) })
return nil return nil
} }

View file

@ -16,4 +16,5 @@ interface MetaResponse {
git_commit: string; git_commit: string;
users: number; users: number;
members: number; members: number;
require_invite: boolean;
} }

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Alert } from "sveltestrap"; import { Alert, Icon } from "sveltestrap";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { APIError, MeUser } from "$lib/api/entities"; import type { APIError, MeUser } from "$lib/api/entities";
@ -82,7 +82,8 @@
aria-describedby="invite-help" aria-describedby="invite-help"
/> />
<div id="invite-help" class="form-text"> <div id="invite-help" class="form-text">
You currently need an invite code to sign up. You can get one from an existing user. <Icon name="info" /> You currently need an invite code to sign up. You can get one from an
existing user.
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -1,5 +1,10 @@
<script> <script lang="ts">
import { page } from "$app/stores";
import type { LayoutData } from "./$types";
import { ListGroup, ListGroupItem } from "sveltestrap"; import { ListGroup, ListGroupItem } from "sveltestrap";
export let data: LayoutData;
</script> </script>
<div class="grid"> <div class="grid">
@ -8,9 +13,32 @@
<h1>Settings</h1> <h1>Settings</h1>
<ListGroup> <ListGroup>
<ListGroupItem tag="a" href="/settings">Your profile</ListGroupItem> <ListGroupItem tag="a" active={$page.url.pathname === "/settings"} href="/settings">
<ListGroupItem tag="a" href="/settings/invites">Invites</ListGroupItem> Your profile
<ListGroupItem tag="a" href="/settings/tokens">API tokens</ListGroupItem> </ListGroupItem>
{#if data.require_invite}
<ListGroupItem
tag="a"
active={$page.url.pathname === "/settings/invites"}
href="/settings/invites"
>
Invites
</ListGroupItem>
{/if}
<ListGroupItem
tag="a"
active={$page.url.pathname === "/settings/tokens"}
href="/settings/tokens"
>
API tokens
</ListGroupItem>
<ListGroupItem
tag="a"
active={$page.url.pathname === "/settings/export"}
href="/settings/export"
>
Data export
</ListGroupItem>
</ListGroup> </ListGroup>
</div> </div>
<div class="col-md m-3"> <div class="col-md m-3">

View file

@ -1 +1,8 @@
import type { LayoutLoad } from "./$types";
export const ssr = false; export const ssr = false;
export const load = (async ({ parent }) => {
const data = await parent();
return data;
}) satisfies LayoutLoad;

View file

@ -0,0 +1,69 @@
<script lang="ts">
import type { APIError, Invite } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import { Alert, Button, Modal, Table } from "sveltestrap";
import type { PageData } from "./$types";
export let data: PageData;
let error: APIError | null = null;
let latestInvite: Invite | null = null;
let open = false;
const toggle = () => (open = !open);
const createInvite = async () => {
try {
const invite = await apiFetchClient<Invite>("/auth/invites", "POST");
error = null;
data.invites = [...data.invites, invite];
latestInvite = invite;
open = true;
} catch (e) {
latestInvite = null;
error = e as APIError;
}
};
</script>
<h1>Invites ({data.invites.length})</h1>
<div>
{#if !data.invitesEnabled}
<p>Invites aren't required to sign up to pronouns.cc right now!</p>
{:else}
<Table striped hover>
<thead>
<th>Code</th>
<th>Created at</th>
<th>Used?</th>
</thead>
<tbody>
{#each data.invites as invite}
<tr>
<td><code>{invite.code}</code></td>
<td>{invite.created}</td>
<td>{invite.used ? "yes" : "no"}</td>
</tr>
{/each}
</tbody>
</Table>
<div class="row">
<div class="col-md-4">
<Button color="primary" on:click={createInvite}>Create invite</Button>
</div>
<div class="col-md">
{#if error}
<Alert color="danger" fade={false}>
<h4 class="alert-heading">An error occurred</h4>
<b>{error.code}</b>: {error.message}
</Alert>
{/if}
</div>
</div>
<Modal body header="Invite created" isOpen={open} {toggle}>
Successfully created a new invite! Give the person you're sending it to this code:
<code>{latestInvite?.code}</code>
</Modal>
{/if}
</div>

View file

@ -0,0 +1,25 @@
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(500, (e as APIError).message);
}
}
return data;
}) satisfies PageLoad;