mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-07 19:18:52 +01:00
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:
parent
65543eb084
commit
dac19038a3
68 changed files with 3968 additions and 3394 deletions
180
api/server/controllers/auth.controller.js
Normal file
180
api/server/controllers/auth.controller.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 }));
|
||||
});
|
||||
|
|
|
|||
64
api/server/routes/oauth.js
Normal file
64
api/server/routes/oauth.js
Normal 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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
197
api/server/services/auth.service.js
Normal file
197
api/server/services/auth.service.js
Normal 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,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue