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>
2026-03-26 11:52:51 +01:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
const envSchema = z.object({
|
|
|
|
|
// Database
|
|
|
|
|
DATABASE_URL: z.string().url().startsWith('postgresql://'),
|
|
|
|
|
|
|
|
|
|
// Redis
|
|
|
|
|
REDIS_URL: z.string().url().startsWith('redis://'),
|
|
|
|
|
|
|
|
|
|
// Auth
|
|
|
|
|
BETTER_AUTH_SECRET: z.string().min(32),
|
|
|
|
|
BETTER_AUTH_URL: z.string().url(),
|
|
|
|
|
CSRF_SECRET: z.string().min(32),
|
|
|
|
|
|
|
|
|
|
// MinIO
|
|
|
|
|
MINIO_ENDPOINT: z.string().min(1),
|
|
|
|
|
MINIO_PORT: z.coerce.number().int().positive(),
|
|
|
|
|
MINIO_ACCESS_KEY: z.string().min(1),
|
|
|
|
|
MINIO_SECRET_KEY: z.string().min(1),
|
|
|
|
|
MINIO_BUCKET: z.string().min(1),
|
|
|
|
|
MINIO_USE_SSL: z.enum(['true', 'false']).transform((v) => v === 'true'),
|
|
|
|
|
|
|
|
|
|
// Documenso
|
|
|
|
|
DOCUMENSO_API_URL: z.string().url(),
|
|
|
|
|
DOCUMENSO_API_KEY: z.string().min(1),
|
|
|
|
|
DOCUMENSO_WEBHOOK_SECRET: z.string().min(16),
|
2026-04-24 18:43:41 +02:00
|
|
|
DOCUMENSO_TEMPLATE_ID_EOI: z.coerce.number().int().positive().default(8),
|
|
|
|
|
DOCUMENSO_CLIENT_RECIPIENT_ID: z.coerce.number().int().positive().default(192),
|
|
|
|
|
DOCUMENSO_DEVELOPER_RECIPIENT_ID: z.coerce.number().int().positive().default(193),
|
|
|
|
|
DOCUMENSO_APPROVAL_RECIPIENT_ID: z.coerce.number().int().positive().default(194),
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
// Email
|
|
|
|
|
SMTP_HOST: z.string().min(1),
|
|
|
|
|
SMTP_PORT: z.coerce.number().int().positive(),
|
feat(portal): replace magic-link with email/password + admin-initiated activation
The client portal no longer uses passwordless / magic-link sign-in. Each
client now has a `portal_users` row with a scrypt-hashed password,
created by an admin from the client detail page; the admin's invite
mails an activation link that the client uses to set their own password.
Forgot-password is wired through the same token mechanism.
Schema (migration `0009_outgoing_rumiko_fujikawa.sql`):
- `portal_users` — one per client account, separate from the CRM
`users` table (better-auth) so the auth realms stay isolated. Email
is globally unique, password is null until activation.
- `portal_auth_tokens` — single-use activation / reset tokens. Stores
only the SHA-256 hash so a DB compromise never leaks live tokens.
Services:
- `src/lib/portal/passwords.ts` — scrypt hash/verify (no new deps;
uses node:crypto), token mint+hash helpers.
- `src/lib/services/portal-auth.service.ts` — createPortalUser,
resendActivation, activateAccount, signIn (timing-safe),
requestPasswordReset, resetPassword. Auth failures throw the new
UnauthorizedError (401); enumeration-safe behaviour everywhere.
Routes:
- POST /api/portal/auth/sign-in — sets the existing portal JWT cookie.
- POST /api/portal/auth/forgot-password — always 200.
- POST /api/portal/auth/reset-password — token + new password.
- POST /api/portal/auth/activate — token + initial password.
- POST /api/v1/clients/:id/portal-user — admin invite (and `?action=resend`).
- Removed: /api/portal/auth/request, /api/portal/auth/verify (magic link).
UI:
- /portal/login — replaced email-only magic-link form with email +
password + "forgot password" link.
- /portal/forgot-password, /portal/reset-password, /portal/activate — new.
- New shared `PasswordSetForm` component used by activate + reset.
- New `PortalInviteButton` rendered on the client detail header.
Email send:
- `createTransporter` now wires SMTP auth when SMTP_USER+SMTP_PASS are
set (gmail app-password or marina-server creds, configured via env).
- `SMTP_FROM` env var lets the sender address be overridden without
pinning it to `noreply@${SMTP_HOST}`.
Tests:
- Smoke spec 17 (client-portal) updated to the new flow: 7/7 green.
- Smoke specs 02-crud-spine, 05-invoices, 20-critical-path updated to
match the post-refactor client + invoice forms (drop companyName,
use OwnerPicker + billingEmail).
- Vitest 652/652 still green; type-check clean.
Drops the dead `requestMagicLink` from portal.service.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:34:02 +02:00
|
|
|
SMTP_USER: z.string().optional(),
|
|
|
|
|
SMTP_PASS: z.string().optional(),
|
|
|
|
|
SMTP_FROM: z.string().optional(),
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
// Encryption
|
|
|
|
|
EMAIL_CREDENTIAL_KEY: z
|
|
|
|
|
.string()
|
|
|
|
|
.length(64)
|
|
|
|
|
.regex(/^[0-9a-f]+$/i, 'Must be a 64-character hex string'),
|
|
|
|
|
|
|
|
|
|
// Google OAuth (optional)
|
|
|
|
|
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
|
|
|
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
|
|
|
|
|
|
|
|
// OpenAI (optional)
|
|
|
|
|
OPENAI_API_KEY: z.string().optional(),
|
|
|
|
|
|
|
|
|
|
// App
|
|
|
|
|
APP_URL: z.string().url(),
|
|
|
|
|
PUBLIC_SITE_URL: z.string().url(),
|
|
|
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
|
|
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type Env = z.infer<typeof envSchema>;
|
|
|
|
|
|
|
|
|
|
function validateEnv(): Env {
|
2026-04-08 15:31:33 -04:00
|
|
|
if (process.env.SKIP_ENV_VALIDATION === '1') {
|
|
|
|
|
return process.env as unknown as Env;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
const result = envSchema.safeParse(process.env);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
console.error('Invalid environment variables:');
|
|
|
|
|
for (const issue of result.error.issues) {
|
|
|
|
|
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
|
|
|
}
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
return result.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const env = validateEnv();
|