feat: Add new scripts and update project structure for database management and user authentication

This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
This commit is contained in:
Tanya
2026-01-06 13:53:24 -05:00
parent 713584dc04
commit de2144be2a
175 changed files with 35854 additions and 0 deletions
@@ -0,0 +1,87 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import bcrypt from 'bcryptjs';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function checkAdminUser() {
try {
console.log('Checking admin user...\n');
if (process.env.DATABASE_URL_AUTH) {
// Mask password in connection string for display
const masked = process.env.DATABASE_URL_AUTH.replace(/:([^:@]+)@/, ':****@');
console.log('DATABASE_URL_AUTH:', masked);
// Check if it has placeholder values
if (process.env.DATABASE_URL_AUTH.includes('username') || process.env.DATABASE_URL_AUTH.includes('password')) {
console.log('\n⚠️ WARNING: DATABASE_URL_AUTH contains placeholder values!');
console.log('Please update .env file with actual database credentials.');
console.log('Format: DATABASE_URL_AUTH="postgresql://actual_username:actual_password@localhost:5432/punimtag_auth"');
return;
}
} else {
console.log('DATABASE_URL_AUTH: NOT SET');
console.log('Please add DATABASE_URL_AUTH to your .env file');
return;
}
console.log('');
const user = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
select: {
id: true,
email: true,
name: true,
isAdmin: true,
passwordHash: true,
},
});
if (!user) {
console.log('❌ Admin user NOT FOUND');
console.log('\nRun: npx tsx scripts/create-admin-user.ts');
return;
}
console.log('✅ Admin user found:');
console.log(' ID:', user.id);
console.log(' Email:', user.email);
console.log(' Name:', user.name);
console.log(' Is Admin:', user.isAdmin);
console.log(' Password Hash:', user.passwordHash.substring(0, 20) + '...');
// Test password
const testPassword = 'admin';
const isValid = await bcrypt.compare(testPassword, user.passwordHash);
console.log('\nPassword test:');
console.log(' Testing password "admin":', isValid ? '✅ VALID' : '❌ INVALID');
if (!isValid) {
console.log('\n⚠️ Password hash does not match. Recreating admin user...');
const newHash = await bcrypt.hash('admin', 10);
await prisma.user.update({
where: { email: 'admin@admin.com' },
data: {
passwordHash: newHash,
isAdmin: true,
},
});
console.log('✅ Admin user password updated');
}
} catch (error: any) {
console.error('Error:', error.message);
if (error.message.includes('P1001')) {
console.error('\n⚠️ Cannot connect to database. Check DATABASE_URL_AUTH in .env');
}
} finally {
await prisma.$disconnect();
}
}
checkAdminUser();
+197
View File
@@ -0,0 +1,197 @@
#!/bin/bash
# Script to check and create databases based on README requirements
# This script checks for punimtag and punimtag_auth databases and creates them if needed
set -e
echo "🔍 Checking databases..."
echo ""
# Load .env file if it exists
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
# Try to extract connection info from DATABASE_URL or use defaults
# Format: postgresql://user:password@host:port/database
if [ -n "$DATABASE_URL" ]; then
# Extract connection details from DATABASE_URL
DB_URL="$DATABASE_URL"
# Remove postgresql:// prefix
DB_URL="${DB_URL#postgresql://}"
# Extract user:password@host:port/database
if [[ "$DB_URL" =~ ^([^:]+):([^@]+)@([^:]+):([^/]+)/(.+)$ ]]; then
PGUSER="${BASH_REMATCH[1]}"
PGPASSWORD="${BASH_REMATCH[2]}"
PGHOST="${BASH_REMATCH[3]}"
PGPORT="${BASH_REMATCH[4]}"
elif [[ "$DB_URL" =~ ^([^@]+)@([^:]+):([^/]+)/(.+)$ ]]; then
PGUSER="${BASH_REMATCH[1]}"
PGHOST="${BASH_REMATCH[2]}"
PGPORT="${BASH_REMATCH[3]}"
elif [[ "$DB_URL" =~ ^([^@]+)@([^/]+)/(.+)$ ]]; then
PGUSER="${BASH_REMATCH[1]}"
PGHOST="${BASH_REMATCH[2]}"
PGPORT="5432"
fi
fi
# For database creation, we need a superuser
# Try to use postgres user, or allow override via POSTGRES_SUPERUSER env var
SUPERUSER=${POSTGRES_SUPERUSER:-postgres}
SUPERUSER_PASSWORD=${POSTGRES_SUPERUSER_PASSWORD:-}
# Use defaults if not set
PGUSER=${PGUSER:-postgres}
PGHOST=${PGHOST:-localhost}
PGPORT=${PGPORT:-5432}
# Export password if set
if [ -n "$PGPASSWORD" ]; then
export PGPASSWORD
fi
# For superuser operations, use separate password if provided
if [ -n "$SUPERUSER_PASSWORD" ]; then
export PGPASSWORD="$SUPERUSER_PASSWORD"
ADMIN_USER="$SUPERUSER"
else
# Try to use the same user/password, or prompt
ADMIN_USER="$SUPERUSER"
fi
# Check if punimtag database exists
echo "Checking punimtag database..."
if psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d postgres -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw punimtag; then
echo "✅ punimtag database exists"
else
echo "⚠️ punimtag database does not exist"
echo " This is the main database with photos - it should already exist."
echo " If you need to create it, please do so manually or ensure your PunimTag setup is complete."
fi
echo ""
# Check if punimtag_auth database exists
echo "Checking punimtag_auth database..."
if psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d postgres -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw punimtag_auth; then
echo "✅ punimtag_auth database exists"
AUTH_DB_EXISTS=true
else
echo "📦 Creating punimtag_auth database..."
echo " (This requires a PostgreSQL superuser - using: $ADMIN_USER)"
psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d postgres -c "CREATE DATABASE punimtag_auth;" 2>&1
if [ $? -eq 0 ]; then
echo "✅ punimtag_auth database created"
AUTH_DB_EXISTS=true
else
echo "❌ Failed to create punimtag_auth database"
echo " You may need to run this as a PostgreSQL superuser:"
echo " sudo -u postgres psql -c 'CREATE DATABASE punimtag_auth;'"
exit 1
fi
fi
echo ""
# Now check and create tables in punimtag_auth
if [ "$AUTH_DB_EXISTS" = true ]; then
echo "🔍 Checking tables in punimtag_auth..."
# Check if users table exists
if psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -c "\dt users" 2>/dev/null | grep -q "users"; then
echo "✅ Tables already exist in punimtag_auth"
else
echo "📋 Creating tables in punimtag_auth..."
# Create tables using create_auth_tables.sql
if [ -f "create_auth_tables.sql" ]; then
echo " Running create_auth_tables.sql..."
psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -f create_auth_tables.sql 2>&1
else
echo " ⚠️ create_auth_tables.sql not found, creating tables manually..."
psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth <<EOF
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
has_write_access BOOLEAN DEFAULT FALSE,
email_verified BOOLEAN DEFAULT FALSE,
email_confirmation_token VARCHAR(255) UNIQUE,
email_confirmation_token_expiry TIMESTAMP,
password_reset_token VARCHAR(255) UNIQUE,
password_reset_token_expiry TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS pending_identifications (
id SERIAL PRIMARY KEY,
face_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
middle_name VARCHAR(255),
maiden_name VARCHAR(255),
date_of_birth DATE,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_pending_identifications_face_id ON pending_identifications(face_id);
CREATE INDEX IF NOT EXISTS idx_pending_identifications_user_id ON pending_identifications(user_id);
CREATE INDEX IF NOT EXISTS idx_pending_identifications_status ON pending_identifications(status);
EOF
fi
echo "✅ Tables created"
fi
echo ""
echo "🔍 Checking for required migrations..."
# Check pending_photos table
if psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -c "\dt pending_photos" 2>/dev/null | grep -q "pending_photos"; then
echo "✅ pending_photos table exists"
else
echo "📋 Creating pending_photos table..."
if [ -f "migrations/add-pending-photos-table.sql" ]; then
psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -f migrations/add-pending-photos-table.sql 2>&1
echo "✅ pending_photos table created"
else
echo " ⚠️ Migration file not found, skipping..."
fi
fi
# Check email verification columns
if psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -c "\d users" 2>/dev/null | grep -q "email_verified"; then
echo "✅ Email verification columns exist"
else
echo "📋 Adding email verification columns..."
if [ -f "migrations/add-email-verification-columns.sql" ]; then
psql -h "$PGHOST" -p "$PGPORT" -U "$ADMIN_USER" -d punimtag_auth -f migrations/add-email-verification-columns.sql 2>&1
echo "✅ Email verification columns added"
else
echo " ⚠️ Migration file not found, skipping..."
fi
fi
fi
echo ""
echo "🎉 Database setup complete!"
echo ""
echo "Note: If you encountered permission errors, you may need to run this script"
echo " with a PostgreSQL superuser. You can set:"
echo " POSTGRES_SUPERUSER=postgres POSTGRES_SUPERUSER_PASSWORD=yourpassword ./scripts/check-and-create-databases.sh"
echo ""
echo "Next steps:"
echo "1. Ensure your .env file has correct DATABASE_URL and DATABASE_URL_AUTH"
echo "2. Run: npm run prisma:generate:all"
echo "3. Create admin user: npx tsx scripts/create-admin-user.ts"
@@ -0,0 +1,261 @@
import { Client } from 'pg';
import * as dotenv from 'dotenv';
import { readFileSync } from 'fs';
import { join } from 'path';
// Load environment variables
dotenv.config({ path: '.env' });
async function checkAndCreateDatabases() {
// Get connection info from DATABASE_URL or use defaults
const mainDbUrl = process.env.DATABASE_URL || 'postgresql://postgres@localhost:5432/postgres';
const authDbUrl = process.env.DATABASE_URL_AUTH || 'postgresql://postgres@localhost:5432/postgres';
// Parse connection strings to get connection details
const parseUrl = (url: string) => {
const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/) ||
url.match(/postgresql:\/\/([^@]+)@([^:]+):(\d+)\/(.+)/) ||
url.match(/postgresql:\/\/([^@]+)@([^:]+)\/(.+)/);
if (match) {
if (match.length === 6) {
// With password
return {
user: match[1],
password: match[2],
host: match[3],
port: parseInt(match[4]),
database: match[5],
};
} else if (match.length === 5) {
// Without password, with port
return {
user: match[1],
host: match[2],
port: parseInt(match[3]),
database: match[4],
};
} else if (match.length === 4) {
// Without password, without port
return {
user: match[1],
host: match[2],
port: 5432,
database: match[3],
};
}
}
// Fallback to defaults
return {
user: 'postgres',
host: 'localhost',
port: 5432,
database: 'postgres',
};
};
const mainConfig = parseUrl(mainDbUrl);
const authConfig = parseUrl(authDbUrl);
// Connect to postgres database to check/create databases
const adminClient = new Client({
user: mainConfig.user,
password: (mainConfig as any).password,
host: mainConfig.host,
port: mainConfig.port,
database: 'postgres', // Connect to postgres database to create other databases
});
try {
console.log('🔍 Checking databases...\n');
await adminClient.connect();
console.log('✅ Connected to PostgreSQL\n');
// Check if punimtag database exists
const mainDbCheck = await adminClient.query(
"SELECT 1 FROM pg_database WHERE datname = 'punimtag'"
);
if (mainDbCheck.rows.length === 0) {
console.log('⚠️ punimtag database does not exist');
console.log(' This is the main database with photos - it should already exist.');
console.log(' If you need to create it, please do so manually or ensure your PunimTag setup is complete.\n');
} else {
console.log('✅ punimtag database exists\n');
}
// Check if punimtag_auth database exists
const authDbCheck = await adminClient.query(
"SELECT 1 FROM pg_database WHERE datname = 'punimtag_auth'"
);
if (authDbCheck.rows.length === 0) {
console.log('📦 Creating punimtag_auth database...');
await adminClient.query('CREATE DATABASE punimtag_auth');
console.log('✅ punimtag_auth database created\n');
} else {
console.log('✅ punimtag_auth database exists\n');
}
await adminClient.end();
// Now connect to punimtag_auth and create tables
const authClient = new Client({
user: authConfig.user,
password: (authConfig as any).password,
host: authConfig.host,
port: authConfig.port,
database: 'punimtag_auth',
});
try {
await authClient.connect();
console.log('🔗 Connected to punimtag_auth database\n');
// Check if users table exists
const usersTableCheck = await authClient.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
);
`);
if (!usersTableCheck.rows[0].exists) {
console.log('📋 Creating tables in punimtag_auth...');
// Read and execute setup-auth-tables.sql
const setupScript = readFileSync(
join(__dirname, '../setup-auth-tables.sql'),
'utf-8'
);
// Split by semicolons and execute each statement
const statements = setupScript
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0 && !s.startsWith('--') && !s.startsWith('\\'));
for (const statement of statements) {
if (statement.length > 0) {
try {
await authClient.query(statement);
} catch (error: any) {
// Ignore "already exists" errors
if (!error.message.includes('already exists')) {
console.error(`Error executing: ${statement.substring(0, 50)}...`);
throw error;
}
}
}
}
console.log('✅ Tables created\n');
} else {
console.log('✅ Tables already exist in punimtag_auth\n');
}
// Check for required migrations
console.log('🔍 Checking for required migrations...\n');
// Check pending_photos table
const pendingPhotosCheck = await authClient.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'pending_photos'
);
`);
if (!pendingPhotosCheck.rows[0].exists) {
console.log('📋 Creating pending_photos table...');
const migrationScript = readFileSync(
join(__dirname, '../migrations/add-pending-photos-table.sql'),
'utf-8'
);
const statements = migrationScript
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0 && !s.startsWith('--') && !s.startsWith('\\'));
for (const statement of statements) {
if (statement.length > 0) {
try {
await authClient.query(statement);
} catch (error: any) {
if (!error.message.includes('already exists')) {
throw error;
}
}
}
}
console.log('✅ pending_photos table created\n');
}
// Check email verification columns
const emailVerificationCheck = await authClient.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'users'
AND column_name = 'email_verified'
`);
if (emailVerificationCheck.rows.length === 0) {
console.log('📋 Adding email verification columns...');
const migrationScript = readFileSync(
join(__dirname, '../migrations/add-email-verification-columns.sql'),
'utf-8'
);
const statements = migrationScript
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0 && !s.startsWith('--') && !s.startsWith('\\'));
for (const statement of statements) {
if (statement.length > 0) {
try {
await authClient.query(statement);
} catch (error: any) {
if (!error.message.includes('already exists')) {
throw error;
}
}
}
}
console.log('✅ Email verification columns added\n');
}
console.log('🎉 Database setup complete!\n');
console.log('Next steps:');
console.log('1. Ensure your .env file has correct DATABASE_URL_AUTH');
console.log('2. Run: npm run prisma:generate:all');
console.log('3. Create admin user: npx tsx scripts/create-admin-user.ts');
} catch (error: any) {
console.error('\n❌ Error setting up tables:', error.message);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. You may need to run this as a PostgreSQL superuser.');
}
throw error;
} finally {
await authClient.end();
}
} catch (error: any) {
console.error('\n❌ Error:', error.message);
if (error.message.includes('password authentication failed')) {
console.error('\n⚠️ Authentication failed. Please check:');
console.error(' 1. Database credentials in .env file');
console.error(' 2. PostgreSQL is running');
console.error(' 3. User has permission to create databases');
}
process.exit(1);
} finally {
await adminClient.end();
}
}
checkAndCreateDatabases();
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env tsx
/**
* Check database permissions and provide helpful error messages
* This script checks if the read-only user has SELECT permissions on required tables
*/
import { PrismaClient } from '@prisma/client';
import { prisma } from '../lib/db';
const REQUIRED_TABLES = [
'photos',
'people',
'faces',
'person_encodings',
'tags',
'phototaglinkage',
'photo_favorites',
];
async function checkPermissions() {
console.log('🔍 Checking database permissions...\n');
// Extract username from DATABASE_URL
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
console.error('❌ DATABASE_URL not found in environment variables');
console.log('\nPlease add DATABASE_URL to your .env file:');
console.log('DATABASE_URL="postgresql://username:password@localhost:5432/punimtag"');
process.exit(1);
}
const match = dbUrl.match(/postgresql:\/\/([^:]+):/);
if (!match) {
console.error('❌ Could not parse username from DATABASE_URL');
process.exit(1);
}
const username = match[1];
console.log(`📋 Checking permissions for user: ${username}\n`);
const errors: string[] = [];
const successes: string[] = [];
// Test each required table
for (const table of REQUIRED_TABLES) {
try {
// Try to query the table
let query: any;
switch (table) {
case 'photos':
query = prisma.photo.findFirst();
break;
case 'people':
query = prisma.person.findFirst();
break;
case 'faces':
query = prisma.face.findFirst();
break;
case 'person_encodings':
// Skip person_encodings if not in schema
try {
query = (prisma as any).personEncoding?.findFirst();
if (!query) continue;
} catch {
continue;
}
break;
case 'tags':
query = prisma.tag.findFirst();
break;
case 'phototaglinkage':
query = prisma.photoTagLinkage.findFirst();
break;
case 'photo_favorites':
query = prisma.photoFavorite.findFirst();
break;
default:
continue;
}
if (query) {
await query;
successes.push(`${table} - SELECT permission OK`);
}
} catch (error: any) {
if (error.message?.includes('permission denied')) {
errors.push(`${table} - Permission denied`);
} else if (error.message?.includes('does not exist')) {
errors.push(`⚠️ ${table} - Table does not exist (may be OK if not used)`);
} else {
errors.push(`${table} - ${error.message}`);
}
}
}
console.log('\n📊 Permission Check Results:\n');
successes.forEach((msg) => console.log(msg));
if (errors.length > 0) {
console.log('');
errors.forEach((msg) => console.log(msg));
}
if (errors.some((e) => e.includes('Permission denied'))) {
console.log('\n❌ Permission errors detected!\n');
console.log('To fix this, run the following SQL as a PostgreSQL superuser:\n');
console.log('```bash');
console.log(`psql -U postgres -d punimtag -f grant_readonly_permissions.sql`);
console.log('```\n');
console.log('Or manually run:\n');
console.log('```sql');
console.log(`-- Connect to database`);
console.log(`\\c punimtag`);
console.log('');
console.log(`-- Grant permissions`);
REQUIRED_TABLES.forEach((table) => {
console.log(`GRANT SELECT ON TABLE ${table} TO ${username};`);
});
console.log(`GRANT USAGE ON SCHEMA public TO ${username};`);
console.log(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${username};`);
console.log('```\n');
process.exit(1);
}
console.log('\n✅ All required permissions are granted!');
process.exit(0);
}
checkPermissions().catch((error) => {
console.error('Error checking permissions:', error);
process.exit(1);
});
@@ -0,0 +1,178 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import { Resend } from 'resend';
import * as dotenv from 'dotenv';
import crypto from 'crypto';
dotenv.config();
const prismaAuth = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
const resend = new Resend(process.env.RESEND_API_KEY);
async function checkAndResend() {
try {
console.log('🔍 Checking users in database...\n');
// Find users that are not verified
const unverifiedUsers = await prismaAuth.user.findMany({
where: {
emailVerified: false,
},
select: {
id: true,
email: true,
name: true,
emailConfirmationToken: true,
emailConfirmationTokenExpiry: true,
createdAt: true,
},
orderBy: {
createdAt: 'desc',
},
});
if (unverifiedUsers.length === 0) {
console.log('✅ No unverified users found.');
console.log('\n📋 All users:');
const allUsers = await prismaAuth.user.findMany({
select: {
id: true,
email: true,
name: true,
emailVerified: true,
createdAt: true,
},
orderBy: {
createdAt: 'desc',
},
});
allUsers.forEach(user => {
console.log(` - ${user.email} (${user.name}) - Verified: ${user.emailVerified}`);
});
return;
}
console.log(`Found ${unverifiedUsers.length} unverified user(s):\n`);
for (const user of unverifiedUsers) {
console.log(`📧 User: ${user.email} (${user.name})`);
console.log(` Created: ${user.createdAt}`);
console.log(` Has token: ${user.emailConfirmationToken ? 'Yes' : 'No'}`);
if (user.emailConfirmationTokenExpiry) {
const isExpired = user.emailConfirmationTokenExpiry < new Date();
console.log(` Token expires: ${user.emailConfirmationTokenExpiry} ${isExpired ? '(EXPIRED)' : ''}`);
}
console.log('');
}
// Get the most recent unverified user
const latestUser = unverifiedUsers[0];
console.log(`\n📤 Attempting to resend confirmation email to: ${latestUser.email}\n`);
// Generate new token if needed
let token = latestUser.emailConfirmationToken;
if (!token || (latestUser.emailConfirmationTokenExpiry && latestUser.emailConfirmationTokenExpiry < new Date())) {
console.log('🔄 Generating new confirmation token...');
token = crypto.randomBytes(32).toString('hex');
const tokenExpiry = new Date();
tokenExpiry.setHours(tokenExpiry.getHours() + 24);
await prismaAuth.user.update({
where: { id: latestUser.id },
data: {
emailConfirmationToken: token,
emailConfirmationTokenExpiry: tokenExpiry,
},
});
console.log('✅ New token generated');
}
// Send email
const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001';
const confirmationUrl = `${baseUrl}/api/auth/verify-email?token=${token}`;
const fromEmail = process.env.RESEND_FROM_EMAIL?.trim().replace(/^["']|["']$/g, '') || 'onboarding@resend.dev';
console.log(`📧 Sending email from: ${fromEmail}`);
console.log(`📧 Sending email to: ${latestUser.email}`);
console.log(`🔗 Confirmation URL: ${confirmationUrl}\n`);
try {
const result = await resend.emails.send({
from: fromEmail,
to: latestUser.email,
subject: 'Confirm your email address',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confirm your email</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
<h1 style="color: #2563eb; margin-top: 0;">Confirm your email address</h1>
<p>Hi ${latestUser.name},</p>
<p>You requested a new confirmation email. Please confirm your email address by clicking the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${confirmationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Confirm Email Address</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #666; font-size: 14px;">${confirmationUrl}</p>
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
This link will expire in 24 hours. If you didn't request this email, you can safely ignore it.
</p>
</div>
</body>
</html>
`,
});
if (result.error) {
console.error('❌ Error from Resend API:');
console.error(' Status:', result.error.statusCode);
console.error(' Message:', result.error.message);
console.error(' Name:', result.error.name);
if (result.error.message?.includes('domain')) {
console.error('\n⚠️ IMPORTANT: Domain verification issue!');
console.error(' Resend\'s test domain (onboarding@resend.dev) can only send to:');
console.error(' - The email address associated with your Resend account');
console.error(' - To send to other addresses, you need to verify your own domain');
console.error(' - Go to: https://resend.com/domains to verify a domain');
}
} else {
console.log('✅ Email sent successfully!');
console.log(' Email ID:', result.data?.id);
console.log(`\n📬 Check the inbox for: ${latestUser.email}`);
console.log(' (Also check spam/junk folder)');
}
} catch (error: any) {
console.error('❌ Error sending email:');
console.error(' Message:', error.message);
if (error.response) {
console.error(' Response:', JSON.stringify(error.response, null, 2));
}
}
} catch (error: any) {
console.error('❌ Error:', error.message);
if (error.message?.includes('email_verified')) {
console.error('\n⚠️ Database migration may not have been run!');
console.error(' Run: sudo -u postgres psql -d punimtag_auth -f migrations/add-email-verification-columns.sql');
}
} finally {
await prismaAuth.$disconnect();
}
}
checkAndResend();
@@ -0,0 +1,70 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import bcrypt from 'bcryptjs';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function createAdminUser() {
try {
console.log('Creating admin user...\n');
// Hash the password
const passwordHash = await bcrypt.hash('admin', 10);
// Check if admin user already exists
const existingAdmin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
});
if (existingAdmin) {
console.log('Admin user already exists. Updating password and admin status...');
await prisma.user.update({
where: { email: 'admin@admin.com' },
data: {
passwordHash,
isAdmin: true,
name: 'Admin',
hasWriteAccess: true, // Admins should have write access
},
});
console.log('✅ Admin user updated');
} else {
console.log('Creating new admin user...');
await prisma.user.create({
data: {
email: 'admin@admin.com',
name: 'Admin',
passwordHash,
isAdmin: true,
hasWriteAccess: true, // Admins should have write access
},
});
console.log('✅ Admin user created');
}
console.log('\n✅ Admin user setup complete!');
console.log('\nAdmin credentials:');
console.log(' Email: admin@admin.com');
console.log(' Password: admin');
console.log(' Role: Admin (can approve identifications)');
} catch (error: any) {
console.error('Error creating admin user:', error);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. Make sure:');
console.error(' 1. Database tables are created (run setup-auth-complete.sql)');
console.error(' 2. Database user has INSERT/UPDATE permissions on users table');
console.error(' 3. DATABASE_URL_AUTH is correctly configured in .env');
}
throw error;
} finally {
await prisma.$disconnect();
}
}
createAdminUser();
+308
View File
@@ -0,0 +1,308 @@
#!/usr/bin/env tsx
/**
* Script to identify corrupted data in the database
* Finds records with invalid characters that cause P2023 Prisma conversion errors
*/
import { PrismaClient } from '@prisma/client';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClient({
log: ['error'],
});
/**
* Check if a string contains invalid UTF-8 characters
*/
function hasInvalidChars(str: string | null | undefined): boolean {
if (!str) return false;
try {
// Try to encode/decode the string
const encoded = new TextEncoder().encode(str);
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(encoded);
return decoded !== str;
} catch (e) {
return true;
}
}
/**
* Find problematic characters in a string
*/
function findProblematicChars(str: string): string[] {
const problematic: string[] = [];
for (let i = 0; i < str.length; i++) {
const char = str[i];
const code = char.charCodeAt(0);
// Check for invalid UTF-8 sequences or control characters (except common ones)
if (
(code >= 0 && code < 32 && code !== 9 && code !== 10 && code !== 13) ||
(code >= 127 && code < 160) ||
code === 0xfffe ||
code === 0xffff
) {
problematic.push(`U+${code.toString(16).padStart(4, '0')} (${char})`);
}
}
return problematic;
}
async function findCorruptedPeople() {
console.log('🔍 Checking Person table for corrupted data...\n');
try {
// Try to fetch all people
const people = await prisma.person.findMany({
select: {
id: true,
first_name: true,
last_name: true,
middle_name: true,
maiden_name: true,
date_of_birth: true,
created_date: true,
},
});
console.log(`✅ Successfully fetched ${people.length} people\n`);
console.log('Checking for invalid characters in text fields...\n');
let corruptedCount = 0;
const corruptedRecords: Array<{
id: number;
field: string;
value: string;
problematicChars: string[];
}> = [];
for (const person of people) {
const fields = [
{ name: 'first_name', value: person.first_name },
{ name: 'last_name', value: person.last_name },
{ name: 'middle_name', value: person.middle_name },
{ name: 'maiden_name', value: person.maiden_name },
];
for (const field of fields) {
if (field.value && hasInvalidChars(field.value)) {
const problematic = findProblematicChars(field.value);
corruptedRecords.push({
id: person.id,
field: field.name,
value: field.value,
problematicChars: problematic,
});
corruptedCount++;
}
}
}
if (corruptedCount === 0) {
console.log('✅ No corrupted text data found in Person table\n');
} else {
console.log(`⚠️ Found ${corruptedCount} corrupted field(s) in ${corruptedRecords.length} record(s):\n`);
for (const record of corruptedRecords) {
console.log(` Person ID ${record.id}, Field: ${record.field}`);
console.log(` Value: ${JSON.stringify(record.value)}`);
console.log(` Problematic characters: ${record.problematicChars.join(', ')}`);
console.log('');
}
}
return corruptedRecords;
} catch (error: any) {
if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) {
console.error('❌ Cannot query Person table due to conversion error');
console.error(' This means there is definitely corrupted data, but we cannot identify which records\n');
console.error(' Error details:', error.message);
// Try to query with raw SQL to identify problematic records
console.log('\n🔍 Attempting to identify corrupted records using raw SQL...\n');
try {
// First, get all records with text fields only
const textFields = await prisma.$queryRaw<Array<{
id: number;
first_name: string;
last_name: string;
middle_name: string | null;
maiden_name: string | null;
}>>`
SELECT id, first_name, last_name, middle_name, maiden_name
FROM people
`;
console.log(`Found ${textFields.length} records via raw SQL (text fields only)\n`);
console.log('Checking text fields for invalid characters...\n');
let corruptedCount = 0;
for (const person of textFields) {
const fields = [
{ name: 'first_name', value: person.first_name },
{ name: 'last_name', value: person.last_name },
{ name: 'middle_name', value: person.middle_name },
{ name: 'maiden_name', value: person.maiden_name },
];
for (const field of fields) {
if (field.value && hasInvalidChars(field.value)) {
const problematic = findProblematicChars(field.value);
console.log(` Person ID ${person.id}, Field: ${field.name}`);
console.log(` Value: ${JSON.stringify(field.value)}`);
console.log(` Problematic characters: ${problematic.join(', ')}`);
console.log('');
corruptedCount++;
}
}
}
if (corruptedCount === 0) {
console.log('✅ No invalid characters found in text fields');
}
// Now check date fields
console.log('\n🔍 Checking date fields (date_of_birth, created_date)...\n');
try {
const dateFields = await prisma.$queryRaw<Array<{
id: number;
date_of_birth: string | null;
created_date: string;
}>>`
SELECT id, date_of_birth, created_date
FROM people
`;
console.log(`Checking ${dateFields.length} records for corrupted date fields...\n`);
let dateCorruptedCount = 0;
for (const person of dateFields) {
const issues: string[] = [];
// Check created_date
if (person.created_date) {
try {
const date = new Date(person.created_date);
if (isNaN(date.getTime())) {
issues.push(`created_date: invalid date value "${person.created_date}"`);
}
} catch (e) {
issues.push(`created_date: cannot parse "${person.created_date}"`);
}
}
// Check date_of_birth
if (person.date_of_birth) {
try {
const date = new Date(person.date_of_birth);
if (isNaN(date.getTime())) {
issues.push(`date_of_birth: invalid date value "${person.date_of_birth}"`);
}
} catch (e) {
issues.push(`date_of_birth: cannot parse "${person.date_of_birth}"`);
}
}
if (issues.length > 0) {
console.log(` Person ID ${person.id}:`);
for (const issue of issues) {
console.log(`${issue}`);
}
console.log('');
dateCorruptedCount++;
}
}
if (dateCorruptedCount === 0) {
console.log('✅ No corrupted date fields found\n');
} else {
console.log(`⚠️ Found ${dateCorruptedCount} record(s) with corrupted date fields\n`);
}
// Try to identify which specific field is causing the issue
console.log('🔍 Testing individual field queries...\n');
// Test querying without date_of_birth
try {
await prisma.$queryRaw`SELECT id, first_name, last_name, middle_name, maiden_name, created_date FROM people LIMIT 1`;
console.log('✅ Query without date_of_birth works');
} catch (e: any) {
console.log('❌ Query without date_of_birth still fails:', e.message);
}
// Test querying without created_date
try {
await prisma.$queryRaw`SELECT id, first_name, last_name, middle_name, maiden_name, date_of_birth FROM people LIMIT 1`;
console.log('✅ Query without created_date works');
} catch (e: any) {
console.log('❌ Query without created_date still fails:', e.message);
}
} catch (dateError: any) {
console.error('❌ Error checking date fields:', dateError.message);
}
} catch (rawError: any) {
console.error('❌ Raw SQL query also failed:', rawError.message);
}
} else {
throw error;
}
}
}
async function findCorruptedTags() {
console.log('\n🔍 Checking Tag table for corrupted data...\n');
try {
const tags = await prisma.tag.findMany({
select: {
id: true,
tag_name: true,
created_date: true,
},
});
console.log(`✅ Successfully fetched ${tags.length} tags\n`);
let corruptedCount = 0;
for (const tag of tags) {
if (hasInvalidChars(tag.tag_name)) {
const problematic = findProblematicChars(tag.tag_name);
console.log(` Tag ID ${tag.id}, Field: tag_name`);
console.log(` Value: ${JSON.stringify(tag.tag_name)}`);
console.log(` Problematic characters: ${problematic.join(', ')}`);
console.log('');
corruptedCount++;
}
}
if (corruptedCount === 0) {
console.log('✅ No corrupted text data found in Tag table\n');
} else {
console.log(`⚠️ Found ${corruptedCount} corrupted tag(s)\n`);
}
} catch (error: any) {
if (error?.code === 'P2023' || error?.message?.includes('Conversion failed')) {
console.error('❌ Cannot query Tag table due to conversion error');
console.error(' Error details:', error.message);
} else {
throw error;
}
}
}
async function main() {
try {
await findCorruptedPeople();
await findCorruptedTags();
} catch (error: any) {
console.error('❌ Unexpected error:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();
+124
View File
@@ -0,0 +1,124 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import bcrypt from 'bcryptjs';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function fixAdminUser() {
try {
console.log('Checking and fixing admin user...\n');
// Check if has_write_access column exists by trying to query it
let hasWriteAccessColumn = true;
try {
await prisma.$queryRaw`SELECT has_write_access FROM users LIMIT 1`;
console.log('✅ has_write_access column exists');
} catch (error: any) {
if (error.message?.includes('has_write_access') || error.message?.includes('column') || error.code === '42703') {
console.log('❌ has_write_access column does NOT exist');
console.log('\n⚠️ You need to run the database migration first:');
console.log(' psql -U postgres -d punimtag -f migrations/add-write-access-column.sql');
console.log('\n Or manually add the column:');
console.log(' ALTER TABLE users ADD COLUMN has_write_access BOOLEAN NOT NULL DEFAULT false;');
hasWriteAccessColumn = false;
} else {
throw error;
}
}
if (!hasWriteAccessColumn) {
console.log('\n⚠️ Please run the migration and then run this script again.');
return;
}
// Hash the password
const passwordHash = await bcrypt.hash('admin', 10);
// Check if admin user exists
const existingAdmin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
});
if (existingAdmin) {
console.log('✅ Admin user found, updating...');
await prisma.user.update({
where: { email: 'admin@admin.com' },
data: {
passwordHash,
isAdmin: true,
name: 'Admin',
hasWriteAccess: true,
},
});
console.log('✅ Admin user updated successfully');
} else {
console.log('Creating new admin user...');
await prisma.user.create({
data: {
email: 'admin@admin.com',
name: 'Admin',
passwordHash,
isAdmin: true,
hasWriteAccess: true,
},
});
console.log('✅ Admin user created successfully');
}
// Verify the user
const admin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
select: {
id: true,
email: true,
name: true,
isAdmin: true,
hasWriteAccess: true,
},
});
console.log('\n✅ Admin user verified:');
console.log(' Email:', admin?.email);
console.log(' Name:', admin?.name);
console.log(' Is Admin:', admin?.isAdmin);
console.log(' Has Write Access:', admin?.hasWriteAccess);
// Test password
const testPassword = 'admin';
const fullUser = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
select: { passwordHash: true },
});
if (fullUser) {
const isValid = await bcrypt.compare(testPassword, fullUser.passwordHash);
console.log(' Password test:', isValid ? '✅ VALID' : '❌ INVALID');
}
console.log('\n✅ Setup complete!');
console.log('\nYou can now login with:');
console.log(' Email: admin@admin.com');
console.log(' Password: admin');
} catch (error: any) {
console.error('\n❌ Error:', error.message);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. Make sure:');
console.error(' 1. Database tables are created');
console.error(' 2. Database user has INSERT/UPDATE permissions');
console.error(' 3. DATABASE_URL_AUTH is correctly configured in .env');
} else if (error.message.includes('relation') || error.message.includes('does not exist')) {
console.error('\n⚠️ Database tables may not exist. Run setup-auth-complete.sql first.');
}
throw error;
} finally {
await prisma.$disconnect();
}
}
fixAdminUser();
@@ -0,0 +1,65 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
// Load environment variables
dotenv.config({ path: '.env' });
async function grantDeletePermission() {
try {
console.log('Granting DELETE permission on inappropriate_photo_reports table...\n');
// Extract username from DATABASE_URL_AUTH
const dbUrl = process.env.DATABASE_URL_AUTH;
if (!dbUrl) {
console.error('❌ DATABASE_URL_AUTH not found in environment variables');
return;
}
// Parse the connection string to get username
const match = dbUrl.match(/postgresql:\/\/([^:]+):/);
if (!match) {
console.error('❌ Could not parse username from DATABASE_URL_AUTH');
console.log('Please run this SQL command manually:');
console.log('GRANT DELETE ON TABLE inappropriate_photo_reports TO your_username;');
return;
}
const username = match[1];
console.log(`Found database user: ${username}`);
console.log('');
// Try to grant permission using psql
const sqlCommand = `GRANT DELETE ON TABLE inappropriate_photo_reports TO ${username};`;
console.log('Attempting to grant DELETE permission...');
console.log(`SQL: ${sqlCommand}\n`);
try {
// Try to run as current user first
const { stdout, stderr } = await execAsync(
`psql -d punimtag_auth -c "${sqlCommand}"`
);
if (stdout) console.log(stdout);
if (stderr && !stderr.includes('WARNING')) console.error(stderr);
console.log('✅ DELETE permission granted successfully!');
} catch (error: any) {
console.log('⚠️ Could not grant permission automatically (may need sudo)');
console.log('\nPlease run this command manually as PostgreSQL superuser:');
console.log(`\nsudo -u postgres psql -d punimtag_auth -c "GRANT DELETE ON TABLE inappropriate_photo_reports TO ${username};"`);
console.log('\nOr connect to PostgreSQL and run:');
console.log(`\\c punimtag_auth`);
console.log(`GRANT DELETE ON TABLE inappropriate_photo_reports TO ${username};`);
}
} catch (error: any) {
console.error('Error:', error.message);
}
}
grantDeletePermission();
@@ -0,0 +1,102 @@
#!/usr/bin/env tsx
/**
* Grant read-only permissions to viewer_readonly user
* This script requires PostgreSQL superuser credentials
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
const SQL_COMMANDS = `
GRANT CONNECT ON DATABASE punimtag TO viewer_readonly;
GRANT USAGE ON SCHEMA public TO viewer_readonly;
GRANT SELECT ON TABLE photos TO viewer_readonly;
GRANT SELECT ON TABLE people TO viewer_readonly;
GRANT SELECT ON TABLE faces TO viewer_readonly;
GRANT SELECT ON TABLE person_encodings TO viewer_readonly;
GRANT SELECT ON TABLE tags TO viewer_readonly;
GRANT SELECT ON TABLE phototaglinkage TO viewer_readonly;
GRANT SELECT ON TABLE photo_favorites TO viewer_readonly;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO viewer_readonly;
`;
async function grantPermissions() {
console.log('🔐 Attempting to grant database permissions...\n');
// Try different connection methods
const methods = [
// Method 1: Try with PGPASSWORD environment variable
() => {
if (process.env.PGPASSWORD) {
console.log('Trying with PGPASSWORD environment variable...');
try {
const result = execSync(
`psql -h localhost -U postgres -d punimtag -c "${SQL_COMMANDS.replace(/\n/g, ' ')}"`,
{
env: { ...process.env, PGPASSWORD: process.env.PGPASSWORD },
stdio: 'inherit'
}
);
return true;
} catch (error) {
return false;
}
}
return false;
},
// Method 2: Try with sudo (if NOPASSWD is configured)
() => {
console.log('Trying with sudo...');
try {
execSync(
`sudo -u postgres psql -d punimtag -c "${SQL_COMMANDS.replace(/\n/g, ' ')}"`,
{ stdio: 'inherit' }
);
return true;
} catch (error) {
return false;
}
},
];
for (const method of methods) {
try {
if (method()) {
console.log('\n✅ Permissions granted successfully!');
return;
}
} catch (error) {
// Continue to next method
}
}
// If all methods fail, provide manual instructions
console.log('\n❌ Could not automatically grant permissions.\n');
console.log('Please run the SQL commands manually as PostgreSQL superuser:\n');
console.log('Option 1: Using psql with password:');
console.log(' PGPASSWORD=your_password psql -U postgres -d punimtag');
console.log(' Then paste these commands:');
console.log(SQL_COMMANDS);
console.log('\nOption 2: Using sudo:');
console.log(' sudo -u postgres psql -d punimtag');
console.log(' Then paste these commands:');
console.log(SQL_COMMANDS);
console.log('\nOption 3: Run the SQL file:');
console.log(' psql -U postgres -d punimtag -f grant_permissions_now.sql');
process.exit(1);
}
grantPermissions().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash
# Install Dependencies Script
# This script installs all required dependencies for the PunimTag Photo Viewer
# including npm packages, system dependencies, and Prisma clients
set -e # Exit on error
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_DIR"
echo "🚀 Installing PunimTag Photo Viewer Dependencies"
echo "================================================"
echo ""
# Check Node.js version
echo "📋 Checking Node.js version..."
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 20 ]; then
echo "⚠️ Warning: Node.js 20+ is recommended (found v$NODE_VERSION)"
echo " Consider upgrading: nvm install 20 && nvm use 20"
read -p "Continue anyway? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
else
echo "✅ Node.js version: $(node --version)"
fi
echo ""
# Check for system dependencies
echo "📦 Checking system dependencies..."
MISSING_DEPS=()
# Check for libvips (optional but recommended)
if ! command -v vips &> /dev/null && ! dpkg -l | grep -q libvips-dev; then
echo "⚠️ libvips-dev not found (optional, for image watermarking)"
MISSING_DEPS+=("libvips-dev")
fi
# Check for FFmpeg (optional)
if ! command -v ffmpeg &> /dev/null; then
echo "⚠️ FFmpeg not found (optional, for video thumbnails)"
MISSING_DEPS+=("ffmpeg")
fi
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
echo ""
echo "Optional system dependencies not installed:"
for dep in "${MISSING_DEPS[@]}"; do
echo " - $dep"
done
echo ""
read -p "Install optional dependencies? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Installing system dependencies..."
if command -v apt-get &> /dev/null; then
sudo apt-get update
for dep in "${MISSING_DEPS[@]}"; do
if [ "$dep" = "libvips-dev" ]; then
sudo apt-get install -y libvips-dev
elif [ "$dep" = "ffmpeg" ]; then
sudo apt-get install -y ffmpeg
fi
done
elif command -v brew &> /dev/null; then
for dep in "${MISSING_DEPS[@]}"; do
if [ "$dep" = "libvips-dev" ]; then
brew install vips
elif [ "$dep" = "ffmpeg" ]; then
brew install ffmpeg
fi
done
else
echo "⚠️ Please install dependencies manually for your system"
fi
fi
else
echo "✅ All system dependencies found"
fi
echo ""
# Install npm dependencies
echo "📦 Installing npm dependencies..."
npm install
echo "✅ npm dependencies installed"
echo ""
# Install build tools for Sharp (if needed)
if [ -d "node_modules/sharp" ]; then
echo "🔧 Setting up Sharp image processing library..."
# Check if Sharp can load
if node -e "try { require('sharp'); console.log('OK'); } catch(e) { console.log('FAIL'); process.exit(1); }" 2>/dev/null; then
echo "✅ Sharp is working correctly"
else
echo "⚠️ Sharp needs additional setup..."
# Install build dependencies if not present
if ! npm list node-gyp &> /dev/null; then
echo " Installing node-gyp..."
npm install --save-dev node-gyp
fi
if ! npm list node-addon-api &> /dev/null; then
echo " Installing node-addon-api..."
npm install --save-dev node-addon-api
fi
# Try to rebuild Sharp
echo " Attempting to rebuild Sharp..."
npm rebuild sharp || echo " ⚠️ Sharp rebuild failed, but wrapper script will handle library path"
fi
echo ""
fi
# Verify Sharp wrapper script exists
if [ ! -f "scripts/with-sharp-libpath.sh" ]; then
echo "📝 Creating Sharp library path wrapper script..."
cat > scripts/with-sharp-libpath.sh << 'EOF'
#!/bin/bash
# Helper script to set LD_LIBRARY_PATH for Sharp before running commands
# This ensures Sharp can find its bundled libvips library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SHARP_LIB_PATH="$PROJECT_DIR/node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64/lib"
if [ -d "$SHARP_LIB_PATH" ]; then
export LD_LIBRARY_PATH="$SHARP_LIB_PATH:${LD_LIBRARY_PATH:-}"
exec "$@"
else
echo "Warning: Sharp libvips library not found at $SHARP_LIB_PATH"
echo "Sharp image processing may not work correctly."
exec "$@"
fi
EOF
chmod +x scripts/with-sharp-libpath.sh
echo "✅ Sharp wrapper script created"
echo ""
fi
# Generate Prisma clients
echo "🔧 Generating Prisma clients..."
if [ -f "prisma/schema.prisma" ]; then
npm run prisma:generate
echo "✅ Main Prisma client generated"
else
echo "⚠️ prisma/schema.prisma not found, skipping Prisma generation"
fi
if [ -f "prisma/schema-auth.prisma" ]; then
npm run prisma:generate:auth
echo "✅ Auth Prisma client generated"
fi
echo ""
# Check for .env file
if [ ! -f ".env" ]; then
echo "⚠️ .env file not found"
echo " Please create a .env file with the following variables:"
echo " - DATABASE_URL"
echo " - DATABASE_URL_AUTH (optional)"
echo " - NEXTAUTH_SECRET"
echo " - NEXTAUTH_URL"
echo ""
else
echo "✅ .env file found"
fi
echo ""
# Test Sharp if available
echo "🧪 Testing Sharp library..."
if node -e "
const path = require('path');
const libPath = path.join(__dirname, 'node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64/lib');
process.env.LD_LIBRARY_PATH = libPath + ':' + (process.env.LD_LIBRARY_PATH || '');
try {
const sharp = require('sharp');
console.log('✅ Sharp loaded successfully');
console.log(' Version:', require('sharp/package.json').version);
console.log(' libvips:', sharp.versions.vips);
} catch(e) {
console.log('⚠️ Sharp not available:', e.message.split('\n')[0]);
console.log(' Image watermarking will be disabled');
console.log(' The wrapper script will handle this at runtime');
}
" 2>/dev/null; then
echo ""
else
echo "⚠️ Sharp test failed, but wrapper script will handle it at runtime"
echo ""
fi
echo "================================================"
echo "✅ Dependency installation complete!"
echo ""
echo "Next steps:"
echo "1. Configure your .env file with database connection strings"
echo "2. Run 'npm run dev' to start the development server"
echo "3. Run 'npm run check:permissions' to verify database access"
echo ""
@@ -0,0 +1,95 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function runMigration() {
try {
console.log('Running migration to make name column required (NOT NULL)...\n');
// Check current state of the name column
const columnInfo = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
}>>`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'name';
`;
if (columnInfo.length === 0) {
throw new Error('Name column not found in users table');
}
const currentState = columnInfo[0];
console.log(`Current state: is_nullable = ${currentState.is_nullable}`);
if (currentState.is_nullable === 'NO') {
console.log('✅ Name column is already NOT NULL');
return;
}
// Check if any users have NULL names
const nullNameCount = await prisma.$queryRaw<Array<{count: bigint}>>`
SELECT COUNT(*) as count FROM users WHERE name IS NULL;
`;
const count = Number(nullNameCount[0].count);
if (count > 0) {
console.log(`⚠️ Found ${count} user(s) with NULL names. Updating them to use email as name...`);
await prisma.$executeRawUnsafe(`
UPDATE users
SET name = email
WHERE name IS NULL;
`);
console.log('✅ Updated users with NULL names');
}
// Alter the column to be NOT NULL
console.log('Altering name column to be NOT NULL...');
await prisma.$executeRawUnsafe(`
ALTER TABLE users
ALTER COLUMN name SET NOT NULL;
`);
console.log('✅ Column altered successfully');
// Verify the change
const verifyInfo = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
}>>`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'name';
`;
const newState = verifyInfo[0];
if (newState.is_nullable === 'NO') {
console.log('\n✅ Migration completed successfully!');
console.log('Name column is now required (NOT NULL).');
} else {
throw new Error('Migration failed: column is still nullable');
}
} catch (error: any) {
console.error('\n❌ Migration failed:');
console.error(error.message);
if (error.code) {
console.error(`Error code: ${error.code}`);
}
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
runMigration();
@@ -0,0 +1,75 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
dotenv.config();
const prismaAuth = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function manuallyVerifyUser() {
try {
console.log('🔍 Finding unverified users...\n');
const unverifiedUsers = await prismaAuth.user.findMany({
where: {
emailVerified: false,
},
select: {
id: true,
email: true,
name: true,
createdAt: true,
},
orderBy: {
createdAt: 'desc',
},
});
if (unverifiedUsers.length === 0) {
console.log('✅ No unverified users found.');
return;
}
console.log(`Found ${unverifiedUsers.length} unverified user(s):\n`);
unverifiedUsers.forEach((user, index) => {
console.log(`${index + 1}. ${user.email} (${user.name}) - Created: ${user.createdAt}`);
});
// Verify all unverified users
console.log('\n✅ Verifying all unverified users...\n');
for (const user of unverifiedUsers) {
await prismaAuth.user.update({
where: { id: user.id },
data: {
emailVerified: true,
emailConfirmationToken: null,
emailConfirmationTokenExpiry: null,
},
});
console.log(`✅ Verified: ${user.email} (${user.name})`);
}
console.log('\n🎉 All users have been verified! They can now log in.');
} catch (error: any) {
console.error('❌ Error:', error.message);
if (error.message?.includes('email_verified')) {
console.error('\n⚠️ Database migration may not have been run!');
console.error(' Run: sudo -u postgres psql -d punimtag_auth -f migrations/add-email-verification-columns.sql');
}
process.exit(1);
} finally {
await prismaAuth.$disconnect();
}
}
manuallyVerifyUser();
@@ -0,0 +1,80 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as fs from 'fs';
import * as path from 'path';
const prismaAuth = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function runMigration() {
try {
console.log('🔄 Running email verification migration...');
console.log('📝 Reading migration file...');
const migrationPath = path.join(__dirname, '../migrations/add-email-verification-columns.sql');
const migrationSQL = fs.readFileSync(migrationPath, 'utf-8');
// Execute each SQL statement individually
const statements = [
`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT true;`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token VARCHAR(255) UNIQUE;`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token_expiry TIMESTAMP;`,
`CREATE INDEX IF NOT EXISTS idx_users_email_confirmation_token ON users(email_confirmation_token);`,
`UPDATE users SET email_verified = true WHERE email_confirmation_token IS NULL;`,
];
console.log(`📋 Executing ${statements.length} SQL statements...`);
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
try {
await prismaAuth.$executeRawUnsafe(statement);
const desc = statement.split(' ').slice(0, 4).join(' ').toLowerCase();
console.log(`✅ [${i + 1}/${statements.length}] ${desc}...`);
} catch (error: any) {
// Ignore "already exists" errors
if (error.message?.includes('already exists') ||
error.message?.includes('duplicate') ||
error.message?.includes('IF NOT EXISTS')) {
const desc = statement.split(' ').slice(0, 4).join(' ').toLowerCase();
console.log(`️ [${i + 1}/${statements.length}] ${desc}... (already exists)`);
} else {
console.error(`❌ Error executing statement ${i + 1}:`, error.message);
throw error;
}
}
}
console.log('✅ Migration completed successfully!');
console.log('');
console.log('📊 Verifying migration...');
// Verify the columns were added
const result = await prismaAuth.$queryRawUnsafe<Array<{column_name: string}>>(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name IN ('email_verified', 'email_confirmation_token', 'email_confirmation_token_expiry')
ORDER BY column_name;
`);
console.log('✅ Found columns:', result.map(r => r.column_name).join(', '));
// Check existing users
const userCount = await prismaAuth.user.count();
const verifiedCount = await prismaAuth.user.count({
where: { emailVerified: true }
});
console.log(`📊 Users: ${userCount} total, ${verifiedCount} verified`);
} catch (error: any) {
console.error('❌ Migration failed:', error.message);
process.exit(1);
} finally {
await prismaAuth.$disconnect();
}
}
runMigration();
@@ -0,0 +1,130 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function runMigration() {
try {
console.log('Running migration to ensure photo_favorites table exists...\n');
// Check if table already exists
let tableExists = true;
try {
await prisma.$queryRaw`SELECT id FROM photo_favorites LIMIT 1`;
console.log('✅ Table photo_favorites already exists');
} catch (error: any) {
if (error.message?.includes('photo_favorites') || error.code === '42P01') {
console.log('Table does not exist, creating it...');
tableExists = false;
} else {
throw error;
}
}
if (!tableExists) {
// Create the table
console.log('Creating photo_favorites table...');
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS photo_favorites (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
favorited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_photo_favorites_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_photo_user_favorite UNIQUE (photo_id, user_id)
);
`);
console.log('✅ Table created');
} catch (error: any) {
// If permission denied, try without IF NOT EXISTS (sometimes works)
if (error.message?.includes('permission denied') || error.code === '42501') {
console.log('Permission denied with IF NOT EXISTS, trying without...');
await prisma.$executeRawUnsafe(`
CREATE TABLE photo_favorites (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
favorited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_photo_favorites_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_photo_user_favorite UNIQUE (photo_id, user_id)
);
`);
console.log('✅ Table created');
} else {
throw error;
}
}
}
// Create indexes
console.log('Creating indexes...');
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_photo_favorites_photo_id ON photo_favorites(photo_id);`
);
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_photo_favorites_user_id ON photo_favorites(user_id);`
);
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_photo_favorites_favorited_at ON photo_favorites(favorited_at);`
);
console.log('✅ Indexes created');
// Add comment (may fail if user doesn't have permission, but that's okay)
try {
console.log('Adding table comment...');
await prisma.$executeRawUnsafe(`
COMMENT ON TABLE photo_favorites IS 'Stores user favorites for photos';
`);
console.log('✅ Comment added');
} catch (error: any) {
console.log('⚠️ Could not add comment (non-critical)');
}
// Verify
const result = await prisma.$queryRaw<Array<{table_name: string}>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'photo_favorites';
`;
if (result.length > 0) {
console.log('\n✅ Migration completed successfully!');
console.log('Table photo_favorites is ready to use.');
} else {
throw new Error('Table was not created');
}
} catch (error: any) {
console.error('\n❌ Migration failed:');
console.error(error.message);
if (error.code) {
console.error(`Error code: ${error.code}`);
}
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
runMigration();
@@ -0,0 +1,150 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function runMigration() {
try {
console.log('Running migration to ensure inappropriate_photo_reports table exists...\n');
// Check if table already exists
let tableExists = true;
try {
await prisma.$queryRaw`SELECT id FROM inappropriate_photo_reports LIMIT 1`;
console.log('✅ Table inappropriate_photo_reports already exists');
} catch (error: any) {
if (error.message?.includes('inappropriate_photo_reports') || error.code === '42P01') {
console.log('Table does not exist, creating it...');
tableExists = false;
} else {
throw error;
}
}
if (!tableExists) {
// Create the table (using same approach as setup-auth.ts)
console.log('Creating inappropriate_photo_reports table...');
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS inappropriate_photo_reports (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'dismissed')),
reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INTEGER,
review_notes TEXT,
report_comment TEXT,
CONSTRAINT fk_inappropriate_photo_reports_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_photo_user_report UNIQUE (photo_id, user_id)
);
`);
console.log('✅ Table created');
} catch (error: any) {
// If permission denied, try without IF NOT EXISTS (sometimes works)
if (error.message?.includes('permission denied') || error.code === '42501') {
console.log('Permission denied with IF NOT EXISTS, trying without...');
await prisma.$executeRawUnsafe(`
CREATE TABLE inappropriate_photo_reports (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'dismissed')),
reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reviewed_at TIMESTAMP,
reviewed_by INTEGER,
review_notes TEXT,
report_comment TEXT,
CONSTRAINT fk_inappropriate_photo_reports_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_photo_user_report UNIQUE (photo_id, user_id)
);
`);
console.log('✅ Table created');
} else {
throw error;
}
}
}
// Ensure the report_comment column exists (for older deployments)
try {
console.log('Ensuring report_comment column exists...');
await prisma.$executeRawUnsafe(`
ALTER TABLE inappropriate_photo_reports
ADD COLUMN IF NOT EXISTS report_comment TEXT;
`);
console.log('✅ report_comment column ready');
} catch (error: any) {
console.log('⚠️ Could not ensure report_comment column (non-critical)', error.message);
}
// Create indexes
console.log('Creating indexes...');
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_photo_id ON inappropriate_photo_reports(photo_id);`
);
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_user_id ON inappropriate_photo_reports(user_id);`
);
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_status ON inappropriate_photo_reports(status);`
);
await prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_reported_at ON inappropriate_photo_reports(reported_at);`
);
console.log('✅ Indexes created');
// Add comment (may fail if user doesn't have permission, but that's okay)
try {
console.log('Adding table comment...');
await prisma.$executeRawUnsafe(`
COMMENT ON TABLE inappropriate_photo_reports IS 'Stores reports of inappropriate photos submitted by users, pending admin review';
`);
console.log('✅ Comment added');
} catch (error: any) {
console.log('⚠️ Could not add comment (non-critical)');
}
// Verify
const result = await prisma.$queryRaw<Array<{table_name: string}>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'inappropriate_photo_reports';
`;
if (result.length > 0) {
console.log('\n✅ Migration completed successfully!');
console.log('Table inappropriate_photo_reports is ready to use.');
} else {
throw new Error('Table was not created');
}
} catch (error: any) {
console.error('\n❌ Migration failed:');
console.error(error.message);
if (error.code) {
console.error(`Error code: ${error.code}`);
}
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
runMigration();
+79
View File
@@ -0,0 +1,79 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function runMigration() {
try {
console.log('Running migration to add has_write_access column...\n');
// Check if column already exists
try {
await prisma.$queryRaw`SELECT has_write_access FROM users LIMIT 1`;
console.log('✅ Column has_write_access already exists');
return;
} catch (error: any) {
if (error.message?.includes('has_write_access') || error.code === '42703') {
console.log('Column does not exist, adding it...');
} else {
throw error;
}
}
// Add the column
console.log('Adding has_write_access column...');
await prisma.$executeRawUnsafe(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS has_write_access BOOLEAN NOT NULL DEFAULT false;
`);
console.log('✅ Column added');
// Create index
console.log('Creating index...');
await prisma.$executeRawUnsafe(`
CREATE INDEX IF NOT EXISTS idx_users_has_write_access ON users(has_write_access);
`);
console.log('✅ Index created');
// Update existing users to have write access = false (except we'll update admin separately)
console.log('Updating existing users...');
await prisma.$executeRawUnsafe(`
UPDATE users
SET has_write_access = false
WHERE has_write_access IS NULL;
`);
console.log('✅ Existing users updated');
// Verify
const result = await prisma.$queryRaw<Array<{column_name: string, data_type: string, column_default: string}>>`
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'has_write_access';
`;
if (result.length > 0) {
console.log('\n✅ Migration completed successfully!');
console.log('Column details:', result[0]);
} else {
console.log('\n⚠️ Column was added but verification query returned no results');
}
} catch (error: any) {
console.error('\n❌ Error running migration:', error.message);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. You may need to run this as a database superuser.');
console.error('Try running the SQL manually:');
console.error(' ALTER TABLE users ADD COLUMN IF NOT EXISTS has_write_access BOOLEAN NOT NULL DEFAULT false;');
}
throw error;
} finally {
await prisma.$disconnect();
}
}
runMigration();
+132
View File
@@ -0,0 +1,132 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import bcrypt from 'bcryptjs';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function setupAuth() {
try {
console.log('Setting up authentication tables and admin user...\n');
// Create tables using raw SQL (Prisma doesn't support CREATE TABLE in migrations easily)
console.log('Creating users table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
has_write_access BOOLEAN DEFAULT FALSE,
email_verified BOOLEAN DEFAULT FALSE,
email_confirmation_token VARCHAR(255) UNIQUE,
email_confirmation_token_expiry TIMESTAMP,
password_reset_token VARCHAR(255) UNIQUE,
password_reset_token_expiry TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// Add missing columns if table already exists with old schema
console.log('Adding missing columns if needed...');
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN DEFAULT FALSE;`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS has_write_access BOOLEAN DEFAULT FALSE;`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE;`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token VARCHAR(255);`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token_expiry TIMESTAMP;`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_token VARCHAR(255);`);
await prisma.$executeRawUnsafe(`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_token_expiry TIMESTAMP;`);
// Add unique constraints if they don't exist
await prisma.$executeRawUnsafe(`CREATE UNIQUE INDEX IF NOT EXISTS users_email_confirmation_token_key ON users(email_confirmation_token) WHERE email_confirmation_token IS NOT NULL;`);
await prisma.$executeRawUnsafe(`CREATE UNIQUE INDEX IF NOT EXISTS users_password_reset_token_key ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;`);
console.log('Creating pending_identifications table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS pending_identifications (
id SERIAL PRIMARY KEY,
face_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
middle_name VARCHAR(255),
maiden_name VARCHAR(255),
date_of_birth DATE,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-- Note: face_id references faces in punimtag database, but we can't use foreign key across databases
);
`);
console.log('Creating indexes...');
await prisma.$executeRawUnsafe(`CREATE INDEX IF NOT EXISTS idx_pending_identifications_face_id ON pending_identifications(face_id);`);
await prisma.$executeRawUnsafe(`CREATE INDEX IF NOT EXISTS idx_pending_identifications_user_id ON pending_identifications(user_id);`);
await prisma.$executeRawUnsafe(`CREATE INDEX IF NOT EXISTS idx_pending_identifications_status ON pending_identifications(status);`);
// Check if admin user already exists
const existingAdmin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
});
if (existingAdmin) {
console.log('Admin user already exists. Updating password...');
const passwordHash = await bcrypt.hash('admin', 10);
await prisma.user.update({
where: { email: 'admin@admin.com' },
data: {
passwordHash,
isAdmin: true,
hasWriteAccess: true,
emailVerified: true,
isActive: true,
},
});
console.log('✅ Admin user password updated (admin@admin.com / admin)');
} else {
console.log('Creating admin user...');
const passwordHash = await bcrypt.hash('admin', 10);
await prisma.user.create({
data: {
email: 'admin@admin.com',
name: 'Admin',
passwordHash,
isAdmin: true,
hasWriteAccess: true,
emailVerified: true,
isActive: true,
},
});
console.log('✅ Admin user created (admin@admin.com / admin)');
}
console.log('\n✅ Setup complete!');
console.log('\nAdmin credentials:');
console.log(' Email: admin@admin.com');
console.log(' Password: admin');
console.log('\nNote: Make sure to grant appropriate database permissions:');
console.log(' - Regular users: INSERT on pending_identifications');
console.log(' - Admin: UPDATE on pending_identifications (for approval)');
} catch (error: any) {
console.error('Error setting up authentication:', error);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. You may need to:');
console.error(' 1. Run this script with a user that has CREATE TABLE permissions');
console.error(' 2. Or manually create the tables using create_auth_tables.sql');
}
throw error;
} finally {
await prisma.$disconnect();
}
}
setupAuth();
+124
View File
@@ -0,0 +1,124 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import bcrypt from 'bcryptjs';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
// Connect to auth database
const dbUrl = process.env.DATABASE_URL_AUTH;
console.log(`Connecting to auth database: ${dbUrl ? dbUrl.replace(/:[^:@]+@/, ':****@') : 'none'}\n`);
const prisma = new PrismaClientAuth({
datasourceUrl: dbUrl,
});
async function setupDatabase() {
try {
console.log('Setting up database tables and admin user...\n');
// Test connection first
await prisma.$connect();
console.log('✅ Connected to auth database\n');
// Create users table
console.log('Creating users table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
console.log('✅ Users table created');
// Create pending_identifications table
console.log('Creating pending_identifications table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS pending_identifications (
id SERIAL PRIMARY KEY,
face_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
middle_name VARCHAR(255),
maiden_name VARCHAR(255),
date_of_birth DATE,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
-- Note: face_id references faces in punimtag database, but we can't use foreign key across databases
);
`);
console.log('✅ Pending identifications table created');
// Create indexes
console.log('Creating indexes...');
await prisma.$executeRawUnsafe(`
CREATE INDEX IF NOT EXISTS idx_pending_identifications_face_id ON pending_identifications(face_id);
CREATE INDEX IF NOT EXISTS idx_pending_identifications_user_id ON pending_identifications(user_id);
CREATE INDEX IF NOT EXISTS idx_pending_identifications_status ON pending_identifications(status);
`);
console.log('✅ Indexes created');
// Create admin user
console.log('\nCreating admin user...');
const passwordHash = await bcrypt.hash('admin', 10);
const existingAdmin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
});
if (existingAdmin) {
await prisma.user.update({
where: { email: 'admin@admin.com' },
data: {
passwordHash,
isAdmin: true,
name: 'Admin',
},
});
console.log('✅ Admin user updated');
} else {
await prisma.user.create({
data: {
email: 'admin@admin.com',
name: 'Admin',
passwordHash,
isAdmin: true,
},
});
console.log('✅ Admin user created');
}
console.log('\n🎉 Database setup complete!');
console.log('\nAdmin credentials:');
console.log(' Email: admin@admin.com');
console.log(' Password: admin');
console.log(' Role: Admin');
} catch (error: any) {
console.error('\n❌ Error setting up database:', error.message);
if (error.message.includes('permission denied')) {
console.error('\n⚠️ Permission denied. You may need to:');
console.error(' 1. Run this script with a user that has CREATE TABLE permissions');
console.error(' 2. Or manually create the tables using setup-auth-complete.sql');
} else if (error.message.includes('Authentication failed')) {
console.error('\n⚠️ Authentication failed. Please check:');
console.error(' 1. DATABASE_URL_AUTH in .env file');
console.error(' 2. Database credentials are correct');
console.error(' 3. Database server is running');
}
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
setupDatabase();
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Setup script that uses PostgreSQL superuser to create tables
# Usage: ./scripts/setup-with-superuser.sh [postgres_user] [postgres_password]
POSTGRES_USER=${1:-postgres}
POSTGRES_PASSWORD=${2:-}
DB_NAME="punimtag"
echo "Creating database tables using PostgreSQL superuser..."
echo ""
if [ -z "$POSTGRES_PASSWORD" ]; then
# Try without password (trust authentication)
PGPASSWORD="" psql -U "$POSTGRES_USER" -d "$DB_NAME" -f setup-auth-complete.sql
else
# Use password
PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$DB_NAME" -f setup-auth-complete.sql
fi
if [ $? -eq 0 ]; then
echo ""
echo "✅ Tables created! Now creating admin user..."
echo ""
npx tsx scripts/create-admin-user.ts
else
echo ""
echo "❌ Failed to create tables. Please check:"
echo " 1. PostgreSQL superuser credentials"
echo " 2. Database name is correct: $DB_NAME"
echo " 3. You have CREATE TABLE permissions"
fi
@@ -0,0 +1,70 @@
import { PrismaClient as PrismaClientAuth } from '../node_modules/.prisma/client-auth';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env' });
const prisma = new PrismaClientAuth({
datasourceUrl: process.env.DATABASE_URL_AUTH,
});
async function testAdminCheck() {
try {
console.log('Testing admin user check...\n');
// Find admin user
const admin = await prisma.user.findUnique({
where: { email: 'admin@admin.com' },
select: {
id: true,
email: true,
name: true,
isAdmin: true,
hasWriteAccess: true,
},
});
if (!admin) {
console.log('❌ Admin user not found!');
return;
}
console.log('✅ Admin user found:');
console.log(' ID:', admin.id);
console.log(' Email:', admin.email);
console.log(' Is Admin:', admin.isAdmin);
console.log(' Has Write Access:', admin.hasWriteAccess);
// Test querying all users (like the API does)
console.log('\nTesting user list query...');
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
isAdmin: true,
hasWriteAccess: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc',
},
});
console.log(`✅ Successfully queried ${users.length} users`);
users.forEach((user, index) => {
console.log(` ${index + 1}. ${user.email} (Admin: ${user.isAdmin}, Write: ${user.hasWriteAccess})`);
});
console.log('\n✅ All checks passed!');
} catch (error: any) {
console.error('\n❌ Error:', error.message);
console.error('Stack:', error.stack);
} finally {
await prisma.$disconnect();
}
}
testAdminCheck();
@@ -0,0 +1,77 @@
import { Resend } from 'resend';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const resend = new Resend(process.env.RESEND_API_KEY);
async function testEmailSending() {
console.log('🧪 Testing email sending configuration...\n');
// Check environment variables
console.log('📋 Environment Variables:');
console.log(' RESEND_API_KEY:', process.env.RESEND_API_KEY ? `${process.env.RESEND_API_KEY.substring(0, 10)}...` : '❌ NOT SET');
console.log(' RESEND_FROM_EMAIL:', process.env.RESEND_FROM_EMAIL || '❌ NOT SET');
console.log(' NEXTAUTH_URL:', process.env.NEXTAUTH_URL || '❌ NOT SET');
console.log('');
if (!process.env.RESEND_API_KEY) {
console.error('❌ RESEND_API_KEY is not set in .env file');
process.exit(1);
}
if (!process.env.RESEND_FROM_EMAIL) {
console.error('❌ RESEND_FROM_EMAIL is not set in .env file');
process.exit(1);
}
// Clean up the from email (remove quotes and spaces)
const fromEmail = process.env.RESEND_FROM_EMAIL.trim().replace(/^["']|["']$/g, '');
console.log('📧 Using FROM email:', fromEmail);
console.log('');
// Test email sending
console.log('📤 Attempting to send test email...');
try {
const result = await resend.emails.send({
from: fromEmail,
to: 'test@example.com', // This will fail but we'll see the error
subject: 'Test Email',
html: '<p>This is a test email</p>',
});
console.log('✅ Email API call successful!');
console.log('Response:', JSON.stringify(result, null, 2));
} catch (error: any) {
console.error('❌ Error sending email:');
console.error(' Message:', error.message);
if (error.response) {
console.error(' Response:', JSON.stringify(error.response, null, 2));
}
// Check for common errors
if (error.message?.includes('domain')) {
console.error('\n⚠️ Domain verification issue:');
console.error(' The email domain needs to be verified in Resend dashboard');
console.error(' For testing, use: onboarding@resend.dev');
}
if (error.message?.includes('unauthorized') || error.message?.includes('Invalid API key')) {
console.error('\n⚠️ API Key issue:');
console.error(' Check that your RESEND_API_KEY is correct');
console.error(' Get a new key from: https://resend.com/api-keys');
}
process.exit(1);
}
}
testEmailSending();
@@ -0,0 +1,147 @@
#!/usr/bin/env tsx
/**
* Test script to identify which field is causing Prisma conversion errors
*/
import { PrismaClient } from '@prisma/client';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const prisma = new PrismaClient({
log: ['error', 'warn'],
});
async function testQueries() {
console.log('Testing different Prisma queries to identify the problematic field...\n');
// Test 1: Query without date_of_birth
console.log('Test 1: Query without date_of_birth field...');
try {
const result1 = await prisma.$queryRaw<Array<{
id: number;
first_name: string;
last_name: string;
created_date: Date;
}>>`
SELECT id, first_name, last_name, created_date
FROM people
`;
console.log('✅ SUCCESS: Query without date_of_birth works');
console.log(` Found ${result1.length} record(s)\n`);
} catch (e: any) {
console.log('❌ FAILED:', e.message);
console.log('');
}
// Test 2: Query with date_of_birth but cast it
console.log('Test 2: Query with date_of_birth (as text)...');
try {
const result2 = await prisma.$queryRaw<Array<{
id: number;
first_name: string;
last_name: string;
date_of_birth: string | null;
created_date: Date;
}>>`
SELECT id, first_name, last_name,
CAST(date_of_birth AS TEXT) as date_of_birth,
created_date
FROM people
`;
console.log('✅ SUCCESS: Query with date_of_birth as TEXT works');
console.log(` Found ${result2.length} record(s)`);
for (const r of result2) {
console.log(` Person ${r.id}: date_of_birth = ${JSON.stringify(r.date_of_birth)}`);
}
console.log('');
} catch (e: any) {
console.log('❌ FAILED:', e.message);
console.log('');
}
// Test 3: Query with date_of_birth using CASE to handle NULL
console.log('Test 3: Query with date_of_birth (using CASE for NULL)...');
try {
const result3 = await prisma.$queryRaw<Array<{
id: number;
first_name: string;
last_name: string;
date_of_birth: string | null;
created_date: Date;
}>>`
SELECT id, first_name, last_name,
CASE
WHEN date_of_birth IS NULL THEN NULL
ELSE CAST(date_of_birth AS TEXT)
END as date_of_birth,
created_date
FROM people
`;
console.log('✅ SUCCESS: Query with date_of_birth using CASE works');
console.log(` Found ${result3.length} record(s)\n`);
} catch (e: any) {
console.log('❌ FAILED:', e.message);
console.log('');
}
// Test 4: Try using findMany with select excluding date_of_birth
console.log('Test 4: Prisma findMany without date_of_birth...');
try {
const result4 = await prisma.person.findMany({
select: {
id: true,
first_name: true,
last_name: true,
middle_name: true,
maiden_name: true,
created_date: true,
// Exclude date_of_birth
},
});
console.log('✅ SUCCESS: Prisma findMany without date_of_birth works');
console.log(` Found ${result4.length} record(s)\n`);
} catch (e: any) {
console.log('❌ FAILED:', e.message);
console.log('');
}
// Test 5: Try using findMany WITH date_of_birth
console.log('Test 5: Prisma findMany WITH date_of_birth...');
try {
const result5 = await prisma.person.findMany({
select: {
id: true,
first_name: true,
last_name: true,
middle_name: true,
maiden_name: true,
date_of_birth: true, // This is the problematic field
created_date: true,
},
});
console.log('✅ SUCCESS: Prisma findMany with date_of_birth works');
console.log(` Found ${result5.length} record(s)\n`);
} catch (e: any) {
console.log('❌ FAILED:', e.message);
if (e.code === 'P2023') {
console.log(' This confirms date_of_birth is the problematic field!\n');
} else {
console.log('');
}
}
}
testQueries()
.then(() => {
console.log('Tests complete.');
process.exit(0);
})
.catch((e) => {
console.error('Unexpected error:', e);
process.exit(1);
})
.finally(() => {
prisma.$disconnect();
});
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Helper script to set LD_LIBRARY_PATH for Sharp before running commands
# This ensures Sharp can find its bundled libvips library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SHARP_LIB_PATH="$PROJECT_DIR/node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64/lib"
if [ -d "$SHARP_LIB_PATH" ]; then
export LD_LIBRARY_PATH="$SHARP_LIB_PATH:${LD_LIBRARY_PATH:-}"
exec "$@"
else
echo "Warning: Sharp libvips library not found at $SHARP_LIB_PATH"
echo "Sharp image processing may not work correctly."
exec "$@"
fi