From 62d88380e078fafe9d1e428ad9df09036604a77e Mon Sep 17 00:00:00 2001
From: Wentao Lyu <35-wentao.lyu@users.noreply.git.stereye.tech>
Date: Tue, 14 Mar 2023 01:24:43 +0800
Subject: [PATCH] feat: add sample multi-user support feat: update README
---
README.md | 31 +++++++++++
api/.env.example | 7 +++
api/models/Conversation.js | 33 +++++++-----
api/package.json | 2 +
api/server/index.js | 35 ++++++++++---
api/server/routes/ask.js | 2 +-
api/server/routes/askBing.js | 3 +-
api/server/routes/askSydney.js | 3 +-
api/server/routes/auth.js | 46 +++++++++++++++++
api/server/routes/authYourLogin.js | 40 ++++++++++++++
api/server/routes/convos.js | 34 ++++++++++--
api/server/routes/index.js | 3 +-
client/src/App.jsx | 69 ++++++++++++++++++-------
client/src/components/Main/TextChat.jsx | 1 +
client/src/components/Nav/Logout.jsx | 23 +++++++++
client/src/components/Nav/NavLinks.jsx | 6 ++-
client/src/store/index.js | 2 +
client/src/store/userReducer.js | 19 +++++++
client/src/utils/fetchers.js | 4 +-
19 files changed, 314 insertions(+), 49 deletions(-)
create mode 100644 api/server/routes/auth.js
create mode 100644 api/server/routes/authYourLogin.js
create mode 100644 client/src/components/Nav/Logout.jsx
create mode 100644 client/src/store/userReducer.js
diff --git a/README.md b/README.md
index a20f9b970f..bde33b02b6 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,7 @@ Currently, this project is only functional with the `text-davinci-003` model.
- [Docker](#docker)
- [Access Tokens](#access-tokens)
- [Proxy](#proxy)
+ - [User System](#user-system)
- [Updating](#updating)
- [Use Cases](#use-cases)
- [Origin](#origin)
@@ -235,6 +236,36 @@ set in docker-compose.yml file, under services - api - environment
+### User System
+
+By default, there is no user system enabled, so anyone can access your server.
+
+**This project is not designed to provide a complete and full-featured user system.** It's not high priority task and might never be provided.
+
+[wtlyu](https://github.com/wtlyu) provide a sample user system structure, that you can implement your own user system. It's simple and not a ready-for-use edition.
+
+(If you want to implement your user system, open this ↓)
+
+
+Implement your own user system
+
+To enable the user system, set `ENABLE_USER_SYSTEM=1` in your `.env` file.
+
+The sample structure is simple. It provide three basic endpoint:
+
+1. `/auth/login` will redirect to your own login url. In the sample code, it's `/auth/your_login_page`.
+2. `/auth/logout` will redirect to your own logout url. In the sample code, it's `/auth/your_login_page/logout`.
+3. `/api/me` will return the userinfo: `{ username, display }`.
+ 1. `username` will be used in db, used to distinguish between users.
+ 2. `display` will be displayed in UI.
+
+The only one thing that drive user system work is `req.session.user`. Once it's set, the client will be trusted. Set to `null` if logout.
+
+Please refer to `/api/server/routes/authYoutLogin.js` file. It's very clear and simple to tell you how to implement your user system.
+
+
+
+
### Updating
- As the project is still a work-in-progress, you should pull the latest and run the steps over. Reset your browser cache/clear site data.
diff --git a/api/.env.example b/api/.env.example
index e8c52c3b01..adf7588083 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -20,3 +20,10 @@ MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
OPENAI_KEY=
CHATGPT_TOKEN=
BING_TOKEN=
+
+# User System
+
+# global enable/disable the sample user system.
+# this is not a ready to use user system.
+# dont't use it, unless you can write your own code.
+ENABLE_USER_SYSTEM=
diff --git a/api/models/Conversation.js b/api/models/Conversation.js
index 45834379e8..fbed894712 100644
--- a/api/models/Conversation.js
+++ b/api/models/Conversation.js
@@ -43,6 +43,9 @@ const convoSchema = mongoose.Schema(
type: String,
required: true
},
+ user: {
+ type: String
+ },
suggestions: [{ type: String }],
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
},
@@ -52,9 +55,9 @@ const convoSchema = mongoose.Schema(
const Conversation =
mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
-const getConvo = async (conversationId) => {
+const getConvo = async (user, conversationId) => {
try {
- return await Conversation.findOne({ conversationId }).exec();
+ return await Conversation.findOne({ user, conversationId }).exec();
} catch (error) {
console.log(error);
return { message: 'Error getting single conversation' };
@@ -62,12 +65,13 @@ const getConvo = async (conversationId) => {
};
module.exports = {
- saveConvo: async ({ conversationId, newConversationId, title, ...convo }) => {
+ saveConvo: async (user, { conversationId, newConversationId, title, ...convo }) => {
try {
const messages = await getMessages({ conversationId });
const update = { ...convo, messages };
if (title) {
update.title = title;
+ update.user = user
}
if (newConversationId) {
update.conversationId = newConversationId;
@@ -81,8 +85,10 @@ module.exports = {
update.promptPrefix = null;
}
+ console.error(user)
+
return await Conversation.findOneAndUpdate(
- { conversationId },
+ { conversationId: conversationId, user: user },
{ $set: update },
{ new: true, upsert: true }
).exec();
@@ -91,9 +97,9 @@ module.exports = {
return { message: 'Error saving conversation' };
}
},
- updateConvo: async ({ conversationId, ...update }) => {
+ updateConvo: async (user, { conversationId, ...update }) => {
try {
- return await Conversation.findOneAndUpdate({ conversationId }, update, {
+ return await Conversation.findOneAndUpdate({ conversationId: conversationId, user: user }, update, {
new: true
}).exec();
} catch (error) {
@@ -101,12 +107,11 @@ module.exports = {
return { message: 'Error updating conversation' };
}
},
- // getConvos: async () => await Conversation.find({}).sort({ createdAt: -1 }).exec(),
- getConvosByPage: async (pageNumber = 1, pageSize = 12) => {
+ getConvosByPage: async (user, pageNumber = 1, pageSize = 12) => {
try {
- const totalConvos = (await Conversation.countDocuments()) || 1;
+ const totalConvos = (await Conversation.countDocuments({ user: user })) || 1;
const totalPages = Math.ceil(totalConvos / pageSize);
- const convos = await Conversation.find()
+ const convos = await Conversation.find({ user: user })
.sort({ createdAt: -1, created: -1 })
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
@@ -119,17 +124,17 @@ module.exports = {
}
},
getConvo,
- getConvoTitle: async (conversationId) => {
+ getConvoTitle: async (user, conversationId) => {
try {
- const convo = await getConvo(conversationId);
+ const convo = await getConvo(user, conversationId);
return convo.title;
} catch (error) {
console.log(error);
return { message: 'Error getting conversation title' };
}
},
- deleteConvos: async (filter) => {
- let deleteCount = await Conversation.deleteMany(filter).exec();
+ deleteConvos: async (user, filter) => {
+ let deleteCount = await Conversation.deleteMany({...filter, user: user}).exec();
deleteCount.messages = await deleteMessages(filter);
return deleteCount;
},
diff --git a/api/package.json b/api/package.json
index 561908fad3..8311456f41 100644
--- a/api/package.json
+++ b/api/package.json
@@ -22,9 +22,11 @@
"@keyv/mongo": "^2.1.8",
"@vscode/vscode-languagedetection": "^1.0.22",
"@waylaidwanderer/chatgpt-api": "^1.28.2",
+ "axios": "^1.3.4",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
+ "express-session": "^1.17.3",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"lodash": "^4.17.21",
diff --git a/api/server/index.js b/api/server/index.js
index bda88b14f4..102811b60e 100644
--- a/api/server/index.js
+++ b/api/server/index.js
@@ -1,4 +1,5 @@
const express = require('express');
+const session = require('express-session')
const dbConnect = require('../models/dbConnect');
const { migrateDb } = require('../models');
const path = require('path');
@@ -7,6 +8,7 @@ const routes = require('./routes');
const app = express();
const port = process.env.PORT || 3080;
const host = process.env.HOST || 'localhost'
+const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false
const projectPath = path.join(__dirname, '..', '..', 'client');
dbConnect().then(() => {
console.log('Connected to MongoDB');
@@ -16,17 +18,38 @@ dbConnect().then(() => {
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(projectPath, 'public')));
+app.set('trust proxy', 1) // trust first proxy
+app.use(session({
+ secret: 'chatgpt-clone-random-secrect',
+ resave: false,
+ saveUninitialized: true,
+}))
-app.get('/', function (req, res) {
+app.get('/', routes.authenticatedOrRedirect, function (req, res) {
console.log(path.join(projectPath, 'public', 'index.html'));
res.sendFile(path.join(projectPath, 'public', 'index.html'));
});
-app.use('/api/ask', routes.ask);
-app.use('/api/messages', routes.messages);
-app.use('/api/convos', routes.convos);
-app.use('/api/customGpts', routes.customGpts);
-app.use('/api/prompts', routes.prompts);
+app.get('/api/me', function (req, res) {
+ if (userSystemEnabled) {
+ const user = req?.session?.user
+
+ if (user)
+ res.send(JSON.stringify({username: user?.username, display: user?.display}));
+ else
+ res.send(JSON.stringify(null));
+ } else {
+ res.send(JSON.stringify({username: 'anonymous_user', display: 'Anonymous User'}));
+ }
+});
+
+app.use('/api/ask', routes.authenticatedOr401, routes.ask);
+app.use('/api/messages', routes.authenticatedOr401, routes.messages);
+app.use('/api/convos', routes.authenticatedOr401, routes.convos);
+app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
+app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
+app.use('/auth', routes.auth);
+
app.listen(port, host, () => {
if (host=='0.0.0.0')
diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js
index ad2a7178c2..f23d5c7dcc 100644
--- a/api/server/routes/ask.js
+++ b/api/server/routes/ask.js
@@ -176,7 +176,7 @@ const ask = async ({
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
await saveMessage(gptResponse);
- await saveConvo(gptResponse);
+ await saveConvo(req?.session?.user?.username, gptResponse);
sendMessage(res, {
title: await getConvoTitle(conversationId),
final: true,
diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js
index 981cd56177..dcf9138374 100644
--- a/api/server/routes/askBing.js
+++ b/api/server/routes/askBing.js
@@ -129,7 +129,8 @@ const ask = async ({
response.text = await handleText(response, true);
await saveMessage(response);
- await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
+ await saveConvo(req?.session?.user?.username, { ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
+
sendMessage(res, {
title: await getConvoTitle(conversationId),
final: true,
diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js
index 00f287fa7b..c2cf02f32e 100644
--- a/api/server/routes/askSydney.js
+++ b/api/server/routes/askSydney.js
@@ -141,7 +141,8 @@ const ask = async ({
response.text = await handleText(response, true);
// Save sydney response & convo, then send
await saveMessage(response);
- await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
+ await saveConvo(req?.session?.user?.username, { ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
+
sendMessage(res, {
title: await getConvoTitle(conversationId),
final: true,
diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js
new file mode 100644
index 0000000000..b4d986c48c
--- /dev/null
+++ b/api/server/routes/auth.js
@@ -0,0 +1,46 @@
+const express = require('express');
+const router = express.Router();
+const authYourLogin = require('./authYourLogin');
+const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false
+
+router.get('/login', function (req, res) {
+ if (userSystemEnabled)
+ res.redirect('/auth/your_login_page')
+ else
+ res.redirect('/')
+})
+
+router.get('/logout', function (req, res) {
+ // clear the session
+ req.session.user = null
+
+ req.session.save(function (error) {
+ if (userSystemEnabled)
+ res.redirect('/auth/your_login_page/logout')
+ else
+ res.redirect('/')
+ })
+})
+
+const authenticatedOr401 = (req, res, next) => {
+ if (userSystemEnabled) {
+ const user = req?.session?.user;
+
+ if (user) next();
+ else res.status(401).end();
+ } else next();
+}
+
+const authenticatedOrRedirect = (req, res, next) => {
+ if (userSystemEnabled) {
+ const user = req?.session?.user;
+
+ if (user) next();
+ else res.redirect('/auth/login').end();
+ } else next();
+}
+
+if (userSystemEnabled)
+ router.use('/your_login_page', authYourLogin);
+
+module.exports = { router, authenticatedOr401, authenticatedOrRedirect };
diff --git a/api/server/routes/authYourLogin.js b/api/server/routes/authYourLogin.js
new file mode 100644
index 0000000000..c2320fbb3b
--- /dev/null
+++ b/api/server/routes/authYourLogin.js
@@ -0,0 +1,40 @@
+const express = require('express');
+const router = express.Router();
+
+// WARNING!
+// THIS IS NOT A READY TO USE USER SYSTEM
+// PLEASE IMPLEMENT YOUR OWN USER SYSTEM
+
+const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false
+
+// Logout
+router.get('/logout', (req, res) => {
+ // Do anything you want
+ console.warn('logout not implemented!')
+
+ // finish
+ res.redirect('/')
+});
+
+// Login
+router.get('/', async (req, res) => {
+ // Do anything you want
+ console.warn('login not implemented! Automatic passed as sample user')
+
+ // save the user info into session
+ // username will be used in db
+ // display will be used in UI
+ req.session.user = {
+ username: 'sample_user',
+ display: 'Sample User',
+ }
+
+ req.session.save(function (error) {
+ if (error) {
+ console.log(error);
+ res.send(`
Login Failed. An error occurred. Please see the server logs for details.
`);
+ } else res.redirect('/')
+ })
+});
+
+module.exports = router;
diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js
index 99fb824708..11dbb7a545 100644
--- a/api/server/routes/convos.js
+++ b/api/server/routes/convos.js
@@ -1,10 +1,38 @@
const express = require('express');
const router = express.Router();
+const { titleConvo } = require('../../app/');
+const { getConvo, saveConvo, getConvoTitle } = require('../../models');
const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation');
+const { getMessages } = require('../../models/Message');
router.get('/', async (req, res) => {
const pageNumber = req.query.pageNumber || 1;
- res.status(200).send(await getConvosByPage(pageNumber));
+ res.status(200).send(await getConvosByPage(req?.session?.user?.username, pageNumber));
+});
+
+router.post('/gen_title', async (req, res) => {
+ const { conversationId } = req.body.arg;
+
+ const convo = await getConvo(req?.session?.user?.username, conversationId)
+ const firstMessage = (await getMessages({ conversationId }))[0]
+ const secondMessage = (await getMessages({ conversationId }))[1]
+
+ const title = convo.jailbreakConversationId
+ ? await getConvoTitle(conversationId)
+ : await titleConvo({
+ model: convo?.model,
+ message: firstMessage?.text,
+ response: JSON.stringify(secondMessage?.text || '')
+ });
+
+ await saveConvo(req?.session?.user?.username,
+ {
+ conversationId,
+ title
+ }
+ )
+
+ res.status(200).send(title);
});
router.post('/clear', async (req, res) => {
@@ -15,7 +43,7 @@ router.post('/clear', async (req, res) => {
}
try {
- const dbResponse = await deleteConvos(filter);
+ const dbResponse = await deleteConvos(req?.session?.user?.username, filter);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
@@ -27,7 +55,7 @@ router.post('/update', async (req, res) => {
const update = req.body.arg;
try {
- const dbResponse = await updateConvo(update);
+ const dbResponse = await updateConvo(req?.session?.user?.username, update);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
diff --git a/api/server/routes/index.js b/api/server/routes/index.js
index 235d08da2f..5164badc3f 100644
--- a/api/server/routes/index.js
+++ b/api/server/routes/index.js
@@ -3,5 +3,6 @@ const messages = require('./messages');
const convos = require('./convos');
const customGpts = require('./customGpts');
const prompts = require('./prompts');
+const { router: auth, authenticatedOr401, authenticatedOrRedirect } = require('./auth');
-module.exports = { ask, messages, convos, customGpts, prompts };
\ No newline at end of file
+module.exports = { ask, messages, convos, customGpts, prompts, auth, authenticatedOr401, authenticatedOrRedirect };
\ No newline at end of file
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 5f1446fd0b..0421d14d93 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -5,34 +5,67 @@ import TextChat from './components/Main/TextChat';
import Nav from './components/Nav';
import MobileNav from './components/Nav/MobileNav';
import useDocumentTitle from '~/hooks/useDocumentTitle';
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
+import { setUser } from './store/userReducer';
+import axios from 'axios'
const App = () => {
+ const dispatch = useDispatch();
+
const { messages, messageTree } = useSelector((state) => state.messages);
+ const { user } = useSelector((state) => state.user);
const { title } = useSelector((state) => state.convo);
const { conversationId } = useSelector((state) => state.convo);
const [ navVisible, setNavVisible ]= useState(false)
useDocumentTitle(title);
- return (
-
-
-
-
-
- {messages.length === 0 ? (
-
- ) : (
-
- )}
-
+ useEffect(() => {
+ axios.get('/api/me', {
+ timeout: 1000,
+ withCredentials: true
+ }).then((res) => {
+ return res.data
+ }).then((user) => {
+ if (user)
+ dispatch(setUser(user))
+ else {
+ console.log('Not login!')
+ window.location.href = "/auth/login";
+ }
+ }).catch((error) => {
+ console.error(error)
+ console.log('Not login!')
+ window.location.href = "/auth/login";
+ })
+ // setUser
+ }, [])
+
+ if (user)
+ return (
+
+
+
+
+
+ {messages.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
-
- );
+ );
+ else
+ return (
+
+
+
+ )
};
export default App;
diff --git a/client/src/components/Main/TextChat.jsx b/client/src/components/Main/TextChat.jsx
index 0de24e7f52..65eae454ce 100644
--- a/client/src/components/Main/TextChat.jsx
+++ b/client/src/components/Main/TextChat.jsx
@@ -18,6 +18,7 @@ export default function TextChat({ messages }) {
const inputRef = useRef(null)
const isComposing = useRef(false);
const dispatch = useDispatch();
+ const { user } = useSelector((state) => state.user);
const convo = useSelector((state) => state.convo);
const { initial } = useSelector((state) => state.models);
const { isSubmitting, stopStream, submission, disabled, model, chatGptLabel, promptPrefix } =
diff --git a/client/src/components/Nav/Logout.jsx b/client/src/components/Nav/Logout.jsx
new file mode 100644
index 0000000000..637c6c57fe
--- /dev/null
+++ b/client/src/components/Nav/Logout.jsx
@@ -0,0 +1,23 @@
+import React, { useState, useContext } from 'react';
+import { useSelector } from 'react-redux';
+import LogOutIcon from '../svg/LogOutIcon';
+
+
+export default function Logout() {
+ const { user } = useSelector((state) => state.user);
+
+ const clickHandler = () => {
+ window.location.href = "/auth/logout";
+ };
+
+ return (
+
+
+ {user?.display || user?.username || 'USER'}
+ Log out
+
+ );
+}
diff --git a/client/src/components/Nav/NavLinks.jsx b/client/src/components/Nav/NavLinks.jsx
index 892471f6df..9c42b5bb39 100644
--- a/client/src/components/Nav/NavLinks.jsx
+++ b/client/src/components/Nav/NavLinks.jsx
@@ -3,16 +3,18 @@ import NavLink from './NavLink';
import LogOutIcon from '../svg/LogOutIcon';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
+import Logout from './Logout';
export default function NavLinks() {
return (
<>
-
+ {/*
+ /> */}
>
);
}
diff --git a/client/src/store/index.js b/client/src/store/index.js
index d2de0d9de1..22f717892d 100644
--- a/client/src/store/index.js
+++ b/client/src/store/index.js
@@ -5,6 +5,7 @@ import messageReducer from './messageSlice.js'
import modelReducer from './modelSlice.js'
import submitReducer from './submitSlice.js'
import textReducer from './textSlice.js'
+import userReducer from './userReducer.js'
export const store = configureStore({
reducer: {
@@ -13,6 +14,7 @@ export const store = configureStore({
models: modelReducer,
text: textReducer,
submit: submitReducer,
+ user: userReducer,
},
devTools: true,
});
\ No newline at end of file
diff --git a/client/src/store/userReducer.js b/client/src/store/userReducer.js
new file mode 100644
index 0000000000..bf0591333b
--- /dev/null
+++ b/client/src/store/userReducer.js
@@ -0,0 +1,19 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+ user: null,
+};
+
+const currentSlice = createSlice({
+ name: 'user',
+ initialState,
+ reducers: {
+ setUser: (state, action) => {
+ state.user = action.payload;
+ },
+ }
+});
+
+export const { setUser } = currentSlice.actions;
+
+export default currentSlice.reducer;
diff --git a/client/src/utils/fetchers.js b/client/src/utils/fetchers.js
index d9cfa54116..3d9a83413e 100644
--- a/client/src/utils/fetchers.js
+++ b/client/src/utils/fetchers.js
@@ -3,10 +3,10 @@ import axios from 'axios';
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
-const fetcher = (url) => fetch(url).then((res) => res.json());
+const fetcher = (url) => fetch(url, {credentials: 'include'}).then((res) => res.json());
const postRequest = async (url, { arg }) => {
- return await axios.post(url, { arg });
+ return await axios.post(url, { withCredentials: true, arg });
};
export const swr = (path, successCallback, options) => {