feat: Auth and User System (#205)

* server-side JWT auth implementation

* move oauth routes and strategies, fix bugs

* backend modifications for wiring up the frontend login and reg forms

* Add frontend data services for login and registration

* Add login and registration forms

* Implment auth context, functional client side auth

* protect routes with jwt auth

* finish local strategy (using local storage)

* Start setting up google auth

* disable token refresh, remove old auth middleware

* refactor client, add ApiErrorBoundary context

* disable google and facebook strategies

* fix: fix presets not displaying specific to user

* fix: fix issue with browser refresh

* fix: casing issue with User.js (#11)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* fix: issue with api error watcher (#12)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* feat: add google auth social login

* fix: make google login url dynamic based on dev/prod

* fix: bug where UI is briefly displayed before redirecting to login

* fix: fix cookie expires value for local auth

* Update README.md

* Update LOCAL_INSTALL structure

* Add local testing instructions

* Only load google strategy if client id and secret are provided

* Update .env.example files with new params

* fix issue with not redirecting to register form

* only show google login button if value is set in .env

* cleanup log messages

* Add label to button for google login on login form

* doc: fix client/server url values in .env.example

* feat: add error message details to registration failure

* Restore preventing paste on confirm password

* auto-login user after registering

* feat: forgot password (#24)

* make login/reg pages look like openai's

* add password reset data services

* new form designs similar to openai, add password reset pages

* add api's for password reset

* email utils for password reset

* remove bcrypt salt rounds from process.env

* refactor: restructure api auth code, consolidate routes (#25)

* add api's for password reset

* remove bcrypt salt rounds from process.env

* refactor: consolidate auth routes, use controller pattern

* refactor: code cleanup

* feat: migrate data to first user (#26)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes after refactor (#27)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: issue with auto-login when logging out then logging in with new browser window (#28)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: Update README and .env.example files with user system information (#29)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: update README and .env.example files

* Fixup: LOCAL_INSTALL.md PS instructions (#200) (#30)

Co-authored-by: alfredo-f <alfredo.fomitchenko@mail.polimi.it>

* feat: send user with completion to protect against abuse (#31)

* Fixup: LOCAL_INSTALL.md PS instructions (#200)

* server-side JWT auth implementation

* move oauth routes and strategies, fix bugs

* backend modifications for wiring up the frontend login and reg forms

* Add frontend data services for login and registration

* Add login and registration forms

* Implment auth context, functional client side auth

* protect routes with jwt auth

* finish local strategy (using local storage)

* Start setting up google auth

* disable token refresh, remove old auth middleware

* refactor client, add ApiErrorBoundary context

* disable google and facebook strategies

* fix: fix presets not displaying specific to user

* fix: fix issue with browser refresh

* fix: casing issue with User.js (#11)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* feat: add google auth social login

* fix: make google login url dynamic based on dev/prod

* fix: bug where UI is briefly displayed before redirecting to login

* fix: fix cookie expires value for local auth

* Only load google strategy if client id and secret are provided

* Update .env.example files with new params

* fix issue with not redirecting to register form

* only show google login button if value is set in .env

* cleanup log messages

* Add label to button for google login on login form

* doc: fix client/server url values in .env.example

* feat: add error message details to registration failure

* Restore preventing paste on confirm password

* auto-login user after registering

* feat: forgot password (#24)

* make login/reg pages look like openai's

* add password reset data services

* new form designs similar to openai, add password reset pages

* add api's for password reset

* email utils for password reset

* remove bcrypt salt rounds from process.env

* refactor: restructure api auth code, consolidate routes (#25)

* add api's for password reset

* remove bcrypt salt rounds from process.env

* refactor: consolidate auth routes, use controller pattern

* refactor: code cleanup

* feat: migrate data to first user (#26)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes after refactor (#27)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: issue with auto-login when logging out then logging in with new browser window (#28)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: Update README and .env.example files with user system information (#29)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: update README and .env.example files

* Send user id to openai to protect against abuse

* add meilisearch to gitignore

* Remove webpack

---------

Co-authored-by: alfredo-f <alfredo.fomitchenko@mail.polimi.it>

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Co-authored-by: Alfredo Fomitchenko <alfredo.fomitchenko@mail.polimi.it>
This commit is contained in:
Dan Orlando 2023-05-07 10:04:51 -07:00 committed by GitHub
parent 65543eb084
commit dac19038a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 3968 additions and 3394 deletions

View file

@ -1,6 +1,5 @@
const Keyv = require('keyv');
const { KeyvFile } = require('keyv-file');
const { saveMessage } = require('../../../models');
const addToCache = async ({ endpoint, endpointOption, userMessage, responseMessage }) => {
try {

View file

@ -4,8 +4,9 @@ const router = express.Router();
const { titleConvo, askBing } = require('../../../app');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
@ -62,7 +63,7 @@ router.post('/', async (req, res) => {
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
@ -205,7 +206,7 @@ const ask = async ({
conversationUpdate.invocationId = response.invocationId;
}
await saveConvo(req?.session?.user?.username, conversationUpdate);
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
@ -218,9 +219,9 @@ const ask = async ({
userMessageId = newUserMassageId;
sendMessage(res, {
title: await getConvoTitle(req?.session?.user?.username, conversationId),
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req?.session?.user?.username, conversationId),
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
@ -229,7 +230,7 @@ const ask = async ({
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});

View file

@ -5,8 +5,9 @@ const { getChatGPTBrowserModels } = require('../endpoints');
const { browserClient } = require('../../../app/');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
@ -49,7 +50,7 @@ router.post('/', async (req, res) => {
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
@ -81,6 +82,7 @@ const ask = async ({
res
}) => {
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
const userId = req.user.id;
res.writeHead(200, {
Connection: 'keep-alive',
@ -121,7 +123,8 @@ const ask = async ({
conversationId,
...endpointOption,
onProgress: progressCallback.call(null, { res, text }),
abortController
abortController,
userId
});
console.log('CLIENT RESPONSE', response);
@ -168,7 +171,7 @@ const ask = async ({
};
}
await saveConvo(req?.session?.user?.username, conversationUpdate);
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
@ -181,9 +184,9 @@ const ask = async ({
userMessageId = newUserMassageId;
sendMessage(res, {
title: await getConvoTitle(req?.session?.user?.username, conversationId),
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req?.session?.user?.username, conversationId),
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
@ -192,7 +195,7 @@ const ask = async ({
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
// const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
const title = await response.details.title;
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});

View file

@ -6,10 +6,11 @@ const { getOpenAIModels } = require('../endpoints');
const { titleConvo, askClient } = require('../../../app/');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
const abortControllers = new Map();
router.post('/abort', async (req, res) => {
router.post('/abort', requireJwtAuth, async (req, res) => {
const { abortKey } = req.body;
console.log(`req.body`, req.body);
if (!abortControllers.has(abortKey)) {
@ -26,7 +27,7 @@ router.post('/abort', async (req, res) => {
res.send(JSON.stringify(ret));
});
router.post('/', async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
@ -74,7 +75,7 @@ router.post('/', async (req, res) => {
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
@ -106,7 +107,7 @@ const ask = async ({
res
}) => {
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
const userId = req.user.id;
let responseMessageId = crypto.randomUUID();
res.writeHead(200, {
@ -159,9 +160,9 @@ const ask = async ({
await addToCache({ endpoint: 'openAI', endpointOption, userMessage, responseMessage });
return {
title: await getConvoTitle(req?.session?.user?.username, conversationId),
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req?.session?.user?.username, conversationId),
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
};
@ -179,7 +180,8 @@ const ask = async ({
text,
parentMessageId: overrideParentMessageId || userMessageId
}),
abortController
abortController,
userId
});
abortControllers.delete(abortKey);
@ -225,7 +227,7 @@ const ask = async ({
};
}
await saveConvo(req?.session?.user?.username, conversationUpdate);
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
@ -238,9 +240,9 @@ const ask = async ({
userMessageId = newUserMassageId;
sendMessage(res, {
title: await getConvoTitle(req?.session?.user?.username, conversationId),
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req?.session?.user?.username, conversationId),
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
@ -248,7 +250,7 @@ const ask = async ({
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
await saveConvo(req?.session?.user?.username, {
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});

View file

@ -1,57 +1,25 @@
const express = require('express');
const {
resetPasswordRequestController,
resetPasswordController,
getUserController,
loginController,
logoutController,
refreshController,
registrationController,
} = require('../controllers/auth.controller');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
const requireLocalAuth = require('../../middleware/requireLocalAuth');
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('/');
}
});
//Local
router.get('/user', requireJwtAuth, getUserController);
router.post('/logout', requireJwtAuth, logoutController);
router.post('/login', requireLocalAuth, loginController);
router.post('/refresh', requireJwtAuth, refreshController);
router.post('/register', registrationController);
router.post('/requestPasswordReset', resetPasswordRequestController);
router.post('/resetPassword', resetPasswordController);
router.get('/logout', function (req, res) {
// clear the session
req.session.user = null;
req.session.save(function () {
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');
}
} else next();
};
if (userSystemEnabled) {
router.use('/your_login_page', authYourLogin);
}
module.exports = { router, authenticatedOr401, authenticatedOrRedirect };
module.exports = router;

View file

@ -1,44 +0,0 @@
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
if (userSystemEnabled) {
req.session.user = {
username: null, // was 'sample_user', but would break previous relationship with previous conversations before v0.1.0
display: 'Sample User'
};
}
req.session.save(function (error) {
if (error) {
console.log(error);
res.send(`<h1>Login Failed. An error occurred. Please see the server logs for details.</h1>`);
} else {
res.redirect('/');
}
});
});
module.exports = router;

View file

@ -1,24 +1,23 @@
const express = require('express');
const router = express.Router();
const { titleConvo } = require('../../app/');
const { getConvo, saveConvo, getConvoTitle } = require('../../models');
const { getConvo, saveConvo } = require('../../models');
const { getConvosByPage, deleteConvos } = require('../../models/Conversation');
const { getMessages } = require('../../models/Message');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/', async (req, res) => {
router.get('/', requireJwtAuth, async (req, res) => {
const pageNumber = req.query.pageNumber || 1;
res.status(200).send(await getConvosByPage(req?.session?.user?.username, pageNumber));
res.status(200).send(await getConvosByPage(req.user.id, pageNumber));
});
router.get('/:conversationId', async (req, res) => {
router.get('/:conversationId', requireJwtAuth, async (req, res) => {
const { conversationId } = req.params;
const convo = await getConvo(req?.session?.user?.username, conversationId);
const convo = await getConvo(req.user.id, conversationId);
if (convo) res.status(200).send(convo.toObject());
else res.status(404).end();
});
router.post('/clear', async (req, res) => {
router.post('/clear', requireJwtAuth, async (req, res) => {
let filter = {};
const { conversationId, source } = req.body.arg;
if (conversationId) {
@ -32,7 +31,7 @@ router.post('/clear', async (req, res) => {
}
try {
const dbResponse = await deleteConvos(req?.session?.user?.username, filter);
const dbResponse = await deleteConvos(req.user.id, filter);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
@ -40,11 +39,11 @@ router.post('/clear', async (req, res) => {
}
});
router.post('/update', async (req, res) => {
router.post('/update', requireJwtAuth, async (req, res) => {
const update = req.body.arg;
try {
const dbResponse = await saveConvo(req?.session?.user?.username, update);
const dbResponse = await saveConvo(req.user.id, update);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);

View file

@ -5,9 +5,9 @@ const presets = require('./presets');
const prompts = require('./prompts');
const search = require('./search');
const tokenizer = require('./tokenizer');
const me = require('./me');
const auth = require('./auth');
const oauth = require('./oauth');
const { router: endpoints } = require('./endpoints');
const { router: auth, authenticatedOr401, authenticatedOrRedirect } = require('./auth');
module.exports = {
search,
@ -17,9 +17,7 @@ module.exports = {
presets,
prompts,
auth,
oauth,
tokenizer,
me,
endpoints,
authenticatedOr401,
authenticatedOrRedirect
};

View file

@ -1,16 +0,0 @@
const express = require('express');
const router = express.Router();
const userSystemEnabled = !!process.env.ENABLE_USER_SYSTEM || false;
router.get('/', 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' }));
}
});
module.exports = router;

View file

@ -1,8 +1,9 @@
const express = require('express');
const router = express.Router();
const { getMessages } = require('../../models/Message');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/:conversationId', async (req, res) => {
router.get('/:conversationId', requireJwtAuth, async (req, res) => {
const { conversationId } = req.params;
res.status(200).send(await getMessages({ conversationId }));
});

View file

@ -0,0 +1,64 @@
const passport = require('passport');
const express = require('express');
const router = express.Router();
const isProduction = process.env.NODE_ENV === 'production';
const clientUrl = isProduction ? process.env.CLIENT_URL_PROD : process.env.CLIENT_URL_DEV;
// Social
router.get(
'/google',
passport.authenticate('google', {
scope: ['openid', 'profile', 'email'],
session: false
})
);
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${clientUrl}/login`,
failureMessage: true,
session: false,
scope: ['openid', 'profile', 'email']
}),
(req, res) => {
const token = req.user.generateToken();
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.redirect(clientUrl);
}
);
router.get(
'/facebook',
passport.authenticate('facebook', {
scope: ['public_profile', 'email'],
session: false
})
);
router.get(
'/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: `${clientUrl}/login`,
failureMessage: true,
session: false,
scope: ['public_profile', 'email']
}),
(req, res) => {
const token = req.user.generateToken();
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.redirect(clientUrl);
}
);
module.exports = router;

View file

@ -2,23 +2,24 @@ const express = require('express');
const router = express.Router();
const { getPresets, savePreset, deletePresets } = require('../../models');
const crypto = require('crypto');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/', async (req, res) => {
const presets = (await getPresets(req?.session?.user?.username)).map((preset) => {
router.get('/', requireJwtAuth, async (req, res) => {
const presets = (await getPresets(req.user.id)).map((preset) => {
return preset.toObject();
});
res.status(200).send(presets);
});
router.post('/', async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const update = req.body || {};
update.presetId = update?.presetId || crypto.randomUUID();
try {
await savePreset(req?.session?.user?.username, update);
await savePreset(req.user.id, update);
const presets = (await getPresets(req?.session?.user?.username)).map((preset) => {
const presets = (await getPresets(req.user.id)).map((preset) => {
return preset.toObject();
});
res.status(201).send(presets);
@ -28,7 +29,7 @@ router.post('/', async (req, res) => {
}
});
router.post('/delete', async (req, res) => {
router.post('/delete', requireJwtAuth, async (req, res) => {
let filter = {};
const { presetId } = req.body.arg || {};
@ -37,9 +38,9 @@ router.post('/delete', async (req, res) => {
console.log('delete preset filter', filter);
try {
await deletePresets(req?.session?.user?.username, filter);
await deletePresets(req.user.id, filter);
const presets = (await getPresets(req?.session?.user?.username)).map(preset => preset.toObject());
const presets = (await getPresets(req.user.id)).map(preset => preset.toObject());
// console.log('delete preset response', presets);
res.status(201).send(presets);

View file

@ -5,6 +5,8 @@ const { Message } = require('../../models/Message');
const { Conversation, getConvosQueried } = require('../../models/Conversation');
const { reduceHits } = require('../../lib/utils/reduceHits');
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
const cache = new Map();
router.get('/sync', async function (req, res) {
@ -13,9 +15,9 @@ router.get('/sync', async function (req, res) {
res.send('synced');
});
router.get('/', async function (req, res) {
router.get('/', requireJwtAuth, async function (req, res) {
try {
let user = req?.session?.user?.username;
let user = req.user.id;
user = user ?? null;
const { q } = req.query;
const pageNumber = req.query.pageNumber || 1;

View file

@ -4,8 +4,9 @@ const { Tiktoken } = require('@dqbd/tiktoken/lite');
const { load } = require('@dqbd/tiktoken/load');
const registry = require('@dqbd/tiktoken/registry.json');
const models = require('@dqbd/tiktoken/model_to_encoding.json');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.post('/', async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
try {
const { arg } = req.body;