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:
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user