From 7848d892e40f9a8e44017eea9f1ba1c3b59add23 Mon Sep 17 00:00:00 2001 From: Nimit Date: Fri, 7 Jun 2024 00:19:34 +0530 Subject: [PATCH 1/3] added routes for mobile login and changed middleware for authorization --- src/app/api/auth/mobile/login/route.ts | 27 ++++ src/app/api/auth/mobile/logout/route.ts | 28 ++++ src/app/data/index.ts | 0 src/components/BreadCrumbComponent.tsx | 6 +- src/lib/auth.ts | 204 ++++++++++++------------ src/lib/middleware-utils.ts | 54 +++++++ src/middleware.ts | 41 +++-- 7 files changed, 242 insertions(+), 118 deletions(-) create mode 100644 src/app/api/auth/mobile/login/route.ts create mode 100644 src/app/api/auth/mobile/logout/route.ts delete mode 100644 src/app/data/index.ts create mode 100644 src/lib/middleware-utils.ts diff --git a/src/app/api/auth/mobile/login/route.ts b/src/app/api/auth/mobile/login/route.ts new file mode 100644 index 000000000..16c2a5dc5 --- /dev/null +++ b/src/app/api/auth/mobile/login/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authorize } from '@/lib/auth'; + +export async function POST(request: NextRequest) { + try { + const reqBody = await request.json(); + const { username, password } = reqBody; + + const user = await authorize({ username, password }); + + if (!user) { + return NextResponse.json( + { error: 'User does not exist' }, + { status: 400 }, + ); + } + const { token, ...otherProperties } = user; + const response = NextResponse.json({ + message: 'Login successful', + token, + user: { ...otherProperties }, + }); + return response; + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/auth/mobile/logout/route.ts b/src/app/api/auth/mobile/logout/route.ts new file mode 100644 index 000000000..8ff04bd39 --- /dev/null +++ b/src/app/api/auth/mobile/logout/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import db from '@/db'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + if (body && body.userId) { + await db.user.update({ + where: { + id: body.userId, + }, + data: { + token: null, + }, + }); + return NextResponse.json({ + message: 'Logout successful', + success: true, + }); + } + return NextResponse.json( + { message: 'User Id not available' }, + { status: 400 }, + ); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/data/index.ts b/src/app/data/index.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/BreadCrumbComponent.tsx b/src/components/BreadCrumbComponent.tsx index 6200f4aa4..428309626 100644 --- a/src/components/BreadCrumbComponent.tsx +++ b/src/components/BreadCrumbComponent.tsx @@ -10,7 +10,7 @@ import { } from '@/components/ui/breadcrumb'; import { FullCourseContent } from '@/db/course'; import Link from 'next/link'; -import { useMemo } from 'react'; +import { Fragment, useMemo } from 'react'; export default function BreadCrumbComponent({ rest, @@ -89,7 +89,7 @@ export default function BreadCrumbComponent({ finalRouteArray = [...rest]; } return ( - <> + {index !== array.length - 1 ? ( <> @@ -111,7 +111,7 @@ export default function BreadCrumbComponent({ )} - + ); })} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8d6ca3eb8..85a866dd5 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -50,6 +50,7 @@ const generateJWT = async (payload: JWTPayload) => { return jwt; }; + async function validateUser( email: string, password: string, @@ -106,6 +107,110 @@ async function validateUser( }; } +export const authorize = async (credentials?: { + username: string; + password: string; +}) => { + if (!credentials?.username || !credentials?.password) { + return null; + } + try { + if (process.env.LOCAL_CMS_PROVIDER) { + return { + id: '1', + name: 'test', + email: 'test@gmail.com', + token: await generateJWT({ + id: '1', + }), + }; + } + const hashedPassword = await bcrypt.hash(credentials.password, 10); + + const userDb = await prisma.user.findFirst({ + where: { + email: credentials.username, + }, + select: { + password: true, + id: true, + name: true, + }, + }); + if ( + userDb && + userDb.password && + (await bcrypt.compare(credentials.password, userDb.password)) + ) { + const jwt = await generateJWT({ + id: userDb.id, + }); + await db.user.update({ + where: { + id: userDb.id, + }, + data: { + token: jwt, + }, + }); + + return { + id: userDb.id, + name: userDb.name, + email: credentials.username, + token: jwt, + }; + } + const user: AppxSigninResponse = await validateUser( + credentials.username, + credentials.password, + ); + + const jwt = await generateJWT({ + id: user.data?.userid, + }); + + if (user.data) { + try { + await db.user.upsert({ + where: { + id: user.data.userid, + }, + create: { + id: user.data.userid, + name: user.data.name, + email: credentials.username, + token: jwt, + password: hashedPassword, + }, + update: { + id: user.data.userid, + name: user.data.name, + email: credentials.username, + token: jwt, + password: hashedPassword, + }, + }); + } catch (e) { + console.log(e); + } + + return { + id: user.data.userid, + name: user.data.name, + email: credentials.username, + token: jwt, + }; + } + + // Return null if user data could not be retrieved + return null; + } catch (e) { + console.error(e); + } + return null; +}; + export const authOptions = { providers: [ CredentialsProvider({ @@ -114,104 +219,7 @@ export const authOptions = { username: { label: 'email', type: 'text', placeholder: '' }, password: { label: 'password', type: 'password', placeholder: '' }, }, - async authorize(credentials: any) { - try { - if (process.env.LOCAL_CMS_PROVIDER) { - return { - id: '1', - name: 'test', - email: 'test@gmail.com', - token: await generateJWT({ - id: '1', - }), - }; - } - const hashedPassword = await bcrypt.hash(credentials.password, 10); - - const userDb = await prisma.user.findFirst({ - where: { - email: credentials.username, - }, - select: { - password: true, - id: true, - name: true, - }, - }); - if ( - userDb && - userDb.password && - (await bcrypt.compare(credentials.password, userDb.password)) - ) { - const jwt = await generateJWT({ - id: userDb.id, - }); - await db.user.update({ - where: { - id: userDb.id, - }, - data: { - token: jwt, - }, - }); - - return { - id: userDb.id, - name: userDb.name, - email: credentials.username, - token: jwt, - }; - } - console.log('not in db'); - const user: AppxSigninResponse = await validateUser( - credentials.username, - credentials.password, - ); - - const jwt = await generateJWT({ - id: user.data?.userid, - }); - - if (user.data) { - try { - await db.user.upsert({ - where: { - id: user.data.userid, - }, - create: { - id: user.data.userid, - name: user.data.name, - email: credentials.username, - token: jwt, - password: hashedPassword, - }, - update: { - id: user.data.userid, - name: user.data.name, - email: credentials.username, - token: jwt, - password: hashedPassword, - }, - }); - } catch (e) { - console.log(e); - } - - return { - id: user.data.userid, - name: user.data.name, - email: credentials.username, - token: jwt, - }; - } - - // Return null if user data could not be retrieved - return null; - } catch (e) { - console.error(e); - } - return null; - }, + authorize, }), ], secret: process.env.NEXTAUTH_SECRET || 'secr3t', diff --git a/src/lib/middleware-utils.ts b/src/lib/middleware-utils.ts new file mode 100644 index 000000000..8ee02c6bd --- /dev/null +++ b/src/lib/middleware-utils.ts @@ -0,0 +1,54 @@ +import { importJWK, jwtVerify } from 'jose'; +import { NextRequestWithAuth, withAuth } from 'next-auth/middleware'; +import { NextResponse } from 'next/server'; + +const PRIVATE_MOBILE_ROUTES = ['/api/auth/mobile/logout']; +const SINGLE_USER_ROUTES = ['/courses', '/questions', '/bookmarks']; + +const shouldRestrictSingleUser = (pathname: string) => { + return SINGLE_USER_ROUTES.some((route) => pathname.startsWith(route)); +}; + +const verifyJWT = async (token: string) => { + const secret = process.env.JWT_SECRET || 'secret'; + const jwk = await importJWK({ k: secret, alg: 'HS256', kty: 'oct' }); + const payload = await jwtVerify(token, jwk); + return payload; +}; + +export const handleMobileAuth = async (request: NextRequestWithAuth) => { + const token = request.headers.get('Authorization')?.split(' ')[1]; + const pathname = request.nextUrl.pathname; + if (token) { + try { + const payload: any = await verifyJWT(token); + request.nextUrl.searchParams.set('userId', payload.id); + return NextResponse.next(); + } catch (error) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + } else if (PRIVATE_MOBILE_ROUTES.includes(pathname)) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } +}; + +export const nextAuthMiddleware = withAuth(async (req) => { + if (process.env.LOCAL_CMS_PROVIDER) return; + + const pathname = req.nextUrl.pathname; + + if (shouldRestrictSingleUser(pathname)) { + const token = req.nextauth.token; + if (!token) { + return NextResponse.redirect(new URL('/invalidsession', req.url)); + } + const user = await fetch( + `${process.env.NEXT_PUBLIC_BASE_URL_LOCAL}/api/user?token=${token.jwtToken}`, + ); + + const json = await user.json(); + if (!json.user) { + return NextResponse.redirect(new URL('/invalidsession', req.url)); + } + } +}); diff --git a/src/middleware.ts b/src/middleware.ts index 8f8d4d953..5fe422898 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,23 +1,30 @@ -import { withAuth } from 'next-auth/middleware'; +import { NextRequestWithAuth } from 'next-auth/middleware'; import { NextResponse } from 'next/server'; +import { handleMobileAuth, nextAuthMiddleware } from './lib/middleware-utils'; -export const config = { - matcher: ['/courses/:path*'], -}; +const PUBLIC_ROUTES = [ + '/', + '/privacy-policy', + '/refund', + '/tnc', + '/api/auth/mobile/login', + '/signin', +]; -export default withAuth(async (req) => { - if (process.env.LOCAL_CMS_PROVIDER) return; +export async function middleware(request: NextRequestWithAuth) { + const pathname = request.nextUrl.pathname; - const token = req.nextauth.token; - if (!token) { - return NextResponse.redirect(new URL('/invalidsession', req.url)); + // Check public routes + if (PUBLIC_ROUTES.includes(pathname)) { + return NextResponse.next(); } - const user = await fetch( - `${process.env.NEXT_PUBLIC_BASE_URL_LOCAL}/api/user?token=${token.jwtToken}`, - ); - const json = await user.json(); - if (!json.user) { - return NextResponse.redirect(new URL('/invalidsession', req.url)); - } -}); + // Mobile Auth + await handleMobileAuth(request); + + /* + NextJS Auth - Other public routes + are also taken care by the next-auth middleware + */ + return (nextAuthMiddleware as any)(request); +} From 79e8f999577d580d243c75f282930134a9a1e26e Mon Sep 17 00:00:00 2001 From: Nimit Date: Tue, 18 Jun 2024 01:02:17 +0530 Subject: [PATCH 2/3] added check for mobile device --- src/lib/middleware-utils.ts | 16 +++++++++++++++- src/middleware.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/lib/middleware-utils.ts b/src/lib/middleware-utils.ts index 8ee02c6bd..d2cdf3d6f 100644 --- a/src/lib/middleware-utils.ts +++ b/src/lib/middleware-utils.ts @@ -1,6 +1,6 @@ import { importJWK, jwtVerify } from 'jose'; import { NextRequestWithAuth, withAuth } from 'next-auth/middleware'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse, userAgent } from 'next/server'; const PRIVATE_MOBILE_ROUTES = ['/api/auth/mobile/logout']; const SINGLE_USER_ROUTES = ['/courses', '/questions', '/bookmarks']; @@ -52,3 +52,17 @@ export const nextAuthMiddleware = withAuth(async (req) => { } } }); + +export const getDeviceType = (request: NextRequest) => { + const { device } = userAgent(request); + return device.type; +}; + +export const isMobile = (request: NextRequest) => { + const deviceType = getDeviceType(request); + return deviceType === 'mobile' || deviceType === 'tablet'; +}; + +export const isDesktop = (request: NextRequest) => { + return !isMobile(request); +}; diff --git a/src/middleware.ts b/src/middleware.ts index 5fe422898..329c61ced 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,10 @@ import { NextRequestWithAuth } from 'next-auth/middleware'; import { NextResponse } from 'next/server'; -import { handleMobileAuth, nextAuthMiddleware } from './lib/middleware-utils'; +import { + handleMobileAuth, + isMobile, + nextAuthMiddleware, +} from './lib/middleware-utils'; const PUBLIC_ROUTES = [ '/', @@ -20,7 +24,9 @@ export async function middleware(request: NextRequestWithAuth) { } // Mobile Auth - await handleMobileAuth(request); + if (isMobile(request)) { + return await handleMobileAuth(request); + } /* NextJS Auth - Other public routes From a9db646b7d3100af5a45800f04253998bb3429c9 Mon Sep 17 00:00:00 2001 From: Sargam Date: Thu, 20 Jun 2024 02:00:16 +0545 Subject: [PATCH 3/3] fix: typo in payload destructuring --- src/app/api/auth/mobile/logout/route.ts | 32 +++++++++++-------------- src/lib/middleware-utils.ts | 4 +++- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/app/api/auth/mobile/logout/route.ts b/src/app/api/auth/mobile/logout/route.ts index 6273acda6..5c546628b 100644 --- a/src/app/api/auth/mobile/logout/route.ts +++ b/src/app/api/auth/mobile/logout/route.ts @@ -3,26 +3,22 @@ import db from '@/db'; export async function POST(request: NextRequest) { try { + // TODO: Get userid from somewhere not body const body = await request.json(); - if (body && body.userId) { - await db.user.update({ - where: { - id: body.userId, - }, - data: { - token: null, - }, - }); - return NextResponse.json({ - message: 'Logout successful', - success: true, - }); - } - return NextResponse.json( - { message: 'User Id not available' }, - { status: 400 }, - ); + await db.user.update({ + where: { + id: body.userId, + }, + data: { + token: null, + }, + }); + return NextResponse.json({ + message: 'Logout successful', + success: true, + }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } } + diff --git a/src/lib/middleware-utils.ts b/src/lib/middleware-utils.ts index d2cdf3d6f..6943cbb24 100644 --- a/src/lib/middleware-utils.ts +++ b/src/lib/middleware-utils.ts @@ -22,7 +22,9 @@ export const handleMobileAuth = async (request: NextRequestWithAuth) => { if (token) { try { const payload: any = await verifyJWT(token); - request.nextUrl.searchParams.set('userId', payload.id); + // @typo here + const userId = payload.payload.userid; + console.log(userId); return NextResponse.next(); } catch (error) { return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });