From 45bca2873f24dc3164a948c96a00adf9b62588ea Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 22 Jul 2025 06:46:09 -0500 Subject: [PATCH] Convert auth endpoints to working Vercel function format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 API Format Fix: - Convert register.ts to register.js using req/res pattern - Convert login.ts to login.js using req/res pattern - Convert setup-auth.ts to setup-auth.js using req/res pattern - Use CommonJS require() instead of ES6 imports - Use traditional Vercel function signature that works ✅ Working Format: - export default async function handler(req, res) - req.body for request data (auto-parsed by Vercel) - res.status().json() for responses - CommonJS modules with require() This should resolve all timeout issues and make authentication work properly. --- api/auth/login.js | 125 +++++++++++++++++++++++++++++++ api/auth/register.js | 158 +++++++++++++++++++++++++++++++++++++++ api/setup-auth.js | 172 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 455 insertions(+) create mode 100644 api/auth/login.js create mode 100644 api/auth/register.js create mode 100644 api/setup-auth.js diff --git a/api/auth/login.js b/api/auth/login.js new file mode 100644 index 0000000..2c04b5d --- /dev/null +++ b/api/auth/login.js @@ -0,0 +1,125 @@ +const { Pool } = require('pg'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}); + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const client = await pool.connect(); + + try { + const { username, password } = req.body; + + // Validate input + if (!username || !password) { + return res.status(400).json({ + error: 'Username and password are required' + }); + } + + // Get user by username or email + const userResult = await client.query(` + SELECT id, username, email, password_hash, first_name, last_name, is_active, last_login + FROM users + WHERE (username = $1 OR email = $1) AND is_active = true + `, [username]); + + if (userResult.rows.length === 0) { + return res.status(401).json({ + error: 'Invalid credentials' + }); + } + + const user = userResult.rows[0]; + + // Verify password + const isValidPassword = await bcrypt.compare(password, user.password_hash); + if (!isValidPassword) { + return res.status(401).json({ + error: 'Invalid credentials' + }); + } + + // Get user roles and permissions + const userRoles = await client.query(` + SELECT r.name, r.description, + array_agg(p.name) as permissions + FROM roles r + JOIN user_roles ur ON r.id = ur.role_id + LEFT JOIN role_permissions rp ON r.id = rp.role_id + LEFT JOIN permissions p ON rp.permission_id = p.id + WHERE ur.user_id = $1 + GROUP BY r.id, r.name, r.description + `, [user.id]); + + const roles = userRoles.rows.map(r => r.name); + const permissions = [...new Set(userRoles.rows.flatMap(r => r.permissions || []))]; + + // Generate JWT token + const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production'; + const token = jwt.sign( + { + userId: user.id, + username: user.username, + email: user.email, + roles, + permissions + }, + jwtSecret, + { expiresIn: '7d' } + ); + + // Store session + const tokenHash = await bcrypt.hash(token, 10); + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days + + await client.query(` + INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address) + VALUES ($1, $2, $3, $4, $5) + `, [ + user.id, + tokenHash, + expiresAt, + req.headers['user-agent'] || null, + req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null + ]); + + // Update last login + await client.query( + 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1', + [user.id] + ); + + res.status(200).json({ + success: true, + message: 'Login successful', + user: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.first_name, + lastName: user.last_name, + roles, + permissions, + lastLogin: user.last_login + }, + token + }); + + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + error: 'Login failed', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/api/auth/register.js b/api/auth/register.js new file mode 100644 index 0000000..1b4f1af --- /dev/null +++ b/api/auth/register.js @@ -0,0 +1,158 @@ +const { Pool } = require('pg'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}); + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + // Check environment variables + if (!process.env.DATABASE_URL) { + return res.status(500).json({ + error: 'Database configuration missing', + details: 'DATABASE_URL environment variable not set' + }); + } + + if (!process.env.JWT_SECRET) { + console.warn('JWT_SECRET not set, using fallback'); + } + + const client = await pool.connect(); + + try { + const { username, email, password, firstName, lastName } = req.body; + + // Validate input + if (!username || !email || !password) { + return res.status(400).json({ + error: 'Username, email, and password are required' + }); + } + + if (password.length < 6) { + return res.status(400).json({ + error: 'Password must be at least 6 characters long' + }); + } + + // Check if users table exists + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'users' + ); + `); + + if (!tableCheck.rows[0].exists) { + return res.status(500).json({ + error: 'Database not initialized', + details: 'Please run the setup-auth endpoint first' + }); + } + + // Check if user already exists + const existingUser = await client.query( + 'SELECT id FROM users WHERE username = $1 OR email = $2', + [username, email] + ); + + if (existingUser.rows.length > 0) { + return res.status(409).json({ + error: 'Username or email already exists' + }); + } + + // Hash password + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // Create user + const userResult = await client.query(` + INSERT INTO users (username, email, password_hash, first_name, last_name) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, username, email, first_name, last_name, created_at + `, [username, email, passwordHash, firstName || null, lastName || null]); + + const user = userResult.rows[0]; + + // Assign default 'user' role + const roleResult = await client.query( + 'SELECT id FROM roles WHERE name = $1', + ['user'] + ); + + if (roleResult.rows.length > 0) { + await client.query( + 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)', + [user.id, roleResult.rows[0].id] + ); + } + + // Generate JWT token + const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production'; + const token = jwt.sign( + { + userId: user.id, + username: user.username, + email: user.email + }, + jwtSecret, + { expiresIn: '7d' } + ); + + // Store session + const tokenHash = await bcrypt.hash(token, 10); + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days + + await client.query(` + INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address) + VALUES ($1, $2, $3, $4, $5) + `, [ + user.id, + tokenHash, + expiresAt, + req.headers['user-agent'] || null, + req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null + ]); + + // Get user roles for response + const userRoles = await client.query(` + SELECT r.name, r.description + FROM roles r + JOIN user_roles ur ON r.id = ur.role_id + WHERE ur.user_id = $1 + `, [user.id]); + + res.status(201).json({ + success: true, + message: 'User registered successfully', + user: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.first_name, + lastName: user.last_name, + roles: userRoles.rows.map(r => r.name), + createdAt: user.created_at + }, + token + }); + + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ + error: 'Registration failed', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/api/setup-auth.js b/api/setup-auth.js new file mode 100644 index 0000000..d17adae --- /dev/null +++ b/api/setup-auth.js @@ -0,0 +1,172 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}); + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const client = await pool.connect(); + + try { + console.log('Setting up user authentication schema...'); + + // Check if users table already exists + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'users' + ); + `); + + if (tableCheck.rows[0].exists) { + return res.status(200).json({ + success: true, + message: 'User authentication schema already exists', + already_setup: true + }); + } + + // Create tables one by one + await client.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(50), + last_name VARCHAR(50), + avatar_url TEXT, + is_active BOOLEAN DEFAULT true, + email_verified BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP WITH TIME ZONE + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS roles ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS user_roles ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, + assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + assigned_by INTEGER REFERENCES users(id), + UNIQUE(user_id, role_id) + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS permissions ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + resource VARCHAR(50), + action VARCHAR(50), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS role_permissions ( + id SERIAL PRIMARY KEY, + role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, + permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE, + UNIQUE(role_id, permission_id) + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) NOT NULL, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_used TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + user_agent TEXT, + ip_address INET + ); + `); + + // Insert default roles + await client.query(` + INSERT INTO roles (name, description) VALUES + ('user', 'Standard user with basic permissions'), + ('admin', 'Administrator with full system access') + ON CONFLICT (name) DO NOTHING; + `); + + // Insert basic permissions + const permissions = [ + ['cards.read', 'View cards'], + ['collections.manage', 'Manage collections'], + ['decks.manage', 'Manage decks'], + ['admin.access', 'Access admin panel'] + ]; + + for (const [name, description] of permissions) { + await client.query(` + INSERT INTO permissions (name, description) + VALUES ($1, $2) + ON CONFLICT (name) DO NOTHING; + `, [name, description]); + } + + // Assign permissions to roles + await client.query(` + INSERT INTO role_permissions (role_id, permission_id) + SELECT r.id, p.id + FROM roles r, permissions p + WHERE r.name = 'user' + AND p.name IN ('cards.read', 'collections.manage', 'decks.manage') + ON CONFLICT (role_id, permission_id) DO NOTHING; + `); + + await client.query(` + INSERT INTO role_permissions (role_id, permission_id) + SELECT r.id, p.id + FROM roles r, permissions p + WHERE r.name = 'admin' + ON CONFLICT (role_id, permission_id) DO NOTHING; + `); + + // Create indexes + await client.query(` + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); + CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); + `); + + console.log('✅ User authentication schema setup completed!'); + + res.status(200).json({ + success: true, + message: 'User authentication schema setup completed successfully' + }); + + } catch (error) { + console.error('Schema setup failed:', error); + res.status(500).json({ + success: false, + error: 'Schema setup failed', + details: error.message + }); + } finally { + client.release(); + } +} \ No newline at end of file