Keep adoption transfer codes stable
This commit is contained in:
+54
-38
@@ -48,6 +48,7 @@ import {
|
|||||||
getBirdById,
|
getBirdById,
|
||||||
getBirdByPublicProfileCode,
|
getBirdByPublicProfileCode,
|
||||||
getOpenBirdTransferCode,
|
getOpenBirdTransferCode,
|
||||||
|
getOpenBirdTransferCodeForBird,
|
||||||
listBirds,
|
listBirds,
|
||||||
listDueBirdMilestoneReminders,
|
listDueBirdMilestoneReminders,
|
||||||
listMemorializedBirds,
|
listMemorializedBirds,
|
||||||
@@ -654,6 +655,33 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
|
|
||||||
const createBirdTransferCodeValue = () => crypto.randomBytes(12).toString('base64url');
|
const createBirdTransferCodeValue = () => crypto.randomBytes(12).toString('base64url');
|
||||||
|
|
||||||
|
const ensureOpenBirdTransferCode = async (birdId: string, sourceWorkspaceId: number, requestedByUserId: string) => {
|
||||||
|
const existingTransferCode = await getOpenBirdTransferCodeForBird(birdId, sourceWorkspaceId);
|
||||||
|
|
||||||
|
if (existingTransferCode) {
|
||||||
|
return existingTransferCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
|
try {
|
||||||
|
return await createBirdTransferCode({
|
||||||
|
code: createBirdTransferCodeValue(),
|
||||||
|
birdId,
|
||||||
|
sourceWorkspaceId,
|
||||||
|
requestedByUserId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
workspaceId: row.workspace_id,
|
workspaceId: row.workspace_id,
|
||||||
@@ -3272,25 +3300,7 @@ app.post(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transferCode = null;
|
const transferCode = await ensureOpenBirdTransferCode(sourceBird.id, req.auth!.workspace.id, req.auth!.user.id);
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
try {
|
|
||||||
transferCode = await createBirdTransferCode({
|
|
||||||
code: createBirdTransferCodeValue(),
|
|
||||||
birdId: sourceBird.id,
|
|
||||||
sourceWorkspaceId: req.auth!.workspace.id,
|
|
||||||
requestedByUserId: req.auth!.user.id,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transferCode) {
|
if (!transferCode) {
|
||||||
throw new Error('Unable to create bird transfer code.');
|
throw new Error('Unable to create bird transfer code.');
|
||||||
@@ -3312,6 +3322,30 @@ app.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get('/api/birds/:birdId/transfer-code', requireAuth, requireWriteAccess, requireSessionAuth, requireWorkspaceRole(['owner', 'assistant']), async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const sourceBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
|
||||||
|
|
||||||
|
if (!sourceBird) {
|
||||||
|
res.status(404).json({ error: 'Bird not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferCode = await getOpenBirdTransferCodeForBird(sourceBird.id, req.auth!.workspace.id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
transferCode: transferCode
|
||||||
|
? {
|
||||||
|
code: transferCode.code,
|
||||||
|
bird: normalizeBird(sourceBird),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
'/api/bird-transfer-codes/:code/accept',
|
'/api/bird-transfer-codes/:code/accept',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@@ -3384,25 +3418,7 @@ app.post(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transferCode = null;
|
const transferCode = await ensureOpenBirdTransferCode(sourceBird.id, req.auth!.workspace.id, req.auth!.user.id);
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
try {
|
|
||||||
transferCode = await createBirdTransferCode({
|
|
||||||
code: createBirdTransferCodeValue(),
|
|
||||||
birdId: sourceBird.id,
|
|
||||||
sourceWorkspaceId: req.auth!.workspace.id,
|
|
||||||
requestedByUserId: req.auth!.user.id,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transferCode) {
|
if (!transferCode) {
|
||||||
throw new Error('Unable to create bird transfer code.');
|
throw new Error('Unable to create bird transfer code.');
|
||||||
|
|||||||
@@ -154,8 +154,12 @@ const drawSimpleWeightChart = (doc: PDFKit.PDFDocument, weights: WeightRow[], bi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const latestDate = new Date(`${plottedWeights[plottedWeights.length - 1].recorded_on.slice(0, 10)}T00:00:00Z`);
|
const latestDate = new Date(`${plottedWeights[plottedWeights.length - 1].recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
|
const earliestDate = new Date(`${plottedWeights[0].recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
const startDate = new Date(latestDate);
|
const startDate = new Date(latestDate);
|
||||||
startDate.setUTCDate(startDate.getUTCDate() - 29);
|
startDate.setUTCDate(startDate.getUTCDate() - 13);
|
||||||
|
if (earliestDate > startDate) {
|
||||||
|
startDate.setTime(earliestDate.getTime());
|
||||||
|
}
|
||||||
const visibleWeights = plottedWeights.filter((entry) => {
|
const visibleWeights = plottedWeights.filter((entry) => {
|
||||||
const recordedOn = new Date(`${entry.recorded_on.slice(0, 10)}T00:00:00Z`);
|
const recordedOn = new Date(`${entry.recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
return recordedOn >= startDate && recordedOn <= latestDate;
|
return recordedOn >= startDate && recordedOn <= latestDate;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getSignedS3ObjectUrl } from '../storage/s3Client.js';
|
|||||||
import type { BirdRow } from '../types.js';
|
import type { BirdRow } from '../types.js';
|
||||||
import { renderAdoptionReportPdf } from './adoptionReport.js';
|
import { renderAdoptionReportPdf } from './adoptionReport.js';
|
||||||
|
|
||||||
const adoptionReportWeightHistoryDays = 425;
|
const adoptionReportWeightHistoryDays = 14;
|
||||||
|
|
||||||
const parseDataImage = (value: string | null) => {
|
const parseDataImage = (value: string | null) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|||||||
@@ -734,6 +734,22 @@ export const createBirdTransferCode = async ({
|
|||||||
return result.rows[0] ?? null;
|
return result.rows[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOpenBirdTransferCodeForBird = async (birdId: string, sourceWorkspaceId: number) => {
|
||||||
|
const result = await db.query<BirdTransferCodeRow>(
|
||||||
|
`SELECT id, code, bird_id, source_workspace_id, requested_by_user_id, completed_at::text, completed_workspace_id, revoked_at::text, created_at
|
||||||
|
FROM bird_transfer_codes
|
||||||
|
WHERE bird_id = $1
|
||||||
|
AND source_workspace_id = $2
|
||||||
|
AND completed_at IS NULL
|
||||||
|
AND revoked_at IS NULL
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
[birdId, sourceWorkspaceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getOpenBirdTransferCode = async (code: string) => {
|
export const getOpenBirdTransferCode = async (code: string) => {
|
||||||
const result = await db.query<
|
const result = await db.query<
|
||||||
BirdRow & {
|
BirdRow & {
|
||||||
|
|||||||
@@ -1665,6 +1665,46 @@ function App() {
|
|||||||
setEditingVeterinaryInfo(false);
|
setEditingVeterinaryInfo(false);
|
||||||
}, [selectedBird]);
|
}, [selectedBird]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedBirdId = selectedBird?.id;
|
||||||
|
|
||||||
|
if (!selectedBirdId || !authToken || adoptionTransferCodes[selectedBirdId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canceled = false;
|
||||||
|
|
||||||
|
const loadOpenTransferCode = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiFetch(`/birds/${selectedBirdId}/transfer-code`, authToken);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data =
|
||||||
|
(await readJsonSafely<{
|
||||||
|
transferCode?: {
|
||||||
|
code?: string;
|
||||||
|
} | null;
|
||||||
|
}>(response)) ?? {};
|
||||||
|
const code = data.transferCode?.code;
|
||||||
|
|
||||||
|
if (!canceled && code) {
|
||||||
|
setAdoptionTransferCodes((current) => ({ ...current, [selectedBirdId]: code }));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Transfer codes are optional until a report/code is created.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadOpenTransferCode();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, [adoptionTransferCodes, authToken, selectedBird?.id]);
|
||||||
|
|
||||||
const overviewWindowStartDate = useMemo(() => {
|
const overviewWindowStartDate = useMemo(() => {
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user