mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 22:46:00 +00:00
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:
@@ -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
223
SUPABASE_SETUP.md
Normal 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)
|
||||
360
database.js
360
database.js
@@ -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(`
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
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 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 TABLE IF NOT EXISTS active_sessions (
|
||||
-- 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 TABLE IF NOT EXISTS games (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
player1_id INT NOT NULL,
|
||||
player2_id INT NOT NULL,
|
||||
-- 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 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 TABLE IF NOT EXISTS game_moves (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
game_id INT NOT NULL,
|
||||
player_id INT NOT NULL,
|
||||
-- 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 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 };
|
||||
|
||||
@@ -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
132
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
92
server.js
92
server.js
@@ -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
50
supabase-functions.sql
Normal 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;
|
||||
114
supabase-schema-complete.sql
Normal file
114
supabase-schema-complete.sql
Normal 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 $$;
|
||||
Reference in New Issue
Block a user