170 lines
5.6 KiB
Vue
170 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import { authClient } from '~~/lib/auth-client';
|
|
import { deriveKey } from '~/utils/crypto';
|
|
|
|
definePageMeta({
|
|
layout: 'auth',
|
|
});
|
|
|
|
const { session } = await useAuth();
|
|
const to = useRoute().query.to as string | undefined;
|
|
|
|
if (session.value !== null) {
|
|
await navigateTo(to ?? '/');
|
|
}
|
|
|
|
if (import.meta.server) {
|
|
if (process.env.DISABLE_SIGNUP?.toLowerCase() === 'true' || process.env.DISABLE_SIGNUP === '1') {
|
|
await navigateTo(to ? `/auth/login?to=${to}` : '/auth/login');
|
|
}
|
|
}
|
|
|
|
const form = reactive({
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
});
|
|
const loading = ref(false);
|
|
|
|
const nameInputEl = ref<HTMLInputElement | null>(null);
|
|
const emailInputEl = ref<HTMLInputElement | null>(null);
|
|
const passwordInputEl = ref<HTMLInputElement | null>(null);
|
|
const confirmPasswordInputEl = ref<HTMLInputElement | null>(null);
|
|
|
|
const tempForm = {
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
};
|
|
|
|
// prevent text fields from clearing on hydration
|
|
onBeforeMount(() => {
|
|
tempForm.name = (document.getElementById('name') as HTMLInputElement)?.value ?? '';
|
|
tempForm.email = (document.getElementById('email') as HTMLInputElement)?.value ?? '';
|
|
tempForm.password = (document.getElementById('password') as HTMLInputElement)?.value ?? '';
|
|
tempForm.confirmPassword = (document.getElementById('confirmPassword') as HTMLInputElement)?.value ?? '';
|
|
});
|
|
|
|
const hydrated = ref(false);
|
|
|
|
onMounted(() => {
|
|
form.name = tempForm.name;
|
|
form.email = tempForm.email;
|
|
form.password = tempForm.password;
|
|
form.confirmPassword = tempForm.confirmPassword;
|
|
hydrated.value = true;
|
|
|
|
nameInputEl.value!.addEventListener('input', () => {
|
|
nameInputEl.value!.setCustomValidity('');
|
|
});
|
|
|
|
emailInputEl.value!.addEventListener('input', () => {
|
|
emailInputEl.value!.setCustomValidity('');
|
|
});
|
|
|
|
passwordInputEl.value!.addEventListener('input', () => {
|
|
passwordInputEl.value!.setCustomValidity('');
|
|
});
|
|
|
|
confirmPasswordInputEl.value!.addEventListener('input', () => {
|
|
if (passwordInputEl.value!.value !== confirmPasswordInputEl.value!.value) {
|
|
confirmPasswordInputEl.value!.setCustomValidity('Passwords do not match');
|
|
confirmPasswordInputEl.value!.reportValidity();
|
|
} else {
|
|
confirmPasswordInputEl.value!.setCustomValidity('');
|
|
}
|
|
});
|
|
});
|
|
|
|
const submit = async () => {
|
|
const inputs = [nameInputEl, emailInputEl, passwordInputEl, confirmPasswordInputEl];
|
|
|
|
for (const input of inputs) {
|
|
if (input.value?.validity.valid === false) {
|
|
input.value!.reportValidity();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (form.password !== form.confirmPassword) {
|
|
alert('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
const { data, error } = await authClient.signUp.email({
|
|
name: form.name,
|
|
email: form.email,
|
|
password: form.password,
|
|
});
|
|
|
|
loading.value = false;
|
|
|
|
if (error) {
|
|
const errorCode = error.code! as keyof typeof authClient.$ERROR_CODES;
|
|
|
|
// TODO: i18n
|
|
// ref https://www.better-auth.com/docs/concepts/client#error-codes
|
|
switch (errorCode) {
|
|
case 'USER_ALREADY_EXISTS':
|
|
case 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL':
|
|
emailInputEl.value!.setCustomValidity('Account with this email already exists');
|
|
emailInputEl.value!.reportValidity();
|
|
break;
|
|
case 'INVALID_EMAIL':
|
|
emailInputEl.value!.setCustomValidity(error.message!);
|
|
emailInputEl.value!.reportValidity();
|
|
break;
|
|
case 'INVALID_PASSWORD':
|
|
passwordInputEl.value!.setCustomValidity(error.message!);
|
|
passwordInputEl.value!.reportValidity();
|
|
break;
|
|
default:
|
|
console.log(error);
|
|
alert('Something went wrong: ' + error.message);
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const key = await deriveKey(form.password, data.user.id);
|
|
localStorage.setItem('encryptionKey', JSON.stringify(key));
|
|
|
|
// force a session refetch
|
|
clearNuxtData();
|
|
|
|
// success
|
|
const triplit = useTriplitClient();
|
|
if ('startSession' in triplit) {
|
|
await triplit.startSession(data.token!);
|
|
}
|
|
|
|
return navigateTo(to ?? '/');
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<h1 class="font-bold text-center mb-4">Register</h1>
|
|
<form class="flex flex-col [&>input]:mb-2" @submit.prevent="submit">
|
|
<label for="name">Name</label>
|
|
<input required ref="nameInputEl" type="text" id="name" v-model="form.name" />
|
|
<label for="email">Email</label>
|
|
<input required pattern=".{1,}@.{1,}\..{2,3}" ref="emailInputEl" type="email" autocomplete="email" id="email"
|
|
v-model="form.email" />
|
|
<label for="password">Password</label>
|
|
<input required minlength="8" maxlength="128" ref="passwordInputEl" type="password" autocomplete="new-password"
|
|
id="password" v-model="form.password" />
|
|
<label for="confirmPassword">Confirm Password</label>
|
|
<input required minlength="8" maxlength="128" ref="confirmPasswordInputEl" type="password"
|
|
autocomplete="new-password" id="confirmPassword" v-model="form.confirmPassword" />
|
|
<button :disabled="!hydrated" class="accent" type="submit">
|
|
<Icon v-if="loading" class="text-6" name="svg-spinners:90-ring-with-bg" />
|
|
<span v-else>Register</span>
|
|
</button>
|
|
</form>
|
|
<p class="text-center">Already have an account? <a :href="to ? `/auth/login?to=${to}` : '/auth/login'">Login</a></p>
|
|
</template> |