Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44ec5e2fb | ||
|
|
32f3013337 | ||
|
|
e66d994219 | ||
|
|
f166581a73 | ||
|
|
ef6657187a | ||
|
|
3b6c48a776 | ||
|
|
b316ac0b4a | ||
|
|
51e8ea8d1a | ||
|
|
60cd0689fb | ||
|
|
74fc850ceb | ||
|
|
339dd1e0c6 | ||
|
|
122704737a | ||
|
|
bd35c21e2f | ||
|
|
c2705e5f2a | ||
|
|
0e5aeddffa |
51
README.md
51
README.md
@@ -4,14 +4,19 @@
|
||||
|
||||
# ChatGPT UI
|
||||
|
||||
---
|
||||
|
||||
A web client for ChatGPT, using OpenAI's API.
|
||||
|
||||
## 📢Updates
|
||||
|
||||
---
|
||||
<details open>
|
||||
<summary><strong>2023-03-04</strong></summary>
|
||||
|
||||
**Update to the latest official chat model ** `gpt-3.5-turbo`
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
|
||||
<summary><strong>2023-02-24</strong></summary>
|
||||
Version 2 is a major update that separates the backend functionality as an independent project, hosted at [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server).
|
||||
|
||||
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
|
||||
@@ -23,10 +28,11 @@ Version 2 introduces the following new features:
|
||||
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
||||
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Quick start with Docker Compose
|
||||
|
||||
---
|
||||
### Run services
|
||||
|
||||
Below is a docker-compose.yml template:
|
||||
@@ -37,48 +43,53 @@ services:
|
||||
client:
|
||||
image: wongsaang/chatgpt-ui-client:latest
|
||||
environment:
|
||||
- SERVER_DOMAIN=http://backend:8000
|
||||
- SERVER_DOMAIN=http://backend-web-server
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
- backend-web-server
|
||||
ports:
|
||||
- '80:80'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
backend:
|
||||
image: wongsaang/chatgpt-ui-server:latest
|
||||
backend-wsgi-server:
|
||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||
environment:
|
||||
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
ports:
|
||||
- '8000:8000'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
backend-web-server:
|
||||
image: wongsaang/chatgpt-ui-web-server:latest
|
||||
environment:
|
||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||
ports:
|
||||
- '9000:80'
|
||||
depends_on:
|
||||
- backend-wsgi-server
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
|
||||
networks:
|
||||
chatgpt_ui_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
backend_static:
|
||||
```
|
||||
|
||||
### After running
|
||||
### Set API key
|
||||
|
||||
After running the services, you can access the web client at http://localhost, and an admin panel at http://localhost/admin.
|
||||
After running the services, you can access the web client at `http://localhost`, and an admin panel at `http://localhost:9000/admin`.
|
||||
|
||||
Before you can start chatting, you need to log in to the admin panel to add an OpenAI API key. In the Settings model, add a record with the name "openai_api_key" and the value as your API key.
|
||||
Default superuser: `admin`
|
||||
|
||||
Default password: `password`
|
||||
|
||||
Before you can start chatting, you need to log in to the admin panel to add an OpenAI API key. In the Settings model, add a record with the name `openai_api_key` and the value as your API key.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
---
|
||||
|
||||
### Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
export const useAuthFetch = async (url, options = {}) => {
|
||||
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)
|
||||
if (res.error.value && res.error.value.status === 401) {
|
||||
await $auth.logout()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
client:
|
||||
image: wongsaang/chatgpt-ui-client:latest
|
||||
environment:
|
||||
- SERVER_DOMAIN=http://backend:8000
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
ports:
|
||||
- '80:80'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
backend:
|
||||
image: wongsaang/chatgpt-ui-server:latest
|
||||
environment:
|
||||
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
ports:
|
||||
- '8000:8000'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
|
||||
networks:
|
||||
chatgpt_ui_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
backend_static:
|
||||
@@ -1,31 +1,43 @@
|
||||
version: '3'
|
||||
services:
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
image: wongsaang/chatgpt-ui-client:latest
|
||||
environment:
|
||||
- SERVER_DOMAIN=http://backend:8000
|
||||
- SERVER_DOMAIN=http://backend-web-server
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
- backend-web-server
|
||||
ports:
|
||||
- '80:80'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
backend:
|
||||
image: 'wongsaang/chatgpt-ui-server:latest'
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
- chatgpt_ui_network
|
||||
backend-wsgi-server:
|
||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||
environment:
|
||||
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||
- 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:
|
||||
- '8000:8000'
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
backend-web-server:
|
||||
image: wongsaang/chatgpt-ui-web-server:latest
|
||||
environment:
|
||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||
ports:
|
||||
- '9000:80'
|
||||
depends_on:
|
||||
- backend-wsgi-server
|
||||
networks:
|
||||
- chatgpt_ui_network
|
||||
|
||||
networks:
|
||||
chatgpt_ui_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
backend_static:
|
||||
driver: bridge
|
||||
@@ -12,12 +12,8 @@ server {
|
||||
{
|
||||
proxy_pass ${SERVER_DOMAIN};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
proxy_pass ${SERVER_DOMAIN};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ export default defineNuxtConfig({
|
||||
devProxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8000/api",
|
||||
prependPath: true
|
||||
prependPath: true,
|
||||
changeOrigin: true,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
81
pages/account/onboarding.vue
Normal file
81
pages/account/onboarding.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'vuetify-app',
|
||||
middleware: ['auth']
|
||||
})
|
||||
const route = useRoute()
|
||||
const sending = ref(false)
|
||||
const resent = ref(false)
|
||||
const errorMsg = ref(null)
|
||||
const resendEmail = async () => {
|
||||
errorMsg.value = null
|
||||
sending.value = true
|
||||
const { data, error } = await useFetch('/api/account/registration/resend-email/', {
|
||||
method: 'POST',
|
||||
})
|
||||
if (error.value) {
|
||||
errorMsg.value = 'Something went wrong. Please try again later.'
|
||||
} else {
|
||||
resent.value = true
|
||||
}
|
||||
sending.value = false
|
||||
}
|
||||
|
||||
onNuxtReady(() => {
|
||||
if (route.query.resend) {
|
||||
resendEmail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
class="h-100vh"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
sm="9"
|
||||
offset-sm="1"
|
||||
md="8"
|
||||
offset-md="2"
|
||||
>
|
||||
<v-card
|
||||
class="mt-20vh"
|
||||
elevation="0"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h2 class="text-h4">Verify your email</h2>
|
||||
<p class="text-body-2 mt-5">
|
||||
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.
|
||||
</p>
|
||||
<p v-if="errorMsg"
|
||||
class="text-red"
|
||||
>{{ errorMsg }}</p>
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="mt-5"
|
||||
color="primary"
|
||||
:loading="sending"
|
||||
@click="resendEmail"
|
||||
:disabled="resent"
|
||||
>
|
||||
{{ resent ? 'Resent' : 'Resend email'}}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.h-100vh {
|
||||
height: 100vh;
|
||||
}
|
||||
.mt-20vh {
|
||||
margin-top: 20vh;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<v-card
|
||||
color="red-lighten-5"
|
||||
style="height: 100vh"
|
||||
>
|
||||
<v-container>
|
||||
@@ -8,13 +7,14 @@
|
||||
<v-col
|
||||
sm="9"
|
||||
offset-sm="1"
|
||||
md="8"
|
||||
offset-md="2"
|
||||
md="6"
|
||||
offset-md="3"
|
||||
>
|
||||
<v-card
|
||||
class="mt-15"
|
||||
elevation="0"
|
||||
>
|
||||
<v-card-title>Sign in</v-card-title>
|
||||
<div class="text-center text-h4">Sign in</div>
|
||||
<v-card-text>
|
||||
<v-form ref="signInForm">
|
||||
<v-text-field
|
||||
@@ -28,18 +28,30 @@
|
||||
:rules="formRules.password"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
@keyup.enter="submit"
|
||||
></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>
|
||||
|
||||
<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
|
||||
color="primary"
|
||||
:loading="submitting"
|
||||
@click="submit"
|
||||
size="large"
|
||||
>Submit</v-btn>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -70,17 +82,30 @@ const errorMsg = ref(null)
|
||||
const signInForm = ref(null)
|
||||
const valid = ref(true)
|
||||
const submitting = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
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()
|
||||
const { data, error } = await useFetch('/api/account/login/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData.value)
|
||||
})
|
||||
if (error.value) {
|
||||
if (error.value.status === 400) {
|
||||
if (error.value.data.non_field_errors) {
|
||||
errorMsg.value = error.value.data.non_field_errors[0]
|
||||
}
|
||||
} else {
|
||||
errorMsg.value = 'Something went wrong. Please try again.'
|
||||
}
|
||||
} else {
|
||||
$auth.setUser(data.value.user)
|
||||
navigateTo(route.query.callback || '/')
|
||||
}
|
||||
errorMsg.value = error
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
172
pages/account/signup.vue
Normal file
172
pages/account/signup.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'vuetify-app'
|
||||
})
|
||||
|
||||
const { $auth } = useNuxtApp()
|
||||
|
||||
const formData = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password1: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
const fieldErrors = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password1: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
const formRules = ref({
|
||||
username: [
|
||||
v => !!v || 'Please enter your username',
|
||||
v => v.length >= 4 || 'Username must be at least 4 characters'
|
||||
],
|
||||
email: [
|
||||
v => !!v || 'Please enter your e-mail address',
|
||||
v => /.+@.+\..+/.test(v) || 'E-mail address must be valid'
|
||||
],
|
||||
password1: [
|
||||
v => !!v || 'Please enter your password',
|
||||
v => v.length >= 8 || 'Password must be at least 8 characters'
|
||||
],
|
||||
password2: [
|
||||
v => !!v || 'Please confirm your password',
|
||||
v => v.length >= 8 || 'Password must be at least 8 characters',
|
||||
v => v === formData.value.password1 || 'Confirm password must match password'
|
||||
]
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const errorMsg = ref(null)
|
||||
const signUpForm = ref(null)
|
||||
|
||||
const submit = async () => {
|
||||
errorMsg.value = null
|
||||
const { valid } = await signUpForm.value.validate()
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
|
||||
const { data, error } = await useFetch('/api/account/registration/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData.value)
|
||||
})
|
||||
|
||||
console.log(error.value)
|
||||
|
||||
if (error.value) {
|
||||
if (error.value.status === 400) {
|
||||
for (const key in formData.value) {
|
||||
if (error.value.data[key]) {
|
||||
fieldErrors.value[key] = error.value.data[key][0]
|
||||
}
|
||||
}
|
||||
if (error.value.data.non_field_errors) {
|
||||
errorMsg.value = error.value.data.non_field_errors[0]
|
||||
}
|
||||
} else {
|
||||
errorMsg.value = 'Something went wrong. Please try again.'
|
||||
}
|
||||
} else {
|
||||
$auth.setUser(data.value.user)
|
||||
navigateTo('/account/onboarding')
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFieldUpdate = (field) => {
|
||||
// fieldErrors.value[field] = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
style="height: 100vh"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
sm="9"
|
||||
offset-sm="1"
|
||||
md="6"
|
||||
offset-md="3"
|
||||
>
|
||||
<v-card
|
||||
class="mt-15"
|
||||
elevation="0"
|
||||
>
|
||||
<div class="text-center text-h4">Create your account</div>
|
||||
<v-card-text>
|
||||
<v-form ref="signUpForm" class="mt-5">
|
||||
<v-text-field
|
||||
v-model="formData.username"
|
||||
:rules="formRules.username"
|
||||
:error-messages="fieldErrors.username"
|
||||
label="User name"
|
||||
variant="underlined"
|
||||
@update:modelValue="handleFieldUpdate('username')"
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.email"
|
||||
:rules="formRules.email"
|
||||
:error-messages="fieldErrors.email"
|
||||
label="Email"
|
||||
variant="underlined"
|
||||
@@update:modelValue="handleFieldUpdate('email')"
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.password1"
|
||||
:rules="formRules.password1"
|
||||
:error-messages="fieldErrors.password1"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
@update:modelValue="handleFieldUpdate('password1')"
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.password2"
|
||||
:rules="formRules.password2"
|
||||
:error-messages="fieldErrors.password2"
|
||||
label="Confirm password"
|
||||
variant="underlined"
|
||||
@update:modelValue="handleFieldUpdate('password2')"
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
</v-form>
|
||||
|
||||
<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
|
||||
size="large"
|
||||
color="primary"
|
||||
:loading="submitting"
|
||||
@click="submit"
|
||||
>Submit</v-btn>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
100
pages/account/verify-email.vue
Normal file
100
pages/account/verify-email.vue
Normal 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>
|
||||
@@ -18,16 +18,14 @@ const abortFetch = () => {
|
||||
fetchingResponse.value = false
|
||||
}
|
||||
const fetchReply = async (message, parentMessageId) => {
|
||||
const token = await $auth.retrieveToken()
|
||||
ctrl = new AbortController()
|
||||
try {
|
||||
await fetchEventSource('/api/conversation', {
|
||||
await fetchEventSource('/api/conversation/', {
|
||||
signal: ctrl.signal,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: currentModel.value,
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
|
||||
const AUTH_ROUTE = {
|
||||
home: '/',
|
||||
login: '/login'
|
||||
}
|
||||
|
||||
const COOKIE_OPTIONS = {
|
||||
prefix: '_Secure-auth',
|
||||
path: '/',
|
||||
tokenName: 'access-token',
|
||||
refreshTokenName: 'refresh-token',
|
||||
login: '/account/signin',
|
||||
}
|
||||
|
||||
const ENDPOINTS = {
|
||||
login: {
|
||||
url: '/api/auth/signin'
|
||||
},
|
||||
refresh: {
|
||||
url: '/api/auth/token/refresh'
|
||||
url: '/api/account/login/'
|
||||
},
|
||||
user: {
|
||||
url: '/api/auth/session'
|
||||
url: '/api/account/user/'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
@@ -42,73 +22,32 @@ export default defineNuxtPlugin(() => {
|
||||
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 () {
|
||||
this.loginIn.value = false
|
||||
this.user.value = null
|
||||
await this.redirectToLogin()
|
||||
}
|
||||
|
||||
setUser (user) {
|
||||
this.user = user
|
||||
this.loginIn.value = true
|
||||
}
|
||||
|
||||
async fetchUser () {
|
||||
const { data, error } = await useAuthFetch(ENDPOINTS.user.url)
|
||||
const { data, error } = await useFetch(ENDPOINTS.user.url, {
|
||||
// withCredentials: true
|
||||
})
|
||||
if (!error.value) {
|
||||
this.user = data.value
|
||||
this.loginIn.value = true
|
||||
this.setUser(data.value)
|
||||
return null
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
async refresh () {
|
||||
const { data, error } = await useFetch(ENDPOINTS.refresh.url, {
|
||||
method: 'POST',
|
||||
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
|
||||
async redirectToLogin (callback) {
|
||||
return await navigateTo(
|
||||
AUTH_ROUTE.login + '?callback=' + encodeURIComponent(callback || AUTH_ROUTE.home)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -117,13 +56,9 @@ export default defineNuxtPlugin(() => {
|
||||
|
||||
addRouteMiddleware('auth', async (to, from) => {
|
||||
if (!auth.loginIn.value) {
|
||||
const token = await auth.retrieveToken()
|
||||
if (!token) {
|
||||
return await auth.redirectToLogin()
|
||||
}
|
||||
const error = await auth.fetchUser()
|
||||
if (error) {
|
||||
return await auth.redirectToLogin()
|
||||
return await auth.redirectToLogin(to.fullPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getDefaultConversionData = () => {
|
||||
}
|
||||
|
||||
export const getConversions = async () => {
|
||||
const { data, error } = await useAuthFetch('/api/chat/conversations')
|
||||
const { data, error } = await useAuthFetch('/api/chat/conversations/')
|
||||
if (!error.value) {
|
||||
return data.value
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const openConversationMessages = async (currentConversation) => {
|
||||
}
|
||||
|
||||
export const genTitle = async (conversationId) => {
|
||||
const { data, error } = await useAuthFetch('/api/gen_title', {
|
||||
const { data, error } = await useAuthFetch('/api/gen_title/', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
conversationId: conversationId
|
||||
|
||||
Reference in New Issue
Block a user