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 01/11] 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 ( -
-