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
|
# ChatGPT UI
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
A web client for ChatGPT, using OpenAI's API.
|
A web client for ChatGPT, using OpenAI's API.
|
||||||
|
|
||||||
## 📢Updates
|
## 📢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).
|
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).
|
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).
|
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
||||||
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## Quick start with Docker Compose
|
## Quick start with Docker Compose
|
||||||
|
|
||||||
---
|
|
||||||
### Run services
|
### Run services
|
||||||
|
|
||||||
Below is a docker-compose.yml template:
|
Below is a docker-compose.yml template:
|
||||||
@@ -37,48 +43,53 @@ services:
|
|||||||
client:
|
client:
|
||||||
image: wongsaang/chatgpt-ui-client:latest
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend:8000
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend-web-server
|
||||||
volumes:
|
|
||||||
- backend_static:/app/static
|
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '80:80'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
backend:
|
backend-wsgi-server:
|
||||||
image: wongsaang/chatgpt-ui-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
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.
|
# - 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_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
|
||||||
volumes:
|
|
||||||
- backend_static:/app/static
|
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- 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:
|
networks:
|
||||||
chatgpt_ui_network:
|
chatgpt_ui_network:
|
||||||
driver: bridge
|
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
|
## Development
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
Make sure to install the dependencies:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
client:
|
client:
|
||||||
build:
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
context: .
|
|
||||||
dockerfile: ./Dockerfile
|
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend:8000
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend-web-server
|
||||||
volumes:
|
|
||||||
- backend_static:/app/static
|
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '80:80'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
backend:
|
backend-wsgi-server:
|
||||||
image: 'wongsaang/chatgpt-ui-server:latest'
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
volumes:
|
environment:
|
||||||
- backend_static:/app/static
|
# - 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:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- 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:
|
networks:
|
||||||
chatgpt_ui_network:
|
chatgpt_ui_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
|
||||||
backend_static:
|
|
||||||
@@ -12,12 +12,8 @@ server {
|
|||||||
{
|
{
|
||||||
proxy_pass ${SERVER_DOMAIN};
|
proxy_pass ${SERVER_DOMAIN};
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
location /admin/ {
|
|
||||||
proxy_pass ${SERVER_DOMAIN};
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export default defineNuxtConfig({
|
|||||||
devProxy: {
|
devProxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8000/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>
|
<template>
|
||||||
<v-card
|
<v-card
|
||||||
color="red-lighten-5"
|
|
||||||
style="height: 100vh"
|
style="height: 100vh"
|
||||||
>
|
>
|
||||||
<v-container>
|
<v-container>
|
||||||
@@ -8,13 +7,14 @@
|
|||||||
<v-col
|
<v-col
|
||||||
sm="9"
|
sm="9"
|
||||||
offset-sm="1"
|
offset-sm="1"
|
||||||
md="8"
|
md="6"
|
||||||
offset-md="2"
|
offset-md="3"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
class="mt-15"
|
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-card-text>
|
||||||
<v-form ref="signInForm">
|
<v-form ref="signInForm">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -28,18 +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>
|
||||||
<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-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-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -70,17 +82,30 @@ 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()
|
||||||
if (valid) {
|
if (valid) {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
const error = await $auth.login(formData.value.username, formData.value.password)
|
const { data, error } = await useFetch('/api/account/login/', {
|
||||||
submitting.value = false
|
method: 'POST',
|
||||||
if (!error) {
|
body: JSON.stringify(formData.value)
|
||||||
return await $auth.callback()
|
})
|
||||||
|
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
|
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,
|
||||||
|
|||||||
@@ -1,40 +1,20 @@
|
|||||||
|
|
||||||
const AUTH_ROUTE = {
|
const AUTH_ROUTE = {
|
||||||
home: '/',
|
home: '/',
|
||||||
login: '/login'
|
login: '/account/signin',
|
||||||
}
|
|
||||||
|
|
||||||
const COOKIE_OPTIONS = {
|
|
||||||
prefix: '_Secure-auth',
|
|
||||||
path: '/',
|
|
||||||
tokenName: 'access-token',
|
|
||||||
refreshTokenName: 'refresh-token',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENDPOINTS = {
|
const ENDPOINTS = {
|
||||||
login: {
|
login: {
|
||||||
url: '/api/auth/signin'
|
url: '/api/account/login/'
|
||||||
},
|
|
||||||
refresh: {
|
|
||||||
url: '/api/auth/token/refresh'
|
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
url: '/api/auth/session'
|
url: '/api/account/user/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,73 +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 useAuthFetch(ENDPOINTS.user.url)
|
const { data, error } = await useFetch(ENDPOINTS.user.url, {
|
||||||
|
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -117,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user