Commit 48cb8d60 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: Phase 0/6 — security hardening, stability, validation

- auth.php: eliminate double token verification (decode JWT payload directly)
- auth.php: cascade delete related data on account deletion (blocks,
  daily_claims, challenges, achievements, group_members)
- auth.php: add server-side username regex validation
- register.js: add client-side username character validation
- profile/view.js: show "Unrated" for players with 0 games at 1200 rating
- i18n: add auth.invalid_username and profile.unrated keys
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c508fdfb
......@@ -51,6 +51,9 @@ function handleSignup(array $input): void {
if (strlen($username) < 3 || strlen($username) > 20) {
jsonError('Username must be 3-20 characters');
}
if (!preg_match('/^[a-zA-Z0-9\x{0600}-\x{06FF}_.-]{3,20}$/u', $username)) {
jsonError('Username contains invalid characters');
}
$result = supabaseAuth('POST', 'signup', [
'email' => $email,
......@@ -248,9 +251,14 @@ function handleDeleteAccount(array $input): void {
$sdb = supabaseService();
$sdb->delete('notifications', ['user_id' => 'eq.' . $userId]);
$sdb->delete('friendships', ['or' => "(user_id.eq.{$userId},friend_id.eq.{$userId})"]);
$sdb->delete('friendships', ['or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})"]);
$sdb->delete('chat_messages', ['sender_id' => 'eq.' . $userId]);
$sdb->delete('matchmaking_queue', ['player_id' => 'eq.' . $userId]);
$sdb->delete('player_blocks', ['or' => "(player_id.eq.{$userId},blocked_id.eq.{$userId})"]);
$sdb->delete('daily_claims', ['user_id' => 'eq.' . $userId]);
$sdb->delete('challenge_progress', ['user_id' => 'eq.' . $userId]);
$sdb->delete('achievement_progress', ['user_id' => 'eq.' . $userId]);
$sdb->delete('group_members', ['user_id' => 'eq.' . $userId]);
$sdb->delete('profiles', ['id' => 'eq.' . $userId]);
$url = SUPABASE_AUTH . '/admin/users/' . $userId;
......
......@@ -93,6 +93,15 @@ function verifyToken(string $token): ?array {
}
function getUserId(string $token): ?string {
// Decode JWT payload without re-verifying (requireAuth already verified)
$parts = explode('.', $token);
if (count($parts) === 3) {
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
if ($payload && !empty($payload['sub'])) {
return $payload['sub'];
}
}
// Fallback: full verification
$user = verifyToken($token);
return $user['id'] ?? null;
}
......
......@@ -588,6 +588,7 @@ const strings = {
'auth.password_mismatch': 'كلمتا المرور غير متطابقتين',
'auth.username_short': 'اسم المستخدم قصير جداً',
'auth.password_short': 'كلمة المرور قصيرة (6 أحرف على الأقل)',
'auth.invalid_username': 'اسم المستخدم يمكن أن يحتوي فقط على حروف وأرقام و _.-',
'app.tagline': 'العب مع أصدقائك',
'common.or': 'أو',
'common.play_again': 'العب مرة أخرى',
......@@ -1274,6 +1275,7 @@ const strings = {
'auth.password_mismatch': 'Passwords do not match',
'auth.username_short': 'Username too short',
'auth.password_short': 'Password too short (min 6 characters)',
'auth.invalid_username': 'Username can only contain letters, numbers, and _.-',
'app.tagline': 'Play with your friends',
'common.or': 'or',
'common.play_again': 'Play Again',
......
......@@ -53,6 +53,10 @@ export function mountRegister(el) {
const email = el.querySelector('#reg-email').value.trim();
const pass = el.querySelector('#reg-pass').value;
if (!username || !email || !pass) return;
if (!/^[a-zA-Z0-9؀-ۿ_.-]{3,20}$/.test(username)) {
errEl.textContent = t('auth.invalid_username');
return;
}
btn.disabled = true;
btn.textContent = t('auth.registering');
......
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