From 062dace04289a7fadc7dd7083a03d5be6bf9e60b Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Sun, 25 Feb 2024 21:58:17 +0530 Subject: [PATCH 1/5] init single session --- package.json | 1 + pnpm-lock.yaml | 9 ++++++--- prisma/schema.prisma | 1 + src/lib/auth.ts | 22 ++++++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7b7a199ec..06325a1f7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dayjs": "^1.11.10", "discord-oauth2": "^2.11.0", "discord.js": "^14.14.1", + "jose": "^5.2.2", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.321.0", "next": "14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66e4db65c..d4870d0de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ dependencies: discord.js: specifier: ^14.14.1 version: 14.14.1 + jose: + specifier: ^5.2.2 + version: 5.2.2 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -189,7 +192,7 @@ packages: '@panva/hkdf': 1.1.1 '@types/cookie': 0.6.0 cookie: 0.6.0 - jose: 5.2.0 + jose: 5.2.2 oauth4webapi: 2.8.1 preact: 10.11.3 preact-render-to-string: 5.2.3(preact@10.11.3) @@ -2851,8 +2854,8 @@ packages: resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} dev: false - /jose@5.2.0: - resolution: {integrity: sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==} + /jose@5.2.2: + resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} dev: false /js-cookie@2.2.1: diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b60dbd284..a71046d63 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -117,6 +117,7 @@ model User { id String @id @default(cuid()) name String? email String? @unique + token String? sessions Session[] purchases UserPurchases[] videoProgress VideoProgress[] diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ee9f7f01e..e2e960c2e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,20 @@ import db from '@/db'; import CredentialsProvider from 'next-auth/providers/credentials'; +import { SignJWT, importJWK } from 'jose'; +const generateJWT = async (payload: any) => { + const secret = process.env.JWT_SECRET_TOKEN || 'secret'; + + const jwk = await importJWK({ k: secret, alg: 'HS256', kty: 'oct' }); + + const jwt = await new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('365d') + .sign(jwk); + + return jwt; +}; async function validateUser( email: string, password: string, @@ -71,6 +85,9 @@ export const authOptions = { credentials.password, ); if (user.data) { + const jwt = await generateJWT({ + id: user.data.userid, + }); try { await db.user.upsert({ where: { @@ -80,11 +97,13 @@ export const authOptions = { id: user.data.userid, name: user.data.name, email: credentials.username, + token: jwt, }, update: { id: user.data.userid, name: user.data.name, email: credentials.username, + token: jwt, }, }); } catch (e) { @@ -95,6 +114,7 @@ export const authOptions = { id: user.data.userid, name: user.data.name, email: credentials.username, + token: jwt, }; } // Return null if user data could not be retrieved @@ -109,8 +129,10 @@ export const authOptions = { secret: process.env.NEXTAUTH_SECRET || 'secr3t', callbacks: { session: async ({ session, token }: any) => { + console.log('session', session, token); if (session?.user) { session.user.id = token.uid; + session.user.authToken = token; } return session; }, From 89230ce14ee124fee59292008b566f8134dcc496 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Mon, 26 Feb 2024 10:13:45 +0530 Subject: [PATCH 2/5] FEAT:single session/login --- .env.example | 2 ++ src/app/api/user/route.ts | 15 +++++++++++++++ src/app/invalidsession/page.tsx | 16 ++++++++++++++++ src/lib/auth.ts | 5 +++-- src/middleware.ts | 21 +++++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/app/api/user/route.ts create mode 100644 src/app/invalidsession/page.tsx create mode 100644 src/middleware.ts diff --git a/.env.example b/.env.example index 0ffe7de64..1bc317abb 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,5 @@ BOT_TOKEN = "123" GUILD_ID = "123" LOCAL_CMS_PROVIDER = true CACHE_EXPIRE_S = 10 +JWT_SECRET_TOKEN="JWT_SECRET_TOKEN" + diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts new file mode 100644 index 000000000..22e8cc269 --- /dev/null +++ b/src/app/api/user/route.ts @@ -0,0 +1,15 @@ +import { type NextRequest, NextResponse } from 'next/server'; +import db from '@/db'; + +export async function GET(req: NextRequest) { + const url = new URL(req.url); + const token = url.searchParams.get('token'); + const user = await db.user.findFirst({ + where: { + token, + }, + }); + return NextResponse.json({ + user, + }); +} diff --git a/src/app/invalidsession/page.tsx b/src/app/invalidsession/page.tsx new file mode 100644 index 000000000..0fdbaddfa --- /dev/null +++ b/src/app/invalidsession/page.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { signOut } from 'next-auth/react'; +import React, { useEffect } from 'react'; + +const page = () => { + useEffect(() => { + signOut({ + callbackUrl: '/signin', + }); + }, []); + + return
page
; +}; + +export default page; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index e2e960c2e..f5622c0b3 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -129,16 +129,17 @@ export const authOptions = { secret: process.env.NEXTAUTH_SECRET || 'secr3t', callbacks: { session: async ({ session, token }: any) => { - console.log('session', session, token); if (session?.user) { session.user.id = token.uid; - session.user.authToken = token; + session.user.jwtToken = token.jwtToken; } + return session; }, jwt: async ({ user, token }: any) => { if (user) { token.uid = user.id; + token.jwtToken = user.token; } return token; }, diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 000000000..605cbcea6 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,21 @@ +import { withAuth } from 'next-auth/middleware'; +import { NextResponse } from 'next/server'; + +export const config = { + matcher: ['/courses/:path*'], +}; + +export default withAuth(async (req) => { + 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)); + } +}); From 2bdb2f1fe3521bd37644190136f276c2b3207de1 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Mon, 26 Feb 2024 10:45:05 +0530 Subject: [PATCH 3/5] use the already JWT SERCRET in the env --- .env.example | 1 - src/app/admin/user/page.tsx | 7 +++++++ src/lib/auth.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/app/admin/user/page.tsx diff --git a/.env.example b/.env.example index 1bc317abb..66d7ef41d 100644 --- a/.env.example +++ b/.env.example @@ -14,5 +14,4 @@ BOT_TOKEN = "123" GUILD_ID = "123" LOCAL_CMS_PROVIDER = true CACHE_EXPIRE_S = 10 -JWT_SECRET_TOKEN="JWT_SECRET_TOKEN" diff --git a/src/app/admin/user/page.tsx b/src/app/admin/user/page.tsx new file mode 100644 index 000000000..420e11ccc --- /dev/null +++ b/src/app/admin/user/page.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const page = () => { + return
page
; +}; + +export default page; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index f5622c0b3..f9fdb0da0 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -3,7 +3,7 @@ import CredentialsProvider from 'next-auth/providers/credentials'; import { SignJWT, importJWK } from 'jose'; const generateJWT = async (payload: any) => { - const secret = process.env.JWT_SECRET_TOKEN || 'secret'; + const secret = process.env.JWT_SECRET || 'secret'; const jwk = await importJWK({ k: secret, alg: 'HS256', kty: 'oct' }); From de241a5976776d7128854d6724ea66990d41a2d4 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Mon, 26 Feb 2024 11:58:39 +0530 Subject: [PATCH 4/5] added admin part --- src/actions/user/index.ts | 27 +++++++++++++ src/app/admin/user/LogoutUser.tsx | 63 +++++++++++++++++++++++++++++++ src/app/admin/user/page.tsx | 11 ++++-- 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/actions/user/index.ts create mode 100644 src/app/admin/user/LogoutUser.tsx diff --git a/src/actions/user/index.ts b/src/actions/user/index.ts new file mode 100644 index 000000000..d85f351e7 --- /dev/null +++ b/src/actions/user/index.ts @@ -0,0 +1,27 @@ +'use server'; +import db from '@/db'; + +export const logoutUser = async (email: string, adminPassword: string) => { + if (adminPassword !== process.env.ADMIN_SECRET) { + return { error: 'Unauthorized' }; + } + + const user = await db.user.findFirst({ + where: { + email, + }, + }); + if (!user) { + return { message: 'User not found' }; + } + await db.user.update({ + where: { + id: user.id, + }, + data: { + token: '', + }, + }); + + return { message: 'User logged out' }; +}; diff --git a/src/app/admin/user/LogoutUser.tsx b/src/app/admin/user/LogoutUser.tsx new file mode 100644 index 000000000..9ff93c25f --- /dev/null +++ b/src/app/admin/user/LogoutUser.tsx @@ -0,0 +1,63 @@ +'use client'; +import { logoutUser } from '@/actions/user'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@radix-ui/react-dropdown-menu'; +import React from 'react'; +import { toast } from 'sonner'; + +const LogoutUserComp = () => { + const formRef = React.useRef(null); + + const handlLogout = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + + const email = formData.get('email') as string; + const adminPassword = formData.get('adminPassword') as string; + const res = await logoutUser(email, adminPassword); + toast.info(res.message); + }; + return ( +
+
+
+
Logout the user
+
+ Enter the information below to logout the user +
+
+ +
+ + +
+
+ + +
+
+ +
+
+
+ ); +}; + +export default LogoutUserComp; diff --git a/src/app/admin/user/page.tsx b/src/app/admin/user/page.tsx index 420e11ccc..e7d534001 100644 --- a/src/app/admin/user/page.tsx +++ b/src/app/admin/user/page.tsx @@ -1,7 +1,12 @@ import React from 'react'; +import LogoutUserComp from './LogoutUser'; -const page = () => { - return
page
; +const UserAdminPage = () => { + return ( +
+ +
+ ); }; -export default page; +export default UserAdminPage; From 43cb7ec4656a037289344b21cc18db2b829119c8 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Mon, 26 Feb 2024 14:40:04 +0530 Subject: [PATCH 5/5] added ui changes --- src/app/invalidsession/page.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/invalidsession/page.tsx b/src/app/invalidsession/page.tsx index 0fdbaddfa..4b1ffc37f 100644 --- a/src/app/invalidsession/page.tsx +++ b/src/app/invalidsession/page.tsx @@ -2,15 +2,27 @@ import { signOut } from 'next-auth/react'; import React, { useEffect } from 'react'; +import { Toaster } from '@/components/ui/sonner'; +import { toast } from 'sonner'; const page = () => { useEffect(() => { signOut({ callbackUrl: '/signin', }); + toast('Too many devices connected. Logging out!', { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); }, []); - return
page
; + return ( +
+ +
+ ); }; export default page;