Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ Thumbs.db
coverage/
*.lcov
.vercel
.env*.local
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,4 @@ With more time, I would add:
## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).
# Test change
16 changes: 0 additions & 16 deletions apps/web/auth.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/web/next-auth.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"diff2html": "^3.4.56",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
"partysocket": "^1.1.14",
"radix-ui": "^1.4.3",
"react": "19.2.3",
Expand Down
20 changes: 9 additions & 11 deletions apps/web/src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { getSession } from "@/lib/session"
import { Octokit } from "@octokit/rest"
import { PRCard, PRCardData } from "@/components/pr-card"

async function fetchUserPRs(accessToken: string): Promise<PRCardData[]> {
try {
const octokit = new Octokit({ auth: accessToken })

// Get user's repos with push access (max 10)

const { data: repos } = await octokit.repos.listForAuthenticatedUser({
sort: "pushed",
direction: "desc",
per_page: 10,
type: "owner",
})

const allPRs: PRCardData[] = []

await Promise.all(
repos.slice(0, 5).map(async (repo) => {
try {
Expand All @@ -28,7 +27,7 @@ async function fetchUserPRs(accessToken: string): Promise<PRCardData[]> {
sort: "updated",
direction: "desc",
})

for (const pr of prs) {
allPRs.push({
number: pr.number,
Expand All @@ -38,7 +37,6 @@ async function fetchUserPRs(accessToken: string): Promise<PRCardData[]> {
author: pr.user?.login ?? "unknown",
authorAvatar: pr.user?.avatar_url ?? "",
createdAt: pr.created_at,
// changed_files/additions/deletions are not in list endpoint; default to 0
fileCount: 0,
additions: 0,
deletions: 0,
Expand All @@ -50,7 +48,7 @@ async function fetchUserPRs(accessToken: string): Promise<PRCardData[]> {
}
})
)

return allPRs.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
Expand All @@ -60,9 +58,9 @@ async function fetchUserPRs(accessToken: string): Promise<PRCardData[]> {
}

export default async function DashboardPage() {
const session = await auth()
const session = await getSession()
if (!session) redirect("/login")
const accessToken = session.accessToken as string
const { accessToken } = session
const prs = accessToken ? await fetchUserPRs(accessToken) : []

return (
Expand All @@ -73,7 +71,7 @@ export default async function DashboardPage() {
{prs.length} open PRs across your repositories
</p>
</div>

{prs.length === 0 ? (
<div className="text-center py-16 text-muted-foreground">
<p className="text-lg font-medium">No open pull requests</p>
Expand Down
22 changes: 9 additions & 13 deletions apps/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { auth, signOut } from "@/auth"
import { redirect } from "next/navigation"
import { getSession } from "@/lib/session"

export default async function AppLayout({ children }: { children: React.ReactNode }) {
const session = await auth()
const session = await getSession()

if (!session) {
redirect("/login")
}
Expand All @@ -13,17 +13,13 @@ export default async function AppLayout({ children }: { children: React.ReactNod
<header className="border-b border-border px-6 py-3 flex items-center justify-between">
<a href="/dashboard" className="font-semibold text-lg">Assert Review</a>
<div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground">{session.user?.name}</span>
<form
action={async () => {
"use server"
await signOut({ redirectTo: "/login" })
}}
<span className="text-sm text-muted-foreground">{session.user.name}</span>
<a
href="/api/auth/logout"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<button type="submit" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Sign out
</button>
</form>
Sign out
</a>
</div>
</header>
<main id="main-content">{children}</main>
Expand Down
18 changes: 9 additions & 9 deletions apps/web/src/app/(app)/pr/[owner]/[repo]/[number]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { getSession } from "@/lib/session"
import { Octokit } from "@octokit/rest"
import { PRReviewView } from "@/components/pr-review/pr-review-view"

Expand All @@ -8,7 +9,7 @@ interface PageProps {

async function fetchPRData(accessToken: string, owner: string, repo: string, number: number) {
const octokit = new Octokit({ auth: accessToken })

const [prResp, filesResp] = await Promise.all([
octokit.pulls.get({ owner, repo, pull_number: number }),
octokit.pulls.listFiles({ owner, repo, pull_number: number, per_page: 100 }),
Expand All @@ -27,7 +28,7 @@ async function fetchPRData(accessToken: string, owner: string, repo: string, num

async function fetchMLData(files: { filename: string; additions: number; deletions: number; patch?: string }[], prId: string) {
const mlApiUrl = process.env.ML_API_URL ?? "http://localhost:8000"

const [rankingRes, clusterRes] = await Promise.allSettled([
fetch(`${mlApiUrl}/rank`, {
method: "POST",
Expand All @@ -51,18 +52,17 @@ async function fetchMLData(files: { filename: string; additions: number; deletio

export default async function PRDetailPage({ params }: PageProps) {
const { owner, repo, number } = await params
const session = await auth()
const session = await getSession()

if (!session?.accessToken) {
return <div>Not authenticated</div>
}
if (!session) redirect("/login")

const { accessToken } = session
let prData = null
let rankingData = null
let clusterData = null

try {
prData = await fetchPRData(session.accessToken, owner, repo, parseInt(number, 10))
prData = await fetchPRData(accessToken, owner, repo, parseInt(number, 10))
const mlData = await fetchMLData(prData.files, number)
rankingData = mlData.rankingData
clusterData = mlData.clusterData
Expand All @@ -82,7 +82,7 @@ export default async function PRDetailPage({ params }: PageProps) {
<p className="text-xs text-muted-foreground">{owner}/{repo}</p>
<h1 className="text-lg font-semibold">{prData?.pr.title ?? `PR #${number}`}</h1>
</div>

<PRReviewView
files={prData?.files ?? []}
rankingData={rankingData}
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/app/api/auth/[...nextauth]/route.ts

This file was deleted.

56 changes: 56 additions & 0 deletions apps/web/src/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from "next/server"

export async function GET(req: NextRequest) {
const code = req.nextUrl.searchParams.get("code")
if (!code) return NextResponse.redirect(new URL("/login", req.url))

const tokenRes = await fetch(
"https://github.com/login/oauth/access_token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
client_id: process.env.AUTH_GITHUB_ID,
client_secret: process.env.AUTH_GITHUB_SECRET,
code,
}),
}
)
const { access_token, error } = await tokenRes.json()
if (error || !access_token) {
return NextResponse.redirect(new URL("/login?error=oauth", req.url))
}

const userRes = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: "application/vnd.github+json",
},
})
const user = await userRes.json()

const session = Buffer.from(
JSON.stringify({
user: {
login: user.login,
name: user.name ?? user.login,
avatar_url: user.avatar_url,
email: user.email ?? "",
},
accessToken: access_token,
})
).toString("base64")

const res = NextResponse.redirect(new URL("/dashboard", req.url))
res.cookies.set("gh_session", session, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
path: "/",
})
return res
}
13 changes: 13 additions & 0 deletions apps/web/src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server"

export async function GET() {
const params = new URLSearchParams({
client_id: process.env.AUTH_GITHUB_ID!,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
scope: "read:user user:email repo",
state: crypto.randomUUID(),
})
return NextResponse.redirect(
`https://github.com/login/oauth/authorize?${params}`
)
}
7 changes: 7 additions & 0 deletions apps/web/src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server"

export async function GET(req: Request) {
const res = NextResponse.redirect(new URL("/login", req.url))
res.cookies.delete("gh_session")
return res
}
9 changes: 9 additions & 0 deletions apps/web/src/app/api/debug-env/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const dynamic = "force-dynamic"
export async function GET() {
return Response.json({
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID ?? "MISSING",
AUTH_GITHUB_ID_len: (process.env.AUTH_GITHUB_ID ?? "").length,
AUTH_GITHUB_SECRET_len: (process.env.AUTH_GITHUB_SECRET ?? "").length,
AUTH_SECRET_len: (process.env.AUTH_SECRET ?? "").length,
})
}
5 changes: 1 addition & 4 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import { SessionProvider } from "next-auth/react"
import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -15,9 +14,7 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<SessionProvider>
{children}
</SessionProvider>
{children}
</body>
</html>
);
Expand Down
32 changes: 9 additions & 23 deletions apps/web/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
import { signIn } from "@/auth"

export default function LoginPage() {
return (
<main className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-6 max-w-md px-4">
<h1 className="text-4xl font-bold tracking-tight">Assert Review</h1>
<p className="text-muted-foreground text-lg">
AI-powered code review — ML-ranked diffs, semantic grouping, real-time collaboration.
</p>
<form
action={async () => {
"use server"
await signIn("github", { redirectTo: "/dashboard" })
}}
<main className="flex min-h-screen items-center justify-center">
<div className="text-center space-y-4">
<h1 className="text-2xl font-bold">Assert Review</h1>
<p className="text-muted-foreground">AI-powered code review</p>
<a
href="/api/auth/login"
className="inline-flex items-center gap-2 rounded-md bg-gray-900 px-6 py-3 text-white hover:bg-gray-700"
>
<button
type="submit"
className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:opacity-90 transition-opacity"
>
Sign in with GitHub
</button>
</form>
<p className="text-xs text-muted-foreground">
Free to use. No data stored beyond your session.
</p>
Sign in with GitHub
</a>
</div>
</main>
)
Expand Down
Loading