added qr, cleaned up profile views, and added the critical alerts

This commit is contained in:
blaisadmin
2026-05-20 21:54:17 -04:00
parent f2c506ec16
commit 1c0d57299d
9 changed files with 949 additions and 60 deletions
+42
View File
@@ -44,6 +44,7 @@ import {
deleteMedicationForBird,
deleteVetVisitForBird,
getBirdById,
getBirdByPublicProfileCode,
listBirds,
listDueBirdMilestoneReminders,
listMemorializedBirds,
@@ -231,6 +232,8 @@ const lostBirdReportSchema = z.object({
message: z.string().trim().max(1000).optional().or(z.literal('')),
});
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
const birdSchema = z.object({
name: z.string().trim().min(1).max(120),
tagId: z.string().trim().max(80).optional().or(z.literal('')),
@@ -245,6 +248,7 @@ const birdSchema = z.object({
photoDataUrl: z.union([photoDataUrlSchema, photoUrlSchema, z.literal('')]).optional(),
notifyOnDob: z.boolean().optional(),
notifyOnGotchaDay: z.boolean().optional(),
publicProfileEnabled: z.boolean().optional(),
});
const memorializeBirdSchema = z.object({
@@ -325,6 +329,7 @@ const hashToken = (token: string) => crypto.createHash('sha256').update(token).d
const createSessionToken = () => crypto.randomBytes(32).toString('hex');
const photoAccessSecret = process.env.PHOTO_ACCESS_SECRET?.trim() || process.env.S3_SECRET_ACCESS_KEY?.trim() || createSessionToken();
const createIntegrationToken = () => `flpt_${crypto.randomBytes(24).toString('hex')}`;
const createPublicProfileCode = () => crypto.randomBytes(9).toString('base64url');
const createRandomId = () => crypto.randomUUID();
const createCodeVerifier = () => crypto.randomBytes(32).toString('base64url');
const createCodeChallenge = (verifier: string) => crypto.createHash('sha256').update(verifier).digest('base64url');
@@ -579,6 +584,8 @@ const normalizeBird = (row: BirdRow) => ({
photoUpdatedAt: row.photo_updated_at,
notifyOnDob: row.notify_on_dob,
notifyOnGotchaDay: row.notify_on_gotcha_day,
publicProfileCode: row.public_profile_code ?? null,
publicProfileEnabled: row.public_profile_enabled ?? false,
memorializedAt: row.memorialized_at,
memorializedOn: row.memorialized_on,
memorialNote: row.memorial_note,
@@ -588,6 +595,15 @@ const normalizeBird = (row: BirdRow) => ({
latestRecordedOn: row.latest_recorded_on,
});
const normalizePublicBirdProfile = (row: BirdRow) => ({
id: row.id,
workspaceId: row.workspace_id,
name: row.name,
gender: row.gender,
dateOfBirth: row.date_of_birth,
photoDataUrl: getBirdPhotoUrl(row),
});
const normalizeWeight = (row: WeightRow) => ({
id: row.id,
birdId: row.bird_id,
@@ -1949,6 +1965,28 @@ app.post('/api/lost-bird/report', lostBirdReportLimiter, async (req: Request, re
}
});
app.get('/api/public/birds/:publicProfileCode', async (req: Request, res: Response, next: NextFunction) => {
const parsed = publicProfileCodeSchema.safeParse(req.params.publicProfileCode);
if (!parsed.success) {
res.status(404).json({ error: 'Public bird profile not found.' });
return;
}
try {
const bird = await getBirdByPublicProfileCode(parsed.data);
if (!bird) {
res.status(404).json({ error: 'Public bird profile not found.' });
return;
}
res.json({ bird: normalizePublicBirdProfile(bird) });
} catch (error) {
next(error);
}
});
app.get('/api/auth/providers', (_req: Request, res: Response) => {
res.json({
providers: Object.values(oauthProviders).map((provider) => ({
@@ -2858,6 +2896,8 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
photoUpdatedAt: photoStorage.photoUpdatedAt,
notifyOnDob: parsed.data.notifyOnDob ?? false,
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
publicProfileCode: createPublicProfileCode(),
publicProfileEnabled: parsed.data.publicProfileEnabled ?? false,
});
uploadedObjectKeyToCleanup = null;
@@ -2998,6 +3038,8 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
photoUpdatedAt: photoStorage.photoUpdatedAt,
notifyOnDob: parsed.data.notifyOnDob ?? false,
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
publicProfileCode: existingBird.public_profile_code ?? createPublicProfileCode(),
publicProfileEnabled: parsed.data.publicProfileEnabled ?? false,
});
if (!bird) {
+8
View File
@@ -225,6 +225,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
photo_updated_at TIMESTAMPTZ,
notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
public_profile_code VARCHAR(32),
public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
memorialized_at TIMESTAMPTZ,
memorialized_on DATE,
memorial_note VARCHAR(1000),
@@ -247,6 +249,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
ADD COLUMN IF NOT EXISTS photo_updated_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS public_profile_code VARCHAR(32),
ADD COLUMN IF NOT EXISTS public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS memorialized_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS memorialized_on DATE,
ADD COLUMN IF NOT EXISTS memorial_note VARCHAR(1000),
@@ -305,6 +309,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
ON birds (photo_object_key)
WHERE photo_object_key IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_public_profile_code
ON birds (public_profile_code)
WHERE public_profile_code IS NOT NULL;
CREATE TABLE IF NOT EXISTS pending_bird_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
+43 -6
View File
@@ -33,6 +33,8 @@ const birdSelectFields = `
birds.photo_updated_at,
birds.notify_on_dob,
birds.notify_on_gotcha_day,
birds.public_profile_code,
birds.public_profile_enabled,
birds.memorialized_at,
birds.memorialized_on::text,
birds.memorial_note,
@@ -62,6 +64,27 @@ export const getBirdById = async (birdId: string, workspaceId: number) => {
return result.rows[0] ?? null;
};
export const getBirdByPublicProfileCode = async (publicProfileCode: string) => {
const result = await db.query<BirdRow>(
`SELECT
${birdSelectFields}
FROM birds
LEFT JOIN LATERAL (
SELECT weight_grams, recorded_on
FROM weight_records
WHERE weight_records.bird_id = birds.id
ORDER BY recorded_on DESC
LIMIT 1
) latest ON TRUE
WHERE birds.public_profile_code = $1
AND birds.public_profile_enabled = TRUE
AND birds.memorialized_at IS NULL`,
[publicProfileCode],
);
return result.rows[0] ?? null;
};
export const listBirds = async (workspaceId: number) => {
const result = await db.query<BirdRow>(
`SELECT
@@ -274,6 +297,8 @@ export const createBird = async ({
photoUpdatedAt = null,
notifyOnDob,
notifyOnGotchaDay,
publicProfileCode = null,
publicProfileEnabled = false,
}: {
birdId?: string;
workspaceId: number;
@@ -293,11 +318,13 @@ export const createBird = async ({
photoUpdatedAt?: string | null;
notifyOnDob: boolean;
notifyOnGotchaDay: boolean;
publicProfileCode?: string | null;
publicProfileEnabled?: boolean;
}) => {
const result = await db.query<BirdRow>(
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day)
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
[
birdId ?? null,
workspaceId,
@@ -317,6 +344,8 @@ export const createBird = async ({
photoUpdatedAt,
notifyOnDob,
notifyOnGotchaDay,
publicProfileCode,
publicProfileEnabled,
],
);
@@ -342,6 +371,8 @@ export const updateBird = async ({
photoUpdatedAt = null,
notifyOnDob,
notifyOnGotchaDay,
publicProfileCode,
publicProfileEnabled,
}: {
birdId: string;
workspaceId: number;
@@ -361,6 +392,8 @@ export const updateBird = async ({
photoUpdatedAt?: string | null;
notifyOnDob: boolean;
notifyOnGotchaDay: boolean;
publicProfileCode: string | null;
publicProfileEnabled: boolean;
}) => {
const result = await db.query<BirdRow>(
`UPDATE birds
@@ -379,11 +412,13 @@ export const updateBird = async ({
photo_content_type = $14,
photo_updated_at = $15,
notify_on_dob = $16,
notify_on_gotcha_day = $17
notify_on_gotcha_day = $17,
public_profile_code = $18,
public_profile_enabled = $19
WHERE id = $1
AND workspace_id = $18
AND workspace_id = $20
AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
@@ -416,6 +451,8 @@ export const updateBird = async ({
photoUpdatedAt,
notifyOnDob,
notifyOnGotchaDay,
publicProfileCode,
publicProfileEnabled,
workspaceId,
],
);
+2
View File
@@ -111,6 +111,8 @@ export type BirdRow = {
photo_updated_at: string | null;
notify_on_dob: boolean;
notify_on_gotcha_day: boolean;
public_profile_code: string | null;
public_profile_enabled: boolean;
memorialized_at: string | null;
memorialized_on: string | null;
memorial_note: string | null;