Add rate limiting, security headers, fix CORS for production
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
admin
|
||||
volunteer
|
||||
donor
|
||||
public
|
||||
}
|
||||
|
||||
enum BurrowStatus {
|
||||
active
|
||||
inactive
|
||||
destroyed
|
||||
}
|
||||
|
||||
enum DonationCampaign {
|
||||
land_preservation
|
||||
volunteer_equipment
|
||||
general
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
cleanup
|
||||
educational
|
||||
fundraiser
|
||||
}
|
||||
|
||||
enum StreamStatus {
|
||||
live
|
||||
offline
|
||||
}
|
||||
|
||||
enum EquipmentStatus {
|
||||
available
|
||||
checked_out
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
role UserRole @default(public)
|
||||
password_hash String
|
||||
created_at DateTime @default(now())
|
||||
|
||||
burrows Burrow[] @relation("AssignedVolunteer")
|
||||
donations Donation[]
|
||||
sightings WildlifeSighting[] @relation("SightingReporter")
|
||||
event_rsvps EventRSVP[]
|
||||
volunteer_hours VolunteerHour[]
|
||||
equipment_items EquipmentItem[] @relation("CheckedOutBy")
|
||||
verified_hours VolunteerHour[] @relation("VerifiedBy")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Burrow {
|
||||
id String @id @default(cuid())
|
||||
gps_lat Float
|
||||
gps_lng Float
|
||||
status BurrowStatus @default(active)
|
||||
location_description String?
|
||||
photos String[] @default([])
|
||||
assigned_volunteer_id String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
assigned_volunteer User? @relation("AssignedVolunteer", fields: [assigned_volunteer_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("burrows")
|
||||
}
|
||||
|
||||
model Donation {
|
||||
id String @id @default(cuid())
|
||||
donor_id String?
|
||||
amount Float
|
||||
campaign DonationCampaign @default(general)
|
||||
stripe_payment_id String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
donor User? @relation(fields: [donor_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("donations")
|
||||
}
|
||||
|
||||
model WildlifeSighting {
|
||||
id String @id @default(cuid())
|
||||
reporter_id String?
|
||||
species String
|
||||
gps_lat Float
|
||||
gps_lng Float
|
||||
photo_url String?
|
||||
description String?
|
||||
verified Boolean @default(false)
|
||||
created_at DateTime @default(now())
|
||||
|
||||
reporter User? @relation("SightingReporter", fields: [reporter_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("wildlife_sightings")
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String?
|
||||
date DateTime
|
||||
location String
|
||||
max_attendees Int?
|
||||
type EventType
|
||||
created_at DateTime @default(now())
|
||||
|
||||
rsvps EventRSVP[]
|
||||
|
||||
@@map("events")
|
||||
}
|
||||
|
||||
model EventRSVP {
|
||||
event_id String
|
||||
user_id String
|
||||
created_at DateTime @default(now())
|
||||
|
||||
event Event @relation(fields: [event_id], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([event_id, user_id])
|
||||
@@map("event_rsvps")
|
||||
}
|
||||
|
||||
model LivestreamSource {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
stream_url String
|
||||
camera_location String
|
||||
status StreamStatus @default(offline)
|
||||
thumbnail_url String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("livestream_sources")
|
||||
}
|
||||
|
||||
model VolunteerHour {
|
||||
id String @id @default(cuid())
|
||||
volunteer_id String
|
||||
date DateTime
|
||||
hours Float
|
||||
task_description String
|
||||
verified_by String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
volunteer User @relation(fields: [volunteer_id], references: [id], onDelete: Cascade)
|
||||
verifier User? @relation("VerifiedBy", fields: [verified_by], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("volunteer_hours")
|
||||
}
|
||||
|
||||
model EquipmentItem {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
status EquipmentStatus @default(available)
|
||||
checked_out_by String?
|
||||
checked_out_at DateTime?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
checker User? @relation("CheckedOutBy", fields: [checked_out_by], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("equipment_items")
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🦉 Seeding Owl Stream database...');
|
||||
|
||||
// Admin user
|
||||
const adminHash = await bcrypt.hash('admin1234', 12);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@owlwatch.org' },
|
||||
update: {},
|
||||
create: { email: 'admin@owlwatch.org', name: 'Admin User', role: 'admin', password_hash: adminHash },
|
||||
});
|
||||
|
||||
// Volunteer user
|
||||
const volHash = await bcrypt.hash('volunteer1234', 12);
|
||||
const volunteer = await prisma.user.upsert({
|
||||
where: { email: 'vol@owlwatch.org' },
|
||||
update: {},
|
||||
create: { email: 'vol@owlwatch.org', name: 'Jane Volunteer', role: 'volunteer', password_hash: volHash },
|
||||
});
|
||||
|
||||
// Donor user
|
||||
const donorHash = await bcrypt.hash('donor1234', 12);
|
||||
await prisma.user.upsert({
|
||||
where: { email: 'donor@example.com' },
|
||||
update: {},
|
||||
create: { email: 'donor@example.com', name: 'Bob Donor', role: 'donor', password_hash: donorHash },
|
||||
});
|
||||
|
||||
// 10 Burrows (Cape Coral GPS coordinates)
|
||||
const burrowData = [
|
||||
{ gps_lat: 26.5629, gps_lng: -81.9495, location_description: 'Burnt Store Rd median' },
|
||||
{ gps_lat: 26.5814, gps_lng: -81.9631, location_description: 'Rotary Park entrance' },
|
||||
{ gps_lat: 26.5677, gps_lng: -81.9712, location_description: 'Veterans Pkwy near Agualinda' },
|
||||
{ gps_lat: 26.5532, gps_lng: -81.9589, location_description: 'Cape Coral Pkwy & Del Prado' },
|
||||
{ gps_lat: 26.5901, gps_lng: -81.9444, location_description: 'Four Mile Cove Ecological Preserve' },
|
||||
{ gps_lat: 26.5743, gps_lng: -81.9822, location_description: 'Yellow Fever Creek Greenway' },
|
||||
{ gps_lat: 26.5465, gps_lng: -81.9367, location_description: 'Cape Harbour Marina' },
|
||||
{ gps_lat: 26.6012, gps_lng: -81.9555, location_description: 'Matlacha Pass Aquatic Preserve edge' },
|
||||
{ gps_lat: 26.5788, gps_lng: -82.0011, location_description: 'Camelot Island access' },
|
||||
{ gps_lat: 26.5620, gps_lng: -81.9198, location_description: 'SE 46th Ln vacant lot' },
|
||||
];
|
||||
|
||||
for (const b of burrowData) {
|
||||
await prisma.burrow.create({
|
||||
data: { ...b, status: 'active', assigned_volunteer_id: volunteer.id },
|
||||
});
|
||||
}
|
||||
|
||||
// 3 Livestream Sources
|
||||
await prisma.livestreamSource.createMany({
|
||||
data: [
|
||||
{
|
||||
name: 'Rotary Park Cam A',
|
||||
stream_url: 'https://stream.owlwatch.org/live/rotary-a',
|
||||
camera_location: 'Rotary Park, Cape Coral FL',
|
||||
status: 'live',
|
||||
thumbnail_url: 'https://stream.owlwatch.org/thumbs/rotary-a.jpg',
|
||||
},
|
||||
{
|
||||
name: 'Burnt Store Rd Nest',
|
||||
stream_url: 'https://stream.owlwatch.org/live/burnt-store',
|
||||
camera_location: 'Burnt Store Rd, Cape Coral FL',
|
||||
status: 'live',
|
||||
thumbnail_url: 'https://stream.owlwatch.org/thumbs/burnt-store.jpg',
|
||||
},
|
||||
{
|
||||
name: 'Veterans Pkwy Cam',
|
||||
stream_url: 'https://stream.owlwatch.org/live/veterans',
|
||||
camera_location: 'Veterans Pkwy, Cape Coral FL',
|
||||
status: 'offline',
|
||||
thumbnail_url: 'https://stream.owlwatch.org/thumbs/veterans.jpg',
|
||||
},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// 5 Events
|
||||
const events = [
|
||||
{
|
||||
title: 'Morning Burrow Count — Spring Survey',
|
||||
description: 'Volunteer survey of all known burrow sites. Meet at Rotary Park 7am.',
|
||||
date: new Date('2026-03-15T07:00:00'),
|
||||
location: 'Rotary Park, Cape Coral',
|
||||
max_attendees: 30,
|
||||
type: 'cleanup' as const,
|
||||
},
|
||||
{
|
||||
title: 'Burrowing Owl Awareness Day',
|
||||
description: 'Educational fair at Cape Coral City Hall. Bring the family!',
|
||||
date: new Date('2026-03-22T10:00:00'),
|
||||
location: 'Cape Coral City Hall',
|
||||
max_attendees: 200,
|
||||
type: 'educational' as const,
|
||||
},
|
||||
{
|
||||
title: 'Night Camera Installation',
|
||||
description: 'Volunteers needed to help install night vision cameras at high-activity burrow sites.',
|
||||
date: new Date('2026-04-05T17:30:00'),
|
||||
location: 'Veterans Pkwy Site, Cape Coral',
|
||||
max_attendees: 10,
|
||||
type: 'cleanup' as const,
|
||||
},
|
||||
{
|
||||
title: 'Owl Fest Fundraiser Dinner',
|
||||
description: 'Annual fundraiser dinner. Tickets include 3-course dinner and owl cam raffle.',
|
||||
date: new Date('2026-04-19T18:00:00'),
|
||||
location: 'Cape Coral Yacht Club',
|
||||
max_attendees: 150,
|
||||
type: 'fundraiser' as const,
|
||||
},
|
||||
{
|
||||
title: 'Habitat Restoration — Camelot Island',
|
||||
description: 'Remove invasive species around burrow zones. Gloves and tools provided.',
|
||||
date: new Date('2026-05-03T08:00:00'),
|
||||
location: 'Camelot Island, Cape Coral',
|
||||
max_attendees: 40,
|
||||
type: 'cleanup' as const,
|
||||
},
|
||||
];
|
||||
|
||||
for (const e of events) {
|
||||
await prisma.event.create({ data: e });
|
||||
}
|
||||
|
||||
// Sample wildlife sightings
|
||||
const sightings = [
|
||||
{ species: 'Burrowing Owl (Athene cunicularia)', gps_lat: 26.5629, gps_lng: -81.9495, description: 'Pair spotted at entrance burrow, chick visible', verified: true },
|
||||
{ species: 'Florida Sandhill Crane', gps_lat: 26.5814, gps_lng: -81.9631, description: 'Family of 3 near Rotary Park pond', verified: false },
|
||||
{ species: 'Osprey', gps_lat: 26.5901, gps_lng: -81.9444, description: 'Nesting on channel marker', verified: true },
|
||||
{ species: 'Burrowing Owl (Athene cunicularia)', gps_lat: 26.5788, gps_lng: -82.0011, description: 'Single owl standing guard at burrow', verified: true },
|
||||
{ species: 'Gopher Tortoise', gps_lat: 26.5532, gps_lng: -81.9589, description: 'Near burrow site 4, likely co-habiting', verified: false },
|
||||
];
|
||||
|
||||
for (const s of sightings) {
|
||||
await prisma.wildlifeSighting.create({
|
||||
data: { ...s, reporter_id: volunteer.id },
|
||||
});
|
||||
}
|
||||
|
||||
// Volunteer hours
|
||||
await prisma.volunteerHour.createMany({
|
||||
data: [
|
||||
{ volunteer_id: volunteer.id, date: new Date('2026-02-10'), hours: 4, task_description: 'Morning burrow survey, Burnt Store Rd section', verified_by: admin.id },
|
||||
{ volunteer_id: volunteer.id, date: new Date('2026-02-15'), hours: 2.5, task_description: 'Camera check and data upload', verified_by: admin.id },
|
||||
{ volunteer_id: volunteer.id, date: new Date('2026-02-18'), hours: 6, task_description: 'Educational booth at Cape Coral Farmers Market' },
|
||||
],
|
||||
});
|
||||
|
||||
// Equipment items
|
||||
await prisma.equipmentItem.createMany({
|
||||
data: [
|
||||
{ name: 'Nikon P950 Spotting Camera', description: '83x optical zoom for burrow photography', status: 'available' },
|
||||
{ name: 'Trail Camera x4 Pack', description: 'Browning night vision trail cams', status: 'checked_out', checked_out_by: volunteer.id, checked_out_at: new Date() },
|
||||
{ name: 'GPS Unit — Garmin eTrex 32x', description: 'For precise burrow coordinate logging', status: 'available' },
|
||||
{ name: 'Binoculars — Vortex Viper 10x42', description: 'Shared pair for volunteer surveys', status: 'available' },
|
||||
{ name: 'First Aid Kit (Field)', description: 'Standard field kit, check quarterly', status: 'available' },
|
||||
],
|
||||
});
|
||||
|
||||
// Sample donation
|
||||
await prisma.donation.create({
|
||||
data: { amount: 250, campaign: 'land_preservation', stripe_payment_id: 'pi_seed_001' },
|
||||
});
|
||||
|
||||
console.log('✅ Seeding complete!');
|
||||
console.log(' Admin: admin@owlwatch.org / admin1234');
|
||||
console.log(' Volunteer: vol@owlwatch.org / volunteer1234');
|
||||
console.log(' Donor: donor@example.com / donor1234');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user