Added missing bird

This commit is contained in:
Corey Blais
2026-04-17 17:11:11 -04:00
parent 328a9a704d
commit e06dae91a3
6 changed files with 366 additions and 4 deletions
+148 -1
View File
@@ -31,6 +31,7 @@ import {
completePendingBirdTransfersForOwner,
createBird,
createPendingBirdTransfer,
findBirdsByBandId,
createVetVisitForBird,
createWeightForBird,
deleteBird,
@@ -60,6 +61,7 @@ import {
listOwnedWorkspacesByOwnerEmail,
listRescueWorkspacesForAdmin,
listMembershipsForUser,
listWorkspaceNotificationEmails,
listWorkspaceMembers,
setWorkspaceStripeCustomerId,
setWorkspaceStripeSubscription,
@@ -75,6 +77,7 @@ import type {
BirdGender,
BirdRow,
IntegrationTokenRow,
LostBirdMatchRow,
ProviderKey,
RescueVerificationStatus,
SubscriptionStatus,
@@ -108,7 +111,16 @@ if (trustProxy) {
app.set('trust proxy', trustProxy === 'true' ? true : Number(trustProxy) || trustProxy);
}
const defaultAllowedOrigins = ['http://localhost:3000', 'http://127.0.0.1:3000', 'http://localhost:5173', 'http://127.0.0.1:5173'];
const defaultAllowedOrigins = [
'http://localhost:3000',
'http://127.0.0.1:3000',
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:8088',
'http://127.0.0.1:8088',
'https://flockpal.app',
'https://www.flockpal.app',
];
const allowedOrigins = Array.from(
new Set(
@@ -172,6 +184,14 @@ const flockTransferSchema = z.object({
destinationOwnerEmail: z.string().trim().email().max(255),
});
const lostBirdReportSchema = z.object({
tagId: z.string().trim().min(1).max(80),
finderName: z.string().trim().max(160).optional().or(z.literal('')),
finderEmail: z.string().trim().email().max(255).optional().or(z.literal('')),
foundLocation: z.string().trim().max(255).optional().or(z.literal('')),
message: z.string().trim().max(1000).optional().or(z.literal('')),
});
const birdSchema = z.object({
name: z.string().trim().min(1).max(120),
tagId: z.string().trim().min(1).max(80),
@@ -456,6 +476,13 @@ app.use(
legacyHeaders: false,
}),
);
const lostBirdReportLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many found bird reports. Please try again later.' },
});
app.post('/api/billing/stripe/webhook', express.raw({ type: 'application/json' }), async (req: Request, res: Response) => {
if (!stripeWebhookSecret) {
res.status(503).json({ error: 'Stripe webhook is not configured.' });
@@ -846,6 +873,71 @@ const issueBirdTransferInvite = async ({
};
};
const sendLostBirdReportNotification = async ({
bird,
recipients,
report,
}: {
bird: LostBirdMatchRow;
recipients: string[];
report: z.infer<typeof lostBirdReportSchema>;
}) => {
const uniqueRecipients = Array.from(new Set(recipients.map((email) => normalizeEmail(email)).filter(Boolean)));
if (!uniqueRecipients.length) {
return { delivered: false };
}
const finderName = emptyToNull(report.finderName) ?? 'Not provided';
const finderEmail = emptyToNull(report.finderEmail) ?? 'Not provided';
const foundLocation = emptyToNull(report.foundLocation) ?? 'Not provided';
const message = emptyToNull(report.message) ?? 'Not provided';
const subject = `Possible found bird report for ${bird.name}`;
const lines = [
`A possible found bird report was submitted for ${bird.name}.`,
'',
`Band ID: ${bird.tag_id}`,
`Species: ${bird.species}`,
`Flock: ${bird.workspace_name}`,
'',
`Finder name: ${finderName}`,
`Finder email: ${finderEmail}`,
`Found location: ${foundLocation}`,
`Message: ${message}`,
'',
'FlockPal does not verify found bird reports. Please use care before sharing personal information or arranging a pickup.',
];
if (!mailTransport) {
console.log(`Found bird report for ${uniqueRecipients.join(', ')}:\n${lines.join('\n')}`);
return { delivered: false };
}
await mailTransport.sendMail({
from: smtpFromName ? `"${smtpFromName}" <${smtpFromEmail}>` : smtpFromEmail,
to: smtpFromEmail,
bcc: uniqueRecipients,
replyTo: emptyToNull(report.finderEmail) ?? undefined,
subject,
text: lines.join('\n'),
html: `
<p>A possible found bird report was submitted for <strong>${escapeHtml(bird.name)}</strong>.</p>
<ul>
<li><strong>Band ID:</strong> ${escapeHtml(bird.tag_id)}</li>
<li><strong>Species:</strong> ${escapeHtml(bird.species)}</li>
<li><strong>Flock:</strong> ${escapeHtml(bird.workspace_name)}</li>
<li><strong>Finder name:</strong> ${escapeHtml(finderName)}</li>
<li><strong>Finder email:</strong> ${escapeHtml(finderEmail)}</li>
<li><strong>Found location:</strong> ${escapeHtml(foundLocation)}</li>
<li><strong>Message:</strong> ${escapeHtml(message)}</li>
</ul>
<p>FlockPal does not verify found bird reports. Please use care before sharing personal information or arranging a pickup.</p>
`,
});
return { delivered: true };
};
const readBearerToken = (authorizationHeader?: string) => {
if (!authorizationHeader) {
return '';
@@ -956,6 +1048,61 @@ app.get('/api/health', (_req: Request, res: Response) => {
res.json({ ok: true });
});
app.post('/api/lost-bird/report', lostBirdReportLimiter, async (req: Request, res: Response) => {
const parsed = lostBirdReportSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: 'Invalid found bird report', details: parsed.error.flatten() });
return;
}
try {
const matches = await findBirdsByBandId(parsed.data.tagId);
let deliveredCount = 0;
for (const bird of matches) {
try {
const recipients = await listWorkspaceNotificationEmails(bird.workspace_id);
const delivery = await sendLostBirdReportNotification({
bird,
recipients,
report: parsed.data,
});
if (delivery.delivered) {
deliveredCount += 1;
}
} catch (error) {
console.error('Lost bird notification failed', error);
}
}
if (!matches.length) {
res.json({
status: 'not_found',
message: 'That band ID is not currently in the FlockPal system.',
});
return;
}
if (deliveredCount > 0) {
res.json({
status: 'contacted',
message: 'A matching bird was found and the flock contacts were notified.',
});
return;
}
res.status(503).json({
status: 'not_contacted',
error: 'A matching bird was found, but FlockPal could not notify the flock right now. Please contact FlockPal support.',
});
} catch (error) {
console.error('Lost bird report handling failed', error);
res.status(500).json({ error: 'Unable to process this found bird report right now.' });
}
});
app.get('/api/auth/providers', (_req: Request, res: Response) => {
res.json({
providers: Object.values(oauthProviders).map((provider) => ({