feat: validate credentials on :CP <platform> login (#310)
## Problem `:CP <platform> login` blindly caches username/password without server-side validation. Bad credentials are only discovered at submit time, which is confusing and wastes a browser session. ## Solution Wire `:CP <platform> login` through the scraper pipeline so each platform actually authenticates before persisting credentials. On failure, the user sees an error and nothing is cached. - CSES: reuses `_check_token` (fast path) and `_web_login`; returns API token in `LoginResult.credentials` so subsequent submits skip re-auth. - AtCoder/Codeforces: new `_login_headless` functions open a StealthySession, solve Turnstile/Cloudflare, fill the login form, and validate success by checking for the logout link. Cookies only persist on confirmed login. - CodeChef/Kattis/USACO: return "not yet implemented" errors. - `scraper.lua`: generalizes submit-only guards (`needs_browser` flag) to cover both `submit` and `login` subcommands. - `credentials.lua`: prompts for username/password, passes cached token for CSES fast path, shows ndjson status notifications, only caches on success.
This commit is contained in:
parent
a202725cc5
commit
2c119774df
10 changed files with 356 additions and 108 deletions
|
|
@ -9,6 +9,7 @@ from .language_ids import get_language_id
|
|||
from .models import (
|
||||
CombinedTest,
|
||||
ContestListResult,
|
||||
LoginResult,
|
||||
MetadataResult,
|
||||
SubmitResult,
|
||||
TestsResult,
|
||||
|
|
@ -58,9 +59,12 @@ class BaseScraper(ABC):
|
|||
credentials: dict[str, str],
|
||||
) -> SubmitResult: ...
|
||||
|
||||
@abstractmethod
|
||||
async def login(self, credentials: dict[str, str]) -> LoginResult: ...
|
||||
|
||||
def _usage(self) -> str:
|
||||
name = self.platform_name
|
||||
return f"Usage: {name}.py metadata <id> | tests <id> | contests"
|
||||
return f"Usage: {name}.py metadata <id> | tests <id> | contests | login"
|
||||
|
||||
def _metadata_error(self, msg: str) -> MetadataResult:
|
||||
return MetadataResult(success=False, error=msg, url="")
|
||||
|
|
@ -82,6 +86,9 @@ class BaseScraper(ABC):
|
|||
def _submit_error(self, msg: str) -> SubmitResult:
|
||||
return SubmitResult(success=False, error=msg)
|
||||
|
||||
def _login_error(self, msg: str) -> LoginResult:
|
||||
return LoginResult(success=False, error=msg)
|
||||
|
||||
async def _run_cli_async(self, args: list[str]) -> int:
|
||||
if len(args) < 2:
|
||||
print(self._metadata_error(self._usage()).model_dump_json())
|
||||
|
|
@ -133,6 +140,16 @@ class BaseScraper(ABC):
|
|||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case "login":
|
||||
creds_raw = os.environ.get("CP_CREDENTIALS", "{}")
|
||||
try:
|
||||
credentials = json.loads(creds_raw)
|
||||
except json.JSONDecodeError:
|
||||
credentials = {}
|
||||
result = await self.login(credentials)
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case _:
|
||||
print(
|
||||
self._metadata_error(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue