feat: instance invites

This commit is contained in:
Zoe
2026-10-08 13:50:44 -05:00
parent f6e1bc1f47
commit fae05ebc4c
17 changed files with 3962 additions and 26 deletions
+66 -4
View File
@@ -178,6 +178,49 @@ const deleteUser = (user: UserRow) => confirmThen({
return true;
}));
// ---------------------------------------------------------------------------
// Invite links
// ---------------------------------------------------------------------------
// only offered while the instance is invite only, links do nothing otherwise
const { data: instance } = await useFetch<{ registration: string }>('/api/admin/instance', {
key: 'admin_instance',
});
const inviteBusy = ref(false);
const inviteLink = ref('');
const inviteCopied = ref(false);
const createInvite = async () => {
if (inviteBusy.value) return;
inviteBusy.value = true;
actionError.value = '';
inviteCopied.value = false;
try {
const invite = await $fetch<{ token: string }>('/api/admin/invites', { method: 'POST' });
inviteLink.value = `${window.location.origin}/auth/register?invite=${invite.token}`;
// the Instance page lists the links, so it has to load the new one
clearNuxtData('admin_invites');
} catch (error) {
console.error('Failed to create invite:', error);
actionError.value = 'The invite link could not be created. Try again.';
inviteBusy.value = false;
return;
}
// copying can be refused by the browser, the link is shown either way
try {
await navigator.clipboard.writeText(inviteLink.value);
inviteCopied.value = true;
} catch (error) {
console.error('Failed to copy invite link:', error);
}
inviteBusy.value = false;
};
const openCreateUser = () => {
openDialog(DialogType.CreateUser, async (result) => {
if (result.ok === false) return;
@@ -208,10 +251,29 @@ const openCreateUser = () => {
</button>
</div>
<button type="button" @click="openCreateUser"
class="flex items-center h-8 px-3 text-[13px] rounded-md border border-[var(--color-border)] @hover:bg-[var(--color-hover)] transition-colors duration-200">
Create user
</button>
<div class="flex gap-2">
<button type="button" @click="openCreateUser"
class="flex items-center h-8 px-3 text-[13px] rounded-md border border-[var(--color-border)] @hover:bg-[var(--color-hover)] transition-colors duration-200">
Create user
</button>
<button v-if="instance?.registration === 'invite'" type="button" :disabled="inviteBusy"
@click="createInvite"
class="flex items-center gap-1.5 h-8 px-3 text-[13px] font-semibold rounded-md bg-[var(--color-accent)] text-[var(--color-accent-text)] disabled:opacity-50 disabled:cursor-not-allowed">
<span class="i-mynaui-link text-4" aria-hidden="true"></span>
Invite link
</button>
</div>
</div>
<div v-if="inviteLink" role="status"
class="flex flex-wrap items-center gap-x-3 gap-y-1 mb-3 px-3 py-2 rounded-lg bg-[var(--bg-container)] text-sm">
<span class="text-[var(--text-secondary)]">
{{ inviteCopied ? 'Invite link copied:' : 'New invite link:' }}
</span>
<span class="flex-1 min-w-55 font-mono text-[13px] overflow-hidden text-ellipsis whitespace-nowrap select-all">
{{ inviteLink }}
</span>
<span class="text-xs text-[var(--text-secondary)]">one use · expires in 7 days</span>
</div>
<p v-if="loadError" role="alert" class="pb-3 text-sm text-red-400">The users could not be loaded.</p>