Add members/invitations and code-based gateway setup to the Setup page

Gateway setup shows a `continuum-proxy enrol` command with a one-time
code; the manual settings stay behind "Set it up by hand". New members
section for inviting by email, plus an /invite page to accept, and the
login page honours a safe `next` redirect.
This commit is contained in:
2026-10-30 08:13:00 +00:00
parent 69f35c2a6b
commit 88b242d914
8 changed files with 336 additions and 6 deletions
+60
View File
@@ -0,0 +1,60 @@
<script setup lang="ts">
// Reached from an invitation link. Excluded from the sign-in redirect (see
// nuxt.config.ts) so the token in the URL survives: a visitor who is not
// signed in is sent to /login and brought back here afterwards.
const route = useRoute();
const user = useSupabaseUser();
const apiFetch = useApiFetch();
const token = computed(() => (typeof route.query.token === "string" ? route.query.token : ""));
const state = ref<"waiting" | "working" | "done" | "failed">("waiting");
const message = ref("");
const joined = ref<{ orgName: string; role: string } | null>(null);
async function accept() {
state.value = "working";
try {
const result = await apiFetch<{ orgName: string; role: string }>("/invitations/accept", {
method: "POST",
body: { token: token.value },
});
joined.value = result;
state.value = "done";
} catch (err) {
message.value = apiErrorMessage(err, "the invitation could not be accepted");
state.value = "failed";
}
}
const next = computed(() => `/invite?token=${encodeURIComponent(token.value)}`);
</script>
<template>
<div class="invite">
<h1>Join an organisation</h1>
<p v-if="!token">This link has no invitation in it.</p>
<template v-else-if="!user">
<p>Sign in with the email address the invitation was sent to, then you will come back here.</p>
<NuxtLink :to="{ path: '/login', query: { next } }">Sign in</NuxtLink>
</template>
<template v-else>
<p>You are signed in as {{ user.email }}.</p>
<button v-if="state === 'waiting' || state === 'working'" type="button" :disabled="state === 'working'" @click="accept">
Accept the invitation
</button>
<p v-if="state === 'done'" role="status">
You have joined {{ joined?.orgName }} as {{ joined?.role }}. <NuxtLink to="/">Go to the dashboard</NuxtLink>
</p>
<p v-if="state === 'failed'" role="alert">{{ message }}</p>
</template>
</div>
</template>
<style scoped>
.invite {
max-width: 28rem;
margin: 4rem auto;
display: grid;
gap: 0.75rem;
}
</style>