authentication with Next.js and MongoDB? - javascript

I'm working on this web application, precisely on the authentication system.
I choosed Next.js (with next-auth) and MongoDB to store all the data.
Now, I succesfully implement the google authentication with next-auth, but I've found so hard to implement authentication through credentials (for signing in and registering a new user)
I've created two new pages for login and register under the /pages folder
I'm using formik for inputs validation so all the onChange methods are handled by formik
login.js
import {Card, InputGroup, Button, Meta} from "../components/ui";
import {AuthNavigationLink} from "../components/navigation";
import { signIn } from "next-auth/react"
import {useFormik} from "formik";
import loginValidate from "../utils/validation/validate";
import {useRouter} from"next/router"
function Login() {
const router = useRouter()
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
validate: loginValidate,
onSubmit
})
async function onSubmit(credentials) {
const status = await signIn("Credentials", {
redirect: false,
email: credentials.email,
password: credentials.password,
})
console.log(status)
}
async function handleGoogleSignIn() {
await signIn("google", {
callbackUrl: "/"
})
}
return <>
<Meta title="Login"/>
<Card>
<div className="mb-10">
<h2 className="text-4xl">Login</h2>
</div>
<form onSubmit={formik.handleSubmit}>
<InputGroup
type="email"
htmlFor="email"
label="Email"
name="email"
{...formik.getFieldProps('email')}
error={formik.errors.email}
isTouched={formik.touched.email}
/>
<InputGroup
type="password"
htmlFor="password"
label="Password"
name="password"
{...formik.getFieldProps('password')}
error={formik.errors.password}
isTouched={formik.touched.password}
/>
<div className="flex flex-col lg:flex-row gap-5">
<Button type="submit">Accedi</Button>
<Button onClick={handleGoogleSignIn}>Accedi con Google</Button>
</div>
</form>
<AuthNavigationLink
href="/register"
text="Non sei registrato?"
linkText="Registrati!"
/>
</Card>
</>
}
export default Login;
register.js
import {Card, InputGroup, Button, Meta} from "../components/ui";
import {AuthNavigationLink} from "../components/navigation";
import {useFormik} from "formik";
import {registerValidate} from "../utils/validation/validate";
function Register() {
const formik = useFormik({
initialValues: {
username: "",
email: "",
password: "",
confirmPassword: ""
},
validate: registerValidate,
onSubmit
})
async function onSubmit(credentials) {
await fetch("http:localhost:3000/api/register", {
method: "POST",
body: {
username: credentials.username,
email: credentials.email,
password: credentials.password,
}
})
}
return <>
<Meta title="Register"/>
<Card>
<div className="mb-10">
<h2 className="text-4xl">Registrati</h2>
</div>
<form onSubmit={formik.handleSubmit}>
<InputGroup
htmlFor="username"
label="Username"
name="username"
{...formik.getFieldProps('username')}
error={formik.errors.username}
isTouched={formik.touched.username}
/>
<InputGroup
type="email"
htmlFor="email"
label="Email"
name="email"
{...formik.getFieldProps('email')}
error={formik.errors.email}
isTouched={formik.touched.email}
/>
<InputGroup
type="password"
htmlFor="password"
label="Password"
name="password"
{...formik.getFieldProps('password')}
error={formik.errors.password}
isTouched={formik.touched.password}
/>
<InputGroup
type="password"
htmlFor="confirmPassword"
label="Conferma Password"
name="confirmPassword"
{...formik.getFieldProps('confirmPassword')}
error={formik.errors.confirmPassword}
isTouched={formik.touched.confirmPassword}
/>
<Button type="submit">Registrati</Button>
</form>
<AuthNavigationLink
href="/login"
text="GiĆ  registrato?"
linkText="Effettua il Login!"
/>
</Card>
</>
}
export default Register;
this is my [...next-auth.js] file
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from "next-auth/providers/credentials";
import connectDB from "./lib/connectDB";
import Users from "../../../models/userModel"
connectDB();
export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
async authorize(credentials, req) {
const email = credentials.email;
const password = credentials.password;
const user = await Users.findOne({ email })
if (!user) {
throw new Error("You haven't registered yet")
}
if (user) return signInUser({ password, user })
}
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
}),
]
})
userModel.js
import mongoose from 'mongoose'
const userSchema = new mongoose.Schema({
name: {
type: String,
default: 'guest'
},
email: {
type: String
},
password: {
type: String
},
}, { timestamps: true })
let Dataset = mongoose.models.users || mongoose.model('users', userSchema)
export default Dataset;
this is the endpoint to register a new user under api/register.js
import bcrypt from 'bcryptjs'
import Users from '../models/userModel'
export default async function handler(req, res) {
const body = req.body
const user = await Users.findOne({ email: body.email })
if (user) {
res.status(200).json({ message: 'already registered' })
return;
}
const salt = await bcrypt.genSalt(10);
const hashPass = await bcrypt.hash(body.password, salt)
const newUser = new Users({ email: body.email, password: hashPass })
await newUser.save()
res.status(200).json({ message: 'success' })
}
this is the file where I'm trying to connect to MongoDB, with no success at least, under /api/auth/lib/connectDB.js
import mongoose from 'mongoose'
const connectDB = () => {
if(mongoose.connections[0].readyState){
console.log('Already connected.')
return;
}
mongoose.connect(process.env.MONGO_URL, err => {
if(err) throw err;
console.log('Connected to mongodb.')
})
}
export default connectDB;
I don't understand why when I try to register a new user using my custom page it throws me a 404, and If I try to Login it redirects me to a default next-auth page.
Sorry if it's a bit messy, but I'm sure something is missing or something is wrong somewhere.
So the problems that I wanna solve are:
Succesfully connect to MongoDB, and how to know if I'm connected to the cluster
Register a new user and send data to MongoDB
Login with the data previously registered
What's missing? It's the first time for me with both Next.js (with the next-auth package) and MongoDB so I really need some help to understand all the process.
Thanks guys

Related

State not updating in application

I am trying to get my application to update the state on my header once I sign in. It was initially doing so but stopped after creating a few sample users. I am not sure whether that is a problem with my Firestore settings or some code in the app is causing this, but it is quite frustrating. This error also prevents me from signing out any users, thus the statement below in my App.js file keeps outputting the last user that signed in, even after refreshing the page.
console.log('THE USER IS >>> ', authUser)
fbConfig.jsx file
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore/lite'
import { getAuth } from 'firebase/auth'
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: 'xxxx',
authDomain: 'xxxx',
projectId: 'xxxx',
storageBucket: 'xxxx',
messagingSenderId: 'xxxx',
appId: 'xxxx',
measurementId: 'xxxx'
}
//Init firebase app
const firebaseApp = initializeApp(firebaseConfig)
//init services
const db = getFirestore(firebaseApp)
const auth = getAuth(firebaseApp)
export { auth, db }
Login.jsx
import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { auth } from '../../fbConfig'
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword
} from 'firebase/auth'
import { icon } from '../../constants'
import './Login.css'
const Login = () => {
const navigate = useNavigate()
const { email, setEmail } = useState('')
const { password, setPassword } = useState('')
const signIn = (e) => {
e.preventDefault()
signInWithEmailAndPassword(auth, email, password)
.then((auth) => {
// Signed in
const user = auth.user
console.log('success' + user + 'registered')
navigate('/', { replace: true })
// ...
})
.catch((error) => {
alert(error.message)
})
}
const register = (e) => {
e.preventDefault()
createUserWithEmailAndPassword(auth, email, password)
.then((auth) => {
// Signed in
const user = auth.user
console.log('success' + user + 'registered')
// ...
})
.catch((error) => {
alert(error.message)
// const errorCode = error.code
// const errorMessage = error.message
})
}
return (
<div className='login'>
<div className='login__mainContainer'>
<Link to='/'>
<img className='login__logo' src={icon} alt='Webiste logo' />
</Link>
<div className='login__container'>
<h1>Sign-in</h1>
<form>
<div className='login__input'>
<h5>E-mail</h5>
<input
className='login__inputEmail'
type='text'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<span className='login__inputSpan'>* Required</span>
</div>
<div className='login__input'>
<h5>Password</h5>
<input
className='login__inputPassword'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<span className='login__inputSpan'>* Required</span>
</div>
<button
type='submit'
className='login__signInButton'
onClick={signIn}
>
Sign In
</button>
</form>
<button className='login__registerButton' onClick={register}>
Create your account
</button>
</div>
</div>
</div>
)
}
export default Login
Reducer.jsx file
export const initialState = {
basket: [],
user: null
}
// Selector
export const getBasketTotal = (basket) =>
basket?.reduce((amount, item) => item.price + amount, 0)
const reducer = (state, action) => {
console.log(action)
switch (action.type) {
case 'ADD_TO_BASKET':
return {
...state,
basket: [...state.basket, action.item]
}
case 'SET_USER':
return {
...state,
user: action.user
}
default:
return state
}
}
export default reducer
App.js
import { useEffect } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Header, Home, Login } from './components'
import { auth } from './fbConfig'
import { useStateValue } from './contextAPI/StateProvider'
import './App.css'
function App() {
const [dispatch] = useStateValue()
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
console.log('THE USER IS >>> ', authUser)
if (authUser) {
// the user just logged in / the user was logged in
dispatch({
type: 'SET_USER',
user: authUser
})
} else {
// the user is logged out
dispatch({
type: 'SET_USER',
user: null
})
}
})
})
return (
<Router>
<div className='app'>
<Header />
<Routes>
<Route path='/login' element={<Login />} />
<Route path='/' element={<Home />} />
</Routes>
</div>
</Router>
)
}

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'type') redux reactjs

Getting the following error while dispatching authenticating users from firebase and firestore. I'm using redux toolkit, I'm registering user with authentication with firestore and after that getting the user information to store in the redux and therefore dispatching the action to send user information. In the firebase I can see the user is register but can't able to see in redux for the payload and getting this error.
Error ScreenShort
store.js
import { configureStore } from '#reduxjs/toolkit';
import userReducer from '../features/counter/userSlice';
export const store = configureStore({
reducer: {
user: userReducer,
},
});
userSlice.js
import { createSlice } from '#reduxjs/toolkit';
// import { fetchCount } from './counterAPI';
const initialState = {
user: null,
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
login: (state, action) => {
return state.user = action.payload;
},
logout: (state) => {
return state.user = null;
},
},
});
export const { login, logout } = userSlice.actions;
//selector
export const selectUser = (state) => state.user.user;
export default userSlice.reducer;
Login.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { auth } from './firebase';
import './Login.css'
import login from './features/counter/userSlice'
function Login() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [pic, setPic] = useState("")
const dispatch = useDispatch();
const register = () => {
if (!name) {
return alert('Please enter a full name')
}
auth.createUserWithEmailAndPassword(email, password)
.then((userAuth) => {
userAuth.user
.updateProfile({
displayName: name,
photoURL: pic,
})
.then(() => {
dispatch(login({
email: userAuth.user.email,
uid: userAuth.user.uid,
displayName: name,
photoUrl: pic,
})
);
});
})
.catch((error) => alert(error));
}
const loginToApp = (e) => {
e.preventDefault();
}
return (
<div className='login'>
<h2>You are not logedin</h2>
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTv5NTn4Iw_QsC7kW0Lbw3LrlPcPAHso2l9A&usqp=CAU" alt='logo' />
<form>
<input placeholder='Full name (Required if registering)' type="text" value={name} onChange={e => setName(e.target.value)} />
<input placeholder='Profile pic (Optional)' type="text" value={pic} onChange={e => setPic(e.target.value)} />
<input placeholder='Email (Required if registering)' type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input placeholder='Password (Required if registering)' type="password" autoComplete='off' value={password} onChange={e => setPassword(e.target.value)} />
<button onClick={loginToApp}>Sign In</button>
</form>
<p>Not a member?{" "}
<span className='login__register' onClick={register}>Register Now</span>
</p>
</div>
);
}
export default Login;
I encountered this error today and it's due to the wrong importation of Login action in Login.js file.Change
import login from './features/counter/userSlice'
to
import { login } from './features/counter/userSlice'

React Context and Provider data doesn't get passed down as expected

I am trying to update an object that another component is using.
My Context:
import React, {Component, createContext} from "react";
import jwt from 'jsonwebtoken';
const LOCAL_STORAGE_AUTH_TOKEN = 'authToken';
interface AuthenticatedUser {
username?: string;
guildName?: string;
guildId?: string;
}
interface AuthContextType {
authenticated: boolean; // to check if authenticated or not
user: AuthenticatedUser; // store user details
token: string; //jwt token
refreshToken: string; //jwt refresh token
handleAuthentication: (username: string, password: string) => Promise<void>; // handle login process
logout: () => Promise<void>; // log out the user
}
export const AuthContext = createContext<AuthContextType>({
authenticated: false, // to check if authenticated or not
user: {}, // store all the user details
token: '', // store all the user details
refreshToken: '', //jwt refresh token
handleAuthentication: (username: string, password: string): Promise<void> =>
Promise.resolve(), // handle login process
logout: (): Promise<void> => Promise.resolve(), // logout the user
});
AuthContext.displayName = 'AuthContext';
export class AuthProvider extends Component {
state = {
authenticated:false,
user: {},
token:'',
refreshToken:''
};
constructor(props: any) {
super(props);
const token = window.localStorage.getItem(LOCAL_STORAGE_AUTH_TOKEN) || '';
const jwtData = jwt.decode(token);
let user = {};
let authenticated = false;
let refreshToken = '';
if(jwtData && typeof jwtData !== 'string'){
authenticated = true;
user = {
username: jwtData.data?.username || '',
// TODO: Add the other sources too
}
}
this.state = {
authenticated,
user,
token,
refreshToken
}
}
handleAuthentication = async (username: string, password: string):Promise<void> => {
fetch(process.env.REACT_APP_API_ENDPOINT + "users/login", {
method:"POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({username:username, password:password})
})
.then(response => response.json())
.then((data) => {
/*
This data is coming back correctly
*/
const user = {
username: data?.user.username,
guildName: data?.user.guild_information[0].guildName,
guildId: "123123"
}
this.setState({
authenticated: true,
user: user,
token: data?.token,
refreshToken: data?.refreshToken
})
window.localStorage.setItem(LOCAL_STORAGE_AUTH_TOKEN, data?.token)
//TODO: Also save token in localstorage
})
.catch((err) => {
console.log(err)
})
}
logout = async ():Promise<void> => {
//TODO: Log out the current user
}
render() {
const authProviderValue = {
...this.state,
handleAuthentication: this.handleAuthentication,
logout: this.logout
}
return (
<AuthContext.Provider value={authProviderValue}>
{this.props.children}
</AuthContext.Provider>
)
}
}
And the App component where I use it:
import React, {useContext} from "react";
import { useStyles} from "./style";
import {AuthContext, AuthProvider} from "../../context/UserContext";
import RoutesProvider from "./Routes";
import Login from "../Login";
export default function App() {
const classes = useStyles();
const { user } = useContext(AuthContext)
return (
<AuthProvider>
{typeof user.username !== "undefined" ? (
<div className={classes.content}>
<RoutesProvider />
</div>
):(
<Login />
)}
</AuthProvider>
)
}
Also wrapped the App component within the AuthProvider as recommended:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { SnackbarProvider } from 'notistack';
import { AuthProvider} from "./context/UserContext";
import './styles/index.css'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
It does take the initial value if I change it in the AuthContext. But after updating it does not update in the App component. I'm a little confused as to why this doesn't work as expected. If anyone knows what causes this (or if I'm doing it wrong) let me know please. Currently 5 hours into debugging...
If I understand your question correctly, you're asking:
"Why your user constant doesn't update when your handleAuthentication method is successful"
The answer is because you're initializing your user constant outside of your provider.
Use your AuthProvider in your index.tsx instead of your App component like this:
<AuthProvider>
<App />
</AuthProvider>
Or transfer your context logic in a children component

On Register, User is created in the database, but JWT token is "undefined" when inspect in browser

I am trying to register a user and then after registering, have a token in the browser that persists. Currently, the user is registered and added to the database, but when I Inspect, there is no token (token: "undefined" in state and props) in the session and the page does not redirect away from the Register page.
In the console, my error is:
POST http://localhost:5000/register 404 (Not Found)
Error: Request failed with status code 404
at createError (createError.js:16)
at settle (settle.js:17)
at XMLHttpRequest.handleLoad (xhr.js:61)
React
App.js
import React from "react";
import "./style.scss";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import HomePage from "./home_page.js";
import TitlePage from "./title_page.js";
import AboutPage from "./about_page.js";
import BarberPage from "./barbers_page.js";
import ContactPage from "./contact_page.js";
import GalleryPage from "./gallery_page.js";
import LocationPage from "./location_page.js";
import SeminarsTrainingPage from "./seminars_training_page.js";
import ServicesPage from "./services_page.js";
import AppointmentsPage from "./appointments_page.js";
import RegisterPage from "./views/pages/RegisterPage";
import { connect } from "react-redux";
class App extends React.Component {
state = {
token: sessionStorage.getItem("token")
};
onRegister = token => {
sessionStorage.setItem("token", token);
this.setState({ token });
};
render() {
return (
<>
<BrowserRouter>
<div>
<Route exact path="/home" component={HomePage} />
<Route exact path="/about" component={AboutPage} />
<Route exact path="/barbers" component={BarberPage} />
<Route exact path="/contact" component={ContactPage} />
<Route exact path="/gallery" component={GalleryPage} />
<Route exact path="/location" component={LocationPage} />
<Route exact path="/seminars-training" component={SeminarsTrainingPage} />
<Route exact path="/services" component={ServicesPage} />
<Route exact path="/appointments" component={AppointmentsPage} />
<Route exact path="/titlepage" component={TitlePage} />
<Route
exact
path="/register"
render={props => {
return <RegisterPage {...props} onRegister={this.onRegister} />;
}}
/>
</div>
</BrowserRouter>
</>
);
}
}
const mapStateToProps = state => {
return {
token: state.auth.token
};
};
export default connect(mapStateToProps)(App);
RegisterPage.js
import React, { Component } from "react";
import RegisterForm from "../../components/Forms/fields/RegistrationForm";
class RegisterPage extends Component {
render() {
console.log(this.props);
return(
<div>
<h1>Register a new user</h1>
<RegisterForm onRegister={this.props.onRegister} />
</div>
);
}
}
export default RegisterPage;
RegistrationForm.js
import React, { Component } from "react";
import axios from "axios";
import { withRouter } from "react-router-dom";
import { setAuthToken } from "../../../actions";
import { connect } from "react-redux"
class RegisterForm extends Component {
state = {
email: "",
password: ""
};
onFormSubmit = event => {
event.preventDefault();
const { email, password } = this.state;
axios
.post('http://localhost:5000/register', { email, password })
// .then(res => console.log(res))
// .catch(err => console.log(err))
.then(res => { this.props.onRegister(res.data.token); this.props.history.push('/') })
.catch(err => console.error(err))
};
onInputChange = (name, event) => {
this.setState({ [name]: event.target.value });
};
render() {
// console.log(this.props);
const { email, password } = this.state;
return (
<form onSubmit={this.onFormSubmit}>
<p>
<label htmlFor="email">Email</label>
<input
type="email"
value={email}
onChange={event => this.onInputChange("email", event)}
/>
</p>
<p>
<label htmlFor="email">Password</label>
<input
type="password"
value={password}
onChange={event => this.onInputChange("password", event)}
/>
</p>
<p>
<input type="submit" value="Register New User" />
</p>
</form>
);
}
}
export default connect(null, {
setAuthToken
})(withRouter(RegisterForm));
Express
index.js
const express = require("express");
const router = express.Router();
const passport = require("passport")
const AuthRoutes = require("./auth_routes");
const PageController = require('../controller/page_controller')
const AuthController = require('../controller/auth_controller')
const { authRedirect, authorise } = require('../middleware/auth_middleware')
router.use("/auth", AuthRoutes);
router.get('/dashboard', passport.authenticate('jwt', {session: false}), PageController.dashboard)
router.get("/", PageController.index);
router.post('/login', passport.authenticate('jwt', {
successRedirect: "/dashboard",
failureRedirect: "/login",
session: false,
}), AuthController.loginCreate)
module.exports = router;
auth_routes.js
const express = require("express");
const router = express.Router();
const { celebrate, Joi } = require("celebrate");
const AuthController = require("../controller/auth_controller");
router.post("/register", celebrate({
body: {
email: Joi.string().email().required(),
password: Joi.string().required()
}
}), AuthController.register);
module.exports = router;
auth_controller.js
const { UserModel } = require('../database/models/User')
const JWTService = require("../services/jwt_service");
function register(req, res, next) {
const { email, password } = req.body;
const user = new UserModel({ email });
UserModel.register(user, password, (err, user) => {
if (err) {
return next(new HTTPError(500, err.message));
}
const token = JWTService.generateToken(user);
return res.json({ token });
});
}
async function logout(req, res) {
req.logout()
res.redirect('/')
}
async function loginNew(req, res) {
res.render('pages/login')
}
async function loginCreate(req, res) {
const token = jwt.sign({ sub: req.user._id }, process.env.SESSION_SECRET)
res.json(token)
}
module.exports = {
register,
logout,
loginNew,
loginCreate
}
User.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const UserSchema = new Schema({
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
});
const UserModel = mongoose.model("user", UserSchema)
module.exports = { UserModel, UserSchema }
jwt_services.js
const JWT = require("jsonwebtoken");
const expiry = "1d";
function generateToken(user) {
const token = JWT.sign(
{
email: user.email
},
process.env.SESSION_SECRET,
{
subject: user._id.toString(),
expiresIn: expiry
}
);
return token;
}
module.exports = {
generateToken
}
server.js
const express = require("express");
const exphbs = require("express-handlebars");
const morgan = require("morgan");
const mongoose = require("mongoose")
const cors = require('cors')
require ("dotenv").config()
const app = express();
app.use(cors({
origin: process.env.FRONT_END_DOMAIN
}))
app.engine("handlebars", exphbs({defaultLayout: "main"}));
app.set("view engine", "handlebars");
mongoose
.connect(
"mongodb://localhost/raw_barbershop",
{ autoIndex: false, useNewUrlParser: true }
)
.then(() => console.log("MongoDB successfully connected"))
.catch(err => console.log(err));
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
const passport = require('./config/passport')
app.use(passport.initialize())
app.use(passport.session())
app.use(morgan("combined"));
app.use(require("./routes"));
app.use(express.static("public"));
app.use(require("./middleware/error_handler_middleware"));
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server up and running on port ${port} !`));
module.exports = app;
You must JSON.stringify your data while using sessionStorage.setItem and JSON.parse while reading it.
Also, in your mapStateToProps, you have this:
return {
token: state.auth.token
};
};
But this is how your state looks like: state = { token: 'value' }
Make sure you have the same structure.

Rendering error messages from failed authentication in React-Redux and React Router DOM v4

TL;DR version: In a React-Redux project with React Router DOM v4 how do I take the server response from an axios.put in an ./actions file and import into my "smart components" in ./containers so it can be used in auth logic and rendered as messages for the user (e.g., "Invalid credentials").
Long version: So I have been learning React-Redux and started with the Stephen Grider courses on Udemy. What is adding to my confusion stems from the fact that the intro/intermediate course is in React Router DOM v4 and the advanced course is in React Router v2. Spending a lot of time converting over to v4.
Anyway, getting protected routes and rerouting after successful login stuff sorted at. The problem I have now is getting responses from the server into a Redux-React container.
For example, if the login credentials are wrong, the login API responds with a 400 error. I should be able to take this response and render a message like The credentials provided are invalid. I can get the response in my ./actions/authentication file and console.log() it, but just can't get it into the ./containers/authentication/signin.js.
The Stephen Grider tuts using React Router v2 work fine. Here is the code from his project, followed by mine:
// ./actions/index.js
import axios from 'axios';
import { browserHistory } from 'react-router';
import {
AUTH_USER,
UNAUTH_USER,
AUTH_ERROR,
FETCH_MESSAGE
} from './types';
const ROOT_URL = 'http://localhost:3090';
export function signinUser({ email, password }) {
return function(dispatch) {
axios.post(`${ROOT_URL}/signin`, { email, password })
.then(response => {
dispatch({ type: AUTH_USER });
localStorage.setItem('token', response.data.token);
// The below no longer works as of React Router DOM v4
browserHistory.push('/feature');
})
.catch(() => {
dispatch(authError('Bad Login Info'));
});
}
}
export function authError(error) {
return {
type: AUTH_ERROR,
payload: error
};
}
// ./components/auth/signin.js
import React, { Component } from 'react';
import { reduxForm } from 'redux-form';
import * as actions from '../../actions';
class Signin extends Component {
handleFormSubmit({ email, password }) {
// Need to do something to log user in
this.props.signinUser({ email, password });
}
renderAlert() {
if (this.props.errorMessage) {
return (
<div className="alert alert-danger">
<strong>Oops!</strong> {this.props.errorMessage}
</div>
);
}
}
render() {
const { handleSubmit, fields: { email, password }} = this.props;
return (
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<fieldset className="form-group">
<label>Email:</label>
<input {...email} className="form-control" />
</fieldset>
<fieldset className="form-group">
<label>Password:</label>
<input {...password} type="password" className="form-control" />
</fieldset>
{this.renderAlert()}
<button action="submit" className="btn btn-primary">Sign in</button>
</form>
);
}
}
function mapStateToProps(state) {
return { errorMessage: state.auth.error };
}
export default reduxForm({
form: 'signin',
fields: ['email', 'password']
}, mapStateToProps, actions)(Signin);
// ./reducers/index.js
import {
AUTH_USER,
UNAUTH_USER,
AUTH_ERROR,
FETCH_MESSAGE
} from '../actions/types';
import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
import authReducer from './auth_reducer';
const rootReducer = combineReducers({
form,
auth: authReducer
});
export default rootReducer;
export default function(state = {}, action) {
switch(action.type) {
case AUTH_USER:
return { ...state, error: '', authenticated: true };
case UNAUTH_USER:
return { ...state, authenticated: false };
case AUTH_ERROR:
return { ...state, error: action.payload };
case FETCH_MESSAGE:
return { ...state, message: action.payload };
}
return state;
}
Mine is just slightly modified due to trying to get this working with React Router DOM v4. I put comments in the code that may be of some interest. The ./reducers have been unchanged except for import... from.
// ./actions/authentication/index.js
import axios from 'axios';
import { ROOT_URL } from '../../../config/settings/secrets.json';
const AUTH_USER = 'auth_user';
const UNAUTH_USER = 'unauth_user';
const AUTH_ERROR = 'auth_error';
export function signinUser({ username, password }) {
return function(dispatch) {
axios.post(`${ROOT_URL}/api/auth/token/`, { username, password })
.then(response => {
dispatch({ type: AUTH_USER });
localStorage.setItem('token', response.data.token);
})
.catch(() => {
dispatch(authError('Bad Login Info'));
});
}
}
export function authError(error) {
// Can console.log() this out and payload does show 'Bad Login Info'
// How to get it innto the signin.js container though?
return {
type: AUTH_ERROR,
payload: error
};
}
// ./containers/authentication/signin.js
import React, { Component } from 'react';
import { reduxForm, Field } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import * as actions from '../../actions/authentication';
const renderInput = field => {
const { input, type } = field;
return (
<div>
<input {...input} type={type} className='form-control' />
</div>
);
}
class Signin extends Component {
handleFormSubmit({ username, password }) {
this.props.signinUser({ username, password });
// This is a bad idea since it will pass anything if username and password is entered
// Thus why trying to take the server response and use that
if (username !== undefined && password !== undefined) {
this.props.history.push('/home');
}
}
renderAlert(errorMessage) {
// Cannot console.log() any of this out
// this.renderAlert() doesn't seem to be getting triggered
if (errorMessage) {
return (
<div className="alert alert-danger">
<strong>Oops!</strong> {this.props.errorMessage}
</div>
);
}
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<div className="form-group">
<label>Username:</label>
<Field name='username' type='username' component={renderInput} />
</div>
<div className="form-group">
<label>Password:</label>
<Field name='password' type='password' component={renderInput} />
</div>
{this.renderAlert()}
<button action="submit" className="btn btn-primary">Sign in</button>
</form>
);
}
}
function mapStateToProps(state) {
console.log(state);
return { errorMessage: state.auth.error };
}
Signin = connect(mapStateToProps, actions)(Signin);
Signin = reduxForm({
form: 'signin'
})(Signin);
export default withRouter(Signin);
Edit: Consulting this image as refresher for the React-Redux data flow. Seems like most everything is working up to getting it back into the container, so is mapStateToProps failing somehow?
Oh man, I wasn't exporting my action types (AUTH_USER, UNAUTH_USER, and AUTH_ERROR) so that they could properly be consumed by the reducers.
// ./actions/authentication/index.js
export const AUTH_USER = 'auth_user';
export const UNAUTH_USER = 'unauth_user';
export const AUTH_ERROR = 'auth_error';
The missing export caused the issue.

Categories

Resources