I have created an app that connects to a mongodb cluster and stores user info. The user is then able to log in with Next-Auth functionality. The app was working just fine before deploying to Vercel. On the live site I ran into some Server Config Errors. I refractored my code yet I am still running into a few errors.
I am successfully able to connect to the database for a new user sign up.
import {
connectToDatabase,
hashedPassword,
} from "../../helper/HelperFunctions";
const isEmpty = (value) => value.trim() === "";
const isTenChars = (value) => value.trim().length >= 10;
const emailValidation = (value) => {
const pattern = /^[^ ]+#[^ ]+\.[a-z]{2,3}$/;
if (value.match(pattern)) {
return true;
} else {
return false;
}
};
export default async function handler(req, res) {
if (req.method == "POST") {
let data = req.body;
const { firstName, lastName, email, password, userName } = data;
const firstNameIsValid = !isEmpty(firstName);
const lastNameisValid = !isEmpty(lastName);
const emailIsValid = emailValidation(email);
const passwordisValid = isTenChars(password);
const userNameIsValid = !isEmpty(userName);
let userDataIsValid =
firstNameIsValid &&
lastNameisValid &&
emailIsValid &&
passwordisValid &&
userNameIsValid;
if (!userDataIsValid) {
return;
}
const client = await connectToDatabase();
const db = client.db();
const existingUser = await db.collection("users").findOne({ email: email });
if (existingUser) {
res.status(422).json({ message: "User already exists, please log in!" });
console.log("User already exists, please log in!");
client.close();
return;
}
const protectedPassword = await hashedPassword(password);
await db.collection("users").insertOne({
firstName: firstName,
lastName: lastName,
email: email,
password: protectedPassword,
userName: userName,
});
client.close();
res.status(201).json({ message: "Signed up!" });
} else {
res.status(200).json({ data: req.body });
}
}
Here is my nextauth api route
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
// Helper Functions
import {
connectToDatabase,
comparePasswords,
} from "../../../helper/HelperFunctions";
export default NextAuth({
session: { strategy: "jwt" },
providers: [
CredentialsProvider({
async authorize(credentials) {
const client = await connectToDatabase();
const userCollection = client.db().collection("users");
const user = await userCollection.findOne({
email: credentials.email,
});
if (!user) {
client.close();
throw new Error("No user found!");
}
const isValid = await comparePasswords(
credentials.password,
user.password
);
if (!isValid) {
client.close();
throw new Error("Invalid password");
}
client.close();
if (user) {
return {
email: user.email,
};
} else {
return null;
}
},
}),
],
});
Before I deployed my site on Vercel, this was working just fine on localhost. The user should then proceed to a new page if the result of logging in has no errors.
const result = await signIn("credentials", {
redirect: false,
email: form.email,
password: form.password,
});
if (!result.error) {
console.log(true);
router.replace("/suggestions");
} else {
console.log(result.error);
setLoginResult(result.error);
}
If you see CLIENT_FETCH_ERROR make sure you have configured the NEXTAUTH_URL environment variable.
when developing you set it to localhost:3000, now you need to set that to your deployed url.
Related
Looking for a backend dev that can simply help me implement MONGODB with nextJS and the current model I have now. I have bought https://www.devias.io admin dashboard, and just want to implement auth and database reading with it.
Just want the basic auth setup. It's already setup in the FILES just wanting to know how to configure it properly based on the devias guides
Has anyone done this before I can't find any documentation on it
It's setup with mock data at the moment
SRC/API/AUTH/index.js
import { createResourceId } from '../../utils/create-resource-id';
import { decode, JWT_EXPIRES_IN, JWT_SECRET, sign } from '../../utils/jwt';
import { wait } from '../../utils/wait';
import { users } from './data';
class AuthApi {
async signIn(request) {
const { email, password } = request;
await wait(500);
return new Promise((resolve, reject) => {
try {
// Find the user
const user = users.find((user) => user.email === email);
if (!user || (user.password !== password)) {
reject(new Error('Please check your email and password'));
return;
}
// Create the access token
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
async signUp(request) {
const { email, name, password } = request;
await wait(1000);
return new Promise((resolve, reject) => {
try {
// Check if a user already exists
let user = users.find((user) => user.email === email);
if (user) {
reject(new Error('User already exists'));
return;
}
user = {
id: createResourceId(),
avatar: undefined,
email,
name,
password,
plan: 'Standard'
};
users.push(user);
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
me(request) {
const { accessToken } = request;
return new Promise((resolve, reject) => {
try {
// Decode access token
const { userId } = decode(accessToken);
// Find the user
const user = users.find((user) => user.id === userId);
if (!user) {
reject(new Error('Invalid authorization token'));
return;
}
resolve({
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
});
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
}
export const authApi = new AuthApi();
then /SRC/API/AUTH/data.js
export const users = [
{
id: '5e86809283e28b96d2d38537',
avatar: '/assets/avatars/avatar-anika-visser.png',
email: 'demo#devias.io',
name: 'Anika Visser',
password: 'Password123!',
plan: 'Premium'
}
];
This is the documentation on it
JSON Web Token (JWT)
Most auth providers use this strategy under the hood to provide access tokens. Currently, the app doesn't cover the backend service, and this service is mocked (faked) using http client interceptors. The implementation is basic, but enough to give you a starting point.
How it was implemented
Since tokens are meant to be created on the backend server, they are built with encrypt, encode and decode utility methods because they are not meant to be used on the client. These utilities can be found in src/utils/jwt. These are for development purposes only, and you must remove (or avoid using) them.
How to use JWT Provider
The app is delivered with JWT Provider as default auth strategy. If you changed or removed it, and you want it back, simply follow these steps:
Step 1: Import the provider
Open src/pages/_app.js file, import the provider and wrap the App component with it.
// src/pages/_app.js
import { AuthConsumer, AuthProvider } from '../contexts/auth/jwt-context';
const App = (props) => {
const { Component, pageProps } = props;
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
};
Step 2: Set the hook context
Open src/hooks/use-auth.js file and replace the current context the following line:
import { AuthContext } from '../contexts/auth/jwt-context';
How to use auth
Retrieve user profile
In the example below, you can find how it can be used in any component not just the App. Should you want to use it in any other component, you'll have to import the useAuth hook and use it as needed.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { user } = useAuth();
return (
<div>
Email: {user.email}
</div>
);
};
Auth methods / actions
For simplicity and space limitations, the code below is used only to exemplify, actual code can be found in the components.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { login } = useAuth();
const handleLogin = () => {
// Email/username and password
login('demo#devias.io', 'Password123!');
};
s
return (
<div>
<button onClick={handleLogin}>
Login
</button>
</div>
);
};
Implemented flows
Currently, the app only covers the main flows:
Register
Login
Logout
const mongoose = require('mongoose');
const jwt = require("jsonwebtoken");
// Connect to MongoDB
mongoose.connect('mongodb://localhost/yourdbname', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const userSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true
},
name: {
type: String,
required: true
},
password: {
type: String,
required: true
},
plan: {
type: String,
default:
'Standard'
},
avatar: {
type: String,
default:
null
},
});
const User = mongoose.model('User', userSchema);
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
class AuthApi {
async signIn(request) {
const {
email,
password
} = request;
const user = await User.findOne({
email
});
if (!user || (user.password !== password)) {
throw new Error('Please check your email and password');
}
const accessToken = jwt.sign({
userId: user.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async signUp(request) {
const {
email,
name,
password
} = request;
const existingUser = await User.findOne({
email
});
if (existingUser) {
throw new Error('User already exists');
}
const newUser = new User({
id: mongoose.Types.ObjectId(),
email,
name,
password,
plan: 'Standard',
avatar: null,
});
await newUser.save();
const accessToken = jwt.sign({
userId: newUser.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async me(request) {
const {
accessToken
} = request;
const decoded = jwt.verify(accessToken, JWT_SECRET);
const {
userId
} = decoded;
const user = await User.findById(userId);
if (!user) {
throw new Error('Invalid authorization token');
}
return {
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
};
}
}
export const authApi = new AuthApi();
I am using NextAuth.js for authentication and MongoDB to save the user information.
When the user registers with email & password, it saves to MongoDB.
If this user login with a Google account that has the same email address, it returns Error: Action login with HTTP GET is not supported by NextAuth.js
If the Google email address does not exist in the DB, then it works okay.
How do I check if the email address exists in the DB before login into the website by using GoogleProvider?
import { MongoDBAdapter } from '#next-auth/mongodb-adapter';
import clientPromise from '../../../lib/mongodb';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import { verifyPassword } from '../../../lib/auth';
import { connectToDatabase } from '../../../lib/db';
const { parseCookies } = require('nookies');
export default async function auth(req, res) {
const cookies = parseCookies({ req });
const maxAge =
cookies.remember === 'true' ? 30 * 24 * 60 * 60 : 1 * 24 * 60 * 60; // 30 days, 1 day
return await NextAuth(req, res, {
providers: [
CredentialsProvider({
async authorize(credentials) {
const client = await connectToDatabase();
const usersCollection = await client.db().collection('users');
const result = await usersCollection.findOne({
email: credentials.email,
});
if (!result) {
client.close();
throw new Error('No user found!');
}
const isValid = await verifyPassword(
credentials.password,
result.password
);
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}
client.close();
const user = {
id: result._id,
email: result.email,
name: result.name,
role: result.role,
remember: credentials.remember,
};
return user;
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: MongoDBAdapter(clientPromise),
callbacks: {
// called after sucessful signin
jwt: async ({ token, user }) => {
user && (token.user = user);
return token;
}, // called whenever session is checked
session: async (data) => {
const { session, token } = data;
session.user = token.user;
return session;
},
},
secret: process.env.SECRET_KEY,
session: {
strategy: 'jwt',
maxAge: maxAge,
},
jwt: {
secret: process.env.SECRET_KEY,
encryption: true,
},
pages: {
signIn: './login',
},
});
}
You can use the SignIn callback to control if a user is allowed to sign in https://next-auth.js.org/configuration/callbacks#sign-in-callback
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
if(account.provider === 'google') {
//check the user on your database and return true if is allowed to signIn
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return true
} else {
// Return false to display a default error message
return false
// Or you can return a URL to redirect to:
// return '/unauthorized'
}
}
}
}
I was writing a couple of tests to my test database but I can't seem to save a document('add user to database' test) . When I run the test I get the correct length of the number of users but when I actually go to the database I only see one user document(firstUser) and cannot see the newUser document. I think it is an issue with the beforeEach function as when I remove it, everything works with the only issue being the same user being repeatedly added to the database when the test is run.
Test Code
const supertest = require('supertest')
const mongoose = require('mongoose')
const app = require('../app')
const User = require('../models/user')
const bcrypt = require('bcrypt')
const api = supertest(app)
beforeEach(async () => {
await User.deleteMany({})
const passwordHash = await bcrypt.hash('12345',10)
const firstUser = new User(
{
username: "Big Mark",
password: passwordHash,
name:"Mark"
}
)
await firstUser.save()
})
describe('user tests', () => {
// Cannot see this document in mongoDB
test('add user to database', async () => {
const newUser = {
username: 'smart',
password: 'dvsgfd',
name: 'Kevin'
}
const result = await api.post('/api/users').send(newUser).expect(201)
const length = await api.get('/api/users')
expect(length._body).toHaveLength(2)
console.log("length is", length)
})
test('see if fetching works', async () => {
const fetchedUsers = await api.get('/api/users').expect(201)
})
// test('returns 404 error if username already exists', async () => {
// })
})
afterAll(() => {
mongoose.connection.close()
},100000)
Router Code
const userRouter = require('express').Router()
const User = require('../models/user')
const bcrypt = require('bcrypt')
userRouter.post('/', async (request,response) => {
console.log("request body is", request.body)
const {username,password,name} = request.body
const alreadyThere = await User.findOne({username})
if(alreadyThere == null && username.length > 2 && password.length > 2) {
const saltRounds = 10 //How many times password it gonna get hashed (Ex: 2^n times)
const passwordHash = await bcrypt.hash(password,saltRounds)
const user = new User(
{
username,
password: passwordHash,
name
}
)
const savedUser = await user.save()
console.log("SavedUser is", savedUser)
response.status(201).json({savedUser})
}
else {
response.status(404).json({error: "Username must be unique"})
}
})
userRouter.get('/', async(request,response) => {
const users = await User.find({})
response.status(201).json(users)
})
module.exports = userRouter
I am trying to test my API and I am not sure what I am doing wrong. The only error I am getting is cannot post. After some research, I am suspecting something wrong with my controller class.
This is my first real large project and I have created an API similar to this just with mongo but this is my first time using seqealize, and MySQL (with node).
The api/student/register router just won't post, but when I remove the controller and but a function that returns just plain JSON it works, that is why I really do believe it has something to do with my controller class.
contoller/student.controller.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const secret = process.env.SERCRET_KEY;
const db = require('../models/index.js');
const Student = db.Student;
class StudentContoller {
static registerStudent(req, res) {
try {
let {
first_name,
last_name,
email,
password,
phone,
shirt_size,
grade,
teacher,
} = req.body;
Student.findAll({
where: {
email : email,
is_Archived: false,
}
})
.then(result => {
if (result.length > 0){
res.status(400).json({ message: 'Email is already registerd'});
} else {
let hashPassword = bcrypt.hashSync(password, 10);
let newStudent = {
first_name,
last_name,
email,
password: hashPassword,
phone,
shirt_size,
grade,
teacher,
school_id,
}
Student.Create(newStudent)
.then(data => {
if(data){
res.status(201).json({message: 'Student Created', student: data })
}
})
.catch(err => res.json({error : err.message}));
}
})
} catch (e) {
res.status(500).json({error: 'error caused in regiserted controller'})
}
}
static async login(req, res) {
let { email, password, } = req.body;
await Student.findAll({
where: {
email: email,
is_Archived: false,
}
})
.then(student => {
if(student.length === 0){
res.status(400).json({ message: 'Sorry, account does not exsist'})
} else {
let passwordIsValid = bcrypt.compareSync(req.body.password, student[0].password.trim());
if (passwordIsValid) {
let studentDetails = {
id: student[0].dataValues.id,
first_name: student[0].first_name,
last_name: student[0].last_name,
}
let token = jwt.sign({
student: studentDetails,
}, secret, {
expiresIn: '1d'
});
res.status(200).json({
success: true,
student: studentDetails,
message: 'Login successfull',
token: token,
});
} else {
res.status(401).json({
success: false,
message: 'Login failed',
});
}
}
})
.catch(err => res.status(500).json({error: err.message}));
}
static async updateStudent(req, res) {
try {
const {
first_name,
last_name,
email,
password,
phone,
shirt_size,
grade,
teacher,
} = req.body;
let hashpassword = bcrypt.hashSync(password, 10);
let updateStudent = {
first_name,
last_name,
email,
password : hashpassword,
phone,
shirt_size,
grade,
teacher,
}
Student.update(updateStudent, {
where: {
id: req.params.id
}
})
.then(response => {
res.status(200).json({success: true, message: "Student updated successfully"})
})
.then(err => res.json({err: err.message}));
} catch (e) {
res.status(500).json({ error: e});
}
}
static async archiveStudent(req, res) {
try{
let id = req.params.id
await Student.findAll({
where: {id: id}
})
.then(result => {
if (result.length == 1){
Student.update(
{is_Archived: true},
{ where: {id: id}}
)
}
})
} catch (e) {
res.status(500).json({ error: e});
}
}
}
module.exports = StudentContoller;
student.routs.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const checkJWT = require('../middlewares/check-token');
const StudentController = require('../controllers/student.controller');
//
//
router.get('student/login', StudentController.login);
router.post('student/register', checkJWT,StudentController.registerStudent);
module.exports = router;
Sever js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');
const app = express();
// Middlewars
app.use(cors()); // Enable Cors
app.use(morgan('dev')); // Enable Logging
app.use(helmet()); // Enables Security Headers
app.use(bodyParser.json()); // Parses requrests of type application/json
app.use(bodyParser.urlencoded({ extended: true})); // Parses request of application/x-www-form-urlencode
//Add database connection
const db = require('./models');
//db.sequelize.sync().then(()=> initial())
// Routing
app.get('/', (req, res)=> res.json({message: "Base API URL"}));
const studnetRoute = require('./routes/student.routes');
app.use('/', studnetRoute);
const PORT = process.env.PORT || 3300;
app.listen(PORT, () => console.log(`Server Running on port ${PORT}`));
you can do like this in your router file .
router.route('student/login')
.get(StudentController.login);
router.route('student/register')
.post(checkJWT, StudentController.registerStudent);
I'm have an express server with MongoDB and Mongoose, and using passport to authenticate with JWT, but getting an error as in the title.
I'm following the passport-jwt documentation, but am still getting the error. What am I doing wrong?
Here is the error message when doing GET call on localhost3090 with a valid JWT:
::1 - - [16/Mar/2018:05:35:47 +0000] "GET / HTTP/1.1" 500 1677 "-" "PostmanRuntime/7.1.1"
Error: passport.initialize() middleware not in use
at IncomingMessage.req.login.req.logIn (/Users/okadachikara/react-courses/projects/server/node_modules/passport/lib/http/request.js:46:34)
at JwtStrategy.strategy.success (/Users/okadachikara/react-courses/projects/server/node_modules/passport/lib/middleware/authenticate.js:248:13)
at verified (/Users/okadachikara/react-courses/projects/server/node_modules/passport-jwt/lib/strategy.js:115:41)
at /Users/okadachikara/react-courses/projects/server/services/passport.js:34:7
at /Users/okadachikara/react-courses/projects/server/node_modules/mongoose/lib/model.js:3930:16
at _init (/Users/okadachikara/react-courses/projects/server/node_modules/mongoose/lib/query.js:2007:5)
at model.Document.init (/Users/okadachikara/react-courses/projects/server/node_modules/mongoose/lib/document.js:393:5)
at completeOne (/Users/okadachikara/react-courses/projects/server/node_modules/mongoose/lib/query.js:1993:12)
at Immediate.<anonymous> (/Users/okadachikara/react-courses/projects/server/node_modules/mongoose/lib/query.js:1520:11)
at Immediate._onImmediate (/Users/okadachikara/react-courses/projects/server/node_modules/mquery/lib/utils.js:119:16)
at runCallback (timers.js:773:18)
at tryOnImmediate (timers.js:734:5)
at processImmediate [as _immediateCallback] (timers.js:711:5)
My server/controllers/authentication.js:
const User = require('../models/user');
const jwt = require('jwt-simple');
const config = require('../config');
function tokenForUser(user) {
const timestamp = new Date().getTime();
return jwt.encode({ sub: user.id, iat: timestamp }, config.secret);
}
exports.signup = function (req, res, next) {
const email = req.body.email;
const password = req.body.password;
if (!email || !password) {
return res.status(422).send({ error: 'You must provide an email and
password' });
}
// see if user with the given email exists
User.findOne({ email: email }, function (err, existingUser) {
if (err) { return next(err); }
if (existingUser) {
return res.status(422).send({ error: 'A user with that email
already exists' });
}
const user = new User({
email: email,
password: password
});
user.save(function (err) {
if (err) { return next(err); }
res.json({ token: tokenForUser(user), iat: jwt.iat });
});
});
};
My server/services/passport.js
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models/user');
const config = require('../config');
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'),
secretOrKey: config.secret
};
const jwtLogin = new JwtStrategy(jwtOptions, function (payload, done) {
User.findById(payload.sub, function (err, user) {
if (err) { return done(err, false); }
if (user) {
done(null, user);
} else {
done(null, false);
}
});
});
passport.use(jwtLogin);
My server/router.js
const passport = require('passport');
const Authentication = require('./controllers/authentication');
const passportService = require('./services/passport');
const requireAuth = passport.authenticate('jwt', { sesssion: false });
module.exports = function (app) {
app.get('/', requireAuth, function (req, res) {
res.send({ hi: 'there' });
});
app.post('/signup', Authentication.signup);
};
You need to initialize the passport module before using it:
let app = express();
app.use(passport.initialize());