Here is a login page of my nextjs web app, however, every time I click on login button and make a login httpPOST request to my backend server and got a 200 ok response, when I try to redirect user to the ‘/market’ route page, it does not work, clicking login button does nothing besides keeps receiving 200 ok response and console logging the response object, it just does not redirect! I had to forced the browser to refresh then it redirects to ‘/market’ page properly, I have no idea what is wrong. I had some stuff defined in middleware, they work fine, but just in case, I will attach my code for middleware below.
Here is the full github repo if you wanna see it clearer: link
"use client";
import { Alert, Button, TextField } from '@mui/material'
import Link from 'next/link';
import React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import { useState } from 'react';
import { useRouter } from "next/navigation";
const LoginPage = () => {
const router = useRouter()
const [open, setOpen] = useState(false);
const [showError, setShowError] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
}
const hideErrorMsg = () => {
setTimeout(() => {
setShowError(false)
}, 1500);
}
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
function handleUsername(e: React.ChangeEvent<HTMLInputElement>) {
// console.log(e.target.value)
setUsername(e.target.value)
}
function handlePassword(e: React.ChangeEvent<HTMLInputElement>) {
// console.log(e.target.value)
setPassword(e.target.value)
}
async function handleOnSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const response = await fetch("/api/login", {
method: 'POST',
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
username: username,
password: password
})
})
// router.push('/market')
if (response.ok) {
console.log(response)
router.push('/market')
window.location.reload()
}
if (!response.ok) {
const data = await response.json()
const { message } = data
setErrorMessage(message)
setShowError(true)
hideErrorMsg();
}
}
return (
<div className='bg-albion_color min-h-albion'>
<div className='pt-20'>
<h1 className='font-bold uppercase text-4xl text-center'>Login</h1>
<form className='pt-5' onSubmit={handleOnSubmit}>
<div className='flex justify-center'>
<div className='flex flex-col'>
<div className='flex items-center mb-10'>
<label className='w-20 mr-2'>Username</label>
<TextField id="filled-basic" label="username" variant="filled" color='success' sx={{ "& .MuiInputBase-input": { height: "10px" } }} onChange={handleUsername} />
</div>
<div className='flex items-center'>
<label className='w-20 mr-2'>Password</label>
<TextField id="filled-basic2" type='password' label="password" variant="filled" color='success' sx={{ "& .MuiInputBase-input": { height: "10px" } }} onChange={handlePassword} />
</div>
{
showError &&
<div className='mt-8'>
<Alert severity="error" variant='outlined'>{errorMessage}</Alert>
</div>
}
<div className='mt-8 ml-24'>
<Button className='font-semibold' variant="contained" type='submit'>Login</Button>
</div>
<div className='ml-52'>
<Button className='text-black font-sans' onClick={handleClickOpen} sx={{ textTransform: "none" }}>Forgot password?</Button>
</div>
<div className='ml-52'>
<Button className='text-black font-sans' sx={{ textTransform: "none" }}>
<Link href={"/sign-up"}>Create an account</Link>
</Button>
</div>
</div>
</div>
</form>
</div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Forgot your password? Really?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Too bad, I can't help you.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} autoFocus sx={{ fontFamily: "inherit" }}>
OK Agree
</Button>
</DialogActions>
</Dialog>
</div>
)
}
export default LoginPage
middleware.ts:
import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
import { decrypt } from './lib'; // Assuming decrypt is a function in your lib
export async function middleware(request: NextRequest) {
const path: string = request.nextUrl.pathname;
// Check if the user has a valid token
const tokenValid = await tokenCheck(request);
// If accessing '/market' without a valid token, redirect to '/login'
if (path === '/market' && !tokenValid) {
return NextResponse.redirect(new URL('/login', request.url));
}
// If accessing '/login' with a valid token, redirect to '/market'
if (path === '/login' && tokenValid) {
return NextResponse.redirect(new URL('/market', request.url));
}
// Proceed as normal if no redirection is needed
return NextResponse.next();
}
// Function to check if the session token is valid
async function tokenCheck(req: NextRequest): Promise<boolean> {
const session = req.cookies.get("session")?.value;
if (!session) {
return false; // No session cookie found
}
try {
const cookie = await decrypt(session); // Attempt to decrypt and validate the session
console.log("Valid token found:", cookie);
return true;
} catch (error) {
console.log("Invalid token:", error);
return false; // Token is either invalid or decryption failed
}
}
// Middleware config to match specific paths
export const config = {
matcher: ['/market', '/login'], // Apply this middleware to the /market and /login paths
};
route.ts for ‘/api/login’ route:
'use server';
import { NextRequest, NextResponse } from 'next/server'
import { sql } from '@vercel/postgres'
import { compare } from 'bcrypt-ts'
import { loginJwt } from '@/lib'
interface loginRequestBody {
username: string,
password: string
}
export async function POST(req: NextRequest) {
const body = await req.json()
// console.log(body)
if (!userDetailsCheck(body)) {
return NextResponse.json({message: "Username and Password cannot be empty"}, {status: 406})
}
// note that this is unsafe, sending credentials over http post request is not a good approach,
// however, implementing https server is not an option at this point.
if (!await dbCredentialCheck(body)) {
return NextResponse.json({message: "Incorrect password."}, {status: 401})
}
const res = NextResponse.json({message: 'Login successful'}, {status: 200});
const cookie = await loginJwt(body)
const expires = new Date(Date.now() + 60 * 60 * 1000 * 2)
res.cookies.set("session", cookie, {expires: expires, httpOnly: true})
return res
}
async function dbCredentialCheck(body: loginRequestBody) {
const {username, password} = body
const queryResult = await sql `select password from players where username=${username}`
// get the password of given username from postgres
if (queryResult.rowCount < 1 || queryResult.rows.length < 1) {
// no query results, user does not exist, return false right away
return false
}
//
const storedHash = queryResult.rows[0].password
// compare hashed passwords we fetched from db with the plaintext password sent from front end,
// again, not a good idea to send credentials over http request, should use https instead
if (!await compare(password, storedHash)) {
return false
}
return true
}
function userDetailsCheck(body: loginRequestBody) {
const {username, password} = body
return username && password
}
I had to add window.location.reload() after router.push(‘/market’) to force browser to refresh, then it will work, I don’t know what the problem is, can someone please help me? This issue also occurs when click on logout, logout button clears the cookies indeed, but if browser is not refreshed, then user can still access certain page despite the fact that they have no tokens since cookies have been cleared, whereas if browser is forced to refresh, this problem goes away