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

@ -0,0 +1,180 @@
const {
loginUser,
logoutUser,
registerUser,
requestPasswordReset,
resetPassword,
} = require("../services/auth.service");
const isProduction = process.env.NODE_ENV === 'production';
const loginController = async (req, res) => {
try {
const token = req.user.generateToken();
const user = await loginUser(req.user)
if(user) {
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.status(200).send({ token, user });
}
else {
return res.status(400).json({ message: 'Invalid credentials' });
}
}
catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
};
const logoutController = async (req, res) => {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
try {
const logout = await logoutUser(req.user, refreshToken);
console.log(logout)
const { status, message } = logout;
if (status === 200) {
res.clearCookie('token');
res.clearCookie('refreshToken');
res.status(status).send({ message });
}
else {
res.status(status).send({ message });
}
}
catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
}
const registrationController = async (req, res) => {
try {
const response = await registerUser(req.body);
if (response.status === 200) {
const { status, user } = response;
const token = user.generateToken();
//send token for automatic login
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.status(status).send({ user });
}
else {
const { status, message } = response;
res.status(status).send({ message });
}
}
catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
};
const getUserController = async (req, res) => {
return res.status(200).send(req.user);
};
const resetPasswordRequestController = async (req, res) => {
try {
const resetService = await requestPasswordReset(
req.body.email
);
if (resetService.link) {
return res.status(200).json(resetService);
}
else {
return res.status(400).json(resetService);
}
}
catch (e) {
console.log(e);
return res.status(400).json({ message: e.message });
}
};
const resetPasswordController = async (req, res) => {
try {
const resetPasswordService = await resetPassword(
req.body.userId,
req.body.token,
req.body.password
);
if(resetPasswordService instanceof Error) {
return res.status(400).json(resetPasswordService);
}
else {
return res.status(200).json(resetPasswordService);
}
}
catch (e) {
console.log(e);
return res.status(400).json({ message: e.message });
}
};
const refreshController = async (req, res, next) => {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
//TODO
// if (refreshToken) {
// try {
// const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// const userId = payload._id;
// User.findOne({ _id: userId }).then(
// (user) => {
// if (user) {
// // Find the refresh token against the user record in database
// const tokenIndex = user.refreshToken.findIndex(item => item.refreshToken === refreshToken);
// if (tokenIndex === -1) {
// res.statusCode = 401;
// res.send('Unauthorized');
// } else {
// const token = req.user.generateToken();
// // If the refresh token exists, then create new one and replace it.
// const newRefreshToken = req.user.generateRefreshToken();
// user.refreshToken[tokenIndex] = { refreshToken: newRefreshToken };
// user.save((err) => {
// if (err) {
// res.statusCode = 500;
// res.send(err);
// } else {
// // setTokenCookie(res, newRefreshToken);
// const user = req.user.toJSON();
// res.status(200).send({ token, user });
// }
// });
// }
// } else {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
// },
// err => next(err)
// );
// } catch (err) {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
// } else {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
};
module.exports = {
getUserController,
loginController,
logoutController,
refreshController,
registrationController,
resetPasswordRequestController,
resetPasswordController,
};

View file

@ -1,12 +1,12 @@
const express = require('express');
const session = require('express-session');
const connectDb = require('../lib/db/connectDb');
const migrateDb = require('../lib/db/migrateDb');
const indexSync = require('../lib/db/indexSync');
const path = require('path');
const cors = require('cors');
const routes = require('./routes');
const errorController = require('./controllers/errorController');
const errorController = require('./controllers/error.controller');
const passport = require('passport');
const port = process.env.PORT || 3080;
const host = process.env.HOST || 'localhost';
@ -20,44 +20,38 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
const app = express();
app.use(errorController);
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(projectPath, 'dist')));
app.set('trust proxy', 1); // trust first proxy
app.use(
session({
secret: 'chatgpt-clone-random-secrect',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 7 days
})
);
// ROUTES
/* chore: potential redirect error here, can only comment out this block;
comment back in if using auth routes i guess */
// 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(cors());
// OAUTH
app.use(passport.initialize());
require('../strategies/jwtStrategy');
require('../strategies/localStrategy');
if(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
require('../strategies/googleStrategy');
}
if(process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
require('../strategies/facebookStrategy');
}
app.use('/oauth', routes.oauth)
// api endpoint
app.use('/api/search', routes.authenticatedOr401, routes.search);
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/presets', routes.authenticatedOr401, routes.presets);
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
app.use('/api/tokenizer', routes.authenticatedOr401, routes.tokenizer);
app.use('/api/endpoints', routes.authenticatedOr401, routes.endpoints);
app.use('/api/auth', routes.auth);
app.use('/api/search', routes.search);
app.use('/api/ask', routes.ask);
app.use('/api/messages', routes.messages);
app.use('/api/convos', routes.convos);
app.use('/api/presets', routes.presets);
app.use('/api/prompts', routes.prompts);
app.use('/api/tokenizer', routes.tokenizer);
app.use('/api/endpoints', routes.endpoints);
// user system
app.use('/auth', routes.auth);
app.use('/api/me', routes.me);
// static files
app.get('/*', routes.authenticatedOrRedirect, function (req, res) {
app.get('/*', function (req, res) {
res.sendFile(path.join(projectPath, 'dist', 'index.html'));
});
@ -71,7 +65,7 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
})();
let messageCount = 0;
process.on('uncaughtException', err => {
process.on('uncaughtException', (err) => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:', err.message);
}

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;

View file

@ -0,0 +1,197 @@
const User = require('../../models/User');
const Token = require('../../models/schema/tokenSchema');
const sendEmail = require('../../utils/sendEmail');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const DebugControl = require('../../utils/debug.js');
const Joi = require('joi');
const { registerSchema } = require('../../strategies/validators');
const migrateDataToFirstUser = require('../../utils/migrateDataToFirstUser');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
DebugControl.log.parameters(parameters);
}
const isProduction = process.env.NODE_ENV === 'production';
const clientUrl = isProduction ? process.env.CLIENT_URL_PROD : process.env.CLIENT_URL_DEV;
const loginUser = async (user) => {
// const refreshToken = req.user.generateRefreshToken();
const dbUser = await User.findById(user._id);
//todo: save refresh token
return dbUser;
};
const logoutUser = async (user, refreshToken) => {
User.findById(user._id).then((user) => {
const tokenIndex = user.refreshToken.findIndex(item => item.refreshToken === refreshToken);
if (tokenIndex !== -1) {
user.refreshToken.id(user.refreshToken[tokenIndex]._id).remove();
}
user.save((err) => {
if (err) {
return { status: 500, message: err.message };
} else {
//res.clearCookie('refreshToken', COOKIE_OPTIONS);
// removeTokenCookie(res);
return { status: 200, message: 'Logout successful' };
}
});
});
return { status: 200, message: 'Logout successful' };
};
const registerUser = async (user) => {
let response = {};
const { error } = Joi.validate(user, registerSchema);
if (error) {
log({
title: 'Route: register - Joi Validation Error',
parameters: [
{ name: 'Request params:', value: user },
{ name: 'Validation error:', value: error.details }
]
});
response = { status: 422, message: error.details[0].message };
return response;
}
const { email, password, name, username } = user;
try {
const existingUser = await User.findOne({ email });
if (existingUser) {
log({
title: 'Register User - Email in use',
parameters: [
{ name: 'Request params:', value: user },
{ name: 'Existing user:', value: existingUser }
]
});
response = { status: 422, message: 'Email is in use' };
return response;
}
//determine if this is the first registered user (not counting anonymous_user)
const isFirstRegisteredUser = await User.countDocuments({}) === 0;
try {
const newUser = await new User({
provider: 'local',
email,
password,
username,
name,
avatar: null,
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
});
// todo: implement refresh token
// const refreshToken = newUser.generateRefreshToken();
// newUser.refreshToken.push({ refreshToken });
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (errh, hash) => {
if (err) {
console.log(err);
}
// set pasword to hash
newUser.password = hash;
newUser.save();
});
});
console.log('newUser', newUser)
if (isFirstRegisteredUser) {
migrateDataToFirstUser(newUser);
// console.log(migrate);
}
response = { status: 200, user: newUser };
return response;
} catch (err) {
response = { status: 500, message: err.message };
return response;
}
} catch (err) {
response = { status: 500, message: err.message };
return response;
}
};
const requestPasswordReset = async (email) => {
const user = await User.findOne({ email });
if (!user) {
return new Error('Email does not exist');
}
let token = await Token.findOne({ userId: user._id });
if (token) await token.deleteOne();
let resetToken = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hash(resetToken, 10);
await new Token({
userId: user._id,
token: hash,
createdAt: Date.now()
}).save();
const link = `${clientUrl}/reset-password?token=${resetToken}&userId=${user._id}`;
sendEmail(
user.email,
'Password Reset Request',
{
name: user.name,
link: link
},
'./template/requestResetPassword.handlebars'
);
return { link };
};
const resetPassword = async (userId, token, password) => {
let passwordResetToken = await Token.findOne({ userId });
if (!passwordResetToken) {
return new Error('Invalid or expired password reset token');
}
const isValid = await bcrypt.compare(token, passwordResetToken.token);
if (!isValid) {
return new Error('Invalid or expired password reset token');
}
const hash = await bcrypt.hash(password, 10);
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
const user = await User.findById({ _id: userId });
sendEmail(
user.email,
'Password Reset Successfnodeully',
{
name: user.name
},
'./template/resetPassword.handlebars'
);
await passwordResetToken.deleteOne();
return { message: 'Password reset was successful' };
};
module.exports = {
// signup,
registerUser,
loginUser,
logoutUser,
requestPasswordReset,
resetPassword,
};