Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

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:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

View 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;
}

View 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 });
}
}

View 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));
}
}

View 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 });
}
});

View 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 });
}
});

View 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 });
}
});

View 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 });
}
});

View 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 });
}
});