Add noVNC for browser streaming login - fully automated TikTok login
This commit is contained in:
parent
01f43ffdc5
commit
abba15f1bc
6 changed files with 337 additions and 87 deletions
26
Dockerfile
26
Dockerfile
|
|
@ -9,7 +9,7 @@ RUN npm run build
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Install system dependencies for Playwright and yt-dlp
|
# Install system dependencies for Playwright, VNC, and noVNC
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
wget \
|
wget \
|
||||||
curl \
|
curl \
|
||||||
|
|
@ -35,10 +35,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libatspi2.0-0 \
|
libatspi2.0-0 \
|
||||||
libgtk-3-0 \
|
libgtk-3-0 \
|
||||||
fonts-liberation \
|
fonts-liberation \
|
||||||
|
# VNC and display
|
||||||
xvfb \
|
xvfb \
|
||||||
xauth \
|
xauth \
|
||||||
|
x11vnc \
|
||||||
|
supervisor \
|
||||||
|
# noVNC dependencies
|
||||||
|
python3-numpy \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install noVNC
|
||||||
|
RUN mkdir -p /opt/noVNC/utils/websockify \
|
||||||
|
&& wget -qO- https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz | tar xz --strip 1 -C /opt/noVNC \
|
||||||
|
&& wget -qO- https://github.com/novnc/websockify/archive/refs/tags/v0.11.0.tar.gz | tar xz --strip 1 -C /opt/noVNC/utils/websockify \
|
||||||
|
&& ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy backend requirements and install
|
# Copy backend requirements and install
|
||||||
|
|
@ -60,20 +71,23 @@ COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
||||||
# Create cache directory
|
# Create cache directory
|
||||||
RUN mkdir -p /app/cache && chmod 777 /app/cache
|
RUN mkdir -p /app/cache && chmod 777 /app/cache
|
||||||
|
|
||||||
|
# Copy supervisor config
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV CACHE_DIR=/app/cache
|
ENV CACHE_DIR=/app/cache
|
||||||
|
ENV DISPLAY=:99
|
||||||
|
|
||||||
# Set working directory to backend for correct imports
|
# Set working directory to backend for correct imports
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
# Expose port
|
# Expose ports (8002 = app, 6080 = noVNC)
|
||||||
EXPOSE 8002
|
EXPOSE 8002 6080
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
CMD curl -f http://localhost:8002/health || exit 1
|
CMD curl -f http://localhost:8002/health || exit 1
|
||||||
|
|
||||||
# Start the application with xvfb for headless browser support
|
# Start services using supervisor
|
||||||
CMD ["sh", "-c", "xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' python -m uvicorn main:app --host 0.0.0.0 --port 8002"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,3 +115,30 @@ async def logout():
|
||||||
if os.path.exists(COOKIES_FILE):
|
if os.path.exists(COOKIES_FILE):
|
||||||
os.remove(COOKIES_FILE)
|
os.remove(COOKIES_FILE)
|
||||||
return {"status": "success", "message": "Logged out"}
|
return {"status": "success", "message": "Logged out"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start-vnc")
|
||||||
|
async def start_vnc_login():
|
||||||
|
"""
|
||||||
|
Start VNC login - opens a visible browser via noVNC.
|
||||||
|
Users interact with the browser stream to login.
|
||||||
|
"""
|
||||||
|
result = await PlaywrightManager.start_vnc_login()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/check-vnc")
|
||||||
|
async def check_vnc_login():
|
||||||
|
"""
|
||||||
|
Check if VNC login is complete (sessionid cookie detected).
|
||||||
|
Frontend polls this endpoint.
|
||||||
|
"""
|
||||||
|
result = await PlaywrightManager.check_vnc_login()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stop-vnc")
|
||||||
|
async def stop_vnc_login():
|
||||||
|
"""Stop the VNC login browser."""
|
||||||
|
result = await PlaywrightManager.stop_vnc_login()
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,17 @@ class PlaywrightManager:
|
||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
|
"--start-maximized",
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
# VNC login state (class-level to persist across requests)
|
||||||
|
_vnc_playwright = None
|
||||||
|
_vnc_browser = None
|
||||||
|
_vnc_context = None
|
||||||
|
_vnc_page = None
|
||||||
|
_vnc_active = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_json_credentials(json_creds: dict) -> tuple[List[dict], str]:
|
def parse_json_credentials(json_creds: dict) -> tuple[List[dict], str]:
|
||||||
|
|
@ -118,6 +126,106 @@ class PlaywrightManager:
|
||||||
with open(USER_AGENT_FILE, "w") as f:
|
with open(USER_AGENT_FILE, "w") as f:
|
||||||
json.dump({"user_agent": user_agent}, f)
|
json.dump({"user_agent": user_agent}, f)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def start_vnc_login(cls) -> dict:
|
||||||
|
"""
|
||||||
|
Start a visible browser for VNC login.
|
||||||
|
The browser displays on DISPLAY=:99 which is streamed via noVNC.
|
||||||
|
Returns immediately - browser stays open for user interaction.
|
||||||
|
"""
|
||||||
|
# Close any existing VNC session
|
||||||
|
if cls._vnc_active:
|
||||||
|
await cls.stop_vnc_login()
|
||||||
|
|
||||||
|
print("DEBUG: Starting VNC login browser...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls._vnc_playwright = await async_playwright().start()
|
||||||
|
cls._vnc_browser = await cls._vnc_playwright.chromium.launch(
|
||||||
|
headless=False, # Visible browser
|
||||||
|
args=cls.BROWSER_ARGS
|
||||||
|
)
|
||||||
|
|
||||||
|
cls._vnc_context = await cls._vnc_browser.new_context(
|
||||||
|
user_agent=cls.DEFAULT_USER_AGENT,
|
||||||
|
viewport={"width": 1200, "height": 750}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls._vnc_page = await cls._vnc_context.new_page()
|
||||||
|
await cls._vnc_page.goto("https://www.tiktok.com/login", wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
cls._vnc_active = True
|
||||||
|
print("DEBUG: VNC browser opened with TikTok login page")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "started",
|
||||||
|
"message": "Browser opened. Please login via the VNC stream."
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: VNC login start error: {e}")
|
||||||
|
cls._vnc_active = False
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Failed to start browser: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def check_vnc_login(cls) -> dict:
|
||||||
|
"""
|
||||||
|
Check if user has logged in by looking for sessionid cookie.
|
||||||
|
Called by frontend via polling.
|
||||||
|
"""
|
||||||
|
if not cls._vnc_active or not cls._vnc_context:
|
||||||
|
return {"status": "not_active", "logged_in": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_cookies = await cls._vnc_context.cookies()
|
||||||
|
cookies_found = {}
|
||||||
|
|
||||||
|
for cookie in all_cookies:
|
||||||
|
if cookie.get("domain", "").endswith("tiktok.com"):
|
||||||
|
cookies_found[cookie["name"]] = cookie["value"]
|
||||||
|
|
||||||
|
if "sessionid" in cookies_found:
|
||||||
|
# Save cookies and close browser
|
||||||
|
cls.save_credentials(cookies_found, cls.DEFAULT_USER_AGENT)
|
||||||
|
await cls.stop_vnc_login()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"logged_in": True,
|
||||||
|
"message": "Login successful!",
|
||||||
|
"cookie_count": len(cookies_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"status": "waiting", "logged_in": False}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: VNC check error: {e}")
|
||||||
|
return {"status": "error", "logged_in": False, "message": str(e)}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def stop_vnc_login(cls) -> dict:
|
||||||
|
"""Close the VNC browser session."""
|
||||||
|
print("DEBUG: Stopping VNC login browser...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if cls._vnc_browser:
|
||||||
|
await cls._vnc_browser.close()
|
||||||
|
if cls._vnc_playwright:
|
||||||
|
await cls._vnc_playwright.stop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error closing VNC browser: {e}")
|
||||||
|
|
||||||
|
cls._vnc_browser = None
|
||||||
|
cls._vnc_context = None
|
||||||
|
cls._vnc_page = None
|
||||||
|
cls._vnc_playwright = None
|
||||||
|
cls._vnc_active = False
|
||||||
|
|
||||||
|
return {"status": "stopped"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def credential_login(username: str, password: str, timeout_seconds: int = 60) -> dict:
|
async def credential_login(username: str, password: str, timeout_seconds: int = 60) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,17 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8002:8002"
|
- "8002:8002"
|
||||||
|
- "6080:6080"
|
||||||
volumes:
|
volumes:
|
||||||
# Persist video cache
|
- ./cache:/app/cache
|
||||||
- purestream_cache:/app/cache
|
- ./session:/app/backend/session
|
||||||
# Persist login session (optional - for persistent TikTok login)
|
|
||||||
- purestream_session:/app/backend/session
|
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- CACHE_DIR=/app/cache
|
- CACHE_DIR=/app/cache
|
||||||
- MAX_CACHE_SIZE_MB=500
|
- MAX_CACHE_SIZE_MB=500
|
||||||
- CACHE_TTL_HOURS=24
|
- CACHE_TTL_HOURS=24
|
||||||
# Required for Playwright browser
|
|
||||||
shm_size: '2gb'
|
shm_size: '2gb'
|
||||||
# Use custom network to avoid IP conflicts
|
# IMPORTANT: You must link the service to the network
|
||||||
networks:
|
networks:
|
||||||
- purestream_net
|
- purestream_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
@ -29,16 +27,11 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
|
||||||
volumes:
|
|
||||||
purestream_cache:
|
|
||||||
driver: local
|
|
||||||
purestream_session:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
purestream_net:
|
purestream_net:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ipam:
|
ipam:
|
||||||
driver: default
|
driver: default
|
||||||
config:
|
config:
|
||||||
- subnet: 172.28.0.0/16
|
# Using 10.10.0.0 is much safer and less likely to overlap
|
||||||
|
- subnet: 10.10.0.0/16
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { API_BASE_URL } from '../config';
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
@ -7,12 +7,20 @@ export const Login: React.FC = () => {
|
||||||
const [sessionId, setSessionId] = useState('');
|
const [sessionId, setSessionId] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loginStatus, setLoginStatus] = useState('');
|
const [showVnc, setShowVnc] = useState(false);
|
||||||
|
const [vncStatus, setVncStatus] = useState('');
|
||||||
|
const pollIntervalRef = useRef<number | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
return () => {
|
||||||
|
// Cleanup polling on unmount
|
||||||
|
if (pollIntervalRef.current) {
|
||||||
|
clearInterval(pollIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
|
|
@ -22,37 +30,70 @@ export const Login: React.FC = () => {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Not authenticated, stay on login
|
// Not authenticated
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try browser login (opens TikTok in server's Playwright browser)
|
// Get the VNC URL (same host, port 6080)
|
||||||
const handleBrowserLogin = async () => {
|
const getVncUrl = () => {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
return `http://${host}:6080/vnc.html?autoconnect=true&resize=scale`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start VNC login
|
||||||
|
const handleVncLogin = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setLoginStatus('Opening TikTok login... Please wait');
|
setVncStatus('Starting browser...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(`${API_BASE_URL}/auth/browser-login`, {}, {
|
const res = await axios.post(`${API_BASE_URL}/auth/start-vnc`);
|
||||||
timeout: 200000 // 3+ minutes timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.data.status === 'success') {
|
if (res.data.status === 'started') {
|
||||||
setLoginStatus('Success! Redirecting...');
|
setShowVnc(true);
|
||||||
setTimeout(() => navigate('/'), 1000);
|
setIsLoading(false);
|
||||||
} else {
|
setVncStatus('Login on the browser below, then wait...');
|
||||||
setError(res.data.message || 'Login timed out. Please try the manual method.');
|
|
||||||
|
// Start polling for login completion
|
||||||
|
pollIntervalRef.current = window.setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const checkRes = await axios.get(`${API_BASE_URL}/auth/check-vnc`);
|
||||||
|
|
||||||
|
if (checkRes.data.logged_in) {
|
||||||
|
// Success!
|
||||||
|
clearInterval(pollIntervalRef.current!);
|
||||||
|
setVncStatus('Success! Redirecting...');
|
||||||
|
setTimeout(() => navigate('/'), 1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('VNC check error:', err);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
setError(res.data.message || 'Failed to start browser');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setLoginStatus('');
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError('Connection failed. Please try the manual method below.');
|
setError(err.response?.data?.detail || 'Failed to start VNC login');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setLoginStatus('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manual sessionid login
|
// Cancel VNC login
|
||||||
|
const handleCancelVnc = async () => {
|
||||||
|
if (pollIntervalRef.current) {
|
||||||
|
clearInterval(pollIntervalRef.current);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axios.post(`${API_BASE_URL}/auth/stop-vnc`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to stop VNC:', err);
|
||||||
|
}
|
||||||
|
setShowVnc(false);
|
||||||
|
setVncStatus('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manual sessionid login (fallback)
|
||||||
const handleManualLogin = async () => {
|
const handleManualLogin = async () => {
|
||||||
if (!sessionId.trim()) return;
|
if (!sessionId.trim()) return;
|
||||||
|
|
||||||
|
|
@ -71,19 +112,57 @@ export const Login: React.FC = () => {
|
||||||
if (res.data.status === 'success') {
|
if (res.data.status === 'success') {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} else {
|
} else {
|
||||||
setError('Invalid session ID. Please try again.');
|
setError('Invalid session ID.');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError('Connection failed. Please check your session ID.');
|
setError('Connection failed.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// VNC View
|
||||||
|
if (showVnc) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex-shrink-0 p-4 bg-gray-900 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-r from-cyan-400 to-pink-500 rounded-lg flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white text-sm font-medium">Login to TikTok</p>
|
||||||
|
<p className="text-gray-400 text-xs">{vncStatus}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelVnc}
|
||||||
|
className="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VNC Iframe */}
|
||||||
|
<div className="flex-1 bg-gray-900">
|
||||||
|
<iframe
|
||||||
|
src={getVncUrl()}
|
||||||
|
className="w-full h-full border-0"
|
||||||
|
title="TikTok Login"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login View
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-black to-gray-950 flex flex-col">
|
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-black to-gray-950 flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 pt-8 pb-4 px-6 text-center">
|
<div className="flex-shrink-0 pt-10 pb-6 px-6 text-center">
|
||||||
<div className="relative inline-block mb-3">
|
<div className="relative inline-block mb-3">
|
||||||
<div className="w-14 h-14 bg-gradient-to-r from-cyan-400 to-pink-500 rounded-2xl rotate-12 absolute -inset-1 blur-lg opacity-50" />
|
<div className="w-14 h-14 bg-gradient-to-r from-cyan-400 to-pink-500 rounded-2xl rotate-12 absolute -inset-1 blur-lg opacity-50" />
|
||||||
<div className="relative w-14 h-14 bg-gradient-to-r from-cyan-400 to-pink-500 rounded-2xl flex items-center justify-center">
|
<div className="relative w-14 h-14 bg-gradient-to-r from-cyan-400 to-pink-500 rounded-2xl flex items-center justify-center">
|
||||||
|
|
@ -105,78 +184,64 @@ export const Login: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Primary: Browser Login Button */}
|
{/* Primary: VNC Login Button */}
|
||||||
<div className="mb-6">
|
<button
|
||||||
<button
|
onClick={handleVncLogin}
|
||||||
onClick={handleBrowserLogin}
|
disabled={isLoading}
|
||||||
disabled={isLoading}
|
className={`w-full py-4 rounded-2xl font-bold text-base flex items-center justify-center gap-3 transition-all ${isLoading
|
||||||
className={`w-full py-4 rounded-2xl font-bold text-base flex items-center justify-center gap-3 transition-all ${isLoading
|
? 'bg-gray-700 text-gray-400 cursor-not-allowed'
|
||||||
? 'bg-gray-700 text-gray-400 cursor-not-allowed'
|
: 'bg-gradient-to-r from-pink-500 to-orange-500 text-white shadow-lg shadow-pink-500/25 hover:shadow-pink-500/40 active:scale-[0.98]'
|
||||||
: 'bg-gradient-to-r from-pink-500 to-orange-500 text-white shadow-lg shadow-pink-500/25 hover:shadow-pink-500/40'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{isLoading ? (
|
||||||
{isLoading ? (
|
<>
|
||||||
<>
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
{vncStatus || 'Please wait...'}
|
||||||
{loginStatus || 'Please wait...'}
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64c.32 0 .6.05.88.13V9.4c-.3-.04-.6-.05-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z" />
|
||||||
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64c.32 0 .6.05.88.13V9.4c-.3-.04-.6-.05-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z" />
|
</svg>
|
||||||
</svg>
|
Login with TikTok
|
||||||
Login with TikTok
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</button>
|
||||||
</button>
|
<p className="text-gray-600 text-xs text-center mt-3">
|
||||||
<p className="text-gray-600 text-xs text-center mt-2">
|
Opens a secure browser window for you to login
|
||||||
Opens TikTok login - complete the login and wait
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="flex items-center gap-3 my-6">
|
<div className="flex items-center gap-3 my-8">
|
||||||
<div className="flex-1 h-px bg-white/10" />
|
<div className="flex-1 h-px bg-white/10" />
|
||||||
<span className="text-gray-500 text-xs">or paste manually</span>
|
<span className="text-gray-600 text-xs">or enter manually</span>
|
||||||
<div className="flex-1 h-px bg-white/10" />
|
<div className="flex-1 h-px bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manual Method */}
|
{/* Manual Method */}
|
||||||
<div className="bg-white/5 rounded-xl p-4">
|
<div className="bg-white/5 rounded-xl p-4">
|
||||||
<p className="text-gray-400 text-xs mb-3">
|
<p className="text-gray-500 text-xs mb-3">
|
||||||
If the button above doesn't work, paste your TikTok <code className="text-pink-400 bg-pink-500/20 px-1 rounded">sessionid</code> cookie:
|
Paste your TikTok <code className="text-pink-400 bg-pink-500/20 px-1 rounded">sessionid</code> cookie:
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={sessionId}
|
value={sessionId}
|
||||||
onChange={(e) => setSessionId(e.target.value)}
|
onChange={(e) => setSessionId(e.target.value)}
|
||||||
placeholder="Paste sessionid here..."
|
placeholder="Paste sessionid..."
|
||||||
className="w-full bg-black border border-white/10 rounded-lg p-3 text-white text-sm font-mono focus:outline-none focus:border-pink-500/50 placeholder:text-gray-600 mb-3"
|
className="w-full bg-black border border-white/10 rounded-lg p-3 text-white text-sm font-mono focus:outline-none focus:border-pink-500/50 placeholder:text-gray-600 mb-3"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleManualLogin}
|
onClick={handleManualLogin}
|
||||||
disabled={!sessionId.trim() || isLoading}
|
disabled={!sessionId.trim() || isLoading}
|
||||||
className={`w-full py-3 rounded-lg font-medium text-sm transition-all ${sessionId.trim() && !isLoading
|
className={`w-full py-2.5 rounded-lg font-medium text-sm transition-all ${sessionId.trim() && !isLoading
|
||||||
? 'bg-white/10 hover:bg-white/20 text-white'
|
? 'bg-white/10 hover:bg-white/20 text-white'
|
||||||
: 'bg-white/5 text-gray-500 cursor-not-allowed'
|
: 'bg-white/5 text-gray-600 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Help */}
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<a
|
|
||||||
href="https://www.youtube.com/results?search_query=how+to+get+tiktok+sessionid+cookie"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-500 text-xs underline hover:text-gray-400"
|
|
||||||
>
|
|
||||||
How to get sessionid →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
43
supervisord.conf
Normal file
43
supervisord.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/dev/stdout
|
||||||
|
logfile_maxbytes=0
|
||||||
|
loglevel=info
|
||||||
|
|
||||||
|
[program:xvfb]
|
||||||
|
command=Xvfb :99 -screen 0 1280x800x24
|
||||||
|
autorestart=true
|
||||||
|
priority=100
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:x11vnc]
|
||||||
|
command=x11vnc -display :99 -forever -shared -rfbport 5900 -nopw -xkb
|
||||||
|
autorestart=true
|
||||||
|
priority=200
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:novnc]
|
||||||
|
command=/opt/noVNC/utils/novnc_proxy --vnc localhost:5900 --listen 6080
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:app]
|
||||||
|
command=python -m uvicorn main:app --host 0.0.0.0 --port 8002
|
||||||
|
directory=/app/backend
|
||||||
|
environment=DISPLAY=":99",PYTHONUNBUFFERED="1"
|
||||||
|
autorestart=true
|
||||||
|
priority=400
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
Loading…
Reference in a new issue