Dokploy deploy: Dockerfile, DB CA cert, blog images, CSP
CI / build-and-test (push) Has been cancelled
CI / build-and-test (push) Has been cancelled
- Add Dockerfile (multi-stage Node 20), .dockerignore, docker-compose.yml, and DEPLOY-DOKPLOY.md for container deployment on Dokploy. - Commit the DigitalOcean managed-Postgres Project CA cert (certs/ca-certificate.crt) so production TLS verification (fail-closed) works in-container. Public CA, safe to commit. - Blog cover images served from DO Spaces; allow *.digitaloceanspaces.com in the prod CSP img-src. - Includes the AI (case summaries) and Cloudflare Turnstile bot-protection features. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d9b807662a
commit
d1d96e4dd2
@@ -23,17 +23,20 @@ import {
|
||||
verifyEmailEmail,
|
||||
} from '../lib/email';
|
||||
import { env } from '../env';
|
||||
import { verifyTurnstile } from '../lib/turnstile';
|
||||
|
||||
const signupBody = z.object({
|
||||
email: z.string().email().max(254).toLowerCase().trim(),
|
||||
password: z.string().min(10).max(200),
|
||||
fullName: z.string().min(1).max(120).trim(),
|
||||
firmName: z.string().min(1).max(160).trim(),
|
||||
turnstileToken: z.string().max(3000).optional(),
|
||||
});
|
||||
|
||||
const loginBody = z.object({
|
||||
email: z.string().email().max(254).toLowerCase().trim(),
|
||||
password: z.string().min(1).max(200),
|
||||
turnstileToken: z.string().max(3000).optional(),
|
||||
});
|
||||
|
||||
const MAX_FAILS_PER_15_MIN = 5;
|
||||
@@ -74,6 +77,11 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
{ config: { rateLimit: { max: 5, timeWindow: '1 hour' } } },
|
||||
async (req, reply) => {
|
||||
const body = signupBody.parse(req.body);
|
||||
|
||||
if (!(await verifyTurnstile(body.turnstileToken, req.ip))) {
|
||||
return reply.code(400).send({ error: 'captcha_failed' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = await db.select({ id: users.id }).from(users).where(eq(users.email, body.email)).limit(1);
|
||||
@@ -144,6 +152,11 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
{ config: { rateLimit: { max: 10, timeWindow: '15 minutes' } } },
|
||||
async (req, reply) => {
|
||||
const body = loginBody.parse(req.body);
|
||||
|
||||
if (!(await verifyTurnstile(body.turnstileToken, req.ip))) {
|
||||
return reply.code(400).send({ error: 'captcha_failed' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const ip = req.ip ?? null;
|
||||
|
||||
@@ -216,10 +229,21 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
app.post(
|
||||
'/api/auth/request-password-reset',
|
||||
{ config: { rateLimit: { max: 5, timeWindow: '15 minutes' } } },
|
||||
async (req) => {
|
||||
const parsed = z.object({ email: z.string().email().max(254).toLowerCase().trim() }).safeParse(req.body);
|
||||
async (req, reply) => {
|
||||
const parsed = z
|
||||
.object({
|
||||
email: z.string().email().max(254).toLowerCase().trim(),
|
||||
turnstileToken: z.string().max(3000).optional(),
|
||||
})
|
||||
.safeParse(req.body);
|
||||
if (!parsed.success) return { ok: true };
|
||||
|
||||
// Bot check is orthogonal to email enumeration — a captcha failure is reported
|
||||
// honestly; only account existence is concealed by the ok-always contract.
|
||||
if (!(await verifyTurnstile(parsed.data.turnstileToken, req.ip))) {
|
||||
return reply.code(400).send({ error: 'captcha_failed' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const [user] = await db.select().from(users).where(eq(users.email, parsed.data.email)).limit(1);
|
||||
if (!user || user.isSuspended) return { ok: true };
|
||||
|
||||
Reference in New Issue
Block a user