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:
parent
1b669bb1b7
commit
20bc4cbb03
5 changed files with 315 additions and 2 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
112
src/app/api/org/invitations/route.ts
Normal file
112
src/app/api/org/invitations/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
83
src/app/api/org/locations/route.ts
Normal file
83
src/app/api/org/locations/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/app/api/org/members/route.ts
Normal file
30
src/app/api/org/members/route.ts
Normal 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
88
src/app/api/org/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue