Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added api docs #12

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/api-docs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client'

import SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'

export default function ApiDocs() {
return (
<div className="container mx-auto p-4">
<SwaggerUI url="/api/api-docs" />
</div>
)
}
47 changes: 47 additions & 0 deletions app/api/api-docs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server'
import { createSwaggerSpec } from 'next-swagger-doc'

const spec = createSwaggerSpec({
apiFolder: 'app/api',
definition: {
openapi: '3.0.0',
info: {
title: 'Pehchan API Documentation',
version: '1.0.0',
description: 'Documentation for Pehchan Authentication Service',
},
servers: [
{
url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
description: 'API Server',
},
],
tags: [
{
name: 'Authentication',
description: 'Authentication related endpoints'
},
{
name: 'User Profile',
description: 'User profile management endpoints'
},
{
name: 'Sessions',
description: 'Session management endpoints'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
},
})

export async function GET() {
return NextResponse.json(spec)
}
57 changes: 57 additions & 0 deletions app/api/auth/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from 'next/server'
import Sendgrid from '@sendgrid/mail'
import { keycloakAdmin } from '@/lib/keycloak-admin'
import jwt from 'jsonwebtoken'

const RESET_TOKEN_SECRET = process.env.RESET_TOKEN_SECRET || 'your-secret-key'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL

export async function POST(request: Request) {
try {
const { email } = await request.json()

// Find user in Keycloak
const user = await keycloakAdmin.getUserByEmail(email)
console.log(user)

if (!user) {
// Return success even if user not found (security best practice)
return NextResponse.json({ success: true })
}

// Generate reset token
const resetToken = jwt.sign(
{
userId: user.id,
email: user.email
},
RESET_TOKEN_SECRET,
{ expiresIn: '1h' }
)

// Create reset URL
const resetUrl = `${APP_URL}/reset-password?token=${resetToken}`

// Send email using SendGrid
Sendgrid.setApiKey(process.env.SENDGRID_API_KEY as string)

const msg = {
to: email,
from: '[email protected]',
templateId: 'd-25d9e6f70e8e463f8a8297560b19b488',
dynamicTemplateData: {
resetUrl: resetUrl
}
}

await Sendgrid.send(msg)

return NextResponse.json({ success: true })
} catch (error) {
console.error('Password reset error:', error)
return NextResponse.json(
{ error: 'Failed to process password reset request' },
{ status: 500 }
)
}
}
28 changes: 28 additions & 0 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { NextResponse } from 'next/server'
import { KEYCLOAK_CONFIG, KEYCLOAK_URLS } from '@/lib/keycloak-config'

/**
* @swagger
* /api/auth/login:
* post:
* tags: [Authentication]
* summary: Authenticate user
* description: Login with username and password
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* username:
* type: string
* password:
* type: string
* clientId:
* type: string
* redirectUri:
* type: string
* responses:
* 200:
* description: Login successful
* 401:
* description: Authentication failed
*/
export async function POST(request: Request) {
try {
const { username, password, clientId, redirectUri } = await request.json()
Expand Down
24 changes: 24 additions & 0 deletions app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from 'next/server'
import { keycloakAdmin } from '@/lib/keycloak-admin'
import jwt from 'jsonwebtoken'

const RESET_TOKEN_SECRET = process.env.RESET_TOKEN_SECRET || 'your-secret-key'

export async function POST(request: Request) {
try {
const { token, password } = await request.json()
const decoded = jwt.verify(token, RESET_TOKEN_SECRET) as {
userId: string
email: string
}

await keycloakAdmin.resetUserPassword(decoded.userId, password)
return NextResponse.json({ success: true })
} catch (error) {
console.error('Password reset error:', error)
return NextResponse.json(
{ error: 'Failed to reset password' },
{ status: 500 }
)
}
}
2 changes: 2 additions & 0 deletions app/api/auth/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { KEYCLOAK_URLS } from '@/lib/keycloak-config'

export const dynamic = 'force-dynamic'

interface KeycloakSession {
id?: string
username?: string
Expand Down
34 changes: 34 additions & 0 deletions app/api/auth/userinfo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ export async function OPTIONS() {
})
}

/**
* @swagger
* /api/auth/userinfo:
* get:
* summary: Get user information
* description: Retrieve authenticated user's profile information
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User information retrieved successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* sub:
* type: string
* email:
* type: string
* email_verified:
* type: boolean
* name:
* type: string
* profile:
* type: object
* properties:
* cnic:
* type: string
* phone:
* type: string
* 401:
* description: Unauthorized
*/
export async function GET(request: Request) {
try {
// Get the access token from Authorization header
Expand Down
30 changes: 30 additions & 0 deletions app/api/profile/update/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { supabase } from '@/lib/supabase'

/**
* @swagger
* /api/profile/update:
* post:
* tags: [User Profile]
* summary: Update user profile
* description: Update authenticated user's profile information
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* full_name:
* type: string
* phone:
* type: string
* cnic:
* type: string
* avatar_url:
* type: string
* responses:
* 200:
* description: Profile updated successfully
* 401:
* description: Unauthorized
*/
export async function POST(request: Request) {
try {
const accessToken = cookies().get('access_token')?.value
Expand Down
80 changes: 80 additions & 0 deletions app/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client"

import { useState } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useToast } from "@/hooks/use-toast"

export default function ForgotPassword() {
const [email, setEmail] = useState('')
const [isLoading, setIsLoading] = useState(false)
const { toast } = useToast()

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)

try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
})

if (!response.ok) {
throw new Error('Failed to send reset email')
}

toast({
title: "Success",
description: "If an account exists with this email, you will receive password reset instructions.",
})

} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to send reset email. Please try again.",
})
} finally {
setIsLoading(false)
}
}

return (
<div className="bg-background flex flex-1 flex-col items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center">Reset Password</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">
Email Address
</label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
</div>
<Button
type="submit"
className="w-full"
disabled={isLoading}
>
{isLoading ? 'Sending...' : 'Send Reset Link'}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}
10 changes: 10 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Input } from "@/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { useToast } from "@/hooks/use-toast"
import { Label } from "@/components/ui/label"
import Link from 'next/link'

export default function LoginPage() {
const router = useRouter()
const { toast } = useToast()
Expand Down Expand Up @@ -126,6 +128,14 @@ export default function LoginPage() {
required
/>
</div>
<div className="text-sm text-right">
<Link
href="/forgot-password"
className="text-primary hover:underline"
>
Forgot password?
</Link>
</div>
</CardContent>
<CardFooter>
<Button type="submit" className="w-full">
Expand Down
Loading