Require email account authentication
All checks were successful
CI / validate (push) Successful in 14m23s
All checks were successful
CI / validate (push) Successful in 14m23s
This commit is contained in:
@@ -19,8 +19,8 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="callback noise">
|
||||
<AppMark />
|
||||
<p v-if="!error">VERIFYING YOUR SIGNAL…</p>
|
||||
<template v-else><p class="error">{{ error }}</p><NuxtLink to="/">REQUEST A NEW LINK</NuxtLink></template>
|
||||
<p v-if="!error">VERIFYING YOUR EMAIL…</p>
|
||||
<template v-else><p class="error">{{ error }}</p><NuxtLink to="/auth/sign-in">BACK TO SIGN IN</NuxtLink></template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
33
apps/web/pages/auth/forgot-password.vue
Normal file
33
apps/web/pages/auth/forgot-password.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
const { requestPasswordReset } = useDngAuth()
|
||||
const email = ref('')
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const sent = ref(false)
|
||||
|
||||
async function submit() {
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await requestPasswordReset(email.value)
|
||||
sent.value = true
|
||||
} catch (cause) {
|
||||
const value = cause as { data?: { msg?: string; message?: string }; message?: string }
|
||||
error.value = value.data?.msg ?? value.data?.message ?? value.message ?? 'Could not send the reset email.'
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthCard eyebrow="ACCOUNT RECOVERY" title="RESET THE SIGNAL." subtitle="Enter your account email. If it exists, Supabase will send a secure password reset link.">
|
||||
<p v-if="sent" class="form-success" role="status">CHECK YOUR EMAIL. The recovery link can be used once and expires automatically.</p>
|
||||
<form v-else class="auth-form" @submit.prevent="submit">
|
||||
<label>EMAIL<input v-model="email" name="email" type="email" autocomplete="email" required placeholder="you@example.com"></label>
|
||||
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
|
||||
<button :disabled="busy">{{ busy ? 'SENDING…' : 'SEND RESET LINK →' }}</button>
|
||||
</form>
|
||||
<template #footer><NuxtLink class="form-link" to="/auth/sign-in">← BACK TO SIGN IN</NuxtLink></template>
|
||||
</AuthCard>
|
||||
</template>
|
||||
54
apps/web/pages/auth/reset-password.vue
Normal file
54
apps/web/pages/auth/reset-password.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
const auth = useDngAuth()
|
||||
const password = ref('')
|
||||
const confirmation = ref('')
|
||||
const busy = ref(false)
|
||||
const ready = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const session = await auth.restore()
|
||||
if (!session) throw new Error('This recovery link is invalid or expired.')
|
||||
ready.value = true
|
||||
} catch (cause) {
|
||||
error.value = cause instanceof Error ? cause.message : 'Could not verify the recovery link.'
|
||||
}
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
if (password.value.length < 8) {
|
||||
error.value = 'Password must contain at least 8 characters.'
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmation.value) {
|
||||
error.value = 'Passwords do not match.'
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
try {
|
||||
await auth.updatePassword(password.value)
|
||||
await navigateTo('/dashboard', { replace: true })
|
||||
} catch (cause) {
|
||||
const value = cause as { data?: { msg?: string; message?: string }; message?: string }
|
||||
error.value = value.data?.msg ?? value.data?.message ?? value.message ?? 'Could not update the password.'
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthCard eyebrow="SECURE RECOVERY" title="CHOOSE A NEW PASSWORD." subtitle="Set a new password for your Dungeons & Ground account.">
|
||||
<p v-if="!ready && !error" class="form-success">VERIFYING RECOVERY LINK…</p>
|
||||
<form v-if="ready" class="auth-form" @submit.prevent="submit">
|
||||
<label>NEW PASSWORD<input v-model="password" name="password" type="password" autocomplete="new-password" minlength="8" required placeholder="At least 8 characters"></label>
|
||||
<label>CONFIRM PASSWORD<input v-model="confirmation" name="password-confirmation" type="password" autocomplete="new-password" minlength="8" required placeholder="Repeat your password"></label>
|
||||
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
|
||||
<button :disabled="busy">{{ busy ? 'UPDATING…' : 'UPDATE PASSWORD →' }}</button>
|
||||
</form>
|
||||
<p v-else-if="error" class="form-error" role="alert">{{ error }}</p>
|
||||
<template #footer><NuxtLink class="form-link" to="/auth/sign-in">BACK TO SIGN IN</NuxtLink></template>
|
||||
</AuthCard>
|
||||
</template>
|
||||
45
apps/web/pages/auth/sign-in.vue
Normal file
45
apps/web/pages/auth/sign-in.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const auth = useDngAuth()
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
function messageFrom(cause: unknown) {
|
||||
const value = cause as { data?: { msg?: string; message?: string; error_description?: string }; message?: string }
|
||||
return value.data?.msg ?? value.data?.message ?? value.data?.error_description ?? value.message ?? 'Sign-in failed. Check your email and password.'
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await auth.signIn(email.value, password.value)
|
||||
const requested = localStorage.getItem('dng-post-auth-redirect')
|
||||
localStorage.removeItem('dng-post-auth-redirect')
|
||||
const destination = requested?.startsWith('/') && !requested.startsWith('//') ? requested : '/dashboard'
|
||||
await navigateTo(destination, { replace: true })
|
||||
} catch (cause) {
|
||||
error.value = messageFrom(cause)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.restore()
|
||||
if (auth.session.value) await navigateTo('/dashboard', { replace: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthCard eyebrow="ACCOUNT ACCESS" title="WELCOME BACK." subtitle="Sign in to continue your private worlds and shared campaigns.">
|
||||
<form class="auth-form" @submit.prevent="submit">
|
||||
<label>EMAIL<input v-model="email" name="email" type="email" autocomplete="email" required placeholder="you@example.com"></label>
|
||||
<label><span class="field-row"><span>PASSWORD</span><NuxtLink to="/auth/forgot-password">FORGOT?</NuxtLink></span><input v-model="password" name="password" type="password" autocomplete="current-password" required placeholder="Your password"></label>
|
||||
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
|
||||
<button :disabled="busy">{{ busy ? 'SIGNING IN…' : 'SIGN IN →' }}</button>
|
||||
</form>
|
||||
<template #footer>New to D&G? <NuxtLink class="form-link" to="/auth/sign-up">CREATE ACCOUNT</NuxtLink></template>
|
||||
</AuthCard>
|
||||
</template>
|
||||
80
apps/web/pages/auth/sign-up.vue
Normal file
80
apps/web/pages/auth/sign-up.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
const auth = useDngAuth()
|
||||
const displayName = ref('')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const confirmation = ref('')
|
||||
const ageConfirmed = ref(false)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const confirmationSent = ref(false)
|
||||
|
||||
function messageFrom(cause: unknown) {
|
||||
const value = cause as { data?: { msg?: string; message?: string; error_description?: string }; message?: string }
|
||||
return value.data?.msg ?? value.data?.message ?? value.data?.error_description ?? value.message ?? 'Registration failed. Try again.'
|
||||
}
|
||||
|
||||
async function finishSignIn() {
|
||||
const requested = localStorage.getItem('dng-post-auth-redirect')
|
||||
localStorage.removeItem('dng-post-auth-redirect')
|
||||
const destination = requested?.startsWith('/') && !requested.startsWith('//') ? requested : '/dashboard'
|
||||
await navigateTo(destination, { replace: true })
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
if (displayName.value.trim().length < 2) {
|
||||
error.value = 'Enter a name with at least 2 characters.'
|
||||
return
|
||||
}
|
||||
if (password.value.length < 8) {
|
||||
error.value = 'Password must contain at least 8 characters.'
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmation.value) {
|
||||
error.value = 'Passwords do not match.'
|
||||
return
|
||||
}
|
||||
if (!ageConfirmed.value) {
|
||||
error.value = 'You must confirm that you are at least 13 years old.'
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
try {
|
||||
const result = await auth.signUp(displayName.value, email.value, password.value)
|
||||
if (result.session) await finishSignIn()
|
||||
else confirmationSent.value = true
|
||||
} catch (cause) {
|
||||
error.value = messageFrom(cause)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.restore()
|
||||
if (auth.session.value) await navigateTo('/dashboard', { replace: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthCard eyebrow="CREATE YOUR ACCOUNT" title="ENTER THE ALPHA." subtitle="Your email keeps your worlds, characters, and campaign access recoverable across devices.">
|
||||
<div v-if="confirmationSent" class="form-success" role="status">
|
||||
CHECK YOUR EMAIL. We sent a confirmation link to {{ email }}. Open it to activate your account.
|
||||
</div>
|
||||
<form v-else class="auth-form" @submit.prevent="submit">
|
||||
<label>DISPLAY NAME<input v-model="displayName" name="name" autocomplete="name" maxlength="80" required placeholder="How your party sees you"></label>
|
||||
<label>EMAIL<input v-model="email" name="email" type="email" autocomplete="email" required placeholder="you@example.com"></label>
|
||||
<label>PASSWORD<input v-model="password" name="password" type="password" autocomplete="new-password" minlength="8" required placeholder="At least 8 characters"></label>
|
||||
<label>CONFIRM PASSWORD<input v-model="confirmation" name="password-confirmation" type="password" autocomplete="new-password" minlength="8" required placeholder="Repeat your password"></label>
|
||||
<label class="consent"><input v-model="ageConfirmed" type="checkbox" required><span>I am at least 13 years old and accept the 13+ content boundary.</span></label>
|
||||
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
|
||||
<button :disabled="busy">{{ busy ? 'CREATING ACCOUNT…' : 'CREATE ACCOUNT →' }}</button>
|
||||
</form>
|
||||
<template #footer>Already have an account? <NuxtLink class="form-link" to="/auth/sign-in">SIGN IN</NuxtLink></template>
|
||||
</AuthCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.consent{grid-template-columns:18px 1fr!important;align-items:start!important;font:400 10px/1.5 var(--body)!important;letter-spacing:0!important;color:var(--muted)!important}.consent input{width:16px!important;min-height:16px!important;margin:1px 0 0;padding:0!important;accent-color:var(--acid)}
|
||||
</style>
|
||||
Reference in New Issue
Block a user