refactor: retire interim magic-link/open-link in favor of password gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-63
@@ -69,7 +69,7 @@ from backend.templates_config import templates
|
||||
# Client-portal auth: an unauthenticated portal request renders the access page
|
||||
# (HTML routes) or returns 401 JSON (/portal/api/* routes). Centralized so every
|
||||
# portal route can simply Depends(get_current_client).
|
||||
from backend.portal_auth import PortalAuthError, PORTAL_OPEN_LINKS
|
||||
from backend.portal_auth import PortalAuthError
|
||||
|
||||
@app.exception_handler(PortalAuthError)
|
||||
async def portal_auth_handler(request: Request, exc: PortalAuthError):
|
||||
@@ -410,83 +410,24 @@ async def project_detail_page(request: Request, project_id: str):
|
||||
return templates.TemplateResponse("projects/detail.html", {
|
||||
"request": request,
|
||||
"project_id": project_id,
|
||||
"portal_open_links": PORTAL_OPEN_LINKS,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/projects/{project_id}/portal-preview")
|
||||
async def project_portal_preview(project_id: str, db: Session = Depends(get_db)):
|
||||
"""Operator testing shortcut: log into the client portal scoped to this project
|
||||
(auto-provisioning a client/link if needed), no CLI. Lives under /projects (not
|
||||
/portal), so a public proxy that exposes only /portal/* won't expose this."""
|
||||
"""Operator testing shortcut: open this project's client portal (no CLI)."""
|
||||
from backend.models import Project
|
||||
from backend.portal_auth import (
|
||||
provision_preview_session, make_session_cookie, COOKIE_NAME, COOKIE_MAX_AGE,
|
||||
)
|
||||
from backend.portal_auth import mint_portal_session, make_session_cookie, COOKIE_NAME, COOKIE_MAX_AGE
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
return JSONResponse(status_code=404, content={"detail": "Project not found"})
|
||||
token_id = provision_preview_session(project, db)
|
||||
token_id = mint_portal_session(project, db)
|
||||
resp = RedirectResponse(url="/portal", status_code=303)
|
||||
resp.set_cookie(COOKIE_NAME, make_session_cookie(token_id),
|
||||
max_age=COOKIE_MAX_AGE, httponly=True, samesite="lax")
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/projects/{project_id}/portal-link")
|
||||
async def project_portal_link_create(project_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""Mint a fresh shareable client link for this project's client. Returns the
|
||||
full /portal/enter/<token> URL (shown once). Operator-only (internal app)."""
|
||||
from backend.models import Project
|
||||
from backend.portal_auth import ensure_project_client, mint_link_token
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
return JSONResponse(status_code=404, content={"detail": "Project not found"})
|
||||
client = ensure_project_client(project, db)
|
||||
raw = mint_link_token(client, db, label="shared link")
|
||||
url = str(request.base_url).rstrip("/") + f"/portal/enter/{raw}"
|
||||
return {"url": url, "client_name": client.name}
|
||||
|
||||
|
||||
@app.get("/projects/{project_id}/portal-links")
|
||||
async def project_portal_links_list(project_id: str, db: Session = Depends(get_db)):
|
||||
"""List active (non-revoked) shareable links for this project's client."""
|
||||
from backend.models import Project, ClientAccessToken, Client
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project or not project.client_id:
|
||||
return {"client_name": None, "links": []}
|
||||
client = db.query(Client).filter_by(id=project.client_id).first()
|
||||
toks = (db.query(ClientAccessToken)
|
||||
.filter_by(client_id=project.client_id, revoked_at=None)
|
||||
.order_by(ClientAccessToken.created_at.desc()).all())
|
||||
return {
|
||||
"client_name": client.name if client else None,
|
||||
"links": [{
|
||||
"id": t.id, "label": t.label,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
"last_used_at": t.last_used_at.isoformat() if t.last_used_at else None,
|
||||
} for t in toks],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/projects/{project_id}/portal-link/{token_id}/revoke")
|
||||
async def project_portal_link_revoke(project_id: str, token_id: str, db: Session = Depends(get_db)):
|
||||
"""Revoke one shareable link (scoped to this project's client). Kills the link
|
||||
and any live session minted from it on the next request."""
|
||||
from datetime import datetime as _dt
|
||||
from backend.models import Project, ClientAccessToken
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project or not project.client_id:
|
||||
return JSONResponse(status_code=404, content={"detail": "Not found"})
|
||||
tok = db.query(ClientAccessToken).filter_by(id=token_id, client_id=project.client_id).first()
|
||||
if not tok:
|
||||
return JSONResponse(status_code=404, content={"detail": "Link not found"})
|
||||
if not tok.revoked_at:
|
||||
tok.revoked_at = _dt.utcnow()
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/projects/{project_id}/portal-access")
|
||||
async def project_portal_access_state(project_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""Current portal-access state for the operator panel."""
|
||||
|
||||
Reference in New Issue
Block a user