Implement secure team builder with PIN verification system
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3098be7f36
commit
9cf2231a03
6 changed files with 476 additions and 16 deletions
227
src/database.py
227
src/database.py
|
|
@ -286,6 +286,35 @@ class Database:
|
|||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pending_team_changes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
new_team_data TEXT NOT NULL,
|
||||
verification_pin TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
verified_at TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS verification_pins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
pin_code TEXT NOT NULL,
|
||||
request_type TEXT NOT NULL,
|
||||
request_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
is_used BOOLEAN DEFAULT FALSE,
|
||||
used_at TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.commit()
|
||||
|
||||
async def get_player(self, nickname: str) -> Optional[Dict]:
|
||||
|
|
@ -1358,4 +1387,202 @@ class Database:
|
|||
"pet": dict(pet),
|
||||
"old_name": pet["nickname"] or pet["species_name"],
|
||||
"new_nickname": nickname
|
||||
}
|
||||
|
||||
# Team Builder PIN System Methods
|
||||
async def generate_verification_pin(self, player_id: int, request_type: str, request_data: str = None) -> Dict:
|
||||
"""Generate a secure PIN for verification. Returns PIN code and expiration."""
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Generate cryptographically secure 6-digit PIN
|
||||
pin_code = ''.join(secrets.choice(string.digits) for _ in range(6))
|
||||
|
||||
# PIN expires in 10 minutes
|
||||
expires_at = datetime.now() + timedelta(minutes=10)
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Clear any existing unused PINs for this player and request type
|
||||
await db.execute("""
|
||||
UPDATE verification_pins
|
||||
SET is_used = TRUE, used_at = CURRENT_TIMESTAMP
|
||||
WHERE player_id = ? AND request_type = ? AND is_used = FALSE
|
||||
""", (player_id, request_type))
|
||||
|
||||
# Insert new PIN
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO verification_pins
|
||||
(player_id, pin_code, request_type, request_data, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (player_id, pin_code, request_type, request_data, expires_at.isoformat()))
|
||||
|
||||
await db.commit()
|
||||
pin_id = cursor.lastrowid
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pin_id": pin_id,
|
||||
"pin_code": pin_code,
|
||||
"expires_at": expires_at,
|
||||
"expires_in_minutes": 10
|
||||
}
|
||||
|
||||
async def verify_pin(self, player_id: int, pin_code: str, request_type: str) -> Dict:
|
||||
"""Verify a PIN code and return request data if valid."""
|
||||
from datetime import datetime
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Find valid PIN
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM verification_pins
|
||||
WHERE player_id = ? AND pin_code = ? AND request_type = ?
|
||||
AND is_used = FALSE AND expires_at > datetime('now')
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""", (player_id, pin_code, request_type))
|
||||
|
||||
pin_record = await cursor.fetchone()
|
||||
if not pin_record:
|
||||
return {"success": False, "error": "Invalid or expired PIN"}
|
||||
|
||||
# Mark PIN as used
|
||||
await db.execute("""
|
||||
UPDATE verification_pins
|
||||
SET is_used = TRUE, used_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""", (pin_record["id"],))
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pin_id": pin_record["id"],
|
||||
"request_data": pin_record["request_data"],
|
||||
"created_at": pin_record["created_at"]
|
||||
}
|
||||
|
||||
async def create_pending_team_change(self, player_id: int, new_team_data: str) -> Dict:
|
||||
"""Create a pending team change request and generate PIN."""
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
# Validate team data is valid JSON
|
||||
try:
|
||||
json.loads(new_team_data)
|
||||
except json.JSONDecodeError:
|
||||
return {"success": False, "error": "Invalid team data format"}
|
||||
|
||||
# Generate PIN for this request
|
||||
pin_result = await self.generate_verification_pin(
|
||||
player_id, "team_change", new_team_data
|
||||
)
|
||||
|
||||
if not pin_result["success"]:
|
||||
return pin_result
|
||||
|
||||
# Store pending change
|
||||
expires_at = datetime.now() + timedelta(minutes=10)
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Clear any existing pending changes for this player
|
||||
await db.execute("""
|
||||
DELETE FROM pending_team_changes
|
||||
WHERE player_id = ? AND is_verified = FALSE
|
||||
""", (player_id,))
|
||||
|
||||
# Insert new pending change
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pending_team_changes
|
||||
(player_id, new_team_data, verification_pin, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (player_id, new_team_data, pin_result["pin_code"], expires_at.isoformat()))
|
||||
|
||||
await db.commit()
|
||||
change_id = cursor.lastrowid
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"change_id": change_id,
|
||||
"pin_code": pin_result["pin_code"],
|
||||
"expires_at": expires_at,
|
||||
"expires_in_minutes": 10
|
||||
}
|
||||
|
||||
async def apply_team_change(self, player_id: int, pin_code: str) -> Dict:
|
||||
"""Apply pending team change after PIN verification."""
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Verify PIN
|
||||
pin_result = await self.verify_pin(player_id, pin_code, "team_change")
|
||||
if not pin_result["success"]:
|
||||
return pin_result
|
||||
|
||||
# Get team data from request
|
||||
new_team_data = pin_result["request_data"]
|
||||
if not new_team_data:
|
||||
return {"success": False, "error": "No team data found for this PIN"}
|
||||
|
||||
try:
|
||||
team_changes = json.loads(new_team_data)
|
||||
except json.JSONDecodeError:
|
||||
return {"success": False, "error": "Invalid team data format"}
|
||||
|
||||
# Apply team changes atomically
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
try:
|
||||
# Begin transaction
|
||||
await db.execute("BEGIN TRANSACTION")
|
||||
|
||||
# Update pet active status based on new team
|
||||
for pet_id, is_active in team_changes.items():
|
||||
await db.execute("""
|
||||
UPDATE pets SET is_active = ?
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (is_active, int(pet_id), player_id))
|
||||
|
||||
# Mark any pending change as verified
|
||||
await db.execute("""
|
||||
UPDATE pending_team_changes
|
||||
SET is_verified = TRUE, verified_at = CURRENT_TIMESTAMP
|
||||
WHERE player_id = ? AND verification_pin = ? AND is_verified = FALSE
|
||||
""", (player_id, pin_code))
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"changes_applied": len(team_changes),
|
||||
"verified_at": datetime.now()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
await db.execute("ROLLBACK")
|
||||
return {"success": False, "error": f"Failed to apply team changes: {str(e)}"}
|
||||
|
||||
async def cleanup_expired_pins(self) -> Dict:
|
||||
"""Clean up expired PINs and pending changes."""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Clean expired verification pins
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM verification_pins
|
||||
WHERE expires_at < datetime('now') OR is_used = TRUE
|
||||
""")
|
||||
pins_cleaned = cursor.rowcount
|
||||
|
||||
# Clean expired pending team changes
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM pending_team_changes
|
||||
WHERE expires_at < datetime('now') OR is_verified = TRUE
|
||||
""")
|
||||
changes_cleaned = cursor.rowcount
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pins_cleaned": pins_cleaned,
|
||||
"changes_cleaned": changes_cleaned
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue