feat: instance invites
This commit is contained in:
@@ -12,13 +12,42 @@ if (session.value !== null) {
|
||||
await navigateTo(to ?? '/');
|
||||
}
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// who may register is an instance setting, admins change it while it runs
|
||||
const { data: instance } = await useFetch('/api/instance', { key: 'instance' });
|
||||
|
||||
if (instance.value && instance.value.registration !== 'open') {
|
||||
if (instance.value?.registration === 'closed') {
|
||||
await navigateTo(to ? `/auth/login?to=${to}` : '/auth/login');
|
||||
}
|
||||
|
||||
const inviteOnly = computed(() => instance.value?.registration === 'invite');
|
||||
|
||||
// invite links look like /auth/register?invite=<token>
|
||||
const inviteToken = typeof useRoute().query.invite === 'string' ? useRoute().query.invite as string : undefined;
|
||||
|
||||
const { data: invite } = await useAsyncData(`invite_${inviteToken ?? 'none'}`, async () => {
|
||||
if (inviteToken === undefined) return null;
|
||||
|
||||
return await $fetch(`/api/invite/${encodeURIComponent(inviteToken)}`);
|
||||
});
|
||||
|
||||
// set when the invite turns out to be used up or expired on submit
|
||||
const inviteRejected = ref(false);
|
||||
|
||||
const validInvite = computed(() => !inviteRejected.value && invite.value?.valid ? invite.value : null);
|
||||
|
||||
// an invite only instance takes nobody without a working link
|
||||
const blocked = computed(() => inviteOnly.value && validInvite.value === null);
|
||||
|
||||
const inviteValidFor = computed(() => {
|
||||
if (!validInvite.value) return '';
|
||||
|
||||
const days = Math.max(1, Math.ceil((new Date(validInvite.value.expiresAt).getTime() - Date.now()) / DAY_MS));
|
||||
|
||||
return `Invite valid for ${days} more ${days === 1 ? 'day' : 'days'}${validInvite.value.singleUse ? ' · single use' : ''}`;
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
@@ -56,6 +85,9 @@ onMounted(() => {
|
||||
form.confirmPassword = tempForm.confirmPassword;
|
||||
hydrated.value = true;
|
||||
|
||||
// there is no form when the invite does not work
|
||||
if (blocked.value) return;
|
||||
|
||||
nameInputEl.value!.addEventListener('input', () => {
|
||||
nameInputEl.value!.setCustomValidity('');
|
||||
});
|
||||
@@ -95,7 +127,7 @@ const submit = async () => {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const res = await signUp(form.email, form.password, form.name);
|
||||
const res = await signUp(form.email, form.password, form.name, inviteOnly.value ? inviteToken : undefined);
|
||||
loading.value = false;
|
||||
if (!res.ok) {
|
||||
if (!res.error.data) {
|
||||
@@ -122,6 +154,10 @@ const submit = async () => {
|
||||
passwordInputEl.value!.setCustomValidity(res.error.data.message!);
|
||||
passwordInputEl.value!.reportValidity();
|
||||
break;
|
||||
case 'INVITE_INVALID':
|
||||
// used by someone else or expired while this page was open
|
||||
inviteRejected.value = true;
|
||||
break;
|
||||
case 'REGISTRATION_CLOSED':
|
||||
// an admin closed registration while this page was open
|
||||
alert('Registration is closed on this instance. Ask an admin for an account.');
|
||||
@@ -138,7 +174,8 @@ const submit = async () => {
|
||||
const key = await deriveKey(form.password, res.data.user.id);
|
||||
localStorage.setItem('encryptionKey', JSON.stringify(key));
|
||||
|
||||
return navigateTo(to ?? '/');
|
||||
// connecting a provider comes next, there is nothing to chat with before that
|
||||
return navigateTo(to ? `/welcome?to=${to}` : '/welcome');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -158,7 +195,30 @@ const submit = async () => {
|
||||
<p class="text-[var(--text-secondary)]">Create your account</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl">
|
||||
<p v-if="blocked" role="alert"
|
||||
class="px-3 py-2.5 mb-1 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)] text-sm leading-snug text-[var(--text-secondary)]">
|
||||
<template v-if="inviteToken === undefined">
|
||||
This instance is invite only. Ask an admin for an invite link to create an account.
|
||||
</template>
|
||||
<template v-else>
|
||||
This invite link doesn't work anymore. It was used already, has expired, or was revoked.
|
||||
Ask an admin for a new one.
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<div v-if="inviteOnly && validInvite"
|
||||
class="flex items-center gap-3 px-3 py-2.5 mb-5 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)]">
|
||||
<UserAvatar :user="validInvite.inviter" class="w-8 h-8 rounded-lg text-sm"></UserAvatar>
|
||||
<div class="flex flex-col gap-0.25 text-sm min-w-0">
|
||||
<span class="overflow-hidden text-ellipsis">
|
||||
<strong class="font-semibold">{{ validInvite.inviter.name }}</strong> invited you to this
|
||||
instance
|
||||
</span>
|
||||
<span class="text-xs text-[var(--text-secondary)]">{{ inviteValidFor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!blocked" class="rounded-2xl">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="submit">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="name"
|
||||
@@ -194,6 +254,14 @@ const submit = async () => {
|
||||
class="focus:ring-2 focus:ring-[var(--color-accent)]/20 border border-[var(--color-border)] transition-all" />
|
||||
</div>
|
||||
|
||||
<p class="flex gap-2 mt-1 text-xs leading-normal text-[var(--text-secondary)]">
|
||||
<span class="i-mynaui-lock text-4 mt-0.25 flex-shrink-0" aria-hidden="true"></span>
|
||||
<span>
|
||||
Your password also encrypts your API keys in the browser. If you forget it, saved
|
||||
keys can't be recovered and have to be re-entered.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<button :disabled="!hydrated"
|
||||
class="accent w-full mt-2 h-11 flex items-center justify-center font-semibold" type="submit">
|
||||
<span v-if="loading" class="i-svg-spinners-90-ring-with-bg text-xl"></span>
|
||||
|
||||
@@ -127,7 +127,8 @@ const submit = async () => {
|
||||
const key = await deriveKey(form.password, res.data.user.id);
|
||||
localStorage.setItem('encryptionKey', JSON.stringify(key));
|
||||
|
||||
return navigateTo('/');
|
||||
// straight on to connecting a provider, the instance is useless without one
|
||||
return navigateTo('/welcome');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user