- Refactored video thumbnail generation logic to fetch thumbnails from the backend instead of generating them locally. - Removed unused placeholder generation code for remote video URLs. - Improved error handling for backend thumbnail fetch failures, returning appropriate responses. - Enhanced caching strategy for fetched thumbnails to optimize performance. These changes streamline the video thumbnail process and improve the overall user experience in the viewer interface.
PunimTag Photo Viewer
A modern, fast, and beautiful photo viewing website that connects to your PunimTag PostgreSQL database.
🚀 Quick Start
Prerequisites
See the Prerequisites Guide for a complete list of required and optional software.
Required:
- Node.js 20+ (currently using 18.19.1 - may need upgrade)
- PostgreSQL database with PunimTag schema
- Read-only database user (see setup below)
Optional:
- FFmpeg (for video thumbnail generation) - See FFmpeg Setup Guide
- libvips (for image watermarking) - See Prerequisites Guide
- Resend API Key (for email verification)
- Network-accessible storage (for photo uploads)
Installation
Quick Setup (Recommended):
# Run the comprehensive setup script
npm run setup
This will:
- Install all npm dependencies
- Set up Sharp library (for image processing)
- Generate Prisma clients
- Set up database tables (if DATABASE_URL_AUTH is configured)
- Create admin user (if needed)
- Verify the setup
Manual Setup:
-
Install dependencies:
npm run install:deps # Or manually: npm install npm run prisma:generate:allThe install script will:
- Check Node.js version
- Install npm dependencies
- Set up Sharp library (for image processing)
- Generate Prisma clients
- Check for optional system dependencies (libvips, FFmpeg)
-
Set up environment variables: Create a
.envfile in the root directory:DATABASE_URL="postgresql://viewer_readonly:password@localhost:5432/punimtag" DATABASE_URL_WRITE="postgresql://viewer_write:password@localhost:5432/punimtag" DATABASE_URL_AUTH="postgresql://viewer_write:password@localhost:5432/punimtag_auth" NEXTAUTH_SECRET="your-secret-key-here" NEXTAUTH_URL="http://localhost:3001" NEXT_PUBLIC_SITE_NAME="PunimTag Photo Viewer" NEXT_PUBLIC_SITE_DESCRIPTION="Family Photo Gallery" # Email verification (Resend) RESEND_API_KEY="re_your_resend_api_key_here" RESEND_FROM_EMAIL="noreply@yourdomain.com" # Optional: Override base URL for email links (defaults to NEXTAUTH_URL) # NEXT_PUBLIC_APP_URL="http://localhost:3001" # Upload directory for pending photos (REQUIRED - must be network-accessible) # RECOMMENDED: Use the same server as your database (see docs/NETWORK_SHARE_SETUP.md) # Examples: # Database server via SSHFS: /mnt/db-server-uploads/pending-photos # Separate network share: /mnt/shared/pending-photos # Windows: \\server\share\pending-photos (mapped to drive) UPLOAD_DIR="/mnt/db-server-uploads/pending-photos" # Or use PENDING_PHOTOS_DIR as an alias # PENDING_PHOTOS_DIR="/mnt/network-share/pending-photos"Note: Generate a secure
NEXTAUTH_SECRETusing:openssl rand -base64 32 -
Grant read-only permissions on main database tables:
The read-only user needs SELECT permissions on all main tables. If you see "permission denied" errors, run:
✅ WORKING METHOD (tested and confirmed):
PGPASSWORD=punimtag_password psql -h localhost -U punimtag -d punimtag -f grant_readonly_permissions.sqlAlternative methods:
# Using postgres user: PGPASSWORD=postgres_password psql -h localhost -U postgres -d punimtag -f grant_readonly_permissions.sql # Using sudo: sudo -u postgres psql -d punimtag -f grant_readonly_permissions.sqlCheck permissions:
npm run check:permissionsThis will verify all required permissions and provide instructions if any are missing.
For Face Identification (Write Access):
You have two options to enable write access for face identification:
Option 1: Grant write permissions to existing user (simpler)
# Run as PostgreSQL superuser: psql -U postgres -d punimtag -f grant_write_permissions.sqlThen use the same
DATABASE_URLfor both read and write operations.Option 2: Create a separate write user (more secure)
# Run as PostgreSQL superuser: psql -U postgres -d punimtag -f create_write_user.sqlThen add to your
.envfile:DATABASE_URL_WRITE="postgresql://viewer_write:password@localhost:5432/punimtag" -
Create database tables for authentication:
# Run as PostgreSQL superuser: psql -U postgres -d punimtag_auth -f create_auth_tables.sqlAdd pending_photos table for photo uploads:
# Run as PostgreSQL superuser: psql -U postgres -d punimtag_auth -f migrations/add-pending-photos-table.sqlAdd email verification columns:
# Run as PostgreSQL superuser: psql -U postgres -d punimtag_auth -f migrations/add-email-verification-columns.sqlThen grant permissions to your write user:
-- If using viewer_write user: GRANT SELECT, INSERT, UPDATE ON TABLE users TO viewer_write; GRANT SELECT, INSERT, UPDATE ON TABLE pending_identifications TO viewer_write; GRANT SELECT, INSERT, UPDATE ON TABLE pending_photos TO viewer_write; GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO viewer_write; GRANT USAGE, SELECT ON SEQUENCE pending_identifications_id_seq TO viewer_write; GRANT USAGE, SELECT ON SEQUENCE pending_photos_id_seq TO viewer_write; -- Or if using viewer_readonly with write permissions: GRANT SELECT, INSERT, UPDATE ON TABLE users TO viewer_readonly; GRANT SELECT, INSERT, UPDATE ON TABLE pending_identifications TO viewer_readonly; GRANT SELECT, INSERT, UPDATE ON TABLE pending_photos TO viewer_readonly; GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO viewer_readonly; GRANT USAGE, SELECT ON SEQUENCE pending_identifications_id_seq TO viewer_readonly; GRANT USAGE, SELECT ON SEQUENCE pending_photos_id_seq TO viewer_readonly; -
Generate Prisma client:
npx prisma generate -
Run development server:
npm run dev -
Open your browser: Navigate to http://localhost:3000
📁 Project Structure
punimtag-viewer/
├── app/ # Next.js 14 App Router
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (photo grid with search)
│ ├── HomePageContent.tsx # Client component for home page
│ ├── search/ # Search page
│ │ ├── page.tsx # Search page
│ │ └── SearchContent.tsx # Search content component
│ └── api/ # API routes
│ ├── search/ # Search API endpoint
│ └── photos/ # Photo API endpoints
├── components/ # React components
│ ├── PhotoGrid.tsx # Photo grid with tooltips
│ ├── search/ # Search components
│ │ ├── CollapsibleSearch.tsx # Collapsible search bar
│ │ ├── FilterPanel.tsx # Filter panel
│ │ ├── PeopleFilter.tsx # People filter
│ │ ├── DateRangeFilter.tsx # Date range filter
│ │ ├── TagFilter.tsx # Tag filter
│ │ └── SearchBar.tsx # Search bar component
│ └── ui/ # shadcn/ui components
├── lib/ # Utilities
│ ├── db.ts # Prisma client
│ └── queries.ts # Database query helpers
├── prisma/
│ └── schema.prisma # Database schema
└── public/ # Static assets
🔐 Database Setup
Create Read-Only User
On your PostgreSQL server, run:
-- Create read-only user
CREATE USER viewer_readonly WITH PASSWORD 'your_secure_password';
-- Grant permissions
GRANT CONNECT ON DATABASE punimtag TO viewer_readonly;
GRANT USAGE ON SCHEMA public TO viewer_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO viewer_readonly;
-- Grant on future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO viewer_readonly;
-- Verify no write permissions
REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public FROM viewer_readonly;
🎨 Features
- ✅ Photo grid with responsive layout
- ✅ Image optimization with Next.js Image
- ✅ Read-only database access
- ✅ Type-safe queries with Prisma
- ✅ Modern, clean design
- ✅ Collapsible search bar on main page with filters
- ✅ Search functionality - Search by people, dates, and tags
- ✅ Photo tooltips - Hover over photos to see people names
- ✅ Search page - Dedicated search page at
/search - ✅ Filter panel - People, date range, and tag filters
✉️ Email Verification
The application includes email verification for new user registrations. Users must verify their email address before they can sign in.
Setup
-
Get a Resend API Key:
- Sign up at resend.com
- Create an API key in your dashboard
- Add it to your
.envfile:RESEND_API_KEY="re_your_api_key_here" RESEND_FROM_EMAIL="noreply@yourdomain.com"
-
Run the Database Migration:
psql -U postgres -d punimtag_auth -f migrations/add-email-verification-columns.sql -
Configure Email Domain (Optional):
- For production, verify your domain in Resend
- Update
RESEND_FROM_EMAILto use your verified domain - For development, you can use Resend's test domain (
onboarding@resend.dev)
How It Works
- Registration: When a user signs up, they receive a confirmation email with a verification link
- Verification: Users click the link to verify their email address
- Login: Users must verify their email before they can sign in
- Resend: Users can request a new confirmation email if needed
Features
- ✅ Secure token-based verification (24-hour expiration)
- ✅ Email verification required before login
- ✅ Resend confirmation email functionality
- ✅ User-friendly error messages
- ✅ Backward compatible (existing users are auto-verified)
📤 Photo Uploads
Users can upload photos for admin review. Uploaded photos are stored on a network-accessible location (required) and tracked in the database.
Storage Location
Uploaded photos are stored in a directory structure organized by user ID:
{UPLOAD_DIR}/
└── {userId}/
└── {timestamp}-{filename}
Configuration (REQUIRED):
- Must set
UPLOAD_DIRorPENDING_PHOTOS_DIRenvironment variable - Must point to a network-accessible location (database server recommended)
- The directory will be created automatically if it doesn't exist
Recommended: Use Database Server
The simplest setup is to use the same server where your PostgreSQL database is located:
-
Create directory on database server:
ssh user@db-server.example.com sudo mkdir -p /var/punimtag/uploads/pending-photos -
Mount database server on web server (via SSHFS):
sudo apt-get install sshfs sudo mkdir -p /mnt/db-server-uploads sudo sshfs user@db-server.example.com:/var/punimtag/uploads /mnt/db-server-uploads -
Set in .env:
UPLOAD_DIR="/mnt/db-server-uploads/pending-photos"
See full setup guide: docs/NETWORK_SHARE_SETUP.md
Important:
- Ensure the web server process has read/write permissions
- The approval system must have read access to the same location
- Test network connectivity and permissions before deploying
Database Tracking
Upload metadata is stored in the pending_photos table in the punimtag_auth database:
- File location and metadata
- User who uploaded
- Status:
pending,approved,rejected - Review information (when reviewed, by whom, rejection reason)
Access for Approval System
The approval system can:
- Read files from disk using the
file_pathfrom the database - Query the database for pending photos:
SELECT * FROM pending_photos WHERE status = 'pending' ORDER BY submitted_at; - Update status after review:
UPDATE pending_photos SET status = 'approved', reviewed_at = NOW(), reviewed_by = {admin_user_id} WHERE id = {photo_id};
🚧 Coming Soon
- Photo detail page with lightbox
- Infinite scroll
- Favorites system
- People and tags browsers
- Authentication (optional)
📚 Documentation
For complete documentation, see:
🛠️ Development
Available Scripts
npm run dev- Start development servernpm run build- Build for productionnpm run start- Start production servernpm run lint- Run ESLintnpm run check:permissions- Check database permissions and provide fix instructions
Prisma Commands
npx prisma generate- Generate Prisma clientnpx prisma studio- Open Prisma Studio (database browser)npx prisma db pull- Pull schema from database
🔍 Troubleshooting
Permission Denied Errors
If you see "permission denied for table photos" errors:
-
Check permissions:
npm run check:permissions -
Grant permissions (WORKING METHOD - tested and confirmed):
PGPASSWORD=punimtag_password psql -h localhost -U punimtag -d punimtag -f grant_readonly_permissions.sqlAlternative methods:
# Using postgres user: PGPASSWORD=postgres_password psql -h localhost -U postgres -d punimtag -f grant_readonly_permissions.sql # Using sudo: sudo -u postgres psql -d punimtag -f grant_readonly_permissions.sql -
Or check health endpoint:
curl http://localhost:3001/api/health
Database Connection Issues
- Verify
DATABASE_URLis set correctly in.env - Check that the database user exists and has the correct password
- Ensure PostgreSQL is running and accessible
⚠️ Known Issues
- Node.js version: Currently using Node 18.19.1, but Next.js 16 requires >=20.9.0
- Solution: Upgrade Node.js or use Node Version Manager (nvm)
📝 Notes
Image Serving (Hybrid Approach)
The application automatically detects and handles two types of photo storage:
-
HTTP/HTTPS URLs (SharePoint, CDN, etc.)
- If
photo.pathstarts withhttp://orhttps://, images are served directly - Next.js Image optimization is applied automatically
- Configure allowed domains in
next.config.ts→remotePatterns
- If
-
File System Paths (Local storage)
- If
photo.pathis a file system path, images are served via API proxy - Make sure photo file paths are accessible from the Next.js server
- No additional configuration needed
- If
Benefits:
- ✅ Works with both SharePoint URLs and local file system
- ✅ Automatic detection - no configuration needed per photo
- ✅ Optimal performance for both storage types
- ✅ No N+1 database queries (path passed via query parameter)
Search Features
The application includes a powerful search system:
-
Collapsible Search Bar (Main Page)
- Minimized by default to save space
- Click to expand and reveal full filter panel
- Shows active filter count badge
- Filters photos in real-time
-
Search Filters
- People Filter: Multi-select searchable dropdown
- Date Range Filter: Presets (Today, This Week, This Month, This Year) or custom range
- Tag Filter: Multi-select searchable tag filter
- All filters work together with AND logic
-
Photo Tooltips
- Hover over any photo to see people names
- Shows "People: Name1, Name2" if people are identified
- Falls back to filename if no people identified
-
Search Page (
/search)- Dedicated search page with full filter panel
- URL query parameter sync for shareable search links
- Pagination support
🤝 Contributing
This is a private project. For questions or issues, refer to the main PunimTag documentation.
Built with: Next.js 14, React, TypeScript, Prisma, Tailwind CSS