diff --git a/src/app/api/auth/jira/authorize/route.ts b/src/app/api/auth/jira/authorize/route.ts new file mode 100644 index 0000000..2ad52d3 --- /dev/null +++ b/src/app/api/auth/jira/authorize/route.ts @@ -0,0 +1,38 @@ +import { type NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; + +export async function GET(request: NextRequest) { + const clientId = process.env.JIRA_CLIENT_ID; + const redirectUri = process.env.JIRA_REDIRECT_URI; + + if (!clientId || !redirectUri) { + return new NextResponse("Jira client ID or redirect URI is not set", { + status: 500, + }); + } + + const state = crypto.randomBytes(16).toString("hex"); + const codeVerifier = crypto.randomBytes(64).toString("hex"); + const codeChallenge = crypto + .createHash("sha256") + .update(codeVerifier) + .digest("base64url"); + + const authorizationUrl = + `https://auth.atlassian.com/authorize` + + `?audience=api.atlassian.com` + + `&client_id=${encodeURIComponent(clientId)}` + + `&scope=${encodeURIComponent("read:jira-user read:jira-work")}` + + `&redirect_uri=${encodeURIComponent(redirectUri)}` + + `&state=${encodeURIComponent(state)}` + + `&response_type=code` + + `&prompt=consent` + + `&code_challenge=${encodeURIComponent(codeChallenge)}` + + `&code_challenge_method=S256`; + + const response = NextResponse.redirect(authorizationUrl); + response.cookies.set("jira_code_verifier", codeVerifier, { httpOnly: true }); + response.cookies.set("jira_state", state, { httpOnly: true }); + + return response; +} diff --git a/src/app/api/auth/jira/callback/route.ts b/src/app/api/auth/jira/callback/route.ts new file mode 100644 index 0000000..66073ea --- /dev/null +++ b/src/app/api/auth/jira/callback/route.ts @@ -0,0 +1,70 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/server/db/db"; +import { getServerAuthSession } from "@/server/auth"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const code = searchParams.get("code"); + const state = searchParams.get("state"); + + const jiraState = request.cookies.get("jira_state")?.value; + const codeVerifier = request.cookies.get("jira_code_verifier")?.value; + + if (!code || !state || !jiraState || !codeVerifier) { + return new NextResponse("Invalid request", { status: 400 }); + } + + if (state !== jiraState) { + return new NextResponse("State mismatch", { status: 400 }); + } + + const clientId = process.env.JIRA_CLIENT_ID ?? ""; + const clientSecret = process.env.JIRA_CLIENT_SECRET ?? ""; + const redirectUri = process.env.JIRA_REDIRECT_URI ?? ""; + + const body = { + grant_type: "authorization_code", + client_id: clientId, + client_secret: clientSecret, + code: code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }; + + const tokenResponse = await fetch("https://auth.atlassian.com/oauth/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!tokenResponse.ok) { + return new NextResponse("Failed to exchange code for token", { + status: 500, + }); + } + + const tokenData = await tokenResponse.json(); + const accessToken = tokenData.access_token; + + if (!accessToken) { + return new NextResponse("Invalid token response", { status: 500 }); + } + + const session = await getServerAuthSession(); + + if (!session?.user) { + return new NextResponse("Not authenticated", { status: 401 }); + } + + const userId = parseInt(session.user.id, 10); + + await db.users.find(userId).update({ + jiraToken: accessToken, + }); + + const response = NextResponse.redirect("/dashboard"); + response.cookies.delete("jira_state"); + response.cookies.delete("jira_code_verifier"); + + return response; +} diff --git a/src/app/dashboard/[org]/[repo]/settings/page.tsx b/src/app/dashboard/[org]/[repo]/settings/page.tsx new file mode 100644 index 0000000..0316ce5 --- /dev/null +++ b/src/app/dashboard/[org]/[repo]/settings/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; + +interface SettingsPageProps { + params: { + org: string; + repo: string; + }; +} + +export default function SettingsPage({ params }: SettingsPageProps) { + const router = useRouter(); + const { data: session } = useSession(); + + const handleLoginWithJira = () => { + router.push("/api/auth/jira/authorize"); + }; + + return ( +
Connected to Jira
+ )} +