Added missing bird
This commit is contained in:
+148
-1
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user