Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/common_utils/html_forms/native_client_consent.py: 33%
14 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1from collections.abc import Sequence
2from html import escape
3from typing import Final
5from litellm.constants import CLI_JWT_EXPIRATION_HOURS
8def render_native_client_consent_page(
9 *,
10 client_origin: str,
11 user_id: str,
12 teams: Sequence[tuple[str, str]],
13 flow_handle: str,
14 complete_url: str,
15) -> str:
16 """The consent page a native client's sign-in lands on: who is signed in, which
17 loopback client asked, which team the credential is attributed to, and an explicit
18 Approve or Deny that POSTs back to ``complete_url``. Every value is client- or
19 user-influenced and HTML-escaped; the flow handle travels only in the form body."""
20 return f"""<!DOCTYPE html>
21<html lang="en">
22<head>
23<meta charset="UTF-8">
24<meta name="viewport" content="width=device-width, initial-scale=1.0">
25<meta name="referrer" content="no-referrer">
26<title>Authorize CLI access - LiteLLM</title>
27<style>
28body {{
29 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
30 background-color: #f8fafc;
31 margin: 0;
32 padding: 20px;
33 display: flex;
34 justify-content: center;
35 align-items: center;
36 min-height: 100vh;
37 color: #1e293b;
38}}
39.container {{
40 background-color: #fff;
41 padding: 40px;
42 border-radius: 8px;
43 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
44 width: 450px;
45 max-width: 100%;
46}}
47h1 {{ margin: 0 0 16px; font-size: 24px; font-weight: 600; }}
48p {{ margin: 0 0 12px; line-height: 1.5; }}
49code {{ background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }}
50label {{ display: block; margin: 16px 0 6px; font-weight: 600; }}
51select {{ width: 100%; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }}
52.actions {{ display: flex; gap: 12px; margin-top: 24px; }}
53button {{ flex: 1; padding: 10px; border-radius: 6px; font-size: 15px; cursor: pointer; border: 1px solid #cbd5e1; }}
54.approve {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
55.deny {{ background: #fff; color: #1e293b; }}
56</style>
57</head>
58<body>
59<div class="container">
60<h1>Authorize CLI access</h1>
61<p>A command-line client at <code>{escape(client_origin)}</code> wants to call LiteLLM as <strong>{escape(user_id)}</strong>.</p>
62<p>Approving issues it a personal credential that expires within {CLI_JWT_EXPIRATION_HOURS} hours. <code>lite logout</code> stops it from being renewed. Only approve if you started this sign-in yourself.</p>
63<form method="post" action="{escape(complete_url)}">
64<input type="hidden" name="flow" value="{escape(flow_handle)}">
65{_team_field(teams)}
66<div class="actions">
67<button type="submit" name="decision" value="deny" class="deny">Deny</button>
68<button type="submit" name="decision" value="approve" class="approve">Approve</button>
69</div>
70</form>
71</div>
72</body>
73</html>
74"""
77def _team_field(teams: Sequence[tuple[str, str]]) -> str:
78 if not teams:
79 return ""
80 if len(teams) == 1:
81 team_id, team_label = teams[0]
82 return (
83 f'<input type="hidden" name="team_id" value="{escape(team_id)}">'
84 f"<p>Requests are attributed to team <strong>{escape(team_label)}</strong>.</p>"
85 )
86 options: Final = "".join(
87 f'<option value="{escape(team_id)}">{escape(team_label)}</option>' for team_id, team_label in teams
88 )
89 return (
90 f'<label for="team_id">Attribute requests to team</label><select id="team_id" name="team_id">{options}</select>'
91 )