Skip to content

Commit c5f4320

Browse files
committed
Migrate to next-firebase-auth-edge
1 parent 7b68fd8 commit c5f4320

53 files changed

Lines changed: 1070 additions & 576 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:next-firebase-auth-edge-docs.vercel.app)",
5+
"WebSearch",
6+
"WebFetch(domain:github.com)"
7+
]
8+
}
9+
}

.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
NEXT_PUBLIC_APP_URL=http://localhost:3000
2+
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
3+
COOKIE_SECRET_CURRENT=local-dev-cookie-secret-minimum-32-chars!!
24
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyA8nQDxehOwI6qzEpxo0qXwqM_BC-t-4wk
35
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=alumni-65507.firebaseapp.com
46
NEXT_PUBLIC_FIREBASE_PROJECT_ID=alumni-65507

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ next-env.d.ts
1818
.nyc_output
1919

2020
# ---- Firebase ----
21+
.emulator-data/
2122
firebase-service-account.json
2223
firebase-debug.log
2324
firestore-debug.log

app/(app)/admin/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { useEffect } from "react";
44
import { useRouter } from "next/navigation";
55
import { useAuth } from "@/lib/auth/context";
66
import { InviteForm } from "@/components/admin/InviteForm";
7+
import { MemberList } from "@/components/admin/MemberList";
78

89
export default function AdminPage() {
9-
const { isAdmin, loading } = useAuth();
10+
const { user, isAdmin, loading } = useAuth();
1011
const router = useRouter();
1112

1213
useEffect(() => {
@@ -15,12 +16,13 @@ export default function AdminPage() {
1516
}
1617
}, [loading, isAdmin, router]);
1718

18-
if (loading || !isAdmin) return null;
19+
if (loading || !isAdmin || !user) return null;
1920

2021
return (
2122
<div className="space-y-6">
2223
<h1 className="text-2xl font-semibold">Admin</h1>
2324
<InviteForm />
25+
<MemberList currentUid={user.uid} />
2426
</div>
2527
);
2628
}

app/(app)/companies/page.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

app/(app)/directory/page.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4-
import { searchUsers } from "@/lib/firestore/users";
5-
import { getCompanies } from "@/lib/firestore/companies";
6-
import { UserProfile, Company } from "@/types";
4+
import { UserProfile, Organization } from "@/types";
75
import { DirectoryFilters } from "@/components/directory/DirectoryFilters";
86
import { AlumniCard } from "@/components/directory/AlumniCard";
97

108
export default function DirectoryPage() {
119
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
12-
const [companies, setCompanies] = useState<Company[]>([]);
10+
const [organizations, setOrganizations] = useState<Organization[]>([]);
1311
const [nameFilter, setNameFilter] = useState("");
1412
const [classYearFilter, setClassYearFilter] = useState("");
1513

1614
useEffect(() => {
17-
searchUsers().then(setAllUsers);
18-
getCompanies().then(setCompanies);
15+
Promise.all([
16+
fetch("/api/users").then((r) => r.json()),
17+
fetch("/api/organizations").then((r) => r.json()),
18+
]).then(([users, orgs]) => {
19+
setAllUsers(users);
20+
setOrganizations(orgs);
21+
});
1922
}, []);
2023

2124
const filtered = allUsers.filter((u) => {
22-
const matchName = !nameFilter || `${u.firstName} ${u.lastName}`.toLowerCase().includes(nameFilter.toLowerCase());
25+
const matchName =
26+
!nameFilter ||
27+
`${u.firstName} ${u.lastName}`.toLowerCase().includes(nameFilter.toLowerCase());
2328
const matchYear = !classYearFilter || u.classYear === parseInt(classYearFilter);
2429
return matchName && matchYear;
2530
});
@@ -35,7 +40,7 @@ export default function DirectoryPage() {
3540
/>
3641
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
3742
{filtered.map((u) => (
38-
<AlumniCard key={u.uid} profile={u} companies={companies} />
43+
<AlumniCard key={u.uid} profile={u} organizations={organizations} />
3944
))}
4045
</div>
4146
{filtered.length === 0 && (

app/(app)/layout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1+
import { cookies } from "next/headers";
2+
import { redirect } from "next/navigation";
3+
import { getTokens } from "next-firebase-auth-edge";
4+
import { adminDb } from "@/lib/firebase/admin";
5+
import { authConfig } from "@/lib/firebase/auth-edge";
16
import { Header } from "@/components/layout/Header";
27

3-
export default function AppLayout({ children }: { children: React.ReactNode }) {
8+
export default async function AppLayout({ children }: { children: React.ReactNode }) {
9+
const tokens = await getTokens(await cookies(), authConfig);
10+
if (tokens) {
11+
const { uid } = tokens.decodedToken;
12+
const userDoc = await adminDb.collection("users").doc(uid).get();
13+
if (!userDoc.exists || !userDoc.data()?.profileComplete) {
14+
redirect("/signup/complete");
15+
}
16+
}
17+
418
return (
519
<>
620
<Header />

app/(app)/organizations/page.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Organization } from "@/types";
5+
import { OrganizationFilters } from "@/components/organizations/OrganizationFilters";
6+
import { OrganizationCard } from "@/components/organizations/OrganizationCard";
7+
8+
export default function OrganizationsPage() {
9+
const [allOrganizations, setAllOrganizations] = useState<Organization[]>([]);
10+
const [nameFilter, setNameFilter] = useState("");
11+
12+
useEffect(() => {
13+
fetch("/api/organizations")
14+
.then((r) => r.json())
15+
.then(setAllOrganizations)
16+
.catch(console.error);
17+
}, []);
18+
19+
const filtered = allOrganizations.filter((o) =>
20+
!nameFilter || o.name.toLowerCase().includes(nameFilter.toLowerCase())
21+
);
22+
23+
return (
24+
<div className="space-y-6">
25+
<h1 className="text-2xl font-semibold">Organizations</h1>
26+
<OrganizationFilters name={nameFilter} onNameChange={setNameFilter} />
27+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
28+
{filtered.map((o) => (
29+
<OrganizationCard key={o.id} organization={o} />
30+
))}
31+
</div>
32+
{filtered.length === 0 && (
33+
<p className="text-center text-muted-foreground py-12">No organizations found.</p>
34+
)}
35+
</div>
36+
);
37+
}

app/(app)/profile/[uid]/page.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import { use, useEffect, useState } from "react";
44
import { notFound } from "next/navigation";
5-
import { getUserProfile } from "@/lib/firestore/users";
6-
import { getCompanies } from "@/lib/firestore/companies";
75
import { ProfileHeader } from "@/components/profile/ProfileHeader";
8-
import { UserProfile, Company } from "@/types";
6+
import { UserProfile, Organization } from "@/types";
97

108
export default function ProfilePage({
119
params,
@@ -14,17 +12,22 @@ export default function ProfilePage({
1412
}) {
1513
const { uid } = use(params);
1614
const [profile, setProfile] = useState<UserProfile | null | "loading">("loading");
17-
const [companies, setCompanies] = useState<Company[]>([]);
15+
const [organizations, setOrganizations] = useState<Organization[]>([]);
1816

1917
useEffect(() => {
20-
Promise.all([getUserProfile(uid), getCompanies()]).then(([p, c]) => {
18+
Promise.all([
19+
fetch(`/api/users/${uid}`).then((r) => (r.ok ? r.json() : null)),
20+
fetch("/api/organizations").then((r) => r.json()),
21+
]).then(([p, o]) => {
2122
setProfile(p);
22-
setCompanies(c);
23+
setOrganizations(o);
2324
});
2425
}, [uid]);
2526

2627
if (profile === "loading") {
27-
return <div className="max-w-2xl mx-auto py-12 text-center text-muted-foreground">Loading…</div>;
28+
return (
29+
<div className="max-w-2xl mx-auto py-12 text-center text-muted-foreground">Loading…</div>
30+
);
2831
}
2932

3033
if (!profile) {
@@ -33,7 +36,7 @@ export default function ProfilePage({
3336

3437
return (
3538
<div className="max-w-2xl mx-auto">
36-
<ProfileHeader initialProfile={profile} companies={companies} />
39+
<ProfileHeader initialProfile={profile} organizations={organizations} />
3740
</div>
3841
);
3942
}

app/api/invitations/route.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { adminAuth, adminDb } from "@/lib/firebase/admin";
2+
import { adminDb } from "@/lib/firebase/admin";
33
import { Resend } from "resend";
44
import { Timestamp } from "firebase-admin/firestore";
5+
import { getTokens } from "next-firebase-auth-edge";
6+
import { authConfig } from "@/lib/firebase/auth-edge";
57

68
const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
79
const isDev = process.env.NODE_ENV === "development";
@@ -28,22 +30,17 @@ async function sendInviteEmail(to: string, signupLink: string) {
2830

2931
export async function POST(request: NextRequest) {
3032
try {
31-
const sessionCookie = request.cookies.get("__session")?.value;
32-
if (!sessionCookie) {
33-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
34-
}
35-
36-
const decoded = await adminAuth.verifySessionCookie(sessionCookie, true);
37-
const userDoc = await adminDb.collection("users").doc(decoded.uid).get();
38-
const userData = userDoc.data();
33+
const tokens = await getTokens(request.cookies, authConfig);
34+
if (!tokens) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
3935

40-
if (!userData || userData.role !== "admin") {
36+
const userDoc = await adminDb.collection("users").doc(tokens.decodedToken.uid).get();
37+
if (userDoc.data()?.role !== "admin") {
4138
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
4239
}
4340

44-
const { email } = await request.json();
45-
if (!email) {
46-
return NextResponse.json({ error: "Email required" }, { status: 400 });
41+
const { email, firstName, lastName } = await request.json();
42+
if (!email || !firstName || !lastName) {
43+
return NextResponse.json({ error: "Email, firstName, and lastName are required" }, { status: 400 });
4744
}
4845

4946
const code = crypto.randomUUID();
@@ -54,8 +51,10 @@ export async function POST(request: NextRequest) {
5451
await adminDb.collection("invitations").doc(code).set({
5552
code,
5653
email,
54+
firstName,
55+
lastName,
5756
sentAt: Timestamp.now(),
58-
sentBy: decoded.uid,
57+
sentBy: tokens.decodedToken.uid,
5958
});
6059

6160
return NextResponse.json({ ok: true });

0 commit comments

Comments
 (0)