14a32bdc0d
- Unified monorepo with backend (Express), frontend (Next.js), and devops - Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example - Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing - DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks - CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration) - DX: Husky pre-commit hooks with smart change detection - Docs: Root README with architecture, CONTRIBUTING.md, PR template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
265 lines
8.7 KiB
TypeScript
265 lines
8.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getSupabaseAdmin } from "@/lib/admin";
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const organizationId = url.searchParams.get("organizationId");
|
|
const userId = url.searchParams.get("userId");
|
|
|
|
if (!organizationId || !userId) {
|
|
return NextResponse.json({ error: "Organization ID and User ID are required" }, { status: 400 });
|
|
}
|
|
|
|
// Verify user has access to this organization
|
|
const { data: userOrg, error: accessError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("organization_id, role")
|
|
.eq("id", userId)
|
|
.eq("organization_id", organizationId)
|
|
.single();
|
|
|
|
if (accessError || !userOrg) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
// Get all members of the organization
|
|
const { data: members, error: membersError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("id, name, email, role, created_at")
|
|
.eq("organization_id", organizationId)
|
|
.order("created_at", { ascending: true });
|
|
|
|
if (membersError) {
|
|
console.error("Error fetching members:", membersError);
|
|
return NextResponse.json({ error: "Failed to fetch members" }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({ members });
|
|
} catch (error) {
|
|
console.error("Error in members GET:", error);
|
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const { organizationId, email, role, invitedBy } = await request.json();
|
|
|
|
if (!organizationId || !email || !role || !invitedBy) {
|
|
return NextResponse.json({
|
|
error: "Organization ID, email, role, and inviter ID are required"
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Verify inviter has permission (must be owner or admin)
|
|
const { data: inviter, error: inviterError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("organization_id, role")
|
|
.eq("id", invitedBy)
|
|
.eq("organization_id", organizationId)
|
|
.single();
|
|
|
|
if (inviterError || !inviter) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
if (inviter.role !== "owner" && inviter.role !== "admin") {
|
|
return NextResponse.json({
|
|
error: "Only owners and admins can invite members"
|
|
}, { status: 403 });
|
|
}
|
|
|
|
// Check if user already exists in the system
|
|
const { data: existingUsers, error: userCheckError } = await getSupabaseAdmin()
|
|
.auth.admin.listUsers();
|
|
|
|
if (userCheckError) {
|
|
console.error("Error checking existing users:", userCheckError);
|
|
return NextResponse.json({ error: "Failed to check existing users" }, { status: 500 });
|
|
}
|
|
|
|
const existingUser = existingUsers.users.find(u => u.email === email);
|
|
|
|
if (existingUser) {
|
|
// Check if user is already in an organization
|
|
const { data: userRecord } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("organization_id")
|
|
.eq("id", existingUser.id)
|
|
.single();
|
|
|
|
if (userRecord?.organization_id) {
|
|
if (userRecord.organization_id === organizationId) {
|
|
return NextResponse.json({
|
|
error: "User is already a member of this organization"
|
|
}, { status: 400 });
|
|
} else {
|
|
return NextResponse.json({
|
|
error: "User is already a member of another organization"
|
|
}, { status: 400 });
|
|
}
|
|
}
|
|
|
|
// Add existing user to organization
|
|
const { error: updateError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.update({ organization_id: organizationId, role })
|
|
.eq("id", existingUser.id);
|
|
|
|
if (updateError) {
|
|
console.error("Error adding existing user to organization:", updateError);
|
|
return NextResponse.json({ error: "Failed to add user to organization" }, { status: 500 });
|
|
}
|
|
|
|
// Get updated user data
|
|
const { data: updatedUser } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("id, name, email, role, created_at")
|
|
.eq("id", existingUser.id)
|
|
.single();
|
|
|
|
return NextResponse.json({
|
|
member: updatedUser,
|
|
message: "Existing user added to organization"
|
|
});
|
|
} else {
|
|
// Create invitation record for new user
|
|
// Note: In a real app, you'd send an email invitation here
|
|
// For now, we'll just create a placeholder record
|
|
|
|
return NextResponse.json({
|
|
message: "Invitation would be sent to new user",
|
|
action: "invitation_sent"
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in members POST:", error);
|
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function PUT(request: Request) {
|
|
try {
|
|
const { memberId, role, updatedBy, organizationId } = await request.json();
|
|
|
|
if (!memberId || !role || !updatedBy || !organizationId) {
|
|
return NextResponse.json({
|
|
error: "Member ID, role, updater ID, and organization ID are required"
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Verify updater has permission (must be owner)
|
|
const { data: updater, error: updaterError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("organization_id, role")
|
|
.eq("id", updatedBy)
|
|
.eq("organization_id", organizationId)
|
|
.single();
|
|
|
|
if (updaterError || !updater) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
if (updater.role !== "owner") {
|
|
return NextResponse.json({
|
|
error: "Only owners can update member roles"
|
|
}, { status: 403 });
|
|
}
|
|
|
|
// Don't allow changing the role of the organization owner
|
|
const { data: targetMember } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("role")
|
|
.eq("id", memberId)
|
|
.single();
|
|
|
|
if (targetMember?.role === "owner" && role !== "owner") {
|
|
return NextResponse.json({
|
|
error: "Cannot change the role of the organization owner"
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Update member role
|
|
const { data: updatedMember, error: updateError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.update({ role })
|
|
.eq("id", memberId)
|
|
.eq("organization_id", organizationId)
|
|
.select("id, name, email, role, created_at")
|
|
.single();
|
|
|
|
if (updateError) {
|
|
console.error("Error updating member role:", updateError);
|
|
return NextResponse.json({ error: "Failed to update member role" }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({ member: updatedMember });
|
|
} catch (error) {
|
|
console.error("Error in members PUT:", error);
|
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: Request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const memberId = url.searchParams.get("memberId");
|
|
const removedBy = url.searchParams.get("removedBy");
|
|
const organizationId = url.searchParams.get("organizationId");
|
|
|
|
if (!memberId || !removedBy || !organizationId) {
|
|
return NextResponse.json({
|
|
error: "Member ID, remover ID, and organization ID are required"
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Verify remover has permission (must be owner or admin)
|
|
const { data: remover, error: removerError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("organization_id, role")
|
|
.eq("id", removedBy)
|
|
.eq("organization_id", organizationId)
|
|
.single();
|
|
|
|
if (removerError || !remover) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
if (remover.role !== "owner" && remover.role !== "admin") {
|
|
return NextResponse.json({
|
|
error: "Only owners and admins can remove members"
|
|
}, { status: 403 });
|
|
}
|
|
|
|
// Don't allow removing the organization owner
|
|
const { data: targetMember } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.select("role")
|
|
.eq("id", memberId)
|
|
.single();
|
|
|
|
if (targetMember?.role === "owner") {
|
|
return NextResponse.json({
|
|
error: "Cannot remove the organization owner"
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Remove member from organization (set organization_id to null)
|
|
const { error: removeError } = await getSupabaseAdmin()
|
|
.from("users")
|
|
.update({ organization_id: null, role: "member" })
|
|
.eq("id", memberId)
|
|
.eq("organization_id", organizationId);
|
|
|
|
if (removeError) {
|
|
console.error("Error removing member:", removeError);
|
|
return NextResponse.json({ error: "Failed to remove member" }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Error in members DELETE:", error);
|
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
} |