From c92a6a6bf5f742ec30a25b2810a7b6937828a512 Mon Sep 17 00:00:00 2001 From: "Khoa.vo" Date: Fri, 19 Dec 2025 15:07:59 +0700 Subject: [PATCH] Add username/password login for mobile users - headless Playwright login --- backend/api/routes/auth.py | 27 ++++ backend/core/playwright_manager.py | 123 ++++++++++++++++ frontend/src/pages/Login.tsx | 221 +++++++++++++++-------------- 3 files changed, 267 insertions(+), 104 deletions(-) diff --git a/backend/api/routes/auth.py b/backend/api/routes/auth.py index 76ea5f7..2a3f2ec 100644 --- a/backend/api/routes/auth.py +++ b/backend/api/routes/auth.py @@ -22,6 +22,33 @@ class CredentialsRequest(BaseModel): credentials: dict # JSON credentials in http.headers format +class CredentialLoginRequest(BaseModel): + username: str + password: str + + +@router.post("/login", response_model=BrowserLoginResponse) +async def credential_login(request: CredentialLoginRequest): + """ + Login with TikTok username/email and password. + Uses headless browser - works on Docker/NAS. + """ + try: + result = await PlaywrightManager.credential_login( + username=request.username, + password=request.password, + timeout_seconds=60 + ) + return BrowserLoginResponse( + status=result["status"], + message=result["message"], + cookie_count=result.get("cookie_count", 0) + ) + except Exception as e: + print(f"DEBUG: Credential login error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/browser-login", response_model=BrowserLoginResponse) async def browser_login(): """ diff --git a/backend/core/playwright_manager.py b/backend/core/playwright_manager.py index 17387e8..55a043b 100644 --- a/backend/core/playwright_manager.py +++ b/backend/core/playwright_manager.py @@ -118,6 +118,129 @@ class PlaywrightManager: with open(USER_AGENT_FILE, "w") as f: json.dump({"user_agent": user_agent}, f) + @staticmethod + async def credential_login(username: str, password: str, timeout_seconds: int = 60) -> dict: + """ + Headless login using username/password. + Works on Docker/NAS deployments without a display. + + Args: + username: TikTok username, email, or phone + password: TikTok password + timeout_seconds: Max time to wait for login + + Returns: {"status": "success/error", "message": "...", "cookie_count": N} + """ + print(f"DEBUG: Starting headless credential login for: {username}") + + async with async_playwright() as p: + browser = await p.chromium.launch( + headless=True, + args=PlaywrightManager.BROWSER_ARGS + ) + + context = await browser.new_context( + user_agent=PlaywrightManager.DEFAULT_USER_AGENT + ) + + page = await context.new_page() + + try: + # Navigate to TikTok login page + await page.goto("https://www.tiktok.com/login/phone-or-email/email", wait_until="domcontentloaded") + await asyncio.sleep(2) + + print("DEBUG: Looking for login form...") + + # Wait for and fill username/email field + username_selector = 'input[name="username"], input[placeholder*="Email"], input[placeholder*="email"], input[type="text"]' + await page.wait_for_selector(username_selector, timeout=10000) + await page.fill(username_selector, username) + await asyncio.sleep(0.5) + + # Fill password field + password_selector = 'input[type="password"]' + await page.wait_for_selector(password_selector, timeout=5000) + await page.fill(password_selector, password) + await asyncio.sleep(0.5) + + print("DEBUG: Credentials filled, clicking login...") + + # Click login button + login_button = 'button[type="submit"], button[data-e2e="login-button"]' + await page.click(login_button) + + # Wait for login to complete - poll for sessionid cookie + print("DEBUG: Waiting for login to complete...") + elapsed = 0 + check_interval = 2 + cookies_found = {} + + while elapsed < timeout_seconds: + await asyncio.sleep(check_interval) + elapsed += check_interval + + # Check for error messages + error_el = await page.query_selector('[class*="error"], [class*="Error"]') + if error_el: + error_text = await error_el.inner_text() + if error_text and len(error_text) > 0: + await browser.close() + return { + "status": "error", + "message": f"Login failed: {error_text[:100]}", + "cookie_count": 0 + } + + # Check cookies + all_cookies = await context.cookies() + for cookie in all_cookies: + if cookie.get("domain", "").endswith("tiktok.com"): + cookies_found[cookie["name"]] = cookie["value"] + + if "sessionid" in cookies_found: + print(f"DEBUG: Login successful! Found {len(cookies_found)} cookies.") + break + + # Check if CAPTCHA or verification needed + captcha = await page.query_selector('[class*="captcha"], [class*="Captcha"], [class*="verify"]') + if captcha: + await browser.close() + return { + "status": "error", + "message": "TikTok requires verification (CAPTCHA). Please try the cookie method.", + "cookie_count": 0 + } + + print(f"DEBUG: Waiting for login... ({elapsed}s)") + + await browser.close() + + if "sessionid" not in cookies_found: + return { + "status": "error", + "message": "Login timed out. Check your credentials or try the cookie method.", + "cookie_count": 0 + } + + # Save credentials + PlaywrightManager.save_credentials(cookies_found, PlaywrightManager.DEFAULT_USER_AGENT) + + return { + "status": "success", + "message": "Successfully logged in!", + "cookie_count": len(cookies_found) + } + + except Exception as e: + await browser.close() + print(f"DEBUG: Login error: {e}") + return { + "status": "error", + "message": f"Login failed: {str(e)[:100]}", + "cookie_count": 0 + } + @staticmethod async def browser_login(timeout_seconds: int = 180) -> dict: """ diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 19e97f4..dd178cd 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,172 +1,185 @@ import React, { useState } from 'react'; -import { useAuthStore } from '../store/authStore'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import { API_BASE_URL } from '../config'; export const Login: React.FC = () => { - const [cookies, setCookies] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); const [error, setError] = useState(''); - const [isConnecting, setIsConnecting] = useState(false); - const [showBrowserLogin, setShowBrowserLogin] = useState(false); - const login = useAuthStore((state) => state.login); + const [isLoading, setIsLoading] = useState(false); + const [showCookieMethod, setShowCookieMethod] = useState(false); + const [cookies, setCookies] = useState(''); const navigate = useNavigate(); - const handleBrowserLogin = async () => { + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim() || !password.trim()) return; + setError(''); - setIsConnecting(true); + setIsLoading(true); try { - const res = await axios.post(`${API_BASE_URL}/auth/browser-login`); + const res = await axios.post(`${API_BASE_URL}/auth/login`, { + username: username.trim(), + password: password.trim() + }); if (res.data.status === 'success') { - setTimeout(() => navigate('/'), 1000); - } else if (res.data.status === 'timeout') { - setError(res.data.message); - setIsConnecting(false); + navigate('/'); + } else { + setError(res.data.message || 'Login failed. Please check your credentials.'); } } catch (err: any) { - setError(err.response?.data?.detail || 'Failed to connect. Use the cookie method above.'); - setIsConnecting(false); + const message = err.response?.data?.detail || err.response?.data?.message || 'Login failed. Please try again.'; + setError(message); + } finally { + setIsLoading(false); } }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleCookieLogin = async () => { if (!cookies.trim()) return; setError(''); + setIsLoading(true); + try { - await login(cookies); - navigate('/'); - } catch (err) { - setError('Invalid format. Make sure you paste the full cookie JSON.'); + // Try to parse as JSON + let jsonCreds; + try { + jsonCreds = JSON.parse(cookies); + } catch { + // If not JSON, wrap it as simple session format + jsonCreds = { + http: { + cookies: { sessionid: cookies.trim() } + } + }; + } + + const res = await axios.post(`${API_BASE_URL}/auth/credentials`, { + credentials: jsonCreds + }); + + if (res.data.status === 'success') { + navigate('/'); + } else { + setError(res.data.message || 'Failed to save cookies.'); + } + } catch (err: any) { + setError(err.response?.data?.detail || 'Invalid cookie format.'); + } finally { + setIsLoading(false); } }; return (
{/* Header */} -
-
-
-
- +
+
+
+
+
-

PureStream

-

Ad-free TikTok viewing

+

PureStream

+

Ad-free TikTok viewing

{/* Scrollable Content */}
{error && ( -
+
{error}
)} - {/* How to Login - Step by Step */} -
-

How to Login

- -
-
-
1
-
-

Open TikTok in browser

-

Use Chrome/Safari on your phone or computer

-
-
- -
-
2
-
-

Export your cookies

-

Use "Cookie-Editor" extension (Chrome/Firefox)

-
-
- -
-
3
-
-

Paste cookies below

-

Copy the JSON and paste it here

-
-
-
-
- - {/* Cookie Input */} -
-
-