diff --git a/app/components/ChatInput.vue b/app/components/ChatInput.vue index ed8c0c1..3b93f58 100644 --- a/app/components/ChatInput.vue +++ b/app/components/ChatInput.vue @@ -178,7 +178,12 @@ watch(() => props.agent?.defaultModelId, (newModelId) => { } }) +// sending needs a model, and there are none until a provider is set up +const noModels = computed(() => allModels.value.length === 0); + const handleSubmit = () => { + if (noModels.value) return; + if (isUploading.value || hasFailedUploads.value) { return; } @@ -454,8 +459,8 @@ onUnmounted(() => {
- +
+ +
diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts index 820f11f..dff7869 100644 --- a/app/composables/useAuth.ts +++ b/app/composables/useAuth.ts @@ -162,7 +162,9 @@ export const useAuth = () => { const signUp = async ( email: string, password: string, - name: string + name: string, + // needed while the instance only takes invited people + inviteToken?: string, ): Promise> => { sessionFetching.value = true; @@ -171,6 +173,8 @@ export const useAuth = () => { email, password, name, + }, { + headers: inviteToken ? { 'x-invite-token': inviteToken } : undefined, }); if (error) { diff --git a/app/pages/auth/register.vue b/app/pages/auth/register.vue index fdb4eff..73f2470 100644 --- a/app/pages/auth/register.vue +++ b/app/pages/auth/register.vue @@ -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= +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'); }; @@ -158,7 +195,30 @@ const submit = async () => {

Create your account

-
+ + +
+ +
+ + {{ validInvite.inviter.name }} invited you to this + instance + + {{ inviteValidFor }} +
+
+ +
+

+ + + 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. + +

+