Files
weircon-front/claudelogin.py
T
agw 6f21c07d91 feat(admin): log Claude ind igen fra telefonen
Claude Codes login på værten holder 28 dage, og et langtidstoken kan ikke
bruges til Remote Control, så der skal logges ind igen hver 4. uge. Hidtil
krævede det en terminal for at kopiere en adresse ud og en kode ind igen.

/admin/claude-login viser hvornår login udløber og har en "Start login"-knap.
Værten (claude-login-agent, som spørger portalen, ikke omvendt) starter
`claude auth login`, og siden viser login-adressen; koden fra Claudes side
indsættes på samme side og hentes af værten et par sekunder senere. Portalen
gemmer kun den korte kode, indtil den er hentet; den virker kun sammen med
login-processen på værten (PKCE).

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-03 08:32:29 +02:00

64 lines
2.3 KiB
Python

"""Claude-login fra telefonen: portalens halvdel af claude-login-agent.
Claude Code på værten skal logges ind igen hver 4. uge. Værten kan nås fra
portalen, men ikke omvendt, så det foregår gennem tre filer i DIR:
request portalen: "start et login" (værten sletter den, når den starter)
state.json værten: status, udløb og login-adressen mens et login venter
code portalen: koden fra Claudes side (værten henter og sletter den)
Selve login-processen kører på værten (claude-login-agent);
koden virker kun sammen med den (PKCE), så portalen gemmer ikke andet end den
korte kode, indtil værten henter den et par sekunder senere.
"""
import json
import os
import re
import time
import config
DIR = config.DATA / "claude-login"
KODE = re.compile(r"^[A-Za-z0-9._~#-]{10,600}$")
# Agenten skriver state.json mindst hvert 5. minut; er den ældre end det her,
# kører agenten på værten ikke.
GAMMEL = 12 * 60
def state() -> dict:
try:
st = json.loads((DIR / "state.json").read_text())
except (OSError, ValueError):
st = {"status": "ukendt"}
if (DIR / "request").exists() and st.get("status") not in ("venter", "tjekker"):
st = {**st, "status": "starter"}
if (DIR / "code").exists():
st = {**st, "status": "tjekker"}
# Mens et login venter, skriver agenten først igen når det slutter (maks 15 min).
graense = 16 * 60 if st.get("status") == "venter" else GAMMEL
st["stale"] = time.time() - st.get("updated", 0) > graense
return st
def request(by: str) -> None:
DIR.mkdir(mode=0o700, exist_ok=True)
tmp = DIR / ".request.tmp"
tmp.write_text(json.dumps({"by": by, "at": int(time.time())}))
os.replace(tmp, DIR / "request")
def submit_code(kode: str) -> str | None:
"""Gem koden til værten. Returnerer en fejltekst, eller None."""
kode = "".join((kode or "").split())
if not KODE.match(kode):
return "Det ligner ikke en login-kode. Kopiér hele koden fra Claudes side."
if state().get("status") != "venter":
return "Der er ikke et login i gang. Start et nyt."
DIR.mkdir(mode=0o700, exist_ok=True)
tmp = DIR / ".code.tmp"
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
f.write(kode)
os.replace(tmp, DIR / "code")
return None