added qr, cleaned up profile views, and added the critical alerts
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user