Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
src/app/api/portal/auth/logout/route.ts
Normal file
12
src/app/api/portal/auth/logout/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { PORTAL_COOKIE } from '@/lib/portal/auth';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
const response = NextResponse.redirect(new URL('/portal/login', env.APP_URL));
|
||||
|
||||
response.cookies.delete(PORTAL_COOKIE);
|
||||
|
||||
return response;
|
||||
}
|
||||
28
src/app/api/portal/auth/request/route.ts
Normal file
28
src/app/api/portal/auth/request/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { requestMagicLink } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
const bodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const parsed = bodySchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Invalid email address' }, { status: 400 });
|
||||
}
|
||||
|
||||
await requestMagicLink(parsed.data.email);
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal magic link request failed');
|
||||
return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
38
src/app/api/portal/auth/verify/route.ts
Normal file
38
src/app/api/portal/auth/verify/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { verifyPortalToken, PORTAL_COOKIE } from '@/lib/portal/auth';
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const token = req.nextUrl.searchParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/portal/login?error=missing_token', env.APP_URL));
|
||||
}
|
||||
|
||||
const session = await verifyPortalToken(token);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.redirect(new URL('/portal/login?error=invalid_token', env.APP_URL));
|
||||
}
|
||||
|
||||
const response = NextResponse.redirect(new URL('/portal/dashboard', env.APP_URL));
|
||||
|
||||
response.cookies.set(PORTAL_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
|
||||
logger.info({ clientId: session.clientId }, 'Portal session created');
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal token verification failed');
|
||||
return NextResponse.redirect(new URL('/portal/login?error=server_error', env.APP_URL));
|
||||
}
|
||||
}
|
||||
20
src/app/api/portal/dashboard/route.ts
Normal file
20
src/app/api/portal/dashboard/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withPortalAuth } from '@/lib/portal/helpers';
|
||||
import { getPortalDashboard } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export const GET = withPortalAuth(async (_req, session) => {
|
||||
try {
|
||||
const dashboard = await getPortalDashboard(session.clientId, session.portId);
|
||||
|
||||
if (!dashboard) {
|
||||
return NextResponse.json({ error: 'Client not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: dashboard });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal dashboard fetch failed');
|
||||
return NextResponse.json({ error: 'Failed to load dashboard' }, { status: 500 });
|
||||
}
|
||||
});
|
||||
26
src/app/api/portal/documents/[documentId]/download/route.ts
Normal file
26
src/app/api/portal/documents/[documentId]/download/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withPortalAuth } from '@/lib/portal/helpers';
|
||||
import { getDocumentDownloadUrl } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export const GET = withPortalAuth(async (_req, session, params) => {
|
||||
try {
|
||||
const documentId = params.documentId;
|
||||
|
||||
if (!documentId) {
|
||||
return NextResponse.json({ error: 'Document ID required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const url = await getDocumentDownloadUrl(session.clientId, documentId, session.portId);
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: 'Document not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ url });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal document download URL fetch failed');
|
||||
return NextResponse.json({ error: 'Failed to generate download URL' }, { status: 500 });
|
||||
}
|
||||
});
|
||||
15
src/app/api/portal/documents/route.ts
Normal file
15
src/app/api/portal/documents/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withPortalAuth } from '@/lib/portal/helpers';
|
||||
import { getClientDocuments } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export const GET = withPortalAuth(async (_req, session) => {
|
||||
try {
|
||||
const data = await getClientDocuments(session.clientId, session.portId);
|
||||
return NextResponse.json({ data });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal documents fetch failed');
|
||||
return NextResponse.json({ error: 'Failed to load documents' }, { status: 500 });
|
||||
}
|
||||
});
|
||||
15
src/app/api/portal/interests/route.ts
Normal file
15
src/app/api/portal/interests/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withPortalAuth } from '@/lib/portal/helpers';
|
||||
import { getClientInterests } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export const GET = withPortalAuth(async (_req, session) => {
|
||||
try {
|
||||
const data = await getClientInterests(session.clientId, session.portId);
|
||||
return NextResponse.json({ data });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal interests fetch failed');
|
||||
return NextResponse.json({ error: 'Failed to load interests' }, { status: 500 });
|
||||
}
|
||||
});
|
||||
15
src/app/api/portal/invoices/route.ts
Normal file
15
src/app/api/portal/invoices/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withPortalAuth } from '@/lib/portal/helpers';
|
||||
import { getClientInvoices } from '@/lib/services/portal.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export const GET = withPortalAuth(async (_req, session) => {
|
||||
try {
|
||||
const data = await getClientInvoices(session.clientId, session.portId);
|
||||
return NextResponse.json({ data });
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Portal invoices fetch failed');
|
||||
return NextResponse.json({ error: 'Failed to load invoices' }, { status: 500 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user