Posts > Login / Logout / User Authentication
June 25, 2023
My first time adding User Authentication / Login Logout integration to my client project was an awesome experience. I’ve always been just doing the CRUD, Component before and giving me the opportunity to do the Login credentials was pretty cool.
Anyway, these “how to’s” are what I’m basing my code from now on ( until I learn a better approach to do it ) going forward.
So the important thing about doing any kind of challenging task is to cut them down first into little pieces and then do them one at a time.
Here’s how the login authentication is built based on what I gather on the web:
So let’s start:
Just a simple login form will do for this how to’s:
export default function Login() {
return (
<>
<form>
<p>
<label htmlFor='username'>Username</label>
<input name='username' id='username' type='text' />
</p>
<p>
<label htmlFor='password'>Password</label>
<input name='password' id='password' type='password' />
</p>
<button type='submit'>Login</button>
</form>
</>
);
}
We added a username
and password
field for the form and submit
button for the form.
handleChange
function for our form to be able to change the input fields, then handleSubmit
to ready our API request later.import { ChangeEvent, FormEvent, useState } from 'react';
interface LoginForm {
username: string;
password: string;
}
export default function Login() {
const [payload, setPayload] = useState<LoginForm>({
username: '',
password: '',
});
// `handleChange` is here to make our dynamic as react will
// have an issue on form that are not properly handled
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPayload((prevState) => ({
...prevState,
[name]: value,
}));
};
// handleSubmit will get our payload/credentials and pass to
// our api to check if the user exist
const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
e.preventDefault();
console.log(payload); // <-- {username: '', password: ''}
};
return (
<>
<form onSubmit={(e) => handleSubmit(e, payload)}>
<p>
//<label ...
<input
//...
onChange={handleChange}
/>
</p>
<p>
//<label ...
<input
//...
onChange={handleChange}
/>
</p>
<button type='submit'>Login</button>
</form>
</>
);
}
2. Interacting with the API with our credentials. We needed to know if we entered the correct or incorrect credentials and check if there’s a return data from our API. Back in our handleSubmit
function, we just wanted to see in our console what data our API is going to give to us.
const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
e.preventDefault();
const requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(payload),
};
// using the fetch API to get our data fetch('url', [method, headers, body])
fetch('[your API url]', requestOptions)
.then((res) => {
if (res.ok) {
// Login successful
return res.json();
} else {
// Login failed
throw new Error('Login failed');
}
})
.then((data) => {
// Access the response data
console.log('Response:', data);
})
.catch((error) => {
// An error occurred during the request
console.error('Error:', error);
// Handle the error appropriately
});
};
👌requestOptions
this object will depends on the server or API you are using, at this moment the API I’m using requires this headers and the credentials are required to be passed in the body.
We now have a basic Login page that responds to our entered credentials. We should have been able to see what kind of message/response it has and use that response to our application.
👌Usually, in any login authentication, the server will respond with a token, token expiration, and user details. With this information, we can store them in our browser's local storage to be used for application.
Such as:
Back on the handleSubmit function, we can store our login API response in the local storage.
const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
e.preventDefault();
const requestOptions = {
//...
};
fetch('[your API url]', requestOptions)
.then((res) => {
//...
})
.then((data) => {
// Add your data to the local storage.
localStorage.setItem('user', data.result.userDetails);
localStorage.setItem('token', data.result.token);
// When logged in, redirect to the `/` page
router.push('/');
})
.catch((error) => {
// An error occurred during the request
console.error('Error:', error);
});
};
👌Again depending on your API returned data, we can add them to ourlocalStorage
to be used on different parts of our site. Now we haveuser
andtoken
that we can use later on our Frontend.
This one is where I struggled a lot when I was doing the user authentication as I was not really familiar with HOC ( High Order Component - more on that soon ) back then and still right now 😅 and Context API. We need to create some kind of a way to create restrictions/protection on some pages/routes that we wanted only logged-in users to view.
In this case, I’m just going to make sure the user can’t see the home page (/)
that’s why in token and session storage, we redirect the user after logging in. Now we’ll create a way to make sure non-logged-in users can’t see our home page (/)
.
Let’s start by making sure that when a valid user logged in, we can access his data everywhere in our App. We’re going to use Context API here.
// AuthContext.js
import { useState, useContext, createContext, ReactNode } from 'react';
import { useRouter } from 'next/router';
interface AuthContextType {
currentUser: string | null;
login: (payload: Payload) => Promise<void | undefined>;
}
interface ResponseData {
result: {
email: string;
token: string;
};
}
type Payload = {
email: string;
password: string;
};
interface AuthProviderProps {
children: ReactNode;
}
const AuthContext = createContext<AuthContextType>({
currentUser: null,
login: async () => undefined,
});
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }: AuthProviderProps) {
// created a state for our currentUser where we can use in our components later
const [currentUser, setCurrentUser] = useState<string | null>(null);
const router = useRouter();
//moving the login() here in the AuthProvider so we can use them anywhere
//made it async await as well.
async function login(payload: Payload): Promise<void | undefined> {
try {
const requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(payload),
};
const response = await fetch(
'https://demo.cardid.app/api/login/',
requestOptions
);
if (response.ok) {
// Login successful
const data: ResponseData = await response.json();
localStorage.setItem('user', data.result.email);
localStorage.setItem('token', data.result.token);
//
setCurrentUser(data.result.email);
// when successfully logged in redirect to home page
router.push('/');
} else {
// Login failed
throw new Error('Login failed');
}
} catch (error) {
console.error('Error:', error);
}
}
const value = {
currentUser,
login,
};
// we can now call the `currentUser` state and `login` function anywhere in our app
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
👌I move the login()
function here as well, so we can use it anywhere in our application in the future.
Don’t forget to add our AuthProvider
in our main app component
//_app.tsx
//...
import { AuthProvider } from '@/components/context/AuthContext';
export default function App({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}
2. Create our High Order Component to make sure that if a non-logged in user cannot access our home page, and redirects to our login page. Basically, what we’re doing is creating a component that takes a component as an argument and returns a new component. With the new component, we’re checking first if the user has logged-in information and is valid before rendering the component. ( too much use of the word component here 🤣 ).
//withAuth.jsx
import { useState, useEffect, ComponentType } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/components/context/AuthContext';
interface AuthProps {
isAuthenticated: boolean;
}
const withAuth = <P extends object>(Component: ComponentType<P>) => {
const AuthenticatedComponent = (props: P & AuthProps) => {
const router = useRouter();
const { currentUser: initialCurrentUser } = useAuth();
const [currentUser, setCurrentUser] = useState(initialCurrentUser);
useEffect(() => {
if (typeof window !== 'undefined' && !initialCurrentUser) {
// Fetch the authentication state on the client-side
// This can be an API request or reading from local storage
const storedUser = localStorage.getItem('currentUser');
if (storedUser) {
setCurrentUser(storedUser);
} else {
router.replace('/login');
}
}
}, [initialCurrentUser, router]);
if (!currentUser) {
return null;
}
return <Component {...props} />;
};
return AuthenticatedComponent;
};
export default withAuth;
We just need to wrap this component in any pages we wanted to ask/require authentication.
function Homepage(){
//...
}
export default withAuth(Homepage);
Now let’s create our logout
function. This will be easy as we just need to clear out the localStorage
that we created earlier when logging in.
// AuthContext.tsx
import { useState, useContext, createContext, ReactNode } from 'react';
import { useRouter } from 'next/router';
interface AuthContextType {
//...
}
interface ResponseData {
//...
}
type Payload = {
//...
};
interface AuthProviderProps {
//...
}
const AuthContext = createContext<AuthContextType>({
//...
});
export function useAuth() {
//...}
export function AuthProvider({ children }: AuthProviderProps) {
async function login(payload: Payload): Promise<void | undefined> {
///....
}
function logout(): void {
// Remove user and token from localStorage
localStorage.removeItem('user');
localStorage.removeItem('token');
// Clear the user state
setCurrentUser(null);
// Navigate back to the login page
router.push('/login');
}
const value = {
//...
logout, // don't forget to add to your provider
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Then just use your logout
function into the component you needed it.
//Homepage.tsx
//...
import { useAuth } from '@/components/context/AuthContext';
const inter = Inter({ subsets: ['latin'] });
function Home() {
const { logout } = useAuth();
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
<button onClick={() => logout()}>Logout</button>
<h1>This the HOME PAGE</h1>
</main>
);
}
export default withAuth(Home);
Congratulations! 🥳 Now you have a working user authentication component.
This is how I did my first ever User Authentication task, and there is for sure a better approach than what I have, and there’s a better tool as well to use for easier user authentication.
👌You can also check the next-auth where I see lot’s of developer is currently using for thier next.js authentication.
Check out the whole code in my GitHub Repository: https://github.com/owaaquino/Login-Authentication