Add missing org API routes and fix registration gate

- Create /api/org (GET details, PUT update)
- Create /api/org/members (GET list)
- Create /api/org/invitations (GET list, POST create)
- Create /api/org/locations (GET list with collection days, POST create)
- Fix registration: check org count instead of user count so
  a fresh domain setup works even if orphaned users exist
  from a prior AUTH_SECRET rotation

Made-with: Cursor
This commit is contained in:
Randall Stillwell 2026-04-15 14:47:15 -05:00
parent 1b669bb1b7
commit 20bc4cbb03
5 changed files with 315 additions and 2 deletions

View file

@ -57,8 +57,8 @@ export async function POST(req: NextRequest) {
role = invitation.role;
organizationId = invitation.organizationId;
} else {
const userCount = await prisma.user.count();
if (userCount > 0) {
const orgCount = await prisma.organization.count();
if (orgCount > 0) {
return NextResponse.json(
{ error: "Registration requires an invitation" },
{ status: 403 }

View file

@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
export async function GET() {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const invitations = await prisma.invitation.findMany({
where: { organizationId: session.user.orgId },
orderBy: { createdAt: "desc" },
select: {
id: true,
email: true,
role: true,
expiresAt: true,
acceptedAt: true,
},
});
return NextResponse.json({ invitations });
} catch (error) {
console.error("[org/invitations] GET error:", error);
return NextResponse.json(
{ error: "Failed to fetch invitations" },
{ status: 500 }
);
}
}
export async function POST(req: NextRequest) {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const membership = await prisma.orgMember.findFirst({
where: {
userId: session.user.id,
organizationId: session.user.orgId,
},
});
if (!membership || !["owner", "admin"].includes(membership.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { email, role } = await req.json();
if (!email) {
return NextResponse.json(
{ error: "Email is required" },
{ status: 400 }
);
}
const existing = await prisma.invitation.findFirst({
where: {
email,
organizationId: session.user.orgId,
acceptedAt: null,
expiresAt: { gt: new Date() },
},
});
if (existing) {
return NextResponse.json(
{ error: "An active invitation already exists for this email" },
{ status: 409 }
);
}
const existingMember = await prisma.orgMember.findFirst({
where: {
organizationId: session.user.orgId,
user: { email },
},
});
if (existingMember) {
return NextResponse.json(
{ error: "This person is already a member" },
{ status: 409 }
);
}
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
await prisma.invitation.create({
data: {
email,
role: role || "viewer",
organizationId: session.user.orgId,
invitedById: session.user.id,
expiresAt,
},
});
return NextResponse.json({ success: true });
} catch (error) {
console.error("[org/invitations] POST error:", error);
return NextResponse.json(
{ error: "Failed to create invitation" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
export async function GET() {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const locations = await prisma.location.findMany({
where: { organizationId: session.user.orgId },
include: {
collectionDays: {
select: {
id: true,
name: true,
dayOfWeek: true,
timeStart: true,
timeEnd: true,
isActive: true,
},
orderBy: { dayOfWeek: "asc" },
},
},
orderBy: { createdAt: "asc" },
});
return NextResponse.json({ locations });
} catch (error) {
console.error("[org/locations] GET error:", error);
return NextResponse.json(
{ error: "Failed to fetch locations" },
{ status: 500 }
);
}
}
export async function POST(req: NextRequest) {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const membership = await prisma.orgMember.findFirst({
where: {
userId: session.user.id,
organizationId: session.user.orgId,
},
});
if (!membership || !["owner", "admin", "editor"].includes(membership.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { name, address } = await req.json();
if (!name) {
return NextResponse.json(
{ error: "Location name is required" },
{ status: 400 }
);
}
const location = await prisma.location.create({
data: {
name,
address: address || null,
organizationId: session.user.orgId,
},
});
return NextResponse.json({ location });
} catch (error) {
console.error("[org/locations] POST error:", error);
return NextResponse.json(
{ error: "Failed to create location" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
export async function GET() {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const members = await prisma.orgMember.findMany({
where: { organizationId: session.user.orgId },
include: {
user: {
select: { id: true, email: true, displayName: true },
},
},
orderBy: { createdAt: "asc" },
});
return NextResponse.json({ members });
} catch (error) {
console.error("[org/members] GET error:", error);
return NextResponse.json(
{ error: "Failed to fetch members" },
{ status: 500 }
);
}
}

88
src/app/api/org/route.ts Normal file
View file

@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
export async function GET() {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const organization = await prisma.organization.findUnique({
where: { id: session.user.orgId },
select: {
id: true,
name: true,
slug: true,
type: true,
timezone: true,
},
});
if (!organization) {
return NextResponse.json(
{ error: "Organization not found" },
{ status: 404 }
);
}
return NextResponse.json({ organization });
} catch (error) {
console.error("[org] GET error:", error);
return NextResponse.json(
{ error: "Failed to fetch organization" },
{ status: 500 }
);
}
}
function slugify(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export async function PUT(req: NextRequest) {
try {
const session = await auth();
if (!session?.user?.orgId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const membership = await prisma.orgMember.findFirst({
where: {
userId: session.user.id,
organizationId: session.user.orgId,
},
});
if (!membership || !["owner", "admin"].includes(membership.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { name, type, timezone } = await req.json();
const data: Record<string, string> = {};
if (name) {
data.name = name;
data.slug = slugify(name);
}
if (type) data.type = type;
if (timezone) data.timezone = timezone;
await prisma.organization.update({
where: { id: session.user.orgId },
data,
});
return NextResponse.json({ success: true });
} catch (error) {
console.error("[org] PUT error:", error);
return NextResponse.json(
{ error: "Failed to update organization" },
{ status: 500 }
);
}
}