fix(api): delete dev endpoints + CI guard (Brief 3 of fix-auth-bypass)
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 <cursoragent@cursor.com>
This commit is contained in:
parent
bc0d1687d0
commit
b6891f1fa6
6 changed files with 32 additions and 247 deletions
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue