Major architectural upgrade preparing Echo OCR for self-hosted SaaS deployment: - Auth: Built-in Auth.js v5 with credentials + Authentik OIDC SSO, JWT sessions, middleware route protection, login/signup/setup pages, registration API - UI: Dashboard layout with collapsible sidebar nav, AppShell wrapper, route groups for (dashboard) and (auth), new pages for events/people/reports - Schema: Auth.js tables (Account, Session, VerificationToken), Organization, OrgMember, Location, CollectionDay, Invitation, SystemConfig, ApiKey models; proper User relations to ResponseCard/ActivityLog/Notification - Permissions: Role hierarchy (owner/admin/editor/reviewer/viewer) with action-based permission map and requirePermission/requireAuth helpers - Onboarding: Multi-step setup wizard for first-user bootstrap (account, org, location) with SystemConfig tracking - Events: CollectionDay model with rrule support for recurring church services - Auto-assign: Event-aware card assignment engine replacing getPreviousSunday() - Migration: seed-migration.ts script for upgrading existing deployments Made-with: Cursor
94 lines
2.4 KiB
TypeScript
94 lines
2.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/db";
|
|
import { requireAuth, withOrgScope } from "@/lib/auth";
|
|
import { PermissionError } from "@/lib/permissions";
|
|
|
|
export async function GET() {
|
|
try {
|
|
const user = await requireAuth("users.invite");
|
|
const scope = withOrgScope(user);
|
|
|
|
const invitations = await prisma.invitation.findMany({
|
|
where: { ...scope, acceptedAt: null },
|
|
orderBy: { createdAt: "desc" },
|
|
});
|
|
|
|
return NextResponse.json(invitations);
|
|
} catch (error) {
|
|
if (error instanceof PermissionError) {
|
|
return NextResponse.json({ error: error.message }, { status: 403 });
|
|
}
|
|
return NextResponse.json({ error: "Failed to fetch invitations" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const user = await requireAuth("users.invite");
|
|
|
|
if (!user.orgId) {
|
|
return NextResponse.json(
|
|
{ error: "No organization found" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const body = await req.json();
|
|
const { email, role } = body;
|
|
|
|
if (!email) {
|
|
return NextResponse.json(
|
|
{ error: "Email is required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const validRoles = ["admin", "editor", "reviewer", "viewer"];
|
|
if (role && !validRoles.includes(role)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid role" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const existing = await prisma.invitation.findFirst({
|
|
where: {
|
|
email,
|
|
organizationId: 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 invitation = await prisma.invitation.create({
|
|
data: {
|
|
email,
|
|
role: role || "viewer",
|
|
organizationId: user.orgId,
|
|
invitedById: user.id,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({
|
|
invitation,
|
|
inviteUrl: `/signup?token=${invitation.token}`,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof PermissionError) {
|
|
return NextResponse.json({ error: error.message }, { status: 403 });
|
|
}
|
|
console.error("[invitations] Error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to create invitation" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|