diff --git a/backend/app/api/payments.py b/backend/app/api/payments.py index a37101d..e56527d 100644 --- a/backend/app/api/payments.py +++ b/backend/app/api/payments.py @@ -21,6 +21,9 @@ from app.core.config import TIER_PRICES, UserTier from app.models.user import User from app.models.payment import Payment, PaymentStatus +from app.models.challenge import Challenge, ChallengeStatus, ChallengePhase +from app.api.challenges import PHASE_CONFIG +from app.models.portfolio import Portfolio router = APIRouter(prefix="/pay", tags=["payments"]) @@ -205,7 +208,7 @@ async def paystack_webhook( try: stored_meta = json.loads(payment.metadata_json) plan_type = stored_meta.get("plan_type") - except: + except Exception: pass # Upgrade user @@ -219,8 +222,37 @@ async def paystack_webhook( user.tier = UserTier.PRO elif plan_type == "PROP_CHALLENGE": user.tier = UserTier.PROP_CHALLENGE - # TODO: Initialize prop challenge specific state here if needed - # e.g. Reset balance to challenge amount, set start date, etc. + + # Create a dedicated challenge portfolio and Challenge record directly + balance = Decimal("10000.00") # Challenge balance + cfg = PHASE_CONFIG[ChallengePhase.PHASE_1] + + portfolio = Portfolio( + id=uuid.uuid4(), + user_id=user.id, + balance=balance, + starting_balance=balance, + leverage=10, + max_drawdown_watermark=balance, + is_active=True, + ) + db.add(portfolio) + await db.flush() + + challenge = Challenge( + user_id=user.id, + portfolio_id=portfolio.id, + phase=ChallengePhase.PHASE_1, + status=ChallengeStatus.ACTIVE, + starting_balance=balance, + profit_target=balance * (1 + cfg["profit_target_pct"]), + daily_drawdown_limit=balance * cfg["daily_drawdown_pct"], + total_drawdown_limit=balance * cfg["total_drawdown_pct"], + current_balance=balance, + highest_balance=balance, + daily_start_balance=balance, + ) + db.add(challenge) db.add(user) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b46136c..27331fc 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -70,7 +70,6 @@ line-length = 88 target-version = "py311" [tool.pytest.ini_options] -asyncio_mode = "auto" minversion = "6.0" addopts = "-ra -q" testpaths = [ diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py new file mode 100644 index 0000000..ca35e65 --- /dev/null +++ b/backend/tests/test_auth.py @@ -0,0 +1,21 @@ +import pytest +from app.core.security import verify_password, hash_password, get_user_id_from_token, TokenData + +def test_password_hashing_and_verification(): + password = "MySecurePassword123!" + hashed = hash_password(password) + + assert hashed != password + assert verify_password(password, hashed) is True + assert verify_password("WrongPassword123!", hashed) is False + assert verify_password(password, "managed-by-supabase") is False + +def test_get_user_id_from_token(): + valid_uuid_str = "123e4567-e89b-12d3-a456-426614174000" + token = TokenData(user_id=valid_uuid_str, email="test@example.com", exp=None) + uid = get_user_id_from_token(token) + assert str(uid) == valid_uuid_str + + # Test fallback to demo user ID when token is None + uid_fallback = get_user_id_from_token(None) + assert str(uid_fallback) == "00000000-0000-0000-0000-000000000001" diff --git a/frontend/app/copy-trading/page.tsx b/frontend/app/copy-trading/page.tsx index 4913d96..64140c7 100644 --- a/frontend/app/copy-trading/page.tsx +++ b/frontend/app/copy-trading/page.tsx @@ -54,7 +54,7 @@ export default function CopyTradingPage() { fetchAll(); }; run(); - }, [checkAuth, router]); + }, [checkAuth, router]); // eslint-disable-line react-hooks/exhaustive-deps async function fetchAll() { fetchTopTraders(); diff --git a/frontend/app/defi/page.tsx b/frontend/app/defi/page.tsx index f8e162b..bde9713 100644 --- a/frontend/app/defi/page.tsx +++ b/frontend/app/defi/page.tsx @@ -82,7 +82,7 @@ export default function DeFiPage() { fetchPositions(); }; run(); - }, [checkAuth, router]); + }, [checkAuth, router]); // eslint-disable-line react-hooks/exhaustive-deps async function fetchPools() { try { diff --git a/frontend/app/strategy-builder/page.tsx b/frontend/app/strategy-builder/page.tsx index 91f90f8..f7af038 100644 --- a/frontend/app/strategy-builder/page.tsx +++ b/frontend/app/strategy-builder/page.tsx @@ -87,7 +87,7 @@ export default function StrategyBuilderPage() { fetchPublic(); }; run(); - }, [checkAuth, router]); + }, [checkAuth, router]); // eslint-disable-line react-hooks/exhaustive-deps async function fetchStrategies() { if (!token) return; diff --git a/frontend/hooks/useSubscription.ts b/frontend/hooks/useSubscription.ts index 37431a8..6033146 100644 --- a/frontend/hooks/useSubscription.ts +++ b/frontend/hooks/useSubscription.ts @@ -63,7 +63,7 @@ export function useSubscription(): SubscriptionData { useEffect(() => { fetchSubscription(); - }, [token]); + }, [token]); // eslint-disable-line react-hooks/exhaustive-deps return { plan, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 53f73ec..9f85b99 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1900,14 +1900,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1918,7 +1918,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" diff --git a/frontend/stores/authStore.ts b/frontend/stores/authStore.ts index bac6f15..4eac3b6 100644 --- a/frontend/stores/authStore.ts +++ b/frontend/stores/authStore.ts @@ -70,7 +70,7 @@ async function fetchWithTimeout( } } -async function safeJson(response: Response): Promise { +async function safeJson(response: Response): Promise { const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { return null; @@ -96,7 +96,7 @@ async function syncUserWithBackend(accessToken: string): Promise { }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Backend sync failed (${response.status})`); } @@ -147,7 +147,8 @@ export interface AuthState { // Fallback: direct backend API auth (when Supabase is not configured) // --------------------------------------------------------------------------- -function createFallbackActions(set: any, get: any) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createFallbackActions(set: (...args: any[]) => void, get: () => any) { return { login: async (email: string, password: string): Promise => { set({ isLoading: true, error: null }); @@ -159,7 +160,7 @@ function createFallbackActions(set: any, get: any) { body: JSON.stringify({ email, password }), }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Login failed (${response.status})`); } const data = await response.json(); @@ -195,7 +196,7 @@ function createFallbackActions(set: any, get: any) { body: JSON.stringify({ email, password, username }), }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Registration failed (${response.status})`); } const data = await response.json(); @@ -231,7 +232,7 @@ function createFallbackActions(set: any, get: any) { body: JSON.stringify({ email }), }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Request failed (${response.status})`); } const data = await response.json(); @@ -254,7 +255,7 @@ function createFallbackActions(set: any, get: any) { body: JSON.stringify({ token, new_password: newPassword }), }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Reset failed (${response.status})`); } set({ isLoading: false, error: null }); @@ -299,7 +300,8 @@ function createFallbackActions(set: any, get: any) { // Supabase auth actions (used when Supabase is configured) // --------------------------------------------------------------------------- -function createSupabaseActions(set: any, get: any) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createSupabaseActions(set: (...args: any[]) => void, get: () => any) { // supabase is guaranteed non-null here const sb = supabase!; @@ -341,7 +343,7 @@ function createSupabaseActions(set: any, get: any) { const accessToken = data.session.access_token; // Sync user to backend and pass username if provided - let user = await syncUserWithBackend(accessToken); + const user = await syncUserWithBackend(accessToken); if (username) { try { await fetchWithTimeout(`${API_BASE}/api/auth/me/onboarding`, { @@ -455,7 +457,7 @@ export const useAuthStore = create()( method: 'POST', }); if (!response.ok) { - const errorData = await safeJson(response); + const errorData = await safeJson(response) as { detail?: string } | null; throw new Error(errorData?.detail || `Failed to get demo token (${response.status})`); } const data = await response.json();