Skip to content

Commit

Permalink
Merge pull request #9 from codeforpakistan/werk
Browse files Browse the repository at this point in the history
SG Integration
  • Loading branch information
aliirz authored Nov 19, 2024
2 parents 700f5d5 + 1630b70 commit c49ebaf
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 31 deletions.
29 changes: 29 additions & 0 deletions app/api/auth/services/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
64 changes: 64 additions & 0 deletions app/api/auth/sessions/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
36 changes: 36 additions & 0 deletions app/api/send-verification/route.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]', // 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 }
)
}
}
16 changes: 3 additions & 13 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -136,19 +138,7 @@ export default function Component() {
</Card>

{/* Active Services */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Active Services</CardTitle>
<CardDescription>Services you are currently logged into</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Tax Portal</Badge>
<Badge variant="secondary">Health Services</Badge>
<Badge variant="secondary">Education Portal</Badge>
</div>
</CardContent>
</Card>
<UserServices />

{/* Notifications */}
<Card className="mb-6">
Expand Down
38 changes: 34 additions & 4 deletions app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ export default function Component() {
switch (currentStep) {
case RegistrationStep.EnterContact:
return (
<form onSubmit={(e) => {
<form onSubmit={async (e) => {
e.preventDefault()
const code = generateVerificationCode();
setGeneratedCode(code);
setVerificationCode(code);

if (!isEmail(contact)) {
if (!contact.startsWith('+92')) {
toast({
Expand All @@ -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)
}}>
<div className="space-y-4">
Expand Down
75 changes: 75 additions & 0 deletions components/dashboard/user-services.tsx
Original file line number Diff line number Diff line change
@@ -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<ClientSession[]>([])
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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Shield className="mr-2 h-4 w-4" />
Connected Services
</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div>Loading active services...</div>
) : (
<div className="space-y-2">
{sessions.length === 0 ? (
<div className="text-muted-foreground text-sm">
No active services found
</div>
) : (
sessions.map((session) => (
<div key={session.clientId} className="flex justify-between items-center p-3 rounded-lg bg-muted">
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<ExternalLink className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{session.clientName}</span>
</div>
<span className="text-xs text-muted-foreground">IP: {session.ipAddress}</span>
<span className="text-xs text-muted-foreground">
Started: {new Date(session.started).toLocaleString()}
</span>
</div>
<Badge className="bg-green-500 text-white">Active</Badge>
</div>
))
)}
</div>
)}
</CardContent>
</Card>
)
}
8 changes: 8 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions lib/keycloak-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading

0 comments on commit c49ebaf

Please sign in to comment.