Commit 4d7736a1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: complete rebuild — all 16 phases, modular game engine

- Phase 0: Core engine (scene manager, store, bus, net, tween, audio, canvas, HUD, i18n)
- Phase 1: Auth (splash, login, register, token management)
- Phase 2: Play World (game carousel, mode picker, bot/time selectors, queue)
- Phase 3-4: Chess (canvas board, drag/drop, chess.js, Stockfish bots, clock, live matchmaking)
- Phase 5: Domino (tile chain logic, bot AI, pip scoring, multiplayer)
- Phase 6: Ludo (cross-board, dice, captures, 4-player bots)
- Phase 7: Social (friends, notifications, realtime)
- Phase 8: Organizations (browse, join, members, detail)
- Phase 9: Rewards (daily claims, streak, coins)
- Phase 10: Profile (player card, stats, settings, logout)
- Phase 11: Shop (cosmetics, purchase flow, equip)
- Phase 12: Rank (leaderboard, tournaments, registration)
- Phase 13: Puzzles (chess training with rating)
- Phase 14-16: All APIs, polish, deployment config
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1b09a424
{
"permissions": {
"allow": [
"Bash(*)",
"Read(*)",
"Write(*)",
"Edit(*)",
"Agent(*)",
"Workflow(*)",
"WebFetch(*)",
"TodoWrite(*)",
"NotebookEdit(*)"
],
"defaultMode": "bypassPermissions"
}
}
RewriteEngine On
# Pass Authorization header to PHP (Apache strips it by default)
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Serve existing files/dirs directly
RewriteCond %{REQUEST_URI} ^/public/ [OR]
RewriteCond %{REQUEST_URI} ^/api/
RewriteRule ^ - [L]
# Force HTTPS (CapRover handles SSL termination via X-Forwarded-Proto)
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Route all non-file requests to index.php
# Everything else -> index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?route=$1 [QSA,L]
RewriteRule ^ index.php [L]
# Security headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, POST, PATCH, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization, apikey"
</IfModule>
# Deny access to sensitive files
<FilesMatch "\.(pem|md|gitignore)$">
Require all denied
</FilesMatch>
<Files "config/*">
Require all denied
</Files>
<Files "storage/*">
Require all denied
</Files>
# Handle OPTIONS preflight
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
This diff is collapsed.
This diff is collapsed.
# EL3AB Database Quick Reference
**Full audit:** [docs/supabase_master_audit.md](docs/supabase_master_audit.md) (992 lines, every table/column/FK/policy)
---
## Connection
| Key | Value |
|-----|-------|
| API Gateway | https://safe-supabase-kong.caprover.al-arcade.com |
| REST endpoint | /rest/v1/{table} |
| Auth endpoint | /auth/v1/ |
| Storage endpoint | /storage/v1/ |
| Realtime WS | wss://safe-supabase-kong.caprover.al-arcade.com/realtime/v1/websocket |
| Anon Key | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84 |
| Service Key | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4 |
---
## Tables Used by Player App (23 core)
### Player Identity
| Table | Key Columns | PK |
|-------|-------------|-----|
| profiles | username, display_name, elo_*, coins, gems, xp, level, is_online, is_banned | id (uuid, = auth.users.id) |
| player_game_ratings | player_id, game_key, rating, games_played, wins, losses, draws | id (uuid) |
| user_profiles | full_name, fide_id, fide_rating_*, nationality | id (uuid) |
### Games
| Table | Key Columns | PK |
|-------|-------------|-----|
| matches | game_key, white/black_player_id, status, result, time_control, moves (jsonb), current_fen | id (uuid) |
| domino_matches | room_code, status, players (jsonb), board (jsonb), hands (jsonb), scores (jsonb) | id (uuid) |
| ludo_matches | room_code, status, players (jsonb), positions (jsonb), dice_value, winners (jsonb) | id (uuid) |
| matchmaking_queue | player_id, game_key, time_control, rating, status, matched_with, match_id | id (uuid) |
| domino_queue | user_id, mode, match_id | id (uuid) |
| ludo_queue | user_id, match_id | id (uuid) |
| game_plugins | game_key (PK), name, name_ar, is_enabled, supports_* | game_key (text) |
| puzzles | fen, moves, rating, themes | id (serial) |
### Competitive
| Table | Key Columns | PK |
|-------|-------------|-----|
| leaderboards | player_id, game_key, time_control_type, period, rank, rating | id (uuid) |
| rating_history | player_id, game_key, rating_before/after, match_id, opponent_id | id (uuid) |
| el3ab_tournaments | org_id, game_key, format, time_control, status, max_players, starts_at | id (uuid) |
| tournament_registrations | tournament_id, player_id, status | id (uuid) |
### Social
| Table | Key Columns | PK |
|-------|-------------|-----|
| friendships | requester_id, addressee_id, status (pending/accepted) | id (uuid) |
| notifications | user_id, type, title, title_ar, body, is_read | id (uuid) |
| chat_messages | channel_type, channel_id, sender_id, content | id (uuid) |
| activity_feed | actor_id, action, target_type, target_id | id (uuid) |
### Economy
| Table | Key Columns | PK |
|-------|-------------|-----|
| economy_transactions | player_id, type, currency (coins/gems), amount, balance_after | id (uuid) |
| cosmetics | name, type (enum), rarity (enum), price_coins, price_gems | id (text) |
| player_cosmetics | player_id, cosmetic_id, is_equipped | id (uuid) |
| achievements | name, category, condition (jsonb), xp_reward, coins_reward | id (text) |
| player_achievements | player_id, achievement_id, progress, is_completed | id (uuid) |
| xp_levels | level (PK), xp_required, reward_coins, reward_gems | level (int) |
---
## Key Enums
```
match_status: waiting, ready, in_progress, paused, completed, aborted, abandoned
match_result: white_wins, black_wins, draw, white_timeout, black_timeout, white_resign, black_resign, white_abandon, black_abandon, stalemate, insufficient_material, threefold_repetition, fifty_moves, mutual_draw, aborted
time_control: bullet_1_0, bullet_1_1, bullet_2_1, blitz_3_0, blitz_3_2, blitz_5_0, blitz_5_3, rapid_10_0, rapid_10_5, rapid_15_10, rapid_30_0, classical_60_0, classical_90_30, custom
cosmetic_type: avatar_frame, board_theme, piece_set, profile_banner, chat_emoji, victory_animation, title_badge, trail_effect, sound_pack
cosmetic_rarity: common, uncommon, rare, epic, legendary
```
---
## RLS Rules (Player App perspective)
### Can read without auth (anon key, no JWT)
profiles, matches, leaderboards, achievements, cosmetics, game_plugins, puzzles, feature_flags, el3ab_tournaments, xp_levels, clubs, platform_assets, platform_theme, rating_history
### Must be authenticated (anon key + JWT)
- **Own data:** notifications, economy_transactions, player_cosmetics, player_achievements, matchmaking_queue, friendships
- **Can INSERT:** matches (own player), friendships (own requester), matchmaking_queue (own), chat_messages (own sender)
- **Can UPDATE:** profiles (own), notifications (own, mark read), matches (own player), matchmaking_queue (own)
- **Can DELETE:** matchmaking_queue (own)
### Requires service key (PHP backend)
All org_* tables (28), bracket_matches, tournament_brackets, tournament_phases, player_frames, profile_frames
---
## Key Foreign Keys (Player App)
```
profiles.id ← auth.users.id (trigger creates on signup)
matches.white_player_id → profiles.id
matches.black_player_id → profiles.id
matches.rematch_of → matches.id
matchmaking_queue.player_id → profiles.id
matchmaking_queue.match_id → matches.id
friendships.requester_id → profiles.id
friendships.addressee_id → profiles.id
notifications.user_id → profiles.id
economy_transactions.player_id → profiles.id
player_cosmetics.player_id → profiles.id
player_cosmetics.cosmetic_id → cosmetics.id
player_achievements.player_id → profiles.id
player_achievements.achievement_id → achievements.id
leaderboards.player_id → profiles.id
rating_history.player_id → profiles.id
rating_history.match_id → matches.id
el3ab_tournaments.org_id → el3ab_organizations.id
tournament_registrations.tournament_id → el3ab_tournaments.id
```
---
## RPC Functions Available
| Function | Args | Returns | Purpose |
|----------|------|---------|---------|
| register_tournament_player | tournament_id, player_auth_id | json | Register for tournament (validates capacity/status) |
| cancel_tournament_registration | tournament_id | json | Cancel registration |
| get_all_tournaments | — | json | Get tournaments + players + brackets |
| get_tournament_by_id | id | json | Single tournament detail |
| get_tournament_room | tournament_id, bracket_id | json | Get lobby ID for match |
| check_opponent_timeout | bracket_id, user_numeric_id | json | Check if opponent timed out |
| check_app_version | client_version | json | Mobile app version check |
| get_friends_by_auth_ids | auth_ids[] | json | Get friend profiles (Unity) |
---
## Storage Buckets
| Bucket | Public | Use in Player App |
|--------|--------|-------------------|
| profile-images | YES | Avatar upload/display |
| avatars | YES | Legacy avatars |
| profile-frames | YES | Frame overlays |
| org-logos | YES | Org logos in listings |
| org-banners | YES | Org banners |
---
## Realtime Subscription Patterns
```javascript
// Match state (chess/domino/ludo)
supabase.channel('game')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'matches',
filter: `id=eq.${matchId}`
}, handleGameUpdate)
// Matchmaking
supabase.channel('queue')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'matchmaking_queue',
filter: `player_id=eq.${userId}`
}, handleMatchFound)
// Notifications
supabase.channel('notifs')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${userId}`
}, handleNewNotification)
```
FROM php:8.3-apache
RUN apt-get update && apt-get install -y libpq-dev \
&& docker-php-ext-install pdo pdo_pgsql pgsql \
RUN apt-get update && apt-get install -y libpq-dev libcurl4-openssl-dev \
&& docker-php-ext-install pdo pdo_pgsql pgsql curl \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN a2enmod rewrite headers
......@@ -10,6 +10,9 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo '<Directory /var/www/html>\n AllowOverride All\n Require all granted\n</Directory>' > /etc/apache2/conf-available/allowoverride.conf \
&& a2enconf allowoverride
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html
......
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token();
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$category = $_GET['category'] ?? null;
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$endpoint = 'achievements?select=*&order=category.asc,tier.asc';
if ($category) {
$endpoint .= '&category=eq.' . $category;
}
$token = requireAuth();
$userId = getUserId($token);
$db = supabase($token);
$achRes = supabase_rest('GET', $endpoint, [], $token);
$achievements = ($achRes['status'] === 200 && is_array($achRes['data']) && !isset($achRes['data']['code'])) ? $achRes['data'] : [];
$achievements = $db->get('achievements', ['select' => 'id,name,name_ar,description,description_ar,category,xp_reward,coins_reward,icon']);
$playerAchievements = $db->get('player_achievements', ['player_id' => 'eq.' . $userId, 'select' => 'achievement_id,progress,is_completed,completed_at']);
if (!empty($achievements)) {
$unlockedRes = supabase_rest('GET', 'player_achievements?select=achievement_id,unlocked_at', [], $token);
$unlocked = [];
if ($unlockedRes['status'] === 200 && is_array($unlockedRes['data'])) {
foreach ($unlockedRes['data'] as $row) {
$unlocked[$row['achievement_id']] = $row['unlocked_at'];
}
$playerMap = [];
if (is_array($playerAchievements) && !isset($playerAchievements['error'])) {
foreach ($playerAchievements as $pa) {
$playerMap[$pa['achievement_id']] = $pa;
}
}
foreach ($achievements as &$ach) {
$ach['unlocked'] = isset($unlocked[$ach['id']]);
$ach['unlocked_at'] = $unlocked[$ach['id']] ?? null;
$ach['progress'] = 0;
$result = [];
if (is_array($achievements) && !isset($achievements['error'])) {
foreach ($achievements as $a) {
$a['player_progress'] = $playerMap[$a['id']]['progress'] ?? 0;
$a['is_completed'] = $playerMap[$a['id']]['is_completed'] ?? false;
$result[] = $a;
}
}
echo json_encode(['achievements' => $achievements]);
jsonResponse(['achievements' => $result]);
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = get_auth_token();
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$gameId = $_GET['id'] ?? '';
if (!$gameId) {
http_response_code(400);
echo json_encode(['error' => 'missing game id']);
exit;
}
$res = supabase_rest('GET', "matches?id=eq.{$gameId}&select=*", [], $token);
if ($res['status'] >= 200 && $res['status'] < 300 && !empty($res['data'])) {
$game = $res['data'][0];
// If analysis already cached in game_state, return it
$gameState = $game['game_state'] ?? null;
if (is_string($gameState)) {
$gameState = json_decode($gameState, true);
}
$hasAnalysis = isset($gameState['analysis']) && !empty($gameState['analysis']);
echo json_encode([
'game' => $game,
'has_analysis' => $hasAnalysis,
'analysis' => $hasAnalysis ? $gameState['analysis'] : null
]);
} else {
http_response_code(404);
echo json_encode(['error' => 'game not found']);
}
exit;
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../config/constants.php';
$token = requireAuth();
$input = getInput();
$fen = $input['fen'] ?? '';
$depth = intval($input['depth'] ?? 18);
$lines = intval($input['lines'] ?? 3);
if (!$fen) jsonError('fen is required');
$payload = json_encode(['fen' => $fen, 'depth' => min($depth, 25), 'lines' => min($lines, 5)]);
$ch = curl_init(STOCKFISH_API . '/api/chess/analyze');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 35);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
$err = json_decode($response, true);
jsonError($err['error'] ?? 'Analysis failed', $httpCode);
}
// POST requests
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'analyze':
$gameId = $input['game_id'] ?? '';
$positions = $input['positions'] ?? []; // Array of FEN strings
if (!$gameId || empty($positions)) {
http_response_code(400);
echo json_encode(['error' => 'missing game_id or positions']);
exit;
}
$evaluations = [];
$apiUrl = STOCKFISH_API . '/api/chess/move';
foreach ($positions as $index => $fen) {
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
];
} else {
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => null,
'evaluation' => 0,
'depth' => 0,
'error' => true
];
}
// Small delay to avoid overwhelming the API
if ($index < count($positions) - 1) {
usleep(100000); // 100ms
}
}
// Cache analysis in game_state
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
// Get existing game_state
$gameRes = supabase_rest('GET', "matches?id=eq.{$gameId}&select=game_state", [], $token);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "matches?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], $token);
echo json_encode([
'ok' => true,
'analysis' => $analysisData
]);
break;
case 'analyze_single':
// Analyze a single position (used for real-time partial analysis)
$fen = $input['fen'] ?? '';
if (!$fen) {
http_response_code(400);
echo json_encode(['error' => 'missing fen']);
exit;
}
$apiUrl = STOCKFISH_API . '/api/chess/move';
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
echo json_encode([
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
]);
} else {
http_response_code(502);
echo json_encode(['error' => 'engine unavailable']);
}
break;
case 'cache':
$gameId = $input['game_id'] ?? '';
$evaluations = $input['evaluations'] ?? [];
if (!$gameId || empty($evaluations)) {
http_response_code(400);
echo json_encode(['error' => 'missing data']);
exit;
}
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
$gameRes = supabase_rest('GET', "matches?id=eq.{$gameId}&select=game_state", [], SUPABASE_SERVICE_KEY);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "matches?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
jsonResponse(json_decode($response, true));
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$input = json_decode(file_get_contents('php://input'), true);
$input = getInput();
$action = $input['action'] ?? '';
if ($action === 'login') {
$email = $input['email'] ?? '';
switch ($action) {
case 'signup':
handleSignup($input);
break;
case 'login':
handleLogin($input);
break;
case 'refresh':
handleRefresh($input);
break;
case 'logout':
handleLogout();
break;
default:
jsonError('Invalid action', 400);
}
function handleSignup(array $input): void {
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
$username = trim($input['username'] ?? '');
if (!$email || !$password) {
http_response_code(400);
echo json_encode(['error' => 'البريد وكلمة المرور مطلوبين']);
exit;
if (!$email || !$password || !$username) {
jsonError('Email, password and username are required');
}
if (strlen($password) < 6) {
jsonError('Password must be at least 6 characters');
}
if (strlen($username) < 3 || strlen($username) > 20) {
jsonError('Username must be 3-20 characters');
}
$result = supabase_auth('token?grant_type=password', [
$result = supabaseAuth('POST', 'signup', [
'email' => $email,
'password' => $password,
'data' => ['username' => $username]
]);
if ($result['status'] !== 200) {
$msg = $result['data']['error_description'] ?? $result['data']['msg'] ?? 'بيانات الدخول غير صحيحة';
http_response_code(401);
echo json_encode(['error' => $msg]);
exit;
if (isset($result['error'])) {
jsonError($result['error'], $result['code'] ?? 400);
}
echo json_encode([
'access_token' => $result['data']['access_token'],
'refresh_token' => $result['data']['refresh_token'],
'user' => $result['data']['user'],
if (isset($result['access_token'])) {
$db = supabase($result['access_token']);
$db->update('profiles', [
'username' => $username,
'display_name' => $username
], ['id' => 'eq.' . $result['user']['id']]);
jsonResponse([
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'user' => $result['user']
]);
}
} elseif ($action === 'register') {
$email = $input['email'] ?? '';
$password = $input['password'] ?? '';
$username = $input['username'] ?? '';
jsonResponse($result);
}
if (!$email || !$password || !$username) {
http_response_code(400);
echo json_encode(['error' => 'جميع الحقول مطلوبة']);
exit;
}
function handleLogin(array $input): void {
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
if (strlen($password) < 6) {
http_response_code(400);
echo json_encode(['error' => 'كلمة المرور يجب ان تكون 6 احرف على الاقل']);
exit;
if (!$email || !$password) {
jsonError('Email and password are required');
}
$result = supabase_auth('signup', [
$result = supabaseAuth('POST', 'token?grant_type=password', [
'email' => $email,
'password' => $password,
'data' => ['username' => $username, 'display_name' => $username],
'password' => $password
]);
if ($result['status'] !== 200 && $result['status'] !== 201) {
$msg = $result['data']['error_description'] ?? $result['data']['msg'] ?? 'فشل التسجيل';
http_response_code(400);
echo json_encode(['error' => $msg]);
exit;
if (isset($result['error'])) {
jsonError($result['error'], $result['code'] ?? 401);
}
if (isset($result['data']['access_token'])) {
echo json_encode([
'access_token' => $result['data']['access_token'],
'refresh_token' => $result['data']['refresh_token'],
'user' => $result['data']['user'],
jsonResponse([
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'user' => $result['user']
]);
} else {
echo json_encode(['message' => 'تم التسجيل بنجاح، يرجى تاكيد البريد']);
}
}
} elseif ($action === 'refresh') {
function handleRefresh(array $input): void {
$refreshToken = $input['refresh_token'] ?? '';
if (!$refreshToken) {
http_response_code(400);
echo json_encode(['error' => 'refresh_token مطلوب']);
exit;
jsonError('Refresh token required');
}
$result = supabase_auth('token?grant_type=refresh_token', [
'refresh_token' => $refreshToken,
$result = supabaseAuth('POST', 'token?grant_type=refresh_token', [
'refresh_token' => $refreshToken
]);
if ($result['status'] !== 200) {
http_response_code(401);
echo json_encode(['error' => 'الجلسة منتهية']);
exit;
if (isset($result['error'])) {
jsonError($result['error'], $result['code'] ?? 401);
}
echo json_encode([
'access_token' => $result['data']['access_token'],
'refresh_token' => $result['data']['refresh_token'],
jsonResponse([
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'user' => $result['user']
]);
}
} else {
http_response_code(400);
echo json_encode(['error' => 'action غير معروف']);
function handleLogout(): void {
$token = getAuthToken();
if ($token) {
supabaseAuth('POST', 'logout', null, $token);
}
jsonResponse(['success' => true]);
}
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if (!check_feature_flag('bot_games_enabled')) {
http_response_code(403);
echo json_encode(['error' => 'feature_disabled', 'message' => 'اللعب ضد البوت معطل حالياً']);
exit;
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../config/constants.php';
$input = getInput();
$action = $input['action'] ?? '';
switch ($action) {
case 'list':
handleList();
break;
case 'move':
handleMove($input);
break;
default:
jsonError('Invalid action');
}
$cacheFile = sys_get_temp_dir() . '/el3ab_bots_cache.json';
$cacheTTL = 300; // 5 minutes
function handleList(): void {
$ch = curl_init(STOCKFISH_API . '/api/chess/bots');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
echo file_get_contents($cacheFile);
exit;
if ($httpCode !== 200) {
jsonError('Failed to fetch bots', 502);
}
$data = json_decode($response, true);
jsonResponse($data);
}
$ch = curl_init(STOCKFISH_API . '/api/chess/bots');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && $response) {
file_put_contents($cacheFile, $response);
echo $response;
} else {
if (file_exists($cacheFile)) {
echo file_get_contents($cacheFile);
} else {
http_response_code(502);
echo json_encode(['error' => 'bot_service_unavailable']);
function handleMove(array $input): void {
$fen = $input['fen'] ?? '';
$botId = $input['bot_id'] ?? '';
if (!$fen || !$botId) {
jsonError('fen and bot_id are required');
}
$payload = json_encode(['fen' => $fen, 'bot_id' => $botId]);
$ch = curl_init(STOCKFISH_API . '/api/chess/move');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
$err = json_decode($response, true);
jsonError($err['error'] ?? 'Stockfish error', $httpCode);
}
jsonResponse(json_decode($response, true));
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$method = $_SERVER['REQUEST_METHOD'];
require_once __DIR__ . '/../includes/supabase.php';
if ($method === 'GET') {
$category = $_GET['category'] ?? '';
$endpoint = 'system_config?select=key,value,category&is_secret=eq.false';
if ($category) {
$endpoint .= '&category=eq.' . urlencode($category);
}
$db = supabaseService();
$flags = $db->get('feature_flags', ['select' => 'key,value,is_enabled']);
$plugins = $db->get('game_plugins', ['select' => '*', 'is_enabled' => 'eq.true']);
$response = [
'features' => [],
'games' => [],
'version' => '1.0.0'
];
$res = supabase_rest('GET', $endpoint);
$config = [];
if (!empty($res['data'])) {
foreach ($res['data'] as $row) {
$config[$row['key']] = json_decode($row['value'], true) ?? $row['value'];
if (is_array($flags) && !isset($flags['error'])) {
foreach ($flags as $f) {
if ($f['is_enabled'] ?? false) {
$response['features'][$f['key']] = $f['value'] ?? true;
}
}
echo json_encode(['config' => $config]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
if (is_array($plugins) && !isset($plugins['error'])) {
$response['games'] = $plugins;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token();
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$method = $_SERVER['REQUEST_METHOD'];
$token = requireAuth();
$userId = getUserId($token);
$input = getInput();
$action = $input['action'] ?? 'claim';
if ($method === 'POST') {
$profileRes = supabase_rest('GET', 'profiles?select=id,coins,xp,level,daily_streak,last_daily_reward', [], $token);
if (empty($profileRes['data'])) {
http_response_code(400);
echo json_encode(['error' => 'profile not found']);
exit;
}
$db = supabase($token);
if ($action === 'claim') {
$profile = $db->getOne('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins,last_daily_claim,daily_streak']);
if (!$profile || isset($profile['error'])) jsonError('Profile not found');
$profile = $profileRes['data'][0];
$lastClaim = $profile['last_daily_reward'] ?? null;
$lastClaim = $profile['last_daily_claim'] ?? null;
$today = date('Y-m-d');
if ($lastClaim === $today) {
echo json_encode(['error' => 'already_claimed', 'message' => 'لقد جمعت المكافأة اليوم']);
exit;
if ($lastClaim && substr($lastClaim, 0, 10) === $today) {
jsonError('Already claimed today');
}
$streak = ($profile['daily_streak'] ?? 0) + 1;
$yesterday = date('Y-m-d', strtotime('-1 day'));
$streak = ($lastClaim === $yesterday) ? ($profile['daily_streak'] ?? 0) + 1 : 1;
// Load reward config from system_config
$cfgRes = supabase_rest('GET', 'system_config?select=key,value&key=in.(daily_reward_base,daily_reward_streak_bonus)', [], SUPABASE_SERVICE_KEY);
$rewardBase = 50;
$rewardBonus = 10;
if (!empty($cfgRes['data'])) {
foreach ($cfgRes['data'] as $cfg) {
if ($cfg['key'] === 'daily_reward_base') $rewardBase = (int)$cfg['value'];
if ($cfg['key'] === 'daily_reward_streak_bonus') $rewardBonus = (int)$cfg['value'];
if ($lastClaim && substr($lastClaim, 0, 10) !== $yesterday) {
$streak = 1;
}
}
$reward = $rewardBase + ($streak - 1) * $rewardBonus;
$newCoins = ($profile['coins'] ?? 0) + $reward;
// XP award for daily claim
$dailyXp = 25;
$newXp = (int)($profile['xp'] ?? 0) + $dailyXp;
$currentLevel = (int)($profile['level'] ?? 1);
$baseReward = 50;
$streakBonus = min($streak * 10, 100);
$totalCoins = $baseReward + $streakBonus;
$profileUpdate = [
'coins' => $newCoins,
'xp' => $newXp,
'daily_streak' => $streak,
'last_daily_reward' => $today,
];
// Check for level up
$levelRes = supabase_rest('GET', "xp_levels?level=eq." . ($currentLevel + 1) . "&select=level,xp_required,reward_coins", [], SUPABASE_SERVICE_KEY);
$levelUpCoinsBonus = 0;
if (!empty($levelRes['data']) && $newXp >= (int)$levelRes['data'][0]['xp_required']) {
$newLevel = (int)$levelRes['data'][0]['level'];
$levelUpCoinsBonus = (int)($levelRes['data'][0]['reward_coins'] ?? 0);
$profileUpdate['level'] = $newLevel;
$newCoins += $levelUpCoinsBonus;
$profileUpdate['coins'] = $newCoins;
}
$newCoins = ($profile['coins'] ?? 0) + $totalCoins;
supabase_rest('PATCH', "profiles?id=eq.{$profile['id']}", $profileUpdate, SUPABASE_SERVICE_KEY);
$db->update('profiles', [
'coins' => $newCoins,
'last_daily_claim' => date('c'),
'daily_streak' => $streak
], ['id' => 'eq.' . $userId]);
// Log economy_transaction for daily reward coins
supabase_rest('POST', 'economy_transactions', [
'player_id' => $profile['id'],
$sdb = supabaseService();
$sdb->insert('economy_transactions', [
'player_id' => $userId,
'type' => 'daily_reward',
'currency' => 'coins',
'amount' => $reward,
'balance_after' => $newCoins - $levelUpCoinsBonus,
'reason' => "Daily reward day {$streak}",
'source_id' => null,
], SUPABASE_SERVICE_KEY);
// Log level up reward separately if applicable
if ($levelUpCoinsBonus > 0) {
supabase_rest('POST', 'economy_transactions', [
'player_id' => $profile['id'],
'type' => 'level_up_reward',
'currency' => 'coins',
'amount' => $levelUpCoinsBonus,
'amount' => $totalCoins,
'balance_after' => $newCoins,
'reason' => 'Level up reward',
'source_id' => null,
], SUPABASE_SERVICE_KEY);
}
'description' => "Daily reward (day {$streak})"
]);
echo json_encode([
'ok' => true,
'reward' => $reward,
jsonResponse([
'coins' => $totalCoins,
'streak' => $streak,
'coins' => $newCoins,
'xp_awarded' => $dailyXp,
'new_xp' => $newXp,
'total_coins' => $newCoins
]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
jsonError('Invalid action');
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
$token = get_auth_token();
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$token = requireAuth();
$userId = getUserId($token);
$db = supabase($token);
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
if ($method === 'GET') {
$action = $_GET['action'] ?? 'list';
switch ($action) {
case 'list':
$res = supabase_rest('GET', 'friends?select=*,friend:profiles!friend_id(id,username,display_name,elo_blitz,is_online)&status=eq.accepted', [], $token);
$friends = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['friend'])) {
$friends[] = $row['friend'];
}
}
}
echo json_encode(['friends' => $friends]);
break;
case 'requests':
$res = supabase_rest('GET', 'friends?select=*,sender:profiles!user_id(id,username,display_name)&status=eq.pending', [], $token);
$requests = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['sender'])) {
$requests[] = array_merge($row['sender'], ['id' => $row['id']]);
if ($action === 'list') {
$friends = $db->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id'
]);
if (!is_array($friends) || isset($friends['error'])) {
jsonResponse(['friends' => []]);
}
$friendIds = [];
foreach ($friends as $f) {
$friendIds[] = $f['requester_id'] === $userId ? $f['addressee_id'] : $f['requester_id'];
}
if (empty($friendIds)) {
jsonResponse(['friends' => []]);
}
echo json_encode(['requests' => $requests]);
break;
case 'search':
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode(['players' => []]);
break;
$idList = implode(',', $friendIds);
$profiles = $db->get('profiles', [
'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,is_online,level'
]);
jsonResponse(['friends' => $profiles ?: []]);
}
$res = supabase_rest('GET', 'profiles?select=id,username,display_name,elo_blitz&or=(username.ilike.*' . urlencode($q) . '*,display_name.ilike.*' . urlencode($q) . '*)&limit=10', [], $token);
echo json_encode(['players' => $res['data'] ?? []]);
break;
if ($action === 'pending') {
$pending = $db->get('friendships', [
'addressee_id' => 'eq.' . $userId,
'status' => 'eq.pending',
'select' => 'id,requester_id'
]);
jsonResponse(['pending' => $pending ?: []]);
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
}
if ($method === 'POST') {
$input = getInput();
$action = $input['action'] ?? '';
// Get current user info for notifications
$currentUser = supabase_auth('user', [], $token, 'GET');
$currentUserId = $currentUser['data']['id'] ?? null;
if ($action === 'request') {
$targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required');
switch ($action) {
case 'add':
$userId = $input['user_id'] ?? '';
$res = supabase_rest('POST', 'friends', [
'friend_id' => $userId,
$result = $db->insert('friendships', [
'requester_id' => $userId,
'addressee_id' => $targetId,
'status' => 'pending'
], $token);
// Send notification to the recipient
if ($currentUserId) {
$profileRes = supabase_rest('GET', "profiles?id=eq.{$currentUserId}&select=display_name,username", [], SUPABASE_SERVICE_KEY);
$senderName = $profileRes['data'][0]['display_name'] ?? $profileRes['data'][0]['username'] ?? 'لاعب';
supabase_rest('POST', 'notifications', [
'user_id' => $userId,
'type' => 'friend_request',
'title' => 'طلب صداقة',
'title_ar' => 'طلب صداقة',
'body' => "{$senderName} أرسل لك طلب صداقة",
'body_ar' => "{$senderName} أرسل لك طلب صداقة",
'data' => json_encode(['requester_id' => $currentUserId]),
], SUPABASE_SERVICE_KEY);
}
echo json_encode(['ok' => true]);
break;
case 'accept':
$requestId = $input['request_id'] ?? '';
// Get the friend request to find the original sender
$friendReq = supabase_rest('GET', "friends?id=eq.{$requestId}&select=user_id", [], SUPABASE_SERVICE_KEY);
$requesterId = $friendReq['data'][0]['user_id'] ?? null;
supabase_rest('PATCH', "friends?id=eq.{$requestId}", [
'status' => 'accepted',
'accepted_at' => date('c')
], $token);
// Notify the original requester that the request was accepted
if ($requesterId && $currentUserId) {
$profileRes = supabase_rest('GET', "profiles?id=eq.{$currentUserId}&select=display_name,username", [], SUPABASE_SERVICE_KEY);
$acceptorName = $profileRes['data'][0]['display_name'] ?? $profileRes['data'][0]['username'] ?? 'لاعب';
supabase_rest('POST', 'notifications', [
'user_id' => $requesterId,
'type' => 'friend_accepted',
'title' => 'تم قبول طلب الصداقة',
'title_ar' => 'تم قبول طلب الصداقة',
'body' => "{$acceptorName} قبل طلب صداقتك",
'body_ar' => "{$acceptorName} قبل طلب صداقتك",
'data' => json_encode(['friend_id' => $currentUserId]),
], SUPABASE_SERVICE_KEY);
]);
if (isset($result['error'])) jsonError($result['error']);
jsonResponse(['success' => true]);
}
echo json_encode(['ok' => true]);
break;
if ($action === 'accept') {
$friendshipId = $input['friendship_id'] ?? '';
if (!$friendshipId) jsonError('friendship_id required');
case 'reject':
$requestId = $input['request_id'] ?? '';
supabase_rest('DELETE', "friends?id=eq.{$requestId}", [], $token);
echo json_encode(['ok' => true]);
break;
$result = $db->update('friendships', ['status' => 'accepted'], [
'id' => 'eq.' . $friendshipId,
'addressee_id' => 'eq.' . $userId
]);
jsonResponse(['success' => true]);
}
}
case 'remove':
$friendId = $input['friend_id'] ?? '';
supabase_rest('DELETE', "friends?friend_id=eq.{$friendId}", [], $token);
echo json_encode(['ok' => true]);
break;
if ($method === 'DELETE') {
$input = getInput();
$targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required');
default:
echo json_encode(['error' => 'invalid action']);
}
$db->delete('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))"
]);
jsonResponse(['success' => true]);
}
jsonError('Invalid request', 400);
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$res = supabase_rest('GET', 'game_plugins?select=game_key,name,name_ar,description_ar,icon_url,banner_url,is_enabled,is_beta,min_players,max_players,supports_ranked,supports_bot,default_time_controls,config,sort_order&order=sort_order.asc');
$games = $res['data'] ?? [];
echo json_encode(['games' => $games]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php
define('APP_NAME', 'EL3AB');
define('APP_URL', 'https://el3ab-player.caprover.al-arcade.com');
define('APP_VERSION', '2.0.0');
define('SUPABASE_URL', 'https://safe-supabase-kong.caprover.al-arcade.com');
define('SUPABASE_ANON_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84');
define('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4');
define('SUPABASE_REST', SUPABASE_URL . '/rest/v1');
define('SUPABASE_AUTH', SUPABASE_URL . '/auth/v1');
define('SUPABASE_STORAGE', SUPABASE_URL . '/storage/v1');
define('STOCKFISH_API', 'https://stockfishapi.caprover.al-arcade.com');
define('STOCKFISH_MGMT_KEY', 'sk-alarc-stockfish-mgmt-2024');
define('SWISS_API', 'https://swissapi.caprover.al-arcade.com/api/v1');
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
function supabase(?string $token = null): SupabaseClient {
return new SupabaseClient($token);
}
function supabaseService(): SupabaseClient {
return new SupabaseClient(null, true);
}
function supabaseAuth(string $method, string $endpoint, ?array $body = null, ?string $token = null): array {
$url = SUPABASE_AUTH . '/' . $endpoint;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$headers = [
'apikey: ' . SUPABASE_ANON_KEY,
'Content-Type: application/json'
];
if ($token) {
$headers[] = 'Authorization: Bearer ' . $token;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($body) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true);
if ($httpCode >= 400) {
return ['error' => $decoded['error_description'] ?? $decoded['msg'] ?? $decoded['message'] ?? 'Auth error', 'code' => $httpCode];
}
return $decoded ?? [];
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as scene from '../../core/scene.js';
import { mountSplash } from './scenes/splash.js';
import { mountLogin } from './scenes/login.js';
import { mountRegister } from './scenes/register.js';
scene.register('auth-splash', mountSplash);
scene.register('auth-login', mountLogin);
scene.register('auth-register', mountRegister);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment