From fc0dd73fdced386e4406021d5c9afb5f4097cb08 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sat, 23 May 2026 09:54:09 -0500 Subject: [PATCH] fix(api): delete dev endpoints + CI guard (Brief 3 of fix-auth-bypass) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes four unauthenticated dev endpoints that were shipped to production: - pages/api/simple.js (info leak) - pages/api/test-auth.js (auth diagnostic / token-mint side door) - pages/api/test-db.js (DB connection diagnostic) - pages/api/setup-database.js (public POST that ran DDL + seeded admin) setup-database is the highest-impact removal: it was a public endpoint that triggered schema bootstrap and seeded the default admin credentials (admin@tcgvault.com / admin123). AGENTS.md gotcha #5. Also adds a new `forbidden-endpoints` job to .github/workflows/ci.yml that fails the build if any of the four deleted paths re-appear OR if any new pages/api/test-*.js file is added. Cheap insurance against a future agent re-introducing a dev endpoint from an outdated tutorial. README: drops the single `GET /api/test-db` line under "Health Check". Rest of the API list is intentionally left for the doc-writer pass. Verified locally: - npm run build exits 0 (no source callers — confirmed via grep across pages/, components/, lib/) - CI guard local simulation: clean → OK; with test-fake.js → FAIL; OK after cleanup Resolves AGENTS.md gotcha #5. Brief 1/2/4/5 still pending in convoy. Convoy: fix-auth-bypass / Brief 3 Co-authored-by: Cursor --- .github/workflows/ci.yml | 32 ++++++++ README.md | 1 - pages/api/setup-database.js | 154 ------------------------------------ pages/api/simple.js | 7 -- pages/api/test-auth.js | 51 ------------ pages/api/test-db.js | 34 -------- 6 files changed, 32 insertions(+), 247 deletions(-) delete mode 100644 pages/api/setup-database.js delete mode 100644 pages/api/simple.js delete mode 100644 pages/api/test-auth.js delete mode 100644 pages/api/test-db.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6eabb7..169ebbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,38 @@ jobs: fi echo "OK: schema map and migration scripts are in sync." + forbidden-endpoints: + name: No dev endpoints in pages/api + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Fail if dev endpoints re-appear under pages/api/ + run: | + BAD_PATHS=( + "pages/api/simple.js" + "pages/api/test-auth.js" + "pages/api/test-db.js" + "pages/api/setup-database.js" + ) + FOUND=() + for path in "${BAD_PATHS[@]}"; do + if [ -f "$path" ]; then + FOUND+=("$path") + fi + done + # Also flag any new pages/api/test-*.js the explicit list missed. + while IFS= read -r path; do + FOUND+=("$path") + done < <(find pages/api -maxdepth 4 -type f -name 'test-*.js' 2>/dev/null || true) + if [ ${#FOUND[@]} -gt 0 ]; then + echo "::error::Forbidden dev endpoints present in pages/api/. Delete them or move to scripts/." + for path in "${FOUND[@]}"; do + echo "::error file=${path}::Forbidden dev endpoint." + done + exit 1 + fi + echo "OK: no forbidden dev endpoints under pages/api/." + # test: # Disabled until a test runner is adopted. Re-enable as: # diff --git a/README.md b/README.md index 79dd20f..485c01e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ The application uses the following tables: ### Health Check - `GET /api/health` - Application health -- `GET /api/test-db` - Database connection test ## 🚀 Deployment diff --git a/pages/api/setup-database.js b/pages/api/setup-database.js deleted file mode 100644 index 2a4c799..0000000 --- a/pages/api/setup-database.js +++ /dev/null @@ -1,154 +0,0 @@ -import { sql } from '@vercel/postgres'; - -export default async function handler(req, res) { - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - // Handle preflight requests - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - try { - // Create users table - await sql` - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - role VARCHAR(50) DEFAULT 'user', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `; - - // Create cards table - await sql` - CREATE TABLE IF NOT EXISTS cards ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - set_name VARCHAR(255), - set_code VARCHAR(50), - card_number VARCHAR(50), - rarity VARCHAR(50), - game VARCHAR(50) NOT NULL, - mana_cost VARCHAR(50), - cmc INTEGER, - card_type VARCHAR(255), - colors JSONB, - oracle_text TEXT, - power VARCHAR(10), - toughness VARCHAR(10), - image_url TEXT, - stock_image_url TEXT, - current_price DECIMAL(10,2), - market_price DECIMAL(10,2), - scryfall_id VARCHAR(255) UNIQUE, - verified BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `; - - // Create user_cards table (collections) - await sql` - CREATE TABLE IF NOT EXISTS user_cards ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE, - quantity INTEGER DEFAULT 1, - condition VARCHAR(50) DEFAULT 'NM', - is_foil BOOLEAN DEFAULT false, - notes TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, card_id, is_foil) - ) - `; - - // Create collections table - await sql` - CREATE TABLE IF NOT EXISTS collections ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - description TEXT, - is_public BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `; - - // Create collection_cards table - await sql` - CREATE TABLE IF NOT EXISTS collection_cards ( - id SERIAL PRIMARY KEY, - collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE, - card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE, - quantity INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(collection_id, card_id) - ) - `; - - // Create decks table - await sql` - CREATE TABLE IF NOT EXISTS decks ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - description TEXT, - game VARCHAR(50), - is_public BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `; - - // Create deck_cards table - await sql` - CREATE TABLE IF NOT EXISTS deck_cards ( - id SERIAL PRIMARY KEY, - deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, - card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE, - quantity INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(deck_id, card_id) - ) - `; - - // Create a default admin user - const bcrypt = await import('bcryptjs'); - const hashedPassword = await bcrypt.hash('admin123', 12); - - await sql` - INSERT INTO users (email, password, role) - VALUES ('admin@tcgvault.com', ${hashedPassword}, 'admin') - ON CONFLICT (email) DO NOTHING - `; - - res.status(200).json({ - success: true, - message: 'Database setup completed successfully!', - tables: ['users', 'cards', 'user_cards', 'collections', 'collection_cards', 'decks', 'deck_cards'], - adminUser: { - email: 'admin@tcgvault.com', - password: 'admin123' - } - }); - - } catch (error) { - console.error('Database setup error:', error); - res.status(500).json({ - error: 'Database setup failed', - details: error.message - }); - } -} \ No newline at end of file diff --git a/pages/api/simple.js b/pages/api/simple.js deleted file mode 100644 index c23b629..0000000 --- a/pages/api/simple.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function handler(req, res) { - res.status(200).json({ - success: true, - message: 'Simple API is working!', - timestamp: new Date().toISOString() - }); -} \ No newline at end of file diff --git a/pages/api/test-auth.js b/pages/api/test-auth.js deleted file mode 100644 index 9e42786..0000000 --- a/pages/api/test-auth.js +++ /dev/null @@ -1,51 +0,0 @@ -import { hashPassword, verifyPassword, generateToken } from './auth-utils.js'; - -export default async function handler(req, res) { - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - // Handle preflight requests - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - try { - // Test password hashing - const password = 'test123'; - const hashedPassword = await hashPassword(password); - const isValid = await verifyPassword(password, hashedPassword); - - // Test token generation - const user = { - id: 1, - email: 'test@example.com', - role: 'user' - }; - const token = generateToken(user); - - res.status(200).json({ - success: true, - message: 'Authentication utilities working!', - passwordTest: { - original: password, - hashed: hashedPassword, - isValid - }, - tokenTest: { - token, - user - }, - timestamp: new Date().toISOString() - }); - - } catch (error) { - console.error('Auth test error:', error); - res.status(500).json({ - error: 'Authentication test failed', - details: error.message - }); - } -} \ No newline at end of file diff --git a/pages/api/test-db.js b/pages/api/test-db.js deleted file mode 100644 index b3b40eb..0000000 --- a/pages/api/test-db.js +++ /dev/null @@ -1,34 +0,0 @@ -import { db } from '../../lib/database.js'; - -export default async function handler(req, res) { - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - // Handle preflight requests - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - try { - // Test database connection - const result = await db.query('SELECT NOW() as current_time'); - - res.status(200).json({ - success: true, - message: 'Database connection successful!', - data: result.rows[0] || { current_time: new Date().toISOString() }, - timestamp: new Date().toISOString(), - environment: process.env.NODE_ENV - }); - } catch (error) { - console.error('Database test error:', error); - res.status(500).json({ - error: 'Database connection failed', - details: error.message, - environment: process.env.NODE_ENV - }); - } -} \ No newline at end of file