feat: implement basic auth workflow
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
export default defineNuxtRouteMiddleware((to) => {
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
const user = useSupabaseUser()
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
const publicRoutes = ['/login', '/signup', '/auth/callback']
|
const publicRoutes = ['/', '/login', '/signup', '/auth/callback']
|
||||||
if (publicRoutes.includes(to.path)) return
|
if (publicRoutes.includes(to.path)) return
|
||||||
|
|
||||||
if (!user.value) {
|
if (!user.value) {
|
||||||
return navigateTo('/login')
|
return navigateTo('/')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<IonPage>
|
<IonPage>
|
||||||
|
<!-- Splash: unauthenticated -->
|
||||||
|
<template v-if="!user">
|
||||||
|
<IonContent class="splash-content">
|
||||||
|
<div class="splash-center">
|
||||||
|
<img src="/oysqn_logo.png" alt="OYS Borrow a Boat" class="splash-logo" />
|
||||||
|
<IonButton expand="block" router-link="/login" class="splash-btn">Log In</IonButton>
|
||||||
|
</div>
|
||||||
|
</IonContent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Home: authenticated -->
|
||||||
|
<template v-else>
|
||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar color="primary">
|
<IonToolbar color="primary">
|
||||||
<IonButtons slot="start">
|
<IonButtons slot="start">
|
||||||
@@ -11,12 +23,43 @@
|
|||||||
<IonContent class="ion-padding">
|
<IonContent class="ion-padding">
|
||||||
<h2>Welcome to OYS Borrow a Boat</h2>
|
<h2>Welcome to OYS Borrow a Boat</h2>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
|
</template>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
|
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
|
||||||
IonButtons, IonMenuButton,
|
IonButtons, IonMenuButton, IonButton,
|
||||||
} from '@ionic/vue'
|
} from '@ionic/vue'
|
||||||
|
|
||||||
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
|
definePageMeta({ layout: false })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.splash-content {
|
||||||
|
--background: var(--ion-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-logo {
|
||||||
|
max-width: 280px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,17 +2,114 @@
|
|||||||
<IonPage>
|
<IonPage>
|
||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar color="primary">
|
<IonToolbar color="primary">
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton default-href="/" />
|
||||||
|
</IonButtons>
|
||||||
<IonTitle>Sign In</IonTitle>
|
<IonTitle>Sign In</IonTitle>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent class="ion-padding">
|
<IonContent class="ion-padding">
|
||||||
<!-- TODO: Auth form -->
|
<div class="login-form">
|
||||||
|
<template v-if="!sent">
|
||||||
|
<p class="ion-text-center">Enter your email address and we'll send you a sign-in link.</p>
|
||||||
|
<IonList>
|
||||||
|
<IonItem>
|
||||||
|
<IonLabel position="stacked">Email address</IonLabel>
|
||||||
|
<IonInput
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
autocomplete="email"
|
||||||
|
inputmode="email"
|
||||||
|
@keyup.enter="send"
|
||||||
|
/>
|
||||||
|
</IonItem>
|
||||||
|
</IonList>
|
||||||
|
<IonButton
|
||||||
|
expand="block"
|
||||||
|
class="ion-margin-top"
|
||||||
|
:disabled="!email || loading"
|
||||||
|
@click="send"
|
||||||
|
>
|
||||||
|
<IonSpinner v-if="loading" name="crescent" slot="start" />
|
||||||
|
Send Sign-In Link
|
||||||
|
</IonButton>
|
||||||
|
<p v-if="error" class="error-text ion-text-center">{{ error }}</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="sent-state ion-text-center">
|
||||||
|
<IonIcon :icon="mailOutline" class="sent-icon" />
|
||||||
|
<h2>Check your email</h2>
|
||||||
|
<p>A sign-in link was sent to <strong>{{ email }}</strong>. Open it on this device to sign in.</p>
|
||||||
|
<IonButton fill="outline" expand="block" class="ion-margin-top" @click="reset">
|
||||||
|
Use a different email
|
||||||
|
</IonButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue'
|
import {
|
||||||
|
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
|
||||||
|
IonButtons, IonBackButton, IonButton, IonList, IonItem,
|
||||||
|
IonLabel, IonInput, IonSpinner, IonIcon,
|
||||||
|
} from '@ionic/vue'
|
||||||
|
import { mailOutline } from 'ionicons/icons'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
definePageMeta({ layout: false })
|
definePageMeta({ layout: false })
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const sent = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
if (!email.value || loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
await auth.sendMagicLink(email.value.trim())
|
||||||
|
sent.value = true
|
||||||
|
} catch (e: unknown) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to send link. Please try again.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
sent.value = false
|
||||||
|
email.value = ''
|
||||||
|
error.value = ''
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sent-state {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sent-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
color: var(--ion-color-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: var(--ion-color-danger);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
supabase: {
|
supabase: {
|
||||||
redirectOptions: {
|
redirectOptions: {
|
||||||
login: '/login',
|
login: '/',
|
||||||
callback: '/auth/callback',
|
callback: '/auth/callback',
|
||||||
exclude: ['/login', '/signup', '/auth/callback'],
|
exclude: ['/', '/login', '/signup', '/auth/callback'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user