diff --git a/README.md b/README.md index ee35ad5ed..787aa6966 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,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 e8c52c3b0..adf758808 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 45834379e..7506fafe4 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; @@ -82,7 +86,7 @@ module.exports = { } return await Conversation.findOneAndUpdate( - { conversationId }, + { conversationId: conversationId, user }, { $set: update }, { new: true, upsert: true } ).exec(); @@ -91,9 +95,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 }, update, { new: true }).exec(); } catch (error) { @@ -101,12 +105,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 })) || 1; const totalPages = Math.ceil(totalConvos / pageSize); - const convos = await Conversation.find() + const convos = await Conversation.find({ user }) .sort({ createdAt: -1, created: -1 }) .skip((pageNumber - 1) * pageSize) .limit(pageSize) @@ -119,17 +122,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}).exec(); deleteCount.messages = await deleteMessages(filter); return deleteCount; }, diff --git a/api/models/CustomGpt.js b/api/models/CustomGpt.js index 1f02bdc4d..af3e09227 100644 --- a/api/models/CustomGpt.js +++ b/api/models/CustomGpt.js @@ -12,16 +12,20 @@ const customGptSchema = mongoose.Schema({ type: String, required: true }, + user: { + type: String + }, }, { timestamps: true }); const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema); -const createCustomGpt = async ({ chatGptLabel, promptPrefix, value }) => { +const createCustomGpt = async ({ chatGptLabel, promptPrefix, value, user }) => { try { await CustomGpt.create({ chatGptLabel, promptPrefix, - value + value, + user }); return { chatGptLabel, promptPrefix, value }; } catch (error) { @@ -31,22 +35,22 @@ const createCustomGpt = async ({ chatGptLabel, promptPrefix, value }) => { }; module.exports = { - getCustomGpts: async (filter) => { + getCustomGpts: async (user, filter) => { try { - return await CustomGpt.find(filter).exec(); + return await CustomGpt.find({ ...filter, user }).exec(); } catch (error) { console.error(error); return { customGpt: 'Error getting customGpts' }; } }, - updateCustomGpt: async ({ value, ...update }) => { + updateCustomGpt: async (user, { value, ...update }) => { try { - const customGpt = await CustomGpt.findOne({ value }).exec(); + const customGpt = await CustomGpt.findOne({ value, user }).exec(); if (!customGpt) { - return await createCustomGpt({ value, ...update }); + return await createCustomGpt({ value, ...update, user }); } else { - return await CustomGpt.findOneAndUpdate({ value }, update, { + return await CustomGpt.findOneAndUpdate({ value, user }, update, { new: true, upsert: true }).exec(); @@ -56,9 +60,9 @@ module.exports = { return { message: 'Error updating customGpt' }; } }, - updateByLabel: async ({ prevLabel, ...update }) => { + updateByLabel: async (user, { prevLabel, ...update }) => { try { - return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel }, update, { + return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel, user }, update, { new: true, upsert: true }).exec(); @@ -67,9 +71,9 @@ module.exports = { return { message: 'Error updating customGpt' }; } }, - deleteCustomGpts: async (filter) => { + deleteCustomGpts: async (user, filter) => { try { - return await CustomGpt.deleteMany(filter).exec(); + return await CustomGpt.deleteMany({ ...filter, user }).exec(); } catch (error) { console.error(error); return { customGpt: 'Error deleting customGpts' }; diff --git a/api/package-lock.json b/api/package-lock.json index f934f00b8..5432865ca 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -12,9 +12,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", @@ -1776,11 +1778,13 @@ } }, "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "dependencies": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -2452,6 +2456,45 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3507,6 +3550,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -3530,6 +3581,14 @@ "form-data": "^4.0.0" } }, + "node_modules/openai/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/ora": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/ora/-/ora-6.1.2.tgz", @@ -3738,6 +3797,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3771,6 +3835,14 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4353,6 +4425,17 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6152,11 +6235,13 @@ } }, "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "requires": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "balanced-match": { @@ -6628,6 +6713,41 @@ } } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -7397,6 +7517,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -7412,6 +7537,16 @@ "requires": { "axios": "^0.26.0", "form-data": "^4.0.0" + }, + "dependencies": { + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + } } }, "ora": { @@ -7569,6 +7704,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -7593,6 +7733,11 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8033,6 +8178,14 @@ "mime-types": "~2.1.24" } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/api/package.json b/api/package.json index 561908fad..8311456f4 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 bda88b14f..102811b60 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 ad2a7178c..f016a49b3 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -37,7 +37,7 @@ router.post('/', async (req, res) => { }); await saveMessage(userMessage); - await saveConvo({ ...userMessage, model, ...convo }); + await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo }); return await ask({ userMessage, @@ -176,9 +176,9 @@ 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), + title: await getConvoTitle(req?.session?.user?.username, conversationId), final: true, requestMessage: userMessage, responseMessage: gptResponse @@ -188,10 +188,13 @@ const ask = async ({ if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { const title = await titleConvo({ model, text, response: gptResponse }); - await saveConvo({ - conversationId, - title - }); + await saveConvo( + req?.session?.user?.username, + { + conversationId, + title + } + ); } } catch (error) { console.log(error); diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js index 981cd5617..0614faf86 100644 --- a/api/server/routes/askBing.js +++ b/api/server/routes/askBing.js @@ -38,7 +38,7 @@ router.post('/', async (req, res) => { }); await saveMessage(userMessage); - await saveConvo({ ...userMessage, model, ...convo }); + await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo }); return await ask({ isNewConversation, @@ -108,10 +108,13 @@ const ask = async ({ // Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId, // but in this situation, don't change the conversationId, but create new convo. if (conversationId != userMessage.conversationId && isNewConversation) - await saveConvo({ - conversationId: conversationId, - newConversationId: userMessage.conversationId - }); + await saveConvo( + req?.session?.user?.username, + { + conversationId: conversationId, + newConversationId: userMessage.conversationId + } + ); conversationId = userMessage.conversationId; response.text = response.response; @@ -129,9 +132,10 @@ 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), + title: await getConvoTitle(req?.session?.user?.username, conversationId), final: true, requestMessage: userMessage, responseMessage: response @@ -141,10 +145,13 @@ const ask = async ({ if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { const title = await titleConvo({ model, text, response }); - await saveConvo({ - conversationId, - title - }); + await saveConvo( + req?.session?.user?.username, + { + conversationId, + title + } + ); } } catch (error) { console.log(error); diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js index 00f287fa7..a2017cf7d 100644 --- a/api/server/routes/askSydney.js +++ b/api/server/routes/askSydney.js @@ -38,7 +38,7 @@ router.post('/', async (req, res) => { }); await saveMessage(userMessage); - await saveConvo({ ...userMessage, model, ...convo }); + await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo }); return await ask({ isNewConversation, @@ -132,18 +132,22 @@ const ask = async ({ // Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId, // but in this situation, don't change the conversationId, but create new convo. if (conversationId != userMessage.conversationId && isNewConversation) - await saveConvo({ - conversationId: conversationId, - newConversationId: userMessage.conversationId - }); + await saveConvo( + req?.session?.user?.username, + { + conversationId: conversationId, + newConversationId: userMessage.conversationId + } + ); conversationId = userMessage.conversationId; 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), + title: await getConvoTitle(req?.session?.user?.username, conversationId), final: true, requestMessage: userMessage, responseMessage: response @@ -153,10 +157,13 @@ const ask = async ({ if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { const title = await titleConvo({ model, text, response }); - await saveConvo({ - conversationId, - title - }); + await saveConvo( + req?.session?.user?.username, + { + conversationId, + title + } + ); } } catch (error) { console.log(error); diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js new file mode 100644 index 000000000..b4d986c48 --- /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 000000000..c2320fbb3 --- /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 99fb82470..0364684fa 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,10 +1,39 @@ 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(req?.session?.user?.username, 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 +44,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 +56,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/customGpts.js b/api/server/routes/customGpts.js index a58143a8b..d430b39ad 100644 --- a/api/server/routes/customGpts.js +++ b/api/server/routes/customGpts.js @@ -8,7 +8,7 @@ const { } = require('../../models'); router.get('/', async (req, res) => { - const models = (await getCustomGpts()).map((model) => { + const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => { model = model.toObject(); model._id = model._id.toString(); return model; @@ -20,8 +20,8 @@ router.post('/delete', async (req, res) => { const { arg } = req.body; try { - await deleteCustomGpts(arg); - const models = (await getCustomGpts()).map((model) => { + await deleteCustomGpts(req?.session?.user?.username, arg); + const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => { model = model.toObject(); model._id = model._id.toString(); return model; @@ -56,7 +56,7 @@ router.post('/', async (req, res) => { } try { - const dbResponse = await setter(update); + const dbResponse = await setter(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 235d08da2..5164badc3 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 5f1446fd0..0421d14d9 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 ( -
-