From 465c089b4f969dd303a74b758900e00184e335f4 Mon Sep 17 00:00:00 2001 From: aliirz Date: Fri, 15 Nov 2024 12:04:33 +0500 Subject: [PATCH 1/4] user sessions view read added --- app/api/auth/services/route.ts | 29 ++++++++++ app/api/auth/sessions/route.ts | 64 ++++++++++++++++++++++ app/dashboard/page.tsx | 16 ++---- components/dashboard/user-services.tsx | 75 ++++++++++++++++++++++++++ lib/auth.ts | 8 +++ lib/keycloak-admin.ts | 8 +++ 6 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 app/api/auth/services/route.ts create mode 100644 app/api/auth/sessions/route.ts create mode 100644 components/dashboard/user-services.tsx create mode 100644 lib/auth.ts diff --git a/app/api/auth/services/route.ts b/app/api/auth/services/route.ts new file mode 100644 index 0000000..97478d3 --- /dev/null +++ b/app/api/auth/services/route.ts @@ -0,0 +1,29 @@ +import { KEYCLOAK_URLS, getKeycloakHeaders } from '@/lib/keycloak-config' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export async function GET() { + try { + const accessToken = cookies().get('access_token')?.value + + if (!accessToken) { + return NextResponse.json({ error: 'No access token found' }, { status: 401 }) + } + + const response = await fetch(KEYCLOAK_URLS.USERINFO, { + headers: getKeycloakHeaders(accessToken) + }) + + if (!response.ok) { + throw new Error('Failed to fetch user services') + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + return NextResponse.json( + { error: 'Failed to fetch user services' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/auth/sessions/route.ts b/app/api/auth/sessions/route.ts new file mode 100644 index 0000000..32d5157 --- /dev/null +++ b/app/api/auth/sessions/route.ts @@ -0,0 +1,64 @@ +import { keycloakAdmin } from '@/lib/keycloak-admin' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import { KEYCLOAK_URLS } from '@/lib/keycloak-config' + +interface KeycloakSession { + id?: string + username?: string + userId?: string + ipAddress?: string + start?: number + lastAccess?: number + clients?: { [key: string]: string } +} + +export async function GET() { + try { + const accessToken = cookies().get('access_token')?.value + if (!accessToken) { + return NextResponse.json({ error: 'No access token found' }, { status: 401 }) + } + + // Get user info to get the user ID + const userInfoResponse = await fetch( + `${KEYCLOAK_URLS.USERINFO}`, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + } + } + ) + const userInfo = await userInfoResponse.json() + + // Get sessions using the admin client + const sessions = await keycloakAdmin.getUserSessions(userInfo.sub) + + console.log('Sessions:', sessions) + + // Transform the sessions data to match our interface + const formattedSessions = sessions.map((session: KeycloakSession) => { + // Get the first client name from the clients object, if it exists + const clientId = session.clients ? Object.keys(session.clients)[0] : undefined + const clientName = clientId && session.clients?.[clientId] + + return { + clientId, + clientName, + active: true, + lastAccess: session.lastAccess ? new Date(session.lastAccess).toISOString() : undefined, + started: session.start ? new Date(session.start).toISOString() : undefined, + ipAddress: session.ipAddress + } + }) + + return NextResponse.json(formattedSessions) + + } catch (error: any) { + console.error('Session fetch error:', error) + return NextResponse.json( + { error: error.message || 'Failed to fetch user sessions' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index db7b180..722f27a 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -11,6 +11,8 @@ import { useRouter } from 'next/navigation' import { useToast } from "@/hooks/use-toast" import { Sidebar } from "@/components/dashboard/sidebar" import { Bell } from "lucide-react" +import { UserServices } from "@/components/dashboard/user-services" + interface UserProfile { sub?: string email?: string @@ -136,19 +138,7 @@ export default function Component() { {/* Active Services */} - - - Active Services - Services you are currently logged into - - -
- Tax Portal - Health Services - Education Portal -
-
-
+ {/* Notifications */} diff --git a/components/dashboard/user-services.tsx b/components/dashboard/user-services.tsx new file mode 100644 index 0000000..7924a9e --- /dev/null +++ b/components/dashboard/user-services.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' +import { Shield, ExternalLink } from 'lucide-react' +import { Badge } from '@/components/ui/badge' + +interface ClientSession { + clientId: string + clientName?: string + active: boolean + lastAccess: string + started: string + ipAddress: string +} + +export function UserServices() { + const [sessions, setSessions] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchSessions = async () => { + try { + const response = await fetch('/api/auth/sessions') + const data = await response.json() + setSessions(Array.isArray(data) ? data : []) + } catch (error) { + console.error('Failed to fetch sessions:', error) + setSessions([]) + } finally { + setLoading(false) + } + } + + fetchSessions() + }, []) + + return ( + + + + + Connected Services + + + + {loading ? ( +
Loading active services...
+ ) : ( +
+ {sessions.length === 0 ? ( +
+ No active services found +
+ ) : ( + sessions.map((session) => ( +
+
+
+ + {session.clientName} +
+ IP: {session.ipAddress} + + Started: {new Date(session.started).toLocaleString()} + +
+ Active +
+ )) + )} +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..dae44c6 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,8 @@ +export async function getAccessToken() { + const response = await fetch('/api/auth/token') + if (!response.ok) { + throw new Error('Failed to get access token') + } + const { accessToken } = await response.json() + return accessToken +} \ No newline at end of file diff --git a/lib/keycloak-admin.ts b/lib/keycloak-admin.ts index 163ff74..f307218 100644 --- a/lib/keycloak-admin.ts +++ b/lib/keycloak-admin.ts @@ -43,6 +43,14 @@ class KeycloakAdmin { const users = await this.adminClient.users.find({ email }) return users[0] } + + async getUserSessions(userId: string) { + await this.init() + return this.adminClient.users.listSessions({ + id: userId, + realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM! + }) + } } export const keycloakAdmin = new KeycloakAdmin() From 97432d0501a403efc25e55fbfe3b4403a1190cf5 Mon Sep 17 00:00:00 2001 From: aliirz Date: Tue, 19 Nov 2024 23:28:41 +0500 Subject: [PATCH 2/4] added SG integration --- app/api/send-verification/route.ts | 36 ++++++++++++++++++++++++++++ app/signup/page.tsx | 38 ++++++++++++++++++++++++++---- package.json | 1 + 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 app/api/send-verification/route.ts diff --git a/app/api/send-verification/route.ts b/app/api/send-verification/route.ts new file mode 100644 index 0000000..d1d8a9d --- /dev/null +++ b/app/api/send-verification/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import Sendgrid from '@sendgrid/mail' + +export async function POST(request: Request) { + try { + const { email, code } = await request.json() + + if (!email || !code) { + return NextResponse.json( + { error: 'Email and code are required' }, + { status: 400 } + ) + } + + Sendgrid.setApiKey(process.env.SENDGRID_API_KEY as string) + + const msg = { + to: email, + from: 'civicflow@codeforpakistan.org', // Replace with your verified sender + templateId: 'd-070a0738fac147eb878f87b86eed664c', + dynamicTemplateData: { + VCODE: code + } + } + + await Sendgrid.send(msg) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Error sending email:', error) + return NextResponse.json( + { error: 'Failed to send verification email' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/signup/page.tsx b/app/signup/page.tsx index d101075..408c23b 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -63,8 +63,12 @@ export default function Component() { switch (currentStep) { case RegistrationStep.EnterContact: return ( -
{ + { e.preventDefault() + const code = generateVerificationCode(); + setGeneratedCode(code); + setVerificationCode(code); + if (!isEmail(contact)) { if (!contact.startsWith('+92')) { toast({ @@ -75,10 +79,36 @@ export default function Component() { return } setPhone(contact) + } else { + try { + const response = await fetch('/api/send-verification', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: contact, + code: code, + }), + }); + + if (!response.ok) { + throw new Error('Failed to send verification email'); + } + + toast({ + title: "Success", + description: "Verification code sent to your email", + }); + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to send verification email. Please try again.", + }); + return; + } } - const code = generateVerificationCode(); - setGeneratedCode(code); - setVerificationCode(code); setCurrentStep(currentStep + 1) }}>
diff --git a/package.json b/package.json index 50549aa..8aa9a9b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.2.2", + "@sendgrid/mail": "^8.1.4", "@supabase/supabase-js": "^2.46.1", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", From 615087acf7e791b619e69fe44ab046f296d5e2d7 Mon Sep 17 00:00:00 2001 From: aliirz Date: Tue, 19 Nov 2024 23:28:55 +0500 Subject: [PATCH 3/4] added lock file --- package-lock.json | 57 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60cc690..661b3bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.2.2", + "@sendgrid/mail": "^8.1.4", "@supabase/supabase-js": "^2.46.1", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", @@ -1142,6 +1143,41 @@ "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "dev": true }, + "node_modules/@sendgrid/client": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.4.tgz", + "integrity": "sha512-VxZoQ82MpxmjSXLR3ZAE2OWxvQIW2k2G24UeRPr/SYX8HqWLV/8UBN15T2WmjjnEb5XSmFImTJOKDzzSeKr9YQ==", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.7.4" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.4.tgz", + "integrity": "sha512-MUpIZykD9ARie8LElYCqbcBhGGMaA/E6I7fEcG7Hc2An26QJyLtwOaKQ3taGp8xO8BICPJrSKuYV4bDeAJKFGQ==", + "dependencies": { + "@sendgrid/client": "^8.1.4", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@supabase/auth-js": { "version": "2.65.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", @@ -1718,8 +1754,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "optional": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -1786,7 +1821,6 @@ "version": "1.7.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "optional": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2236,7 +2270,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2456,6 +2489,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "devOptional": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2508,7 +2549,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "optional": true, "engines": { "node": ">=0.4.0" } @@ -3571,7 +3611,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "optional": true, "engines": { "node": ">=4.0" }, @@ -3609,7 +3648,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -4922,7 +4960,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "optional": true, "engines": { "node": ">= 0.6" } @@ -4931,7 +4968,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "optional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5780,8 +5816,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "optional": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.2", From 1630b708d5c4422b0fdd90e89b9e86e6ae426131 Mon Sep 17 00:00:00 2001 From: aliirz Date: Tue, 19 Nov 2024 23:33:02 +0500 Subject: [PATCH 4/4] fixed package name --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 661b3bb..c3be994 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "next-template", + "name": "pehchan", "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "next-template", + "name": "pehchan", "version": "0.0.2", "dependencies": { "@keycloak/keycloak-admin-client": "^26.0.2", diff --git a/package.json b/package.json index 8aa9a9b..d712c88 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "next-template", + "name": "pehchan", "version": "0.0.2", "private": true, "scripts": {