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,5 +1,5 @@
##########################
# Server configuration.
# Server configuration:
##########################
# The server will listen to localhost:3080 by default. You can change the target IP as you want.
@ -7,15 +7,16 @@
# or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface.
# Tips: Setting host to 0.0.0.0 means listening on all interfaces. It's not a real IP.
# Use localhost:port rather than 0.0.0.0:port to access the server.
# Set Node env to development if running in dev mode.
HOST=localhost
PORT=3080
NODE_ENV=development
NODE_ENV=production
# Change this to proxy any API request.
# It's useful if your machine has difficulty calling the original API server.
# PROXY=
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
# Change this to your MongoDB URI if different. I recommend appending chatgpt-clone.
MONGO_URI=mongodb://127.0.0.1:27017/chatgpt-clone
##########################
@ -44,7 +45,7 @@ OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4
# BingAI Tokens: the "_U" cookies value from bing.com
# Set to "user_provided" to allow the user to provide its token from the UI.
# Leave it blank to disable this endpoint.
BINGAI_TOKEN=user_provided
BINGAI_TOKEN="user_provided"
# BingAI Host:
# Necessary for some people in different countries, e.g. China (https://cn.bing.com)
@ -60,7 +61,7 @@ BINGAI_TOKEN=user_provided
# Exposes your access token to `CHATGPT_REVERSE_PROXY`
# Set to "user_provided" to allow the user to provide its token from the UI.
# Leave it blank to disable this endpoint
CHATGPT_TOKEN=user_provided
CHATGPT_TOKEN="user_provided"
# Identify the available models, separated by commas. The first will be default.
# Leave it blank to use internal settings.
@ -78,7 +79,7 @@ CHATGPT_MODELS=text-davinci-002-render-sha,text-davinci-002-render-paid,gpt-4
# ENABLING SEARCH MESSAGES/CONVOS
# Requires the installation of the free self-hosted Meilisearch or a paid Remote Plan (Remote not tested)
# The easiest setup for this is through docker-compose, which takes care of it for you.
SEARCH=TRUE
SEARCH=false
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for the API server to connect to the search server.
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
@ -94,14 +95,35 @@ MEILI_HTTP_ADDR=0.0.0.0:7700
# or if it is under 16 bytes. MeiliSearch will suggest a secure autogenerated master key.
# Using docker, it seems recognized as production so use a secure key.
# This is a ready made secure key for docker-compose, you can replace it with your own.
MEILI_MASTER_KEY=JKMW-hGc7v_D1FkJVdbRSDNFLZcUv3S75yrxXP0SmcU
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
##########################
# User System
# User System:
##########################
# ENABLING THE USER SYSTEM
# This is not a ready to use user system.
# Don't use it, unless you can write your own code.
# Do not uncomment this unless you implemented your own user system
# ENABLE_USER_SYSTEM=
# Google:
# Add your Google Client ID and Secret here, you must register an app with Google Cloud to get these values
# https://cloud.google.com/
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback
#JWT:
JWT_SECRET_DEV=secret
# Add a secure secret for production if deploying to live domain.
JWT_SECRET_PROD=secret
# Set the expiration delay for the secure cookie with the JWT token
# Delay is in millisecond e.g. 7 days is 1000*60*60*24*7
SESSION_EXPIRY=1000 * 60 * 60 * 24 * 7
# Site URLs:
# Don't forget to set Node env to development in the Server configuration section above
# if you want to run in dev mode
CLIENT_URL_DEV=http://localhost:3090
SERVER_URL_DEV=http://localhost:3080
# Change these values to domain if deploying:
CLIENT_URL_PROD=http://localhost:3080
SERVER_URL_PROD=http://localhost:3080

View file

@ -8,7 +8,8 @@ const browserClient = async ({
model,
token,
onProgress,
abortController
abortController,
userId
}) => {
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
const store = {
@ -21,8 +22,9 @@ const browserClient = async ({
// Access token from https://chat.openai.com/api/auth/session
accessToken: process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
model: model,
// debug: true
proxy: process.env.PROXY || null
debug: false,
proxy: process.env.PROXY || null,
user: userId
};
const client = new ChatGPTBrowserClient(clientOptions, store);

View file

@ -14,7 +14,8 @@ const askClient = async ({
presence_penalty,
frequency_penalty,
onProgress,
abortController
abortController,
userId
}) => {
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
const store = {
@ -36,7 +37,8 @@ const askClient = async ({
chatGptLabel,
promptPrefix,
proxy: process.env.PROXY || null,
debug: false
debug: false,
user: userId
};
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);

View file

@ -0,0 +1,5 @@
const passport = require('passport');
const requireJwtAuth = passport.authenticate('jwt', { session: false });
module.exports = requireJwtAuth;

View file

@ -0,0 +1,31 @@
const passport = require('passport');
const DebugControl = require('../utils/debug.js');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
if (parameters) {
DebugControl.log.parameters(parameters);
}
}
const requireLocalAuth = (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
log({
title: '(requireLocalAuth) Error at passport.authenticate',
parameters: [{ name: 'error', value: err }]
});
return next(err);
}
if (!user) {
log({
title: '(requireLocalAuth) Error: No user',
});
return res.status(422).send(info);
}
req.user = user;
next();
})(req, res, next);
};
module.exports = requireLocalAuth;

View file

@ -3,6 +3,7 @@ const Conversation = require('./schema/convoSchema');
const { getMessages, deleteMessages } = require('./Message');
const getConvo = async (user, conversationId) => {
console.log('getConvo -> userId', user);
try {
return await Conversation.findOne({ user, conversationId }).exec();
} catch (error) {
@ -39,7 +40,6 @@ module.exports = {
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
.exec();
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
} catch (error) {
console.log(error);

177
api/models/User.js Normal file
View file

@ -0,0 +1,177 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const Joi = require('joi');
const DebugControl = require('../utils/debug.js');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
DebugControl.log.parameters(parameters);
}
const Session = mongoose.Schema({
refreshToken: {
type: String,
default: ''
}
});
const userSchema = mongoose.Schema(
{
name: {
type: String
},
username: {
type: String,
lowercase: true,
required: [true, "can't be blank"],
match: [/^[a-zA-Z0-9_]+$/, 'is invalid'],
index: true
},
email: {
type: String,
required: [true, "can't be blank"],
lowercase: true,
unique: true,
match: [/\S+@\S+\.\S+/, 'is invalid'],
index: true
},
emailVerified: {
type: Boolean,
required: true,
default: false
},
password: {
type: String,
trim: true,
minlength: 8,
maxlength: 60
},
avatar: {
type: String,
required: false
},
provider: {
type: String,
required: true,
default: 'local'
},
role: {
type: String,
default: 'USER'
},
googleId: {
type: String,
unique: true,
sparse: true
},
facebookId: {
type: String,
unique: true,
sparse: true
},
refreshToken: {
type: [Session]
}
},
{ timestamps: true }
);
//Remove refreshToken from the response
userSchema.set('toJSON', {
transform: function (doc, ret, options) {
delete ret.refreshToken;
return ret;
}
});
userSchema.methods.toJSON = function () {
return {
id: this._id,
provider: this.provider,
email: this.email,
name: this.name,
username: this.username,
avatar: this.avatar,
role: this.role,
emailVerified: this.emailVerified,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
};
const isProduction = process.env.NODE_ENV === 'production';
const secretOrKey = isProduction ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV;
const refreshSecret = isProduction
? process.env.REFRESH_TOKEN_SECRET_PROD
: process.env.REFRESH_TOKEN_SECRET_DEV;
userSchema.methods.generateToken = function () {
const token = jwt.sign(
{
id: this._id,
username: this.username,
provider: this.provider,
email: this.email
},
secretOrKey,
{ expiresIn: eval(process.env.SESSION_EXPIRY) }
);
return token;
};
userSchema.methods.generateRefreshToken = function () {
const refreshToken = jwt.sign(
{
id: this._id,
username: this.username,
provider: this.provider,
email: this.email
},
refreshSecret,
{ expiresIn: eval(process.env.REFRESH_TOKEN_EXPIRY) }
);
return refreshToken;
};
userSchema.methods.comparePassword = function (candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) return callback(err);
callback(null, isMatch);
});
};
module.exports.hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) reject(err);
else resolve(hash);
});
});
return hashedPassword;
};
module.exports.validateUser = (user) => {
log({
title: 'Validate User',
parameters: [{ name: 'Validate User', value: user }]
});
const schema = {
avatar: Joi.any(),
name: Joi.string().min(2).max(80).required(),
username: Joi.string()
.min(2)
.max(80)
.regex(/^[a-zA-Z0-9_]+$/)
.required(),
password: Joi.string().min(8).max(60).allow('').allow(null)
};
return Joi.validate(user, schema);
};
const User = mongoose.model('User', userSchema);
module.exports = User;

View file

@ -0,0 +1,22 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const tokenSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
required: true,
ref: "user",
},
token: {
type: String,
required: true,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
expires: 900,
},
});
module.exports = mongoose.model("Token", tokenSchema);

1199
api/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,19 +23,32 @@
"@keyv/mongo": "^2.1.8",
"@waylaidwanderer/chatgpt-api": "^1.35.0",
"axios": "^1.3.4",
"bcrypt": "^5.1.0",
"bcryptjs": "^2.4.3",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.0.3",
"eslint": "^8.36.0",
"express": "^4.18.2",
"express-session": "^1.17.3",
"handlebars": "^4.7.7",
"html": "^1.0.0",
"joi": "^14.3.1",
"jsonwebtoken": "^9.0.0",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"lodash": "^4.17.21",
"meilisearch": "^0.31.1",
"mongoose": "^6.9.0",
"nodemailer": "^6.9.1",
"openai": "^3.1.0",
"passport": "^0.6.0",
"passport-facebook": "^3.0.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"sanitize": "^2.1.2"
},
"devDependencies": {

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,
};

View file

@ -0,0 +1,60 @@
const passport = require('passport');
const FacebookStrategy = require('passport-facebook').Strategy;
const User = require('../models/User');
const serverUrl =
process.env.NODE_ENV === 'production' ? process.env.SERVER_URL_PROD : process.env.SERVER_URL_DEV;
// facebook strategy
const facebookLogin = new FacebookStrategy(
{
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_SECRET,
callbackURL: `${serverUrl}${process.env.FACEBOOK_CALLBACK_URL}`,
proxy: true,
// profileFields: [
// 'id',
// 'email',
// 'gender',
// 'profileUrl',
// 'displayName',
// 'locale',
// 'name',
// 'timezone',
// 'updated_time',
// 'verified',
// 'picture.type(large)'
// ]
},
async (accessToken, refreshToken, profile, done) => {
console.log('facebookLogin => profile', profile);
try {
const oldUser = await User.findOne({ email: profile.emails[0].value });
if (oldUser) {
console.log('FACEBOOK LOGIN => found user', oldUser);
return done(null, oldUser);
}
} catch (err) {
console.log(err);
}
// register user
try {
const newUser = await new User({
provider: 'facebook',
facebookId: profile.id,
username: profile.name.givenName + profile.name.familyName,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
}).save();
done(null, newUser);
} catch (err) {
console.log(err);
}
}
);
passport.use(facebookLogin);

View file

@ -0,0 +1,44 @@
const passport = require('passport');
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const User = require('../models/User');
const serverUrl =
process.env.NODE_ENV === 'production' ? process.env.SERVER_URL_PROD : process.env.SERVER_URL_DEV;
// google strategy
const googleLogin = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${serverUrl}${process.env.GOOGLE_CALLBACK_URL}`,
proxy: true
},
async (accessToken, refreshToken, profile, cb) => {
try {
const oldUser = await User.findOne({ email: profile.emails[0].value });
if (oldUser) {
return cb(null, oldUser);
}
} catch (err) {
console.log(err);
}
try {
const newUser = await new User({
provider: 'google',
googleId: profile.id,
username: profile.name.givenName + profile.name.familyName,
email: profile.emails[0].value,
emailVerified: profile.emails[0].verified,
name: `${profile.name.givenName} ${profile.name.familyName}`,
avatar: profile.photos[0].value
}).save();
cb(null, newUser);
} catch (err) {
console.log(err);
}
}
);
passport.use(googleLogin);

View file

@ -0,0 +1,29 @@
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const User = require('../models/User');
const isProduction = process.env.NODE_ENV === 'production';
const secretOrKey = isProduction ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV;
// JWT strategy
const jwtLogin = new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey
},
async (payload, done) => {
try {
const user = await User.findById(payload.id);
if (user) {
done(null, user);
} else {
console.log('JwtStrategy => no user found');
done(null, false);
}
} catch (err) {
done(err, false);
}
}
);
passport.use(jwtLogin);

View file

@ -0,0 +1,68 @@
const passport = require('passport');
const PassportLocalStrategy = require('passport-local').Strategy;
const Joi = require('joi');
const User = require('../models/User');
const { loginSchema } = require('./validators');
const DebugControl = require('../utils/debug.js');
const passportLogin = new PassportLocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
session: false,
passReqToCallback: true
},
async (req, email, password, done) => {
const { error } = Joi.validate(req.body, loginSchema);
if (error) {
log({
title: 'Passport Local Strategy - Validation Error',
parameters: [{ name: 'req.body', value: req.body }]
});
return done(null, false, { message: error.details[0].message });
}
try {
const user = await User.findOne({ email: email.trim() });
if (!user) {
log({
title: 'Passport Local Strategy - User Not Found',
parameters: [{ name: 'email', value: email }]
});
return done(null, false, { message: 'Email does not exists.' });
}
user.comparePassword(password, function (err, isMatch) {
if (err) {
log({
title: 'Passport Local Strategy - Compare password error',
parameters: [{ name: 'error', value: err }]
});
return done(err);
}
if (!isMatch) {
log({
title: 'Passport Local Strategy - Password does not match',
parameters: [{ name: 'isMatch', value: isMatch }]
});
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
} catch (err) {
return done(err);
}
}
);
passport.use(passportLogin);
function log({ title, parameters }) {
DebugControl.log.functionName(title);
if (parameters) {
DebugControl.log.parameters(parameters);
}
}

View file

@ -0,0 +1,24 @@
const Joi = require('joi');
const loginSchema = Joi.object().keys({
email: Joi.string().trim().email().required(),
password: Joi.string().trim().min(6).max(20).required()
});
const registerSchema = Joi.object().keys({
name: Joi.string().trim().min(2).max(30).required(),
username: Joi.string()
.trim()
.min(2)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/)
.required(),
email: Joi.string().trim().email().required(),
password: Joi.string().trim().min(6).max(20).required(),
confirm_password: Joi.string().trim().min(6).max(20).required()
});
module.exports = {
loginSchema,
registerSchema
};

46
api/utils/debug.js Normal file
View file

@ -0,0 +1,46 @@
const levels = {
NONE: 0,
LOW: 1,
MEDIUM: 2,
HIGH: 3
};
let level = levels.HIGH;
module.exports = {
levels,
setLevel: (l) => (level = l),
log: {
parameters: (parameters) => {
if (levels.HIGH > level) return;
console.group();
parameters.forEach((p) => console.log(`${p.name}:`, p.value));
console.groupEnd();
},
functionName: (name) => {
if (levels.MEDIUM > level) return;
console.log(`\nEXECUTING: ${name}\n`);
},
flow: (flow) => {
if (levels.LOW > level) return;
console.log(`\n\n\nBEGIN FLOW: ${flow}\n\n\n`);
},
variable: ({ name, value }) => {
if (levels.HIGH > level) return;
console.group();
console.group();
console.log(`VARIABLE ${name}:`, value);
console.groupEnd();
console.groupEnd();
},
request: () => (req, res, next) => {
if (levels.HIGH > level) return next();
console.log('Hit URL', req.url, 'with following:');
console.group();
console.log('Query:', req.query);
console.log('Body:', req.body);
console.groupEnd();
return next();
}
}
};

View file

@ -0,0 +1,11 @@
<html>
<head>
<style>
</style>
</head>
<body>
<p>Hi {{name}},</p>
<p>Your password has been changed successfully.</p>
</body>
</html>

View file

@ -0,0 +1,13 @@
<html>
<head>
<style>
</style>
</head>
<body>
<p>Hi {{name}},</p>
<h1>You have requested to reset your password.</h1>
<p> Please click the link below to reset your password.</p>
<a href="{{link}}">Reset Password</a>
</body>
</html>

View file

@ -0,0 +1,30 @@
const Conversation = require('../models/schema/convoSchema');
const Preset = require('../models/schema/presetSchema');
const migrateConversations = async (userId) => {
try {
return await Conversation.updateMany({ user: null }, { $set: { user: userId }}).exec();
} catch (error) {
console.log(error);
return { message: 'Error saving conversation' };
}
}
const migratePresets = async (userId) => {
try {
return await Preset.updateMany({ user: null }, { $set: { user: userId }}).exec();
} catch (error) {
console.log(error);
return { message: 'Error saving conversation' };
}
}
const migrateDataToFirstUser = async (user) => {
const conversations = await migrateConversations(user.id);
console.log(conversations);
const presets = await migratePresets(user.id);
console.log(presets);
}
module.exports = migrateDataToFirstUser;

54
api/utils/sendEmail.js Normal file
View file

@ -0,0 +1,54 @@
const nodemailer = require("nodemailer");
const handlebars = require("handlebars");
const fs = require("fs");
const path = require("path");
const sendEmail = async (email, subject, payload, template) => {
try {
// create reusable transporter object using the default SMTP transport
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: 465,
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD,
},
});
const source = fs.readFileSync(path.join(__dirname, template), "utf8");
const compiledTemplate = handlebars.compile(source);
const options = () => {
return {
from: process.env.FROM_EMAIL,
to: email,
subject: subject,
html: compiledTemplate(payload),
};
};
// Send email
transporter.sendMail(options(), (error, info) => {
if (error) {
return error;
} else {
return res.status(200).json({
success: true,
});
}
});
} catch (error) {
return error;
}
};
/*
Example:
sendEmail(
"youremail@gmail.com,
"Email subject",
{ name: "Eze" },
"./templates/layouts/main.handlebars"
);
*/
module.exports = sendEmail;