Convert auth endpoints to working Vercel function format

🔧 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.
This commit is contained in:
Randall Stillwell 2025-07-22 06:46:09 -05:00
parent b462010434
commit 45bca2873f
3 changed files with 455 additions and 0 deletions

125
api/auth/login.js Normal file
View file

@ -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();
}
}

158
api/auth/register.js Normal file
View file

@ -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();
}
}

172
api/setup-auth.js Normal file
View file

@ -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();
}
}