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
+50 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import type { AppFarm } from "../types";
import type { AppFarm, EnrolmentCode } from "../types";
const props = defineProps<{
farm: AppFarm;
@@ -10,8 +10,39 @@ const props = defineProps<{
uplinkUrl: string;
/** Mints a new proxy token. Rejects with a readable message. */
mint: () => Promise<string>;
/** Makes a one-time enrolment code. Rejects with a readable message. */
enrol: () => Promise<EnrolmentCode>;
/** The API's address as a gateway reaches it, for the enrol command. */
apiUrl: string;
}>();
const enrolment = ref<EnrolmentCode | null>(null);
const enrolBusy = ref(false);
const enrolCopied = ref(false);
const command = computed(() => `continuum-proxy enrol ${props.apiUrl} ${enrolment.value?.code ?? ""}`);
async function makeCode() {
enrolBusy.value = true;
failure.value = "";
try {
enrolment.value = await props.enrol();
enrolCopied.value = false;
} catch (err) {
failure.value = err instanceof Error ? err.message : String(err);
} finally {
enrolBusy.value = false;
}
}
async function copyCommand() {
try {
await navigator.clipboard.writeText(command.value);
enrolCopied.value = true;
} catch {
enrolCopied.value = false;
}
}
const token = ref("");
const confirming = ref(false);
const busy = ref(false);
@@ -68,6 +99,23 @@ async function copy() {
settings below.
</p>
<div class="gateway-setup__enrol">
<template v-if="!enrolment">
<button type="button" :disabled="enrolBusy" @click="makeCode">Connect a gateway with a code</button>
</template>
<template v-else>
<p>
On the computer that will run the gateway, run this within 15 minutes. The code works once, and it does not
replace a gateway that is already connected.
</p>
<pre class="gateway-setup__settings">{{ command }}</pre>
<button type="button" @click="copyCommand">{{ enrolCopied ? "Copied" : "Copy" }}</button>
<button type="button" @click="enrolment = null">Done</button>
</template>
</div>
<details class="gateway-setup__manual">
<summary>Set it up by hand instead</summary>
<template v-if="!token">
<button v-if="!confirming" type="button" :disabled="busy" @click="start">
{{ gatewayOnline ? "Create a new gateway token" : "Create gateway settings" }}
@@ -91,6 +139,7 @@ async function copy() {
<button type="button" @click="copy">{{ copied ? "Copied" : "Copy" }}</button>
<button type="button" @click="token = ''">Done</button>
</template>
</details>
<p v-if="failure" role="alert" class="gateway-setup__error">{{ failure }}</p>
</section>