I am trying to implement a mechanism to allow new users to verify their account with a link sent to their email address.
Here's is my subscribe route in routes/user/index.js, which is working to send the email correctly:
require('dotenv').config();
const { v4 } = require('uuid');
const nodemailer = require('nodemailer');
const sibApiV3Sdk = require('sib-api-v3-sdk');
const express = require('express');
const stripe = require('../middlewares/stripe')
const db = require("../models");
const cors = require('cors');
const User = db.user;
const Feedback = db.feedback;
const defaultClient = sibApiV3Sdk.ApiClient.instance;
// Configure API key authorization: api-key
const apiKey = defaultClient.authentications['api-key'];
apiKey.apiKey = process.env.SENDINBLUE_API_KEY;
const transactionalEmailsApi = new sibApiV3Sdk.TransactionalEmailsApi();
// Configure nodemailer
const transporter = nodemailer.createTransport({
host: "smtp-relay.sendinblue.com",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: "noreply#redacted.ai",
pass: "redacted"
}
});
// Define generateVerificationToken function
function generateVerificationToken() {
return v4();
}
// Prepare Core Router
let app = express.Router()
app.post('/stripe/subscribe', async (req, res) => {
const domainURL = process.env.DOMAIN;
const { priceId, trial } = req.body
try {
let user = await User.findOne({ _id: req.user._id });
let customer = user.customerId ? { customer: user.customerId } : {customer_email: user.email};
const subscription_data = trial ? { trial_period_days: 7 } : {};
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
...customer,
line_items: [
{
price: priceId,
quantity: 1,
},
],
subscription_data,
success_url: `${domainURL}/index.html`,
cancel_url: `${domainURL}/signup/failed`,
});
let credits = 0;
if (priceId === process.env.STRIPE_PRODUCT_FREE) {
credits = 0;
} else if (priceId === process.env.STRIPE_PRODUCT_ENTRY) {
credits = 0;
} else if (priceId === process.env.STRIPE_PRODUCT_PRO) {
credits = 0;
}
// Check if user already has a verification_token
if (!user.verification_token) {
const token = generateVerificationToken();
console.log("generated token: ", token);
const updatedUser = await User.findOneAndUpdate({ _id: req.user._id },
{
credits: credits,
verification_token: token,
isVerified: false
},
{
new: true
});
console.log("updatedUser: ", updatedUser);
const verificationLink = `${domainURL}/verify?token=${updatedUser.verification_token}`;
const mailOptions = {
from: 'noreply#redacted.ai',
to: user.email,
subject: 'Verify your email address',
html: `Please click on the following link to verify your email address: ${verificationLink}`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
} else {
console.log('User already has a verification token.');
}
const updateResult = await User.updateOne({_id: req.user._id}, {verification_token: user.verification_token}, {upsert: true});
console.log("updateResult: ", updateResult);
// Send verification email after stripe step completes
if (!user.isVerified) {
const verificationLink = `${domainURL}/verify?token=${user.verification_token}`;
const mailOptions = {
from: 'noreply#redacted.ai',
to: user.email,
subject: 'Verify your email address',
html: `Please click on the following link to verify your email address: ${verificationLink}`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
// Redirect user to the verify route
res.redirect(303, session.url);
} catch (e) {
res.status(400);
return res.send({
error: {
message: e.message,
}
});
}
});
Immediately after this, I then have a verify route:
app.get('/verify', cors(), async (req, res) => {
const { token } = req.query;
try {
// Find user with matching token
let user = await User.findOne({ verification_token: token });
if (!user) {
throw new Error('Invalid token');
}
// Update user verification status
user.isVerified = true;
const savedUser = await user.save();
// Send verification email
if (savedUser.isVerified) {
const mailOptions = {
from: 'noreply#redacted.ai',
to: savedUser.email,
subject: 'Email Verified!',
html: `Your email has been verified! Welcome to redacted.ai`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
// Redirect user to success page
res.redirect('/index.html#/verify/success');
} catch (e) {
res.status(400);
return res.send({
error: {
message: e.message,
}
});
}
});
I also have a file called src/verify.js to deal with things on the front-end:
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
function Verify(props) {
const [isVerified, setIsVerified] = useState(false);
const { token } = props.match.params;
const history = useHistory();
useEffect(() => {
// logic to process token and set isVerified to true/false
fetch(`/verify/${token}`)
.then(res => res.json())
.then(data => {
if (data.isVerified) {
setIsVerified(true);
}
});
}, [token]);
useEffect(() => {
// redirect to success page if isVerified is true
if (isVerified) {
history.push('/index.html#/verify/success');
}
}, [isVerified, history]);
return (
<div>
{isVerified ? (
<div>Your email has been verified! Welcome to redacted.ai</div>
) : (
<div>Invalid token.</div>
)}
</div>
);
}
export default Verify;
However, when the user clicks on the link in their email, the verify route does not initiate. Rather, nothing happens at all in the terminal except for profile refresh, and I'm not sure why.
As an aside, I can see in the database that the new user has a verification_token and isVerfied is set to false. That verification_token matches the one sent in the email.
I'm very new to Node JS so suspect I'm missing something obvious! Thanks so much in advance.
PS.
As a work around, I attempted to try a different approach, and created a form that the user could go to and copy and paste in their verification_token: the form looked like this:
import React, { useState } from 'react';
import TOOLS from './tools'
import config from './config'
import Header from './Header'
let baseURL = config.baseURL
const VerifyPage = () => {
const [verificationToken, setVerificationToken] = useState('');
const [error, setError] = useState('');
const [responseData, setResponseData] = useState({});
const handleSubmit = async (event) => {
event.preventDefault();
const token = window.localStorage.getItem('token');
console.log('Token: ', token);
console.log('Authorization header: ', `Bearer ${token}`);
// Make a request to your API to verify the token
try {
const response = await fetch(`${baseURL}user/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verificationToken })
});
console.log('Request URL: ', `${baseURL}user/verify`);
console.log('Request body: ', JSON.stringify({ verificationToken }));
const responseJson = await response.json();
setResponseData(responseJson);
console.log('Response status: ', response.status);
console.log('Response text: ', await response.text());
if (!response.ok) {
throw new Error(responseJson.message);
}
// Show a success message or redirect to another page
} catch (error) {
setError(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="verificationToken">Verification Token:</label>
<input
type="text"
id="verificationToken"
value={verificationToken}
onChange={(event) => setVerificationToken(event.target.value)}
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Verify</button>
{responseData && JSON.stringify(responseData)}
</form>
);
};
export default VerifyPage;
And with this in place, I altered my verify route to looks like this:
app.post("/verify", (req, res) =\> {
console.log('Received request to /verify');
const { token } = req.body;
console.log('Received verification token:', token);
User.findOne({ token }, (err, user) => {
if (err) {
console.error('Error finding user:', err);
return res.status(500).send({ error: "Error verifying account" });
}
if (!user) {
console.error('No user found with provided verification token');
return res.status(400).send({ error: "Invalid Token" });
}
user.isVerified = true;
user.verificationToken = undefined;
user.save((err) => {
if (err) {
console.error('Error saving user:', err);
return res.status(500).send({ error: "Error verifying account" });
}
console.log('User verified successfully');
return res.send({ message: "Account verified successfully" });
});
});
});`
But whenever the user attempted to put in their valid token, they would get something like this:
"Unexpected token 'F', "Forbidden" is not valid JSON"
Moreover, this is all that would show up in the terminal:
`"OPTIONS /api/user/verify 204 0.285 ms - 0 POST /api/user/verify 403 1.558 ms - 9" `
And, under the JWT code, this would show up in the console: "Failed to load resource: the server responded with a status of 403 (Forbidden)"
Truth be told, I'd be happy to get this working either via the form, or by having users click the link in the email. The latter is preferable - but either would work.
Related
Hello i am trying to use my token in my application after user is logged in but am getting an undefined response in my console. Below are my codes. How can i correct my code to be able to access token inside application and use to do other features of the application?
my controller
import User from "../models/user";
import Stripe from "stripe";
const stripe = Stripe(process.env.STRIPE_SECRET);
export const createConnectAccount = async (req, res) => {
console.log(req.user);
try {
const user = await User.findById(req.user._id).exec();
console.log("USER ==> ", user);
if (!user.stripe_account_id) {
const account = await stripe.accounts.create({
type: "express",
});
console.log("ACCOUNT ===>", account);
user.stripe_account_id = account.id;
user.save();
}
} catch (error) {
res.status(500).json();
}
};
my middleware
var { expressjwt: jwt } = require("express-jwt");
// req.user
export const requireSignin = jwt({
//secret, expiryDate
secret: process.env.JWT_SECRET,
algorithms: ["HS256"],
});
my routes
import express from "express";
const router = express.Router();
import { requireSignin } from "../middlewares";
import { createConnectAccount } from "../controllers/stripe";
router.post("/create-connect-account", requireSignin, createConnectAccount);
module.exports = router;
my auth controller
import User from "../models/user";
import jwt from "jsonwebtoken";
export const register = async (req, res) => {
console.log(req.body);
const { name, email, password } = req.body;
if (!name) return res.status(400).send("Name is required");
if (!password || password.length < 6)
return res
.status(400)
.send("Password is required and should be minimum 6 characters long");
let userExist = await User.findOne({ email }).exec();
if (userExist) return res.status(400).send("Email is taken");
const user = new User(req.body);
try {
await user.save();
console.log("User saved successfully", user);
return res.json({ ok: true });
} catch (err) {
console.log("CREATE USER FAILED", err);
return res.status(400).send("Error.Try again");
}
};
export const login = async (req, res) => {
// console.log(req.body);
const { email, password } = req.body;
try {
//check if user with credentials
let user = await User.findOne({ email }).exec();
// console.log("USER EXISTS", user);
if (!user) res.status(400).send("User with email not found");
//compare password
user.comparePassword(password, (err, match) => {
console.log("COMPARE PASSWORD IN LOGIN ERR", err);
if (!match || err) return res.status(400).send("Wrong password");
//("GENERATE A TOKEN THEN SEND AS RESPONSE TO CLIENT");
let token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
res.json({
token,
user: {
_id: user._id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
});
});
} catch (err) {
console.log("LOGIN ERROR", err);
res.status(400).send("Signin failed");
}
};
my terminal output
POST /api/login 200 1142.309 ms - 349
undefined
POST /api/create-connect-account 500 9.092 ms - -
Headers
import axios from "axios";
export const createConnectAccount = async (token) => {
await axios.post(
`${process.env.REACT_APP_API}/create-connect-account`,
{},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
};
I'm sorry to tell you your code has other errors in it.
My guess is that your res is not well written in auth controller, login function :
res.status(201).json({
token :token,
user: user
})
Also when reading your token trying to authenticate : it will be easier to use the same package than the one that sign it.
const jwt = require("jsonwebtoken");
exports. requireSignin = () => {
return async (req, res, next) => {
try {
const token = req?.headers?.authorization?.split(" ")[1];
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
const userId = decodedToken._id;
const user = await User.findOne({ _id: userId });
if (user) {
req.auth = {
user: user,
};
} else {
throw new Error("user not found");
}
next();
} catch (error) {
console.log(error.message);
res.status(401).json({ error: "failed to authenticate" });
}
};
};
But your code is pretty hard to read :
To make it easier to read and clearer for you, try and use joy or yup
Joi : https://www.npmjs.com/package/joi
Yup : https://www.npmjs.com/package/yup
With those you will be able to create middlewares to avoid wrong entries in your body : for example
if (!name) return res.status(400).send("Name is required");
is processed automatically with those packages
Also, you shouldn't use 'import' and 'require' in the same project, choose either one of them
I hope this will help
I am working on a user registration project on ReactJS and got stuck on a problem when I try to activate the user account. The backend server is giving me the error 401: Failed to load resource: the server responded with a status of 401 (Unauthorized). What might be the problem here, I was not able to find out any solution related to my code, can anyone help ??
Here is my code for Activation.jsx:
import React, { useState, useEffect } from 'react';
import authSvg from '../assests/welcome.svg';
import axios from 'axios';
import jwt_decode from 'jwt-decode';
import { isAuth } from '../helpers/auth';
import { Navigate , useParams} from 'react-router-dom';
const Activate = ({ match }) => {
const [formData, setFormData] = useState({
name: '',
token: '',
show: true
});
match = useParams();
useEffect(() => {
let token = match.token;
console.log(token);
let { name } = jwt_decode(token);
if (token) {
setFormData({ ...formData, name, token });
}
console.log(token, name);
}, [match.params]);
const { name, token, show } = formData;
const handleSubmit = e => {
e.preventDefault();
axios
.post(`${process.env.REACT_APP_API_URL}/activation`, {
token
})
.then(res => {
setFormData({
...formData,
show: false
});
console.log(res.data.message);
})
.catch(err => {
console.log(err);
});
};
Here is the code from activation controller :
const User = require('../models/auth.model');
const expressJwt = require('express-jwt');
const _ = require('lodash');
const { OAuth2Client } = require('google-auth-library');
const fetch = require('node-fetch');
const { validationResult } = require('express-validator');
const jwt_decode = require('jwt-decode')
const { errorHandler } = require('../helpers/dbErrorHandling');
const nodemailer = require('nodemailer')
exports.activationController = (req, res) => {
const { token } = req.body;
if (token) {
jwt_decode.verify(token, process.env.JWT_ACCOUNT_ACTIVATION, (err, decoded) => {
if (err) {
console.log('Activation error');
return res.status(401).json({
errors: 'Expired link. Signup again'
});
} else {
const { name, email, password } = jwt_decode(token);
console.log(email);
const user = new User({
name,
email,
password
});
user.save((err, user) => {
if (err) {
console.log('Save error', errorHandler(err));
return res.status(401).json({
errors: errorHandler(err)
});
} else {
return res.json({
success: true,
message: user,
message: 'Signup success'
});
}
});
const apiKey = user._id;
user.apiKey = apiKey
user.save((err) => {
console.log(err);
})
}
});
} else {
return res.json({
message: 'error happening please try again'
});
}
};
As you have said in the comments , I think you have apiKey and path as an object properties in your user model which are not filled , and also the problem is you are using jwt_decode for verify and sign purposes which is wrong , the purpose for jwt_decode here is for decoding the token and nothing else , so use jsonwebtoken library for verfication and sign purposes .
Also remove the api key generation code , as what are you doing you are calling user.save() method 2 times in a row so the first one hasn't even finished saving before the 2nd one gets called .
I created a Backend server and posted it to Heroku and now I am using the server URL to post and get data from it. However when I display an error message it's getting me the status code instead of the actual error.
My Backend login code.
export const loginUser = asyncHandler(async(req, res) => {
const { email, password } = req.body;
const user = await userModel.findOne({ email });
const token = generateToken(user._id)
if(user && (await user.matchPassword(password))){
res.json({
_id:user._id,
username: user.username,
email: user.email,
profilePic:user.profilePic,
admin:user.admin,
token:token,
});
} else {
res.status(400)
throw new Error("invalid email or password please check and try again!");
}
});
My user Actions code since I am suing redux
export const login = (email, password) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const url = `${process.env.REACT_APP_SERVER_URL}api/loginuser`;
const config = {
headers: {
"Content-type": "application/json",
},
};
const { data } = await axios.post(
url,
{
email,
password,
},
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
Error in frontend display
First you need to send an error message from your Back End like so:
export const loginUser = asyncHandler(async(req, res) => {
const { email, password } = req.body;
const user = await userModel.findOne({ email });
const token = generateToken(user._id)
if(user && (await user.matchPassword(password))){
res.json({
_id:user._id,
username: user.username,
email: user.email,
profilePic:user.profilePic,
admin:user.admin,
token:token,
});
} else {
res.status(400).json({errorMessage :"invalid email or password please check and try again!" })
}
});
Then get it in the React part (make sure to read the comment I added after that console.log):
export const login = (email, password) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const url = `${process.env.REACT_APP_SERVER_URL}api/loginuser`;
const config = {
headers: {
"Content-type": "application/json",
},
};
const { data } = await axios.post(
url,
{
email,
password,
},
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
console.log(error); // look at the console, as I may miss the correct retuned error object
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response.data.errorMessage
});
}
};
you need to return response instead of throwing the error:
res.status(400)
res.json({
message: "invalid email or password please check and try again!"
});
I would like to be able to redirect from registration-page to login-page on successfull registration and again from login-page to home-page afteer successfull login.
I dont know what methods to use or where to call them.
This is the register call.
app.post("/api/register", async (req, res) => {
const { username, password: plainTextPassword } = req.body;
const password = await bcrypt.hash(plainTextPassword, 10);
try {
const response = await User.create({
username,
password
})
console.log("User created", response)
} catch (error) {
if (error.code === 11000) {
return res.json({ status: "error", error: "Username already in use" })
}
throw error
}
res.json({ status: "ok" });
});
This is the script
<script>
const form = document.getElementById("reg-form");
form.addEventListener("submit", registerUser);
async function registerUser(event) {
event.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const result = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username,
password
})
}).then((res) => res.json())
if (result.status === "ok") {
alert("Success");
} else {
alert(result.error)
}
}
</script>
</body>
</html>
You should return the line that redirects
return res.redirect('/UserHomePage');
In React JS App there is forgot password functionality, on entering email end users get password reset link if the user's email exists in DB. When user clicks the link inside email it gets redirect and load blank screen. I can't figure out the problem, I had checked all routes and looks like perfect.
This is ResetPassword Component where user can enter set his new password.
const ResetPassword = (props) => {
const userId = props.match.params.id
const [password,setPassword] = useState({
password: '', confirmPassword: ''
})
const baseUrl = process.env.REACT_APP_baseUrl
const changePassUrl = `${baseUrl}/user/reset/${userId}`
const history = useHistory()
const resetPassword = async e => {
e.preventDefault()
const options = {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(password)
}
await fetch(changePassUrl, options)
.then(response => response.json())
.then(user => {
console.log('Success:', user);
if(user.status===true){
toast.success('Password changed Successfully. Login with New Password',{position: 'top-center', autoClose: 8000})
history.push('/login')
}
else if(user.status === false){
toast.error('Please try again!',{position: 'top-center', autoClose: 8000})
}
})
.catch((error) => {
console.error('Error:', error);
});
}
App.js route of above component.
<Route exact path='/reset/:id' component={ResetPassword}/>
Node API that sends the reset link via NodeMailer.
module.exports.resetUserPassword = (req, res) => {
User.findOne({email: req.body.email},
(err, users) => {
if (err) {
res.status(500).json({
status: false,
message: 'some error occured in Finding User.',
error: err,
});
}
if (users) {
const url = process.env.Url
var transporter = nodemailer.createTransport({
host: process.env.mailHost,
port: 587,
auth: {
user: process.env.mailUser,
pass: process.env.mailPass
}
});
var mailOptions = {
from: process.env.mailUser,
to: users.email,
subject: 'Password Reset Link',
html: `
<div style='color: #000'>
<h2>Hello ${users.fullName},</h2>
<h3>A request has been received to change the password for your Jobs#MyCityMyStore Account.</h3>
<h3>Click here to reset your password</h3>
<p>If you did not initiate this request, Please contact us immediately at </p><a href='#'>support#support.com</a><br/>
<p>Thank you.</p>
<p>support#support Team</p>
</div>`
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
res.status(200).json({ status: true, email: users.email });
}else{
res.status(200).json({status: false, data: 'User not found '+users})
}
}
);
};