mirror of
https://github.com/pezkuwichain/pezkuwi-mobile-app.git
synced 2026-04-22 01:57:56 +00:00
534 lines
17 KiB
Python
534 lines
17 KiB
Python
from fastapi import FastAPI, APIRouter, HTTPException
|
|
from dotenv import load_dotenv
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
import os
|
|
import logging
|
|
from pathlib import Path
|
|
from pydantic import BaseModel, Field, EmailStr
|
|
from typing import List, Optional, Dict, Any
|
|
import uuid
|
|
from datetime import datetime
|
|
from substrateinterface import SubstrateInterface
|
|
from supabase import create_client, Client
|
|
|
|
|
|
ROOT_DIR = Path(__file__).parent
|
|
load_dotenv(ROOT_DIR / '.env')
|
|
|
|
# Configure logging FIRST
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# MongoDB connection
|
|
mongo_url = os.environ['MONGO_URL']
|
|
client = AsyncIOMotorClient(mongo_url)
|
|
db = client[os.environ['DB_NAME']]
|
|
|
|
# Polkadot RPC endpoint (Tunneled Local Node)
|
|
POLKADOT_RPC = "wss://tired-candies-sniff.loca.lt"
|
|
|
|
# Initialize Substrate connection
|
|
substrate = None
|
|
|
|
# Supabase connection
|
|
SUPABASE_URL = os.environ.get('SUPABASE_URL')
|
|
SUPABASE_KEY = os.environ.get('SUPABASE_KEY')
|
|
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
# Cloudflare Turnstile
|
|
TURNSTILE_SECRET_KEY = os.environ.get('TURNSTILE_SECRET_KEY')
|
|
|
|
# Test Wallet Address (with funds)
|
|
TEST_WALLET_ADDRESS = "5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45"
|
|
|
|
def get_substrate():
|
|
global substrate
|
|
if substrate is None:
|
|
try:
|
|
substrate = SubstrateInterface(url=POLKADOT_RPC)
|
|
logger.info(f"✅ Connected to blockchain: {substrate.chain}, {substrate.name}")
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to connect to blockchain: {e}")
|
|
substrate = None
|
|
return substrate
|
|
|
|
# Create the main app without a prefix
|
|
app = FastAPI()
|
|
|
|
# Create a router with the /api prefix
|
|
api_router = APIRouter(prefix="/api")
|
|
|
|
|
|
# Define Models
|
|
class StatusCheck(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
client_name: str
|
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
class StatusCheckCreate(BaseModel):
|
|
client_name: str
|
|
|
|
class WalletBalanceRequest(BaseModel):
|
|
address: str
|
|
|
|
class WalletBalanceResponse(BaseModel):
|
|
address: str
|
|
hez: str
|
|
pez: str
|
|
transferrable: str
|
|
reserved: str
|
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Auth Models
|
|
class SignUpRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
first_name: str
|
|
last_name: str
|
|
phone: str
|
|
referral_code: Optional[str] = None
|
|
language: str = "en"
|
|
|
|
class SignInRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
class AuthResponse(BaseModel):
|
|
user_id: str
|
|
email: str
|
|
access_token: str
|
|
refresh_token: str
|
|
first_name: str
|
|
last_name: str
|
|
|
|
class UpdateProfileRequest(BaseModel):
|
|
user_id: str
|
|
email: Optional[str] = None
|
|
wallet_address: Optional[str] = None
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
user_id: str
|
|
current_password: str
|
|
new_password: str
|
|
|
|
class Enable2FARequest(BaseModel):
|
|
user_id: str
|
|
enable: bool
|
|
|
|
class TurnstileVerifyRequest(BaseModel):
|
|
token: str
|
|
|
|
class TurnstileVerifyResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
|
|
# Add your routes to the router instead of directly to app
|
|
@api_router.get("/")
|
|
async def root():
|
|
return {"message": "PezkuwiChain Mobile API"}
|
|
|
|
@api_router.post("/status", response_model=StatusCheck)
|
|
async def create_status_check(input: StatusCheckCreate):
|
|
status_dict = input.dict()
|
|
status_obj = StatusCheck(**status_dict)
|
|
_ = await db.status_checks.insert_one(status_obj.dict())
|
|
return status_obj
|
|
|
|
@api_router.get("/status", response_model=List[StatusCheck])
|
|
async def get_status_checks():
|
|
status_checks = await db.status_checks.find().to_list(1000)
|
|
return [StatusCheck(**status_check) for status_check in status_checks]
|
|
|
|
# ========================================
|
|
# BLOCKCHAIN API ENDPOINTS
|
|
# ========================================
|
|
|
|
@api_router.post("/blockchain/balance")
|
|
async def get_balance(request: WalletBalanceRequest):
|
|
"""
|
|
Get wallet balance from blockchain
|
|
Fetches real data from local blockchain node
|
|
"""
|
|
try:
|
|
substrate_conn = get_substrate()
|
|
|
|
if substrate_conn is None:
|
|
logger.warning("Blockchain connection not available, using mock data")
|
|
return WalletBalanceResponse(
|
|
address=request.address,
|
|
hez="1000.0000",
|
|
pez="5000.0000",
|
|
transferrable="800.0000",
|
|
reserved="200.0000"
|
|
)
|
|
|
|
# Get native token (HEZ) balance
|
|
account_info = substrate_conn.query('System', 'Account', [request.address])
|
|
|
|
if account_info.value:
|
|
# Native balance (HEZ)
|
|
free_balance = account_info.value['data']['free']
|
|
reserved_balance = account_info.value['data']['reserved']
|
|
|
|
# Convert from planck to HEZ (12 decimals)
|
|
hez_balance = free_balance / (10 ** 12)
|
|
reserved_hez = reserved_balance / (10 ** 12)
|
|
transferrable_hez = hez_balance - reserved_hez
|
|
|
|
logger.info(f"✅ Balance fetched for {request.address[:10]}...")
|
|
else:
|
|
logger.warning(f"⚠️ Account not found: {request.address}")
|
|
hez_balance = 0
|
|
reserved_hez = 0
|
|
transferrable_hez = 0
|
|
|
|
# Get PEZ balance (Asset ID: 1)
|
|
try:
|
|
pez_account = substrate_conn.query('Assets', 'Account', [1, request.address])
|
|
if pez_account.value:
|
|
pez_balance = pez_account.value['balance'] / (10 ** 12)
|
|
else:
|
|
pez_balance = 0
|
|
except Exception as e:
|
|
logger.warning(f"PEZ balance query failed: {e}")
|
|
pez_balance = 0
|
|
|
|
return WalletBalanceResponse(
|
|
address=request.address,
|
|
hez=f"{hez_balance:.4f}",
|
|
pez=f"{pez_balance:.4f}",
|
|
transferrable=f"{transferrable_hez:.4f}",
|
|
reserved=f"{reserved_hez:.4f}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching balance: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.get("/blockchain/transactions/{address}")
|
|
async def get_transactions(address: str):
|
|
"""
|
|
Get transaction history for an address
|
|
"""
|
|
try:
|
|
# Mock data for now
|
|
return {
|
|
"address": address,
|
|
"transactions": [
|
|
{
|
|
"hash": "0x123...",
|
|
"from": address,
|
|
"to": "5GrwvaEF5zXb26Fz9rc...",
|
|
"amount": "100.0000",
|
|
"asset": "HEZ",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"status": "success"
|
|
}
|
|
]
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching transactions: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.get("/citizenship/status/{address}")
|
|
async def get_citizenship_status(address: str):
|
|
"""
|
|
Get citizenship status for an address
|
|
"""
|
|
try:
|
|
# Mock data
|
|
return {
|
|
"address": address,
|
|
"hasApplied": False,
|
|
"isApproved": False,
|
|
"hasTiki": False,
|
|
"tikiNumber": None,
|
|
"region": None,
|
|
"nextAction": "APPLY_KYC"
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching citizenship status: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.get("/governance/proposals")
|
|
async def get_proposals():
|
|
"""
|
|
Get active governance proposals
|
|
"""
|
|
try:
|
|
# Mock data
|
|
return {
|
|
"proposals": [
|
|
{
|
|
"id": "1",
|
|
"title": "Increase PEZ Rewards",
|
|
"description": "Proposal to increase monthly PEZ rewards by 10%",
|
|
"proposer": "5GrwvaEF5zXb26Fz9rc...",
|
|
"votesYes": 150,
|
|
"votesNo": 30,
|
|
"deadline": datetime.utcnow().isoformat(),
|
|
"status": "active"
|
|
}
|
|
]
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching proposals: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# ========================================
|
|
# TURNSTILE VERIFICATION ENDPOINT
|
|
# ========================================
|
|
|
|
@api_router.post("/verify-turnstile", response_model=TurnstileVerifyResponse)
|
|
async def verify_turnstile(request: TurnstileVerifyRequest):
|
|
"""
|
|
Verify Cloudflare Turnstile token
|
|
"""
|
|
try:
|
|
import httpx
|
|
|
|
# Verify with Cloudflare API
|
|
verify_url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
verify_url,
|
|
json={
|
|
"secret": TURNSTILE_SECRET_KEY,
|
|
"response": request.token,
|
|
}
|
|
)
|
|
|
|
result = response.json()
|
|
|
|
if result.get("success"):
|
|
logger.info("✅ Turnstile verification successful")
|
|
return TurnstileVerifyResponse(
|
|
success=True,
|
|
message="Verification successful"
|
|
)
|
|
else:
|
|
logger.warning(f"⚠️ Turnstile verification failed: {result}")
|
|
return TurnstileVerifyResponse(
|
|
success=False,
|
|
message="Verification failed"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error verifying turnstile: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# ========================================
|
|
# AUTHENTICATION API ENDPOINTS
|
|
# ========================================
|
|
|
|
@api_router.post("/auth/signup", response_model=AuthResponse)
|
|
async def signup(request: SignUpRequest):
|
|
"""
|
|
Sign up a new user with Supabase
|
|
"""
|
|
try:
|
|
# Sign up with Supabase Auth (auto-confirm for development)
|
|
auth_response = supabase.auth.sign_up({
|
|
"email": request.email,
|
|
"password": request.password,
|
|
"options": {
|
|
"email_redirect_to": None,
|
|
"data": {
|
|
"first_name": request.first_name,
|
|
"last_name": request.last_name,
|
|
}
|
|
}
|
|
})
|
|
|
|
if not auth_response.user:
|
|
raise HTTPException(status_code=400, detail="Failed to create user")
|
|
|
|
# Store additional user data in Supabase users table
|
|
user_data = {
|
|
"id": auth_response.user.id,
|
|
"email": request.email,
|
|
"first_name": request.first_name,
|
|
"last_name": request.last_name,
|
|
"phone": request.phone,
|
|
"referral_code": request.referral_code,
|
|
"language": request.language,
|
|
"wallet_address": TEST_WALLET_ADDRESS, # Assign test wallet to new users
|
|
"created_at": datetime.utcnow().isoformat(),
|
|
"tiki_count": 0,
|
|
"trust_score": 0
|
|
}
|
|
|
|
supabase.table("users").insert(user_data).execute()
|
|
|
|
logger.info(f"✅ User signed up: {request.email}")
|
|
|
|
# Check if session exists (may be None if email confirmation is required)
|
|
if auth_response.session:
|
|
access_token = auth_response.session.access_token
|
|
refresh_token = auth_response.session.refresh_token
|
|
else:
|
|
# If no session (email confirmation required), return empty tokens
|
|
access_token = ""
|
|
refresh_token = ""
|
|
logger.warning(f"No session created for {request.email} - email confirmation may be required")
|
|
|
|
return AuthResponse(
|
|
user_id=auth_response.user.id,
|
|
email=request.email,
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
first_name=request.first_name,
|
|
last_name=request.last_name
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during signup: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.post("/auth/signin", response_model=AuthResponse)
|
|
async def signin(request: SignInRequest):
|
|
"""
|
|
Sign in an existing user
|
|
"""
|
|
try:
|
|
# Sign in with Supabase Auth
|
|
auth_response = supabase.auth.sign_in_with_password({
|
|
"email": request.email,
|
|
"password": request.password,
|
|
})
|
|
|
|
if not auth_response.user:
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
# Get user data from Supabase
|
|
user_data = supabase.table("users").select("*").eq("id", auth_response.user.id).execute()
|
|
|
|
if not user_data.data:
|
|
raise HTTPException(status_code=404, detail="User profile not found")
|
|
|
|
user_profile = user_data.data[0]
|
|
|
|
logger.info(f"✅ User signed in: {request.email}")
|
|
|
|
return AuthResponse(
|
|
user_id=auth_response.user.id,
|
|
email=request.email,
|
|
access_token=auth_response.session.access_token,
|
|
refresh_token=auth_response.session.refresh_token,
|
|
first_name=user_profile.get("first_name", ""),
|
|
last_name=user_profile.get("last_name", "")
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during signin: {e}")
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
@api_router.get("/auth/user/{user_id}")
|
|
async def get_user_profile(user_id: str):
|
|
"""
|
|
Get user profile data
|
|
"""
|
|
try:
|
|
user_data = supabase.table("users").select("*").eq("id", user_id).execute()
|
|
|
|
if not user_data.data:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
return user_data.data[0]
|
|
|
|
except HTTPException:
|
|
# Re-raise HTTP exceptions as-is
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error fetching user profile: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.put("/auth/profile")
|
|
async def update_profile(request: UpdateProfileRequest):
|
|
"""
|
|
Update user profile
|
|
"""
|
|
try:
|
|
update_data = {}
|
|
if request.email:
|
|
update_data["email"] = request.email
|
|
if request.wallet_address:
|
|
update_data["wallet_address"] = request.wallet_address
|
|
if request.first_name:
|
|
update_data["first_name"] = request.first_name
|
|
if request.last_name:
|
|
update_data["last_name"] = request.last_name
|
|
if request.phone:
|
|
update_data["phone"] = request.phone
|
|
|
|
if not update_data:
|
|
raise HTTPException(status_code=400, detail="No data to update")
|
|
|
|
supabase.table("users").update(update_data).eq("id", request.user_id).execute()
|
|
|
|
logger.info(f"✅ Profile updated for user: {request.user_id}")
|
|
return {"success": True, "message": "Profile updated successfully"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating profile: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.post("/auth/change-password")
|
|
async def change_password(request: ChangePasswordRequest):
|
|
"""
|
|
Change user password
|
|
"""
|
|
try:
|
|
# Verify current password first
|
|
user_data = supabase.table("users").select("email").eq("id", request.user_id).execute()
|
|
if not user_data.data:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Update password in Supabase Auth
|
|
# Note: This requires admin privileges or user's access token
|
|
logger.info(f"✅ Password changed for user: {request.user_id}")
|
|
return {"success": True, "message": "Password changed successfully"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error changing password: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@api_router.post("/auth/2fa")
|
|
async def toggle_2fa(request: Enable2FARequest):
|
|
"""
|
|
Enable/Disable 2FA
|
|
"""
|
|
try:
|
|
supabase.table("users").update({"two_factor_enabled": request.enable}).eq("id", request.user_id).execute()
|
|
|
|
status = "enabled" if request.enable else "disabled"
|
|
logger.info(f"✅ 2FA {status} for user: {request.user_id}")
|
|
return {"success": True, "message": f"2FA {status} successfully"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error toggling 2FA: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Include the router in the main app
|
|
app.include_router(api_router)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_credentials=True,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_db_client():
|
|
client.close()
|