Compare commits

..

17 Commits

Author SHA1 Message Date
Rafi
002db29717 Improved the style of the chat window, using message bubbles. 2023-03-04 20:25:52 +08:00
Rafi
6402f156dd Add the try_files directive to the nginx configuration 2023-03-04 14:45:29 +08:00
Rafi
a44ec5e2fb update readme 2023-03-04 00:26:16 +08:00
Rafi
32f3013337 update readme 2023-03-04 00:24:22 +08:00
Wong Saang
e66d994219 Merge pull request #12 from WongSaang/rest-auth
Support for the official ChatGPT model: gpt-3.5-turbo
2023-03-04 00:16:59 +08:00
Rafi
f166581a73 email verification 2023-03-03 18:50:02 +08:00
Rafi
ef6657187a account 2023-03-03 00:10:50 +08:00
Rafi
3b6c48a776 account 2023-03-02 23:10:56 +08:00
Rafi
b316ac0b4a update README.md 2023-03-01 16:37:56 +08:00
Rafi
51e8ea8d1a modify README.md 2023-03-01 09:17:34 +08:00
Rafi
60cd0689fb modify README.md 2023-02-28 23:02:30 +08:00
Rafi
74fc850ceb modify README.md 2023-02-28 22:53:50 +08:00
Rafi
339dd1e0c6 update nginx.conf 2023-02-28 16:43:02 +08:00
Rafi
122704737a update readme 2023-02-28 15:15:12 +08:00
Rafi
bd35c21e2f Delete the proxy of the admin path 2023-02-28 14:38:31 +08:00
Rafi
c2705e5f2a Update readme 2023-02-28 14:26:29 +08:00
Rafi
0e5aeddffa Update readme 2023-02-28 14:04:57 +08:00
16 changed files with 522 additions and 212 deletions

View File

@@ -1,17 +1,22 @@
<p align="center"> <p align="center">
<img alt="demo" src="./demos/demo.gif?v=1"> <img alt="demo" src="./demos/demo.png?v=1">
</p> </p>
# 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:

View File

@@ -20,6 +20,7 @@ const contentHtml = computed(() => {
<template> <template>
<div <div
v-html="contentHtml" v-html="contentHtml"
class="text-body-1 text-justify"
></div> ></div>
</template> </template>

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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

BIN
demos/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -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:

View File

@@ -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:

View File

@@ -6,18 +6,16 @@ server {
location / { location / {
root /app; root /app;
index index.html; index index.html;
try_files $uri $uri/ /index.html;
} }
location /api/ location /api/
{ {
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;
} }
} }

View File

@@ -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,
} }
} }

View 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>

View File

@@ -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
View 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>

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

@@ -3,6 +3,7 @@ definePageMeta({
middleware: ["auth"] middleware: ["auth"]
}) })
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source' import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
import { nextTick } from 'vue'
const { $i18n, $auth } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -18,16 +19,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,
@@ -51,7 +50,7 @@ const fetchReply = async (message, parentMessageId) => {
onerror(err) { onerror(err) {
throw err; throw err;
}, },
onmessage(message) { async onmessage(message) {
// console.log(message) // console.log(message)
const event = message.event const event = message.event
const data = JSON.parse(message.data) const data = JSON.parse(message.data)
@@ -121,6 +120,7 @@ const showSnackbar = (text) => {
snackbar.value = true snackbar.value = true
} }
</script> </script>
<template> <template>
@@ -128,21 +128,41 @@ const showSnackbar = (text) => {
v-if="currentConversation.messages.length > 0" v-if="currentConversation.messages.length > 0"
ref="chatWindow" ref="chatWindow"
> >
<v-card <v-container>
rounded="0" <v-row>
elevation="0" <v-col
v-for="(conversation, index) in currentConversation.messages" v-for="(message, index) in currentConversation.messages" :key="index"
:key="index" cols="12"
:variant="conversation.is_bot ? 'tonal' : 'text'" >
> <div
<v-container> class="d-flex"
<v-card-text class="text-caption text-disabled">{{ $t(`roles.${conversation.is_bot?'ai':'me'}`) }}</v-card-text> :class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'"
<v-card-text> >
<MsgContent :content="conversation.message" /> <v-card
</v-card-text> :color="message.is_bot ? '' : 'primary'"
</v-container> rounded="lg"
<v-divider></v-divider> elevation="2"
</v-card> >
<v-card-text>
<MsgContent :content="message.message" />
</v-card-text>
<!-- <v-card-actions-->
<!-- v-if="message.is_bot"-->
<!-- >-->
<!-- <v-spacer></v-spacer>-->
<!-- <v-tooltip text="Copy">-->
<!-- <template v-slot:activator="{ props }">-->
<!-- <v-btn v-bind="props" icon="content_copy"></v-btn>-->
<!-- </template>-->
<!-- </v-tooltip>-->
<!-- </v-card-actions>-->
</v-card>
</div>
</v-col>
</v-row>
</v-container>
<div ref="grab" class="w-100" style="height: 200px;"></div> <div ref="grab" class="w-100" style="height: 200px;"></div>
</div> </div>
<Welcome v-else /> <Welcome v-else />

View File

@@ -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)
} }
} }
}) })

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