Migrate database from MySQL to Supabase PostgreSQL

- Added @supabase/supabase-js client library
- Rewrote database.js to use Supabase API
- Updated server.js health check for Supabase
- Updated db.config.example.js with Supabase format
- Created comprehensive SUPABASE_SETUP.md guide
- Added SQL schema files for easy deployment
- Updated README_DB_CONFIG.md for Supabase

Benefits:
- Managed PostgreSQL database
- Built-in Row Level Security
- Real-time capabilities
- Easy monitoring via dashboard
- Free tier for development
This commit is contained in:
2025-12-21 15:40:57 +11:00
parent 5238fc8d22
commit 054cbf3e77
9 changed files with 872 additions and 276 deletions

View File

@@ -1,117 +1,76 @@
# Database Configuration Setup
# Database Configuration Setup - Supabase
## Overview
Database credentials are stored in a separate configuration file (`db.config.js`) that is **NOT committed to GitHub** for security reasons.
This project now uses **Supabase** (PostgreSQL) instead of MySQL.
## Files
### 1. `db.config.example.js` (Committed to Git)
Template file showing the required configuration structure.
Template file showing the required Supabase configuration structure.
### 2. `db.config.js` (NOT Committed - in .gitignore)
Contains actual database credentials. This file must be created manually.
Contains actual Supabase credentials. This file must be created manually.
### 3. `.gitignore`
Ensures `db.config.js` is never committed to the repository.
## Setup Instructions
## Quick Setup
### For Local Development
See **[SUPABASE_SETUP.md](SUPABASE_SETUP.md)** for detailed step-by-step instructions.
1. **Copy the example file:**
```bash
cp db.config.example.js db.config.js
```
### Quick Start
2. **Edit `db.config.js` with your credentials:**
1. **Create Supabase project** at [app.supabase.com](https://app.supabase.com)
2. **Copy credentials** from Project Settings → API
3. **Update `db.config.js`:**
```javascript
module.exports = {
host: 'localhost', // or your database host
user: 'your_username',
password: 'your_password',
database: 'appgconnect5_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
supabaseUrl: 'https://xxxxx.supabase.co',
supabaseAnonKey: 'eyJhbGci...',
supabasePassword: 't1hWsackxbYzRIPD'
};
```
3. **Start the server:**
```bash
npm start
```
### For Production Deployment
1. **Pull the latest code on your server:**
```bash
git pull origin main
```
2. **Create `db.config.js` on the production server:**
```bash
nano db.config.js
# or
vi db.config.js
```
3. **Add your production database credentials:**
```javascript
module.exports = {
host: 'your-production-db-host.com',
user: 'production_user',
password: 'secure_production_password',
database: 'appgconnect5_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
};
```
4. **Save and restart the server:**
```bash
pm2 restart connect5
# or your restart command
```
4. **Run SQL schema** in Supabase SQL Editor (see SUPABASE_SETUP.md)
5. **Start server:** `npm start`
## Security Features
✅ **Credentials not in git** - `db.config.js` is in `.gitignore`
✅ **Template provided** - `db.config.example.js` shows the structure
✅ **Comments in code** - Clear instructions in `database.js`
✅ **Supabase RLS** - Row Level Security policies protect data
✅ **Separate config** - Easy to update without touching main code
## Troubleshooting
### Error: Cannot find module './db.config.js'
**Solution:** You need to create the `db.config.js` file:
**Solution:** Create the `db.config.js` file:
```bash
cp db.config.example.js db.config.js
# Then edit with your credentials
# Then edit with your Supabase credentials
```
### Error: Access denied for user
### Error: Invalid API key
**Solution:** Check your credentials in `db.config.js`:
- Verify username
- Verify password
- Check host address
- Ensure user has proper permissions
- Verify `supabaseUrl` is correct
- Verify `supabaseAnonKey` (should start with `eyJ...`)
- Get credentials from Supabase dashboard → Project Settings → API
### Connection timeout
### Error: Table 'players' does not exist
**Solution:**
- Check if MySQL server is running
- Verify firewall allows connection
- Check host address is correct
- Run the SQL schema in Supabase SQL Editor
- See SUPABASE_SETUP.md Step 4 for the complete schema
## Important Notes
⚠️ **NEVER commit `db.config.js` to git**
⚠️ **Keep production credentials secure**
⚠️ **Use different credentials for dev/prod**
⚠️ **Regularly rotate passwords**
⚠️ **Keep credentials secure**
⚠️ **Use different projects for dev/prod**
⚠️ **The anon key is safe for client-side use** (protected by RLS)
## File Structure
@@ -121,5 +80,7 @@ Connect-5/
├── db.config.js ← Your credentials (NOT in git)
├── .gitignore ← Protects db.config.js
├── database.js ← Imports from db.config.js
├── supabase-functions.sql ← Helper functions for Supabase
├── SUPABASE_SETUP.md ← Detailed setup guide
└── README_DB_CONFIG.md ← This file
```

223
SUPABASE_SETUP.md Normal file
View File

@@ -0,0 +1,223 @@
# Supabase Setup Guide for Connect-5
This guide will help you set up Supabase for the Connect-5 multiplayer game.
## Step 1: Create Supabase Project
1. Go to [https://app.supabase.com](https://app.supabase.com)
2. Sign in or create an account
3. Click "New Project"
4. Fill in the project details:
- **Organization**: Select or create your organization (e.g., "DeNNiiInc's Org")
- **Project name**: `Connect5`
- **Database password**: `t1hWsackxbYzRIPD` (or your chosen password)
- **Region**: Oceania (Sydney) - or closest to your users
- **Pricing Plan**: Free tier is sufficient for development
5. Click "Create new project"
6. Wait for the project to be provisioned (takes 1-2 minutes)
## Step 2: Get Your Credentials
Once your project is ready:
1. Go to **Project Settings** (gear icon in sidebar)
2. Navigate to **API** section
3. Copy the following values:
- **Project URL** (e.g., `https://xxxxxxxxxxxxx.supabase.co`)
- **anon/public key** (long JWT token starting with `eyJ...`)
## Step 3: Configure Your Application
1. Open `db.config.js` in your project
2. Replace the placeholder values:
```javascript
module.exports = {
supabaseUrl: 'https://YOUR_PROJECT_ID.supabase.co', // Paste your Project URL here
supabaseAnonKey: 'YOUR_ANON_KEY_HERE', // Paste your anon key here
supabasePassword: 't1hWsackxbYzRIPD', // Your database password
// Optional: Direct PostgreSQL connection
postgresConnectionString: 'postgresql://postgres:t1hWsackxbYzRIPD@db.YOUR_PROJECT_ID.supabase.co:5432/postgres'
};
```
## Step 4: Create Database Tables
1. In your Supabase dashboard, click on **SQL Editor** in the sidebar
2. Click **New Query**
3. Copy and paste the following SQL:
```sql
-- Create players table
CREATE TABLE IF NOT EXISTS players (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_username ON players(username);
-- Create active sessions table
CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
);
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
);
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
-- Enable Row Level Security (RLS)
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
```
4. Click **Run** or press `Ctrl+Enter`
5. You should see "Success. No rows returned" message
## Step 5: Create Helper Functions
1. In the same SQL Editor, create a new query
2. Copy and paste the contents of `supabase-functions.sql`:
```sql
-- Function to increment wins
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment losses
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment draws
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
```
3. Click **Run**
## Step 6: Test Your Connection
1. Install dependencies:
```bash
npm install
```
2. Start the server:
```bash
npm start
```
3. Check the console output for:
- ✅ Database schema verified successfully
- 🗄️ Database connected
4. Open your browser to `http://localhost:3000`
5. Check the bottom status bar - it should show:
- **SQL**: Connected (green)
- **Latency**: Should be reasonable (depends on your location to Sydney)
- **Write**: Enabled (green)
## Troubleshooting
### "Invalid API key" Error
- Double-check your `supabaseAnonKey` in `db.config.js`
- Make sure you copied the **anon/public** key, not the service_role key
### "Cannot reach Supabase" Error
- Verify your `supabaseUrl` is correct
- Check your internet connection
- Ensure no firewall is blocking Supabase
### "Table 'players' does not exist" Error
- Make sure you ran the SQL schema in Step 4
- Check the SQL Editor for any error messages
- Verify all tables were created in the **Table Editor**
### High Latency
- This is normal if you're far from the Sydney region
- Consider changing the region when creating your project
- Latency doesn't significantly affect gameplay for turn-based games
## Security Notes
- The `db.config.js` file is in `.gitignore` and will NOT be committed to Git
- Never share your database password or anon key publicly
- The anon key is safe to use in client-side code (it's protected by RLS policies)
- For production, consider implementing more restrictive RLS policies
## Next Steps
Once connected, you can:
- Test multiplayer functionality
- Monitor your database in the Supabase dashboard
- View real-time data in the **Table Editor**
- Check logs in the **Logs** section
- Set up database backups (available in paid plans)

View File

@@ -1,82 +1,110 @@
const mysql = require('mysql2/promise');
const { createClient } = require('@supabase/supabase-js');
// Import database configuration from external file
// This file (db.config.js) is not committed to git for security
// Use db.config.example.js as a template
const dbConfig = require('./db.config.js');
// Create connection pool
const pool = mysql.createPool(dbConfig);
// Create Supabase client
const supabase = createClient(dbConfig.supabaseUrl, dbConfig.supabaseAnonKey);
// Initialize database schema
async function initializeDatabase() {
try {
const connection = await pool.getConnection();
console.log('🔄 Initializing Supabase database schema...');
// Create players table
await connection.query(`
const { error: playersError } = await supabase.rpc('create_players_table', {});
// Since we can't run raw SQL directly with the JS client in the same way,
// we'll use Supabase's SQL editor or migrations
// For now, we'll check if tables exist by trying to query them
const { data: playersCheck, error: playersCheckError } = await supabase
.from('players')
.select('id')
.limit(1);
if (playersCheckError && playersCheckError.code === '42P01') {
console.log('⚠️ Tables not found. Please run the following SQL in your Supabase SQL Editor:');
console.log(`
-- Create players table
CREATE TABLE IF NOT EXISTS players (
id INT AUTO_INCREMENT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username)
)
`);
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_username ON players(username);
// Create active sessions table
await connection.query(`
-- Create active sessions table
CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id INT NOT NULL,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
)
`);
);
// Create games table
await connection.query(`
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id INT AUTO_INCREMENT PRIMARY KEY,
player1_id INT NOT NULL,
player2_id INT NOT NULL,
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id INT,
game_state ENUM('pending', 'active', 'completed', 'abandoned') DEFAULT 'pending',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
)
`);
);
// Create game moves table
await connection.query(`
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id INT AUTO_INCREMENT PRIMARY KEY,
game_id INT NOT NULL,
player_id INT NOT NULL,
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id),
INDEX idx_game (game_id)
)
`);
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
connection.release();
console.log('✅ Database schema initialized successfully');
-- Enable Row Level Security (RLS) - Optional but recommended
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
`);
throw new Error('Database tables not initialized. Please run the SQL above in Supabase SQL Editor.');
}
console.log('✅ Database schema verified successfully');
} catch (error) {
console.error('❌ Error initializing database:', error);
console.error('❌ Error initializing database:', error.message);
throw error;
}
}
@@ -86,11 +114,26 @@ const db = {
// Create or get player
async createPlayer(username) {
try {
const [result] = await pool.query(
'INSERT INTO players (username) VALUES (?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)',
[username]
);
return result.insertId;
// First try to get existing player
const { data: existingPlayer, error: selectError } = await supabase
.from('players')
.select('id')
.eq('username', username)
.single();
if (existingPlayer) {
return existingPlayer.id;
}
// If not found, create new player
const { data, error } = await supabase
.from('players')
.insert([{ username }])
.select('id')
.single();
if (error) throw error;
return data.id;
} catch (error) {
console.error('Error creating player:', error);
throw error;
@@ -99,128 +142,217 @@ const db = {
// Get player by username
async getPlayer(username) {
const [rows] = await pool.query(
'SELECT * FROM players WHERE username = ?',
[username]
);
return rows[0];
const { data, error } = await supabase
.from('players')
.select('*')
.eq('username', username)
.single();
if (error && error.code !== 'PGRST116') throw error; // PGRST116 = not found
return data;
},
// Get player by ID
async getPlayerById(playerId) {
const [rows] = await pool.query(
'SELECT * FROM players WHERE id = ?',
[playerId]
);
return rows[0];
const { data, error } = await supabase
.from('players')
.select('*')
.eq('id', playerId)
.single();
if (error && error.code !== 'PGRST116') throw error;
return data;
},
// Add active session
async addSession(sessionId, playerId, username) {
await pool.query(
'INSERT INTO active_sessions (session_id, player_id, username) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE last_heartbeat = CURRENT_TIMESTAMP',
[sessionId, playerId, username]
);
const { error } = await supabase
.from('active_sessions')
.upsert([{
session_id: sessionId,
player_id: playerId,
username: username,
last_heartbeat: new Date().toISOString()
}], {
onConflict: 'session_id'
});
if (error) throw error;
},
// Remove session
async removeSession(sessionId) {
await pool.query(
'DELETE FROM active_sessions WHERE session_id = ?',
[sessionId]
);
const { error } = await supabase
.from('active_sessions')
.delete()
.eq('session_id', sessionId);
if (error) throw error;
},
// Get all active players
async getActivePlayers() {
const [rows] = await pool.query(`
SELECT s.session_id, s.username, p.total_wins, p.total_losses, p.total_draws
FROM active_sessions s
JOIN players p ON s.player_id = p.id
WHERE s.last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE)
`);
return rows;
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
const { data, error } = await supabase
.from('active_sessions')
.select(`
session_id,
username,
players!inner(total_wins, total_losses, total_draws)
`)
.gt('last_heartbeat', twoMinutesAgo);
if (error) throw error;
// Flatten the response to match the old format
return data.map(row => ({
session_id: row.session_id,
username: row.username,
total_wins: row.players.total_wins,
total_losses: row.players.total_losses,
total_draws: row.players.total_draws
}));
},
// Create new game
async createGame(player1Id, player2Id, player1Username, player2Username, boardSize) {
const [result] = await pool.query(
'INSERT INTO games (player1_id, player2_id, player1_username, player2_username, board_size, game_state) VALUES (?, ?, ?, ?, ?, ?)',
[player1Id, player2Id, player1Username, player2Username, boardSize, 'active']
);
return result.insertId;
const { data, error } = await supabase
.from('games')
.insert([{
player1_id: player1Id,
player2_id: player2Id,
player1_username: player1Username,
player2_username: player2Username,
board_size: boardSize,
game_state: 'active'
}])
.select('id')
.single();
if (error) throw error;
return data.id;
},
// Record move
async recordMove(gameId, playerId, row, col, moveNumber) {
await pool.query(
'INSERT INTO game_moves (game_id, player_id, row_position, col_position, move_number) VALUES (?, ?, ?, ?, ?)',
[gameId, playerId, row, col, moveNumber]
);
const { error } = await supabase
.from('game_moves')
.insert([{
game_id: gameId,
player_id: playerId,
row_position: row,
col_position: col,
move_number: moveNumber
}]);
if (error) throw error;
},
// Complete game
async completeGame(gameId, winnerId) {
await pool.query(
'UPDATE games SET game_state = ?, winner_id = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?',
['completed', winnerId, gameId]
);
// Update game status
const { error: gameError } = await supabase
.from('games')
.update({
game_state: 'completed',
winner_id: winnerId,
completed_at: new Date().toISOString()
})
.eq('id', gameId);
if (gameError) throw gameError;
// Update player stats
if (winnerId) {
// Get game details
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]);
if (game.length > 0) {
const loserId = game[0].player1_id === winnerId ? game[0].player2_id : game[0].player1_id;
const { data: game, error: selectError } = await supabase
.from('games')
.select('player1_id, player2_id')
.eq('id', gameId)
.single();
if (selectError) throw selectError;
if (game) {
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
// Update winner
await pool.query('UPDATE players SET total_wins = total_wins + 1 WHERE id = ?', [winnerId]);
await supabase.rpc('increment_wins', { player_id: winnerId });
// Update loser
await pool.query('UPDATE players SET total_losses = total_losses + 1 WHERE id = ?', [loserId]);
await supabase.rpc('increment_losses', { player_id: loserId });
}
} else {
// Draw - update both players
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]);
if (game.length > 0) {
await pool.query('UPDATE players SET total_draws = total_draws + 1 WHERE id IN (?, ?)',
[game[0].player1_id, game[0].player2_id]);
const { data: game, error: selectError } = await supabase
.from('games')
.select('player1_id, player2_id')
.eq('id', gameId)
.single();
if (selectError) throw selectError;
if (game) {
await supabase.rpc('increment_draws', { player_id: game.player1_id });
await supabase.rpc('increment_draws', { player_id: game.player2_id });
}
}
},
// Abandon game
async abandonGame(gameId, winnerId) {
await pool.query(
'UPDATE games SET game_state = ?, winner_id = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?',
['abandoned', winnerId, gameId]
);
// Update game status
const { error: gameError } = await supabase
.from('games')
.update({
game_state: 'abandoned',
winner_id: winnerId,
completed_at: new Date().toISOString()
})
.eq('id', gameId);
if (gameError) throw gameError;
// Update stats (winner gets win, other player gets loss)
if (winnerId) {
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]);
if (game.length > 0) {
const loserId = game[0].player1_id === winnerId ? game[0].player2_id : game[0].player1_id;
await pool.query('UPDATE players SET total_wins = total_wins + 1 WHERE id = ?', [winnerId]);
await pool.query('UPDATE players SET total_losses = total_losses + 1 WHERE id = ?', [loserId]);
const { data: game, error: selectError } = await supabase
.from('games')
.select('player1_id, player2_id')
.eq('id', gameId)
.single();
if (selectError) throw selectError;
if (game) {
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
await supabase.rpc('increment_wins', { player_id: winnerId });
await supabase.rpc('increment_losses', { player_id: loserId });
}
}
},
// Update heartbeat
async updateHeartbeat(sessionId) {
await pool.query(
'UPDATE active_sessions SET last_heartbeat = CURRENT_TIMESTAMP WHERE session_id = ?',
[sessionId]
);
const { error } = await supabase
.from('active_sessions')
.update({ last_heartbeat: new Date().toISOString() })
.eq('session_id', sessionId);
if (error) throw error;
},
// Clean up stale sessions
async cleanupStaleSessions() {
await pool.query(
'DELETE FROM active_sessions WHERE last_heartbeat < DATE_SUB(NOW(), INTERVAL 2 MINUTE)'
);
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
const { error } = await supabase
.from('active_sessions')
.delete()
.lt('last_heartbeat', twoMinutesAgo);
if (error) throw error;
}
};
module.exports = { pool, initializeDatabase, db };
module.exports = { supabase, initializeDatabase, db };

View File

@@ -1,13 +1,18 @@
// Database Configuration File
// IMPORTANT: This file contains sensitive credentials and should NEVER be committed to git
// Copy this file to db.config.js and update with your actual database credentials
// Database Configuration File - EXAMPLE
// Copy this file to db.config.js and fill in your actual Supabase credentials
// DO NOT commit db.config.js to git - it's in .gitignore
// Supabase Configuration
// Get these values from your Supabase project dashboard:
// 1. Go to https://app.supabase.com
// 2. Select your project
// 3. Go to Project Settings → API
module.exports = {
host: 'your-database-host.com',
user: 'your-database-username',
password: 'your-secure-password',
database: 'your-database-name',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
supabaseUrl: 'https://xxxxxxxxxxxxx.supabase.co', // Your Supabase project URL
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', // Your Supabase anon/public key
supabasePassword: 'your_database_password_here', // Your database password
// Optional: Direct PostgreSQL connection string
// Found in Project Settings → Database → Connection String
postgresConnectionString: 'postgresql://postgres:your_password@db.xxxxxxxxxxxxx.supabase.co:5432/postgres'
};

132
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@supabase/supabase-js": "^2.39.0",
"bad-words": "^3.0.4",
"cors": "^2.8.5",
"express": "^4.18.2",
@@ -25,6 +26,107 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz",
"integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz",
"integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz",
"integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz",
"integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@supabase/storage-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz",
"integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz",
"integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.89.0",
"@supabase/functions-js": "2.89.0",
"@supabase/postgrest-js": "2.89.0",
"@supabase/realtime-js": "2.89.0",
"@supabase/storage-js": "2.89.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -43,6 +145,21 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -706,6 +823,15 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1597,6 +1723,12 @@
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@@ -20,6 +20,7 @@
"express": "^4.18.2",
"socket.io": "^4.6.1",
"mysql2": "^3.6.5",
"@supabase/supabase-js": "^2.39.0",
"bad-words": "^3.0.4",
"cors": "^2.8.5"
},

View File

@@ -3,7 +3,7 @@ const http = require('http');
const socketIO = require('socket.io');
const cors = require('cors');
const path = require('path');
const { initializeDatabase, db } = require('./database');
const { initializeDatabase, db, supabase } = require('./database');
const GameManager = require('./gameManager');
const app = express();
@@ -39,96 +39,74 @@ app.get('/api/db-status', async (req, res) => {
timestamp: new Date().toISOString(),
error: null,
// Additional diagnostic info for testing phase
host: dbConfig.host || 'unknown',
database: dbConfig.database || 'unknown',
user: dbConfig.user || 'unknown',
connectionLimit: dbConfig.connectionLimit || 'unknown',
poolStats: null
supabaseUrl: dbConfig.supabaseUrl || 'unknown',
database: 'Supabase PostgreSQL',
connectionType: 'Supabase Client'
};
try {
console.log(`[DB-STATUS] Testing connection to ${status.host}/${status.database}...`);
console.log(`[DB-STATUS] Testing Supabase connection to ${status.supabaseUrl}...`);
// Test connection with a simple query
const [result] = await db.pool.query('SELECT 1 as test');
const { data, error } = await supabase
.from('players')
.select('id')
.limit(1);
const latency = Date.now() - startTime;
if (result && result[0].test === 1) {
if (!error || error.code === 'PGRST116') { // PGRST116 = no rows found (table exists but empty)
status.connected = true;
status.latency = latency;
console.log(`[DB-STATUS] ✅ Connection successful (${latency}ms)`);
// Get pool statistics
try {
status.poolStats = {
totalConnections: db.pool.pool._allConnections.length,
freeConnections: db.pool.pool._freeConnections.length,
queuedRequests: db.pool.pool._connectionQueue.length
};
console.log(`[DB-STATUS] Pool stats:`, status.poolStats);
} catch (poolError) {
console.log(`[DB-STATUS] Could not retrieve pool stats:`, poolError.message);
}
// Test write capability
try {
console.log(`[DB-STATUS] Testing write capability...`);
const testTableName = '_health_check_test';
// Create test table if it doesn't exist
await db.pool.query(`
CREATE TABLE IF NOT EXISTS ${testTableName} (
id INT AUTO_INCREMENT PRIMARY KEY,
test_value VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Try to insert a test record
const testValue = `test_${Date.now()}`;
await db.pool.query(
`INSERT INTO ${testTableName} (test_value) VALUES (?)`,
[testValue]
);
const testUsername = `_test_${Date.now()}`;
const { data: insertData, error: insertError } = await supabase
.from('players')
.insert([{ username: testUsername }])
.select('id')
.single();
// Clean up old test records (keep only last 10)
await db.pool.query(`
DELETE FROM ${testTableName}
WHERE id NOT IN (
SELECT id FROM (
SELECT id FROM ${testTableName}
ORDER BY created_at DESC
LIMIT 10
) AS keep_records
)
`);
if (!insertError && insertData) {
// Clean up test record
await supabase
.from('players')
.delete()
.eq('id', insertData.id);
status.writeCapable = true;
console.log(`[DB-STATUS] ✅ Write test successful`);
} else {
throw insertError;
}
} catch (writeError) {
console.error(`[DB-STATUS] ❌ Write test failed:`, writeError.message);
status.writeCapable = false;
status.error = `Write test failed: ${writeError.message}`;
}
} else {
throw error;
}
} catch (error) {
console.error(`[DB-STATUS] ❌ Connection failed:`, error.message);
console.error(`[DB-STATUS] Error code:`, error.code);
console.error(`[DB-STATUS] Error errno:`, error.errno);
console.error(`[DB-STATUS] Error details:`, error);
status.connected = false;
status.latency = Date.now() - startTime;
// Provide more detailed error messages
let errorMessage = error.message;
if (error.code === 'ECONNREFUSED') {
errorMessage = `Connection refused to ${status.host}. Is MySQL running?`;
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
errorMessage = `Access denied for user '${status.user}'. Check credentials.`;
} else if (error.code === 'ER_BAD_DB_ERROR') {
errorMessage = `Database '${status.database}' does not exist.`;
} else if (error.code === 'ENOTFOUND') {
errorMessage = `Host '${status.host}' not found. Check hostname.`;
if (error.code === '42P01') {
errorMessage = `Table 'players' does not exist. Please run the SQL schema in Supabase SQL Editor.`;
} else if (error.message && error.message.includes('Invalid API key')) {
errorMessage = `Invalid Supabase API key. Check your db.config.js file.`;
} else if (error.message && error.message.includes('fetch')) {
errorMessage = `Cannot reach Supabase. Check your supabaseUrl in db.config.js.`;
}
status.error = errorMessage;

50
supabase-functions.sql Normal file
View File

@@ -0,0 +1,50 @@
-- Supabase SQL Helper Functions for Connect-5
-- Run this in your Supabase SQL Editor after creating the tables
-- Function to increment wins
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment losses
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment draws
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Optional: Function to get player stats
CREATE OR REPLACE FUNCTION get_player_stats(player_username VARCHAR)
RETURNS TABLE (
id BIGINT,
username VARCHAR,
total_wins INT,
total_losses INT,
total_draws INT,
created_at TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.username, p.total_wins, p.total_losses, p.total_draws, p.created_at
FROM players p
WHERE p.username = player_username;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,114 @@
-- Complete Supabase Schema for Connect-5
-- Copy and paste this entire file into Supabase SQL Editor and run it
-- Create players table
CREATE TABLE IF NOT EXISTS players (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_username ON players(username);
-- Create active sessions table
CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
);
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
);
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
-- Enable Row Level Security (RLS)
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
DROP POLICY IF EXISTS "Allow all operations on players" ON players;
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on active_sessions" ON active_sessions;
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on games" ON games;
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on game_moves" ON game_moves;
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
-- Helper Functions
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Success message
DO $$
BEGIN
RAISE NOTICE '✅ Connect-5 database schema created successfully!';
END $$;