email verification

This commit is contained in:
Rafi
2023-03-03 18:50:02 +08:00
parent ef6657187a
commit f166581a73
10 changed files with 185 additions and 203 deletions

View File

@@ -1,18 +1,6 @@
export const useAuthFetch = async (url, options = {}) => { export const useAuthFetch = async (url, options = {}) => {
const { $auth } = useNuxtApp() const { $auth } = useNuxtApp()
const token = await $auth.retrieveToken()
if (!token) {
return await $auth.redirectToLogin()
}
options = Object.assign(options, {
headers: {
'Authorization': 'Bearer ' + token
}
})
const res = await useFetch(url, options) const res = await useFetch(url, options)
if (res.error.value && res.error.value.status === 401) { if (res.error.value && res.error.value.status === 401) {
await $auth.logout() await $auth.logout()

View File

@@ -17,6 +17,12 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name - DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password - DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email - DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
# If you want to use the email verification function, you need to configure the following parameters
# - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port
# - EMAIL_HOST_USER=
# - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True
ports: ports:
- '8000:8000' - '8000:8000'
networks: networks:

View File

@@ -3,18 +3,29 @@ definePageMeta({
layout: 'vuetify-app', layout: 'vuetify-app',
middleware: ['auth'] middleware: ['auth']
}) })
const email = ref('') const route = useRoute()
const sending = ref(false) const sending = ref(false)
const resent = ref(false)
const errorMsg = ref(null)
const resendEmail = async () => { const resendEmail = async () => {
errorMsg.value = null
sending.value = true sending.value = true
const { data, error } = await useFetch('/api/account/resend-verification-email/', { const { data, error } = await useFetch('/api/account/registration/resend-email/', {
method: 'POST' method: 'POST',
}) })
if (error.value) { if (error.value) {
console.log(error.value) errorMsg.value = 'Something went wrong. Please try again later.'
} else {
resent.value = true
} }
sending.value = false sending.value = false
} }
onNuxtReady(() => {
if (route.query.resend) {
resendEmail()
}
})
</script> </script>
<template> <template>
@@ -36,17 +47,21 @@ const resendEmail = async () => {
<div class="text-center"> <div class="text-center">
<h2 class="text-h4">Verify your email</h2> <h2 class="text-h4">Verify your email</h2>
<p class="text-body-2 mt-5"> <p class="text-body-2 mt-5">
We've sent a verification email to <strong>{{ email }}</strong>. <br> We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
Please check your inbox and click the link to verify your email address. Please check your inbox and click the link to verify your email address.
</p> </p>
<p v-if="errorMsg"
class="text-red"
>{{ errorMsg }}</p>
<v-btn <v-btn
variant="text" variant="text"
class="mt-5" class="mt-5"
color="primary" color="primary"
:loading="sending" :loading="sending"
@click="resendEmail" @click="resendEmail"
:disabled="resent"
> >
Resend email {{ resent ? 'Resent' : 'Resend email'}}
</v-btn> </v-btn>
</div> </div>
</v-card> </v-card>

View File

@@ -28,19 +28,30 @@
:rules="formRules.password" :rules="formRules.password"
label="Password" label="Password"
variant="underlined" variant="underlined"
@keyup.enter="submit"
></v-text-field> ></v-text-field>
</v-form> </v-form>
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div> <div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
<div
class="mt-5 d-flex justify-space-between"
>
<v-btn
@click="navigateTo('/account/signup')"
variant="text"
color="primary"
>Create account</v-btn>
<v-btn <v-btn
block
color="primary" color="primary"
:loading="submitting" :loading="submitting"
@click="submit" @click="submit"
class="mt-5" size="large"
>Submit</v-btn> >Submit</v-btn>
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
@@ -71,6 +82,8 @@ const errorMsg = ref(null)
const signInForm = ref(null) const signInForm = ref(null)
const valid = ref(true) const valid = ref(true)
const submitting = ref(false) const submitting = ref(false)
const route = useRoute()
const submit = async () => { const submit = async () => {
errorMsg.value = null errorMsg.value = null
const { valid } = await signInForm.value.validate() const { valid } = await signInForm.value.validate()
@@ -89,6 +102,8 @@ const submit = async () => {
errorMsg.value = 'Something went wrong. Please try again.' errorMsg.value = 'Something went wrong. Please try again.'
} }
} else { } else {
$auth.setUser(data.value.user)
navigateTo(route.query.callback || '/')
} }
submitting.value = false submitting.value = false
} }

View File

@@ -3,6 +3,8 @@ definePageMeta({
layout: 'vuetify-app' layout: 'vuetify-app'
}) })
const { $auth } = useNuxtApp()
const formData = ref({ const formData = ref({
username: '', username: '',
email: '', email: '',
@@ -52,6 +54,8 @@ const submit = async () => {
body: JSON.stringify(formData.value) body: JSON.stringify(formData.value)
}) })
console.log(error.value)
if (error.value) { if (error.value) {
if (error.value.status === 400) { if (error.value.status === 400) {
for (const key in formData.value) { for (const key in formData.value) {
@@ -66,6 +70,7 @@ const submit = async () => {
errorMsg.value = 'Something went wrong. Please try again.' errorMsg.value = 'Something went wrong. Please try again.'
} }
} else { } else {
$auth.setUser(data.value.user)
navigateTo('/account/onboarding') navigateTo('/account/onboarding')
} }
@@ -141,14 +146,23 @@ const handleFieldUpdate = (field) => {
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div> <div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
<div
class="mt-5 d-flex justify-space-between"
>
<v-btn
@click="navigateTo('/account/signin')"
variant="text"
color="primary"
>Sign in instead</v-btn>
<v-btn <v-btn
block
size="large" size="large"
color="primary" color="primary"
:loading="submitting" :loading="submitting"
@click="submit" @click="submit"
class="mt-5"
>Submit</v-btn> >Submit</v-btn>
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>

View File

@@ -0,0 +1,100 @@
<script setup>
definePageMeta({
layout: 'vuetify-app',
path: '/account/verify-email/:token',
title: 'Verify Email'
})
const route = useRoute()
const verifying = ref(false)
const status = ref('')
const verifyEmail = async () => {
verifying.value = true
const { data, error } = await useFetch(`/api/account/registration/verify-email/`, {
method: 'POST',
body: JSON.stringify({
key: route.params.token
})
})
if (!error.value && data.value.detail === 'ok') {
status.value = 'success'
} else {
status.value = 'error'
}
verifying.value = false
}
onNuxtReady(() => {
verifyEmail()
})
</script>
<template>
<v-container class="h-100vh">
<v-row
class="fill-height"
align-content="center"
justify="center"
>
<v-col
class="text-subtitle-1 text-center"
cols="12"
v-if="verifying"
>
Verifying your email
</v-col>
<v-col
cols="6"
v-if="verifying"
>
<v-progress-linear
color="deep-purple-accent-4"
indeterminate
rounded
height="6"
></v-progress-linear>
</v-col>
<v-col
cols="12"
v-if="status === 'success'"
class="text-center"
>
<h2 class="text-h4">
Your email has been verified.
</h2>
<p class="text-subtitle-1">
You can now sign in to your account.
</p>
<v-btn
color="primary"
variant="text"
@click="navigateTo('/account/login')"
>
Sign in
</v-btn>
</v-col>
<v-col
cols="12"
v-if="status === 'error'"
class="text-center"
>
<h2 class="text-h4">
There was an error verifying your email.
</h2>
<v-btn
color="primary"
variant="text"
@click="navigateTo('/account/onboarding?resend=1')"
>
Resend email
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.h-100vh {
height: 100vh;
}
</style>

View File

@@ -18,16 +18,14 @@ const abortFetch = () => {
fetchingResponse.value = false fetchingResponse.value = false
} }
const fetchReply = async (message, parentMessageId) => { const fetchReply = async (message, parentMessageId) => {
const token = await $auth.retrieveToken()
ctrl = new AbortController() ctrl = new AbortController()
try { try {
await fetchEventSource('/api/conversation', { await fetchEventSource('/api/conversation/', {
signal: ctrl.signal, signal: ctrl.signal,
method: 'POST', method: 'POST',
headers: { headers: {
'accept': 'application/json', 'accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
model: currentModel.value, model: currentModel.value,

View File

@@ -1,87 +0,0 @@
<template>
<v-card
color="red-lighten-5"
style="height: 100vh"
>
<v-container>
<v-row>
<v-col
sm="9"
offset-sm="1"
md="8"
offset-md="2"
>
<v-card
class="mt-15"
>
<v-card-title>Sign in</v-card-title>
<v-card-text>
<v-form ref="signInForm">
<v-text-field
v-model="formData.username"
:rules="formRules.username"
label="User name"
variant="underlined"
></v-text-field>
<v-text-field
v-model="formData.password"
:rules="formRules.password"
label="Password"
variant="underlined"
></v-text-field>
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
variant="elevated"
color="primary"
:loading="submitting"
@click="submit"
>Submit</v-btn>
</v-card-actions>
</v-form>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card>
</template>
<script setup>
definePageMeta({
layout: 'vuetify-app'
})
const formData = ref({
username: '',
password: ''
})
const formRules = ref({
username: [
v => !!v || 'Username is required'
],
password: [
v => !!v || 'Password is required'
]
})
const { $auth } = useNuxtApp()
const errorMsg = ref(null)
const signInForm = ref(null)
const valid = ref(true)
const submitting = ref(false)
const submit = async () => {
errorMsg.value = null
const { valid } = await signInForm.value.validate()
if (valid) {
submitting.value = true
const error = await $auth.login(formData.value.username, formData.value.password)
submitting.value = false
if (!error) {
return await $auth.callback()
}
errorMsg.value = error
}
}
</script>

View File

@@ -4,20 +4,10 @@ const AUTH_ROUTE = {
login: '/account/signin', login: '/account/signin',
} }
const COOKIE_OPTIONS = {
prefix: '_Secure-auth',
path: '/',
tokenName: 'access-token',
refreshTokenName: 'refresh-token',
}
const ENDPOINTS = { const ENDPOINTS = {
login: { login: {
url: '/api/account/login/' url: '/api/account/login/'
}, },
refresh: {
url: '/api/auth/token/refresh'
},
user: { user: {
url: '/api/account/user/' url: '/api/account/user/'
} }
@@ -25,16 +15,6 @@ const ENDPOINTS = {
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const tokenKey = COOKIE_OPTIONS.prefix + '.' + COOKIE_OPTIONS.tokenName
const refreshTokenKey = COOKIE_OPTIONS.prefix + '.' + COOKIE_OPTIONS.refreshTokenName
const tokenOptions = {
maxAge: 60 * 5,
}
const refreshTokenOptions = {
maxAge: 60 * 60 * 24,
}
const token = useCookie(tokenKey, tokenOptions)
const refreshToken = useCookie(refreshTokenKey, refreshTokenOptions)
class Auth { class Auth {
constructor() { constructor() {
@@ -42,75 +22,32 @@ export default defineNuxtPlugin(() => {
this.user = useState('user') this.user = useState('user')
} }
async login (username, password) {
const { data, error } = await useFetch(ENDPOINTS.login.url, {
method: 'POST',
body: {
username,
password
}
})
if (!error.value) {
token.value = data.value.access
refreshToken.value = data.value.refresh
return null
}
if (error.value.status === 401) {
return error.value.data.detail
}
return 'Request failed, please try again.'
}
async logout () { async logout () {
this.loginIn.value = false this.loginIn.value = false
this.user.value = null this.user.value = null
await this.redirectToLogin() await this.redirectToLogin()
} }
setUser (user) {
this.user = user
this.loginIn.value = true
}
async fetchUser () { async fetchUser () {
const { data, error } = await useFetch(ENDPOINTS.user.url, { const { data, error } = await useFetch(ENDPOINTS.user.url, {
// withCredentials: true // withCredentials: true
}) })
if (!error.value) { if (!error.value) {
this.user = data.value this.setUser(data.value)
this.loginIn.value = true
return null return null
} }
return error return error
} }
async refresh () { async redirectToLogin (callback) {
const { data, error } = await useFetch(ENDPOINTS.refresh.url, { return await navigateTo(
method: 'POST', AUTH_ROUTE.login + '?callback=' + encodeURIComponent(callback || AUTH_ROUTE.home)
body: { )
'refresh': refreshToken.value
}
})
if (!error.value) {
token.value = data.value.access
return data.value.access
}
return null
}
async callback () {
return await navigateTo(AUTH_ROUTE.home)
}
async redirectToLogin () {
return await navigateTo(AUTH_ROUTE.login)
}
async retrieveToken () {
const token = useCookie(tokenKey, tokenOptions)
const refreshToken = useCookie(refreshTokenKey, refreshTokenOptions)
if (!refreshToken.value) {
return null
}
if (!token.value) {
return await this.refresh()
}
return token.value
} }
} }
@@ -119,13 +56,9 @@ export default defineNuxtPlugin(() => {
addRouteMiddleware('auth', async (to, from) => { addRouteMiddleware('auth', async (to, from) => {
if (!auth.loginIn.value) { if (!auth.loginIn.value) {
// const token = await auth.retrieveToken()
// if (!token) {
// return await auth.redirectToLogin()
// }
const error = await auth.fetchUser() const error = await auth.fetchUser()
if (error) { if (error) {
return await auth.redirectToLogin() return await auth.redirectToLogin(to.fullPath)
} }
} }
}) })

View File

@@ -9,7 +9,7 @@ export const getDefaultConversionData = () => {
} }
export const getConversions = async () => { export const getConversions = async () => {
const { data, error } = await useAuthFetch('/api/chat/conversations') const { data, error } = await useAuthFetch('/api/chat/conversations/')
if (!error.value) { if (!error.value) {
return data.value return data.value
} }
@@ -33,7 +33,7 @@ export const openConversationMessages = async (currentConversation) => {
} }
export const genTitle = async (conversationId) => { export const genTitle = async (conversationId) => {
const { data, error } = await useAuthFetch('/api/gen_title', { const { data, error } = await useAuthFetch('/api/gen_title/', {
method: 'POST', method: 'POST',
body: { body: {
conversationId: conversationId conversationId: conversationId