feat: add invites page
This commit is contained in:
parent
fb10f29e2b
commit
1647ec16a4
8 changed files with 141 additions and 7 deletions
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,5 @@ interface MetaResponse {
|
||||||
git_commit: string;
|
git_commit: string;
|
||||||
users: number;
|
users: number;
|
||||||
members: number;
|
members: number;
|
||||||
|
require_invite: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
|
69
frontend/src/routes/settings/invites/+page.svelte
Normal file
69
frontend/src/routes/settings/invites/+page.svelte
Normal 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>
|
25
frontend/src/routes/settings/invites/+page.ts
Normal file
25
frontend/src/routes/settings/invites/+page.ts
Normal 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;
|
Loading…
Reference in a new issue