ha-mcp-dev 6.3.1.dev137__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ha_mcp/__init__.py +51 -0
- ha_mcp/__main__.py +753 -0
- ha_mcp/_pypi_marker +0 -0
- ha_mcp/auth/__init__.py +10 -0
- ha_mcp/auth/consent_form.py +501 -0
- ha_mcp/auth/provider.py +786 -0
- ha_mcp/client/__init__.py +10 -0
- ha_mcp/client/rest_client.py +924 -0
- ha_mcp/client/websocket_client.py +656 -0
- ha_mcp/client/websocket_listener.py +340 -0
- ha_mcp/config.py +175 -0
- ha_mcp/errors.py +399 -0
- ha_mcp/py.typed +0 -0
- ha_mcp/resources/card_types.json +48 -0
- ha_mcp/resources/dashboard_guide.md +573 -0
- ha_mcp/server.py +189 -0
- ha_mcp/smoke_test.py +108 -0
- ha_mcp/tools/__init__.py +11 -0
- ha_mcp/tools/backup.py +457 -0
- ha_mcp/tools/device_control.py +687 -0
- ha_mcp/tools/enhanced.py +191 -0
- ha_mcp/tools/helpers.py +184 -0
- ha_mcp/tools/registry.py +199 -0
- ha_mcp/tools/smart_search.py +797 -0
- ha_mcp/tools/tools_addons.py +343 -0
- ha_mcp/tools/tools_areas.py +478 -0
- ha_mcp/tools/tools_blueprints.py +279 -0
- ha_mcp/tools/tools_bug_report.py +570 -0
- ha_mcp/tools/tools_calendar.py +391 -0
- ha_mcp/tools/tools_camera.py +149 -0
- ha_mcp/tools/tools_config_automations.py +508 -0
- ha_mcp/tools/tools_config_dashboards.py +1502 -0
- ha_mcp/tools/tools_config_entry_flow.py +223 -0
- ha_mcp/tools/tools_config_helpers.py +925 -0
- ha_mcp/tools/tools_config_info.py +258 -0
- ha_mcp/tools/tools_config_scripts.py +267 -0
- ha_mcp/tools/tools_entities.py +76 -0
- ha_mcp/tools/tools_filesystem.py +589 -0
- ha_mcp/tools/tools_groups.py +306 -0
- ha_mcp/tools/tools_hacs.py +787 -0
- ha_mcp/tools/tools_history.py +714 -0
- ha_mcp/tools/tools_integrations.py +297 -0
- ha_mcp/tools/tools_labels.py +713 -0
- ha_mcp/tools/tools_mcp_component.py +305 -0
- ha_mcp/tools/tools_registry.py +1115 -0
- ha_mcp/tools/tools_resources.py +622 -0
- ha_mcp/tools/tools_search.py +573 -0
- ha_mcp/tools/tools_service.py +211 -0
- ha_mcp/tools/tools_services.py +352 -0
- ha_mcp/tools/tools_system.py +363 -0
- ha_mcp/tools/tools_todo.py +530 -0
- ha_mcp/tools/tools_traces.py +496 -0
- ha_mcp/tools/tools_updates.py +476 -0
- ha_mcp/tools/tools_utility.py +519 -0
- ha_mcp/tools/tools_voice_assistant.py +345 -0
- ha_mcp/tools/tools_zones.py +387 -0
- ha_mcp/tools/util_helpers.py +199 -0
- ha_mcp/utils/__init__.py +30 -0
- ha_mcp/utils/domain_handlers.py +380 -0
- ha_mcp/utils/fuzzy_search.py +339 -0
- ha_mcp/utils/operation_manager.py +442 -0
- ha_mcp/utils/usage_logger.py +290 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_constants.py +15 -0
- tests/test_env_manager.py +336 -0
ha_mcp/_pypi_marker
ADDED
|
File without changes
|
ha_mcp/auth/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth 2.1 authentication for Home Assistant MCP Server.
|
|
3
|
+
|
|
4
|
+
This module provides OAuth 2.1 authentication with Dynamic Client Registration (DCR)
|
|
5
|
+
and a consent form for collecting Home Assistant credentials.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .provider import HomeAssistantOAuthProvider
|
|
9
|
+
|
|
10
|
+
__all__ = ["HomeAssistantOAuthProvider"]
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Consent form HTML template for Home Assistant OAuth authentication.
|
|
3
|
+
|
|
4
|
+
This module provides the HTML consent form where users enter their
|
|
5
|
+
Home Assistant URL and Long-Lived Access Token (LLAT).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_consent_html(
|
|
10
|
+
client_id: str,
|
|
11
|
+
client_name: str | None,
|
|
12
|
+
redirect_uri: str,
|
|
13
|
+
state: str,
|
|
14
|
+
scopes: list[str],
|
|
15
|
+
error_message: str | None = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Generate HTML consent form for Home Assistant authentication.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
client_id: OAuth client ID
|
|
22
|
+
client_name: Human-readable client name
|
|
23
|
+
redirect_uri: OAuth redirect URI
|
|
24
|
+
state: OAuth state parameter
|
|
25
|
+
scopes: Requested OAuth scopes
|
|
26
|
+
error_message: Optional error message to display
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
HTML string for the consent form
|
|
30
|
+
"""
|
|
31
|
+
display_name = client_name or client_id
|
|
32
|
+
scopes_display = ", ".join(scopes) if scopes else "full access"
|
|
33
|
+
|
|
34
|
+
error_html = ""
|
|
35
|
+
if error_message:
|
|
36
|
+
error_html = f"""
|
|
37
|
+
<div class="error-message">
|
|
38
|
+
<strong>Error:</strong> {error_message}
|
|
39
|
+
</div>
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
return f"""
|
|
43
|
+
<!DOCTYPE html>
|
|
44
|
+
<html lang="en">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8">
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
48
|
+
<title>Connect to Home Assistant</title>
|
|
49
|
+
<style>
|
|
50
|
+
:root {{
|
|
51
|
+
--primary-color: #03a9f4;
|
|
52
|
+
--primary-hover: #0288d1;
|
|
53
|
+
--error-color: #f44336;
|
|
54
|
+
--error-bg: #ffebee;
|
|
55
|
+
--success-color: #4caf50;
|
|
56
|
+
--text-color: #212121;
|
|
57
|
+
--text-secondary: #757575;
|
|
58
|
+
--border-color: #e0e0e0;
|
|
59
|
+
--bg-color: #fafafa;
|
|
60
|
+
--card-bg: #ffffff;
|
|
61
|
+
}}
|
|
62
|
+
|
|
63
|
+
@media (prefers-color-scheme: dark) {{
|
|
64
|
+
:root {{
|
|
65
|
+
--primary-color: #29b6f6;
|
|
66
|
+
--primary-hover: #4fc3f7;
|
|
67
|
+
--error-color: #ef5350;
|
|
68
|
+
--error-bg: #3e2723;
|
|
69
|
+
--success-color: #66bb6a;
|
|
70
|
+
--text-color: #e0e0e0;
|
|
71
|
+
--text-secondary: #9e9e9e;
|
|
72
|
+
--border-color: #424242;
|
|
73
|
+
--bg-color: #121212;
|
|
74
|
+
--card-bg: #1e1e1e;
|
|
75
|
+
}}
|
|
76
|
+
}}
|
|
77
|
+
|
|
78
|
+
* {{
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding: 0;
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
body {{
|
|
85
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
86
|
+
background-color: var(--bg-color);
|
|
87
|
+
color: var(--text-color);
|
|
88
|
+
min-height: 100vh;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
padding: 20px;
|
|
93
|
+
}}
|
|
94
|
+
|
|
95
|
+
.container {{
|
|
96
|
+
background: var(--card-bg);
|
|
97
|
+
border-radius: 16px;
|
|
98
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
99
|
+
max-width: 440px;
|
|
100
|
+
width: 100%;
|
|
101
|
+
padding: 32px;
|
|
102
|
+
}}
|
|
103
|
+
|
|
104
|
+
.header {{
|
|
105
|
+
text-align: center;
|
|
106
|
+
margin-bottom: 24px;
|
|
107
|
+
}}
|
|
108
|
+
|
|
109
|
+
.logo {{
|
|
110
|
+
width: 80px;
|
|
111
|
+
height: 80px;
|
|
112
|
+
margin-bottom: 16px;
|
|
113
|
+
}}
|
|
114
|
+
|
|
115
|
+
h1 {{
|
|
116
|
+
font-size: 24px;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
margin-bottom: 8px;
|
|
119
|
+
}}
|
|
120
|
+
|
|
121
|
+
.subtitle {{
|
|
122
|
+
color: var(--text-secondary);
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
}}
|
|
125
|
+
|
|
126
|
+
.client-info {{
|
|
127
|
+
background: var(--bg-color);
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
padding: 16px;
|
|
130
|
+
margin-bottom: 24px;
|
|
131
|
+
}}
|
|
132
|
+
|
|
133
|
+
.client-info p {{
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
color: var(--text-secondary);
|
|
136
|
+
}}
|
|
137
|
+
|
|
138
|
+
.client-info strong {{
|
|
139
|
+
color: var(--text-color);
|
|
140
|
+
}}
|
|
141
|
+
|
|
142
|
+
.scopes {{
|
|
143
|
+
margin-top: 8px;
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
color: var(--text-secondary);
|
|
146
|
+
}}
|
|
147
|
+
|
|
148
|
+
.error-message {{
|
|
149
|
+
background: var(--error-bg);
|
|
150
|
+
border: 1px solid var(--error-color);
|
|
151
|
+
border-radius: 8px;
|
|
152
|
+
padding: 12px 16px;
|
|
153
|
+
margin-bottom: 20px;
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
color: var(--error-color);
|
|
156
|
+
}}
|
|
157
|
+
|
|
158
|
+
.form-group {{
|
|
159
|
+
margin-bottom: 20px;
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
label {{
|
|
163
|
+
display: block;
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
margin-bottom: 8px;
|
|
167
|
+
}}
|
|
168
|
+
|
|
169
|
+
input[type="text"],
|
|
170
|
+
input[type="url"],
|
|
171
|
+
input[type="password"] {{
|
|
172
|
+
width: 100%;
|
|
173
|
+
padding: 12px 16px;
|
|
174
|
+
font-size: 16px;
|
|
175
|
+
border: 1px solid var(--border-color);
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
background: var(--card-bg);
|
|
178
|
+
color: var(--text-color);
|
|
179
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
180
|
+
}}
|
|
181
|
+
|
|
182
|
+
input:focus {{
|
|
183
|
+
outline: none;
|
|
184
|
+
border-color: var(--primary-color);
|
|
185
|
+
box-shadow: 0 0 0 3px rgba(3, 169, 244, 0.1);
|
|
186
|
+
}}
|
|
187
|
+
|
|
188
|
+
input::placeholder {{
|
|
189
|
+
color: var(--text-secondary);
|
|
190
|
+
}}
|
|
191
|
+
|
|
192
|
+
.help-text {{
|
|
193
|
+
font-size: 12px;
|
|
194
|
+
color: var(--text-secondary);
|
|
195
|
+
margin-top: 6px;
|
|
196
|
+
}}
|
|
197
|
+
|
|
198
|
+
.help-text a {{
|
|
199
|
+
color: var(--primary-color);
|
|
200
|
+
text-decoration: none;
|
|
201
|
+
}}
|
|
202
|
+
|
|
203
|
+
.help-text a:hover {{
|
|
204
|
+
text-decoration: underline;
|
|
205
|
+
}}
|
|
206
|
+
|
|
207
|
+
.button-group {{
|
|
208
|
+
display: flex;
|
|
209
|
+
gap: 12px;
|
|
210
|
+
margin-top: 24px;
|
|
211
|
+
}}
|
|
212
|
+
|
|
213
|
+
button {{
|
|
214
|
+
flex: 1;
|
|
215
|
+
padding: 14px 24px;
|
|
216
|
+
font-size: 16px;
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition: all 0.2s;
|
|
221
|
+
}}
|
|
222
|
+
|
|
223
|
+
.btn-primary {{
|
|
224
|
+
background: var(--primary-color);
|
|
225
|
+
color: white;
|
|
226
|
+
border: none;
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
.btn-primary:hover {{
|
|
230
|
+
background: var(--primary-hover);
|
|
231
|
+
}}
|
|
232
|
+
|
|
233
|
+
.btn-primary:disabled {{
|
|
234
|
+
background: var(--border-color);
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
}}
|
|
237
|
+
|
|
238
|
+
.btn-secondary {{
|
|
239
|
+
background: transparent;
|
|
240
|
+
color: var(--text-color);
|
|
241
|
+
border: 1px solid var(--border-color);
|
|
242
|
+
}}
|
|
243
|
+
|
|
244
|
+
.btn-secondary:hover {{
|
|
245
|
+
background: var(--bg-color);
|
|
246
|
+
}}
|
|
247
|
+
|
|
248
|
+
.security-note {{
|
|
249
|
+
margin-top: 20px;
|
|
250
|
+
padding: 12px;
|
|
251
|
+
background: var(--bg-color);
|
|
252
|
+
border-radius: 8px;
|
|
253
|
+
font-size: 12px;
|
|
254
|
+
color: var(--text-secondary);
|
|
255
|
+
text-align: center;
|
|
256
|
+
}}
|
|
257
|
+
|
|
258
|
+
.security-note svg {{
|
|
259
|
+
width: 14px;
|
|
260
|
+
height: 14px;
|
|
261
|
+
vertical-align: middle;
|
|
262
|
+
margin-right: 4px;
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
.loading {{
|
|
266
|
+
display: none;
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
.loading.active {{
|
|
270
|
+
display: inline-block;
|
|
271
|
+
}}
|
|
272
|
+
|
|
273
|
+
@keyframes spin {{
|
|
274
|
+
to {{ transform: rotate(360deg); }}
|
|
275
|
+
}}
|
|
276
|
+
|
|
277
|
+
.spinner {{
|
|
278
|
+
width: 16px;
|
|
279
|
+
height: 16px;
|
|
280
|
+
border: 2px solid transparent;
|
|
281
|
+
border-top-color: currentColor;
|
|
282
|
+
border-radius: 50%;
|
|
283
|
+
animation: spin 0.8s linear infinite;
|
|
284
|
+
display: inline-block;
|
|
285
|
+
vertical-align: middle;
|
|
286
|
+
margin-right: 8px;
|
|
287
|
+
}}
|
|
288
|
+
</style>
|
|
289
|
+
</head>
|
|
290
|
+
<body>
|
|
291
|
+
<div class="container">
|
|
292
|
+
<div class="header">
|
|
293
|
+
<svg class="logo" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
|
|
294
|
+
<path fill="#18BCF2" d="M120 0C53.7 0 0 53.7 0 120s53.7 120 120 120 120-53.7 120-120S186.3 0 120 0zm0 220c-55.2 0-100-44.8-100-100S64.8 20 120 20s100 44.8 100 100-44.8 100-100 100z"/>
|
|
295
|
+
<path fill="#18BCF2" d="M120 40c-44.1 0-80 35.9-80 80s35.9 80 80 80 80-35.9 80-80-35.9-80-80-80zm0 140c-33.1 0-60-26.9-60-60s26.9-60 60-60 60 26.9 60 60-26.9 60-60 60z"/>
|
|
296
|
+
<circle fill="#18BCF2" cx="120" cy="120" r="40"/>
|
|
297
|
+
</svg>
|
|
298
|
+
<h1>Connect to Home Assistant</h1>
|
|
299
|
+
<p class="subtitle">Authorize {display_name} to access your smart home</p>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="client-info">
|
|
303
|
+
<p>Application: <strong>{display_name}</strong></p>
|
|
304
|
+
<p class="scopes">Requested access: <strong>{scopes_display}</strong></p>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{error_html}
|
|
308
|
+
|
|
309
|
+
<form method="POST" id="consent-form">
|
|
310
|
+
<input type="hidden" name="client_id" value="{client_id}">
|
|
311
|
+
<input type="hidden" name="redirect_uri" value="{redirect_uri}">
|
|
312
|
+
<input type="hidden" name="state" value="{state}">
|
|
313
|
+
|
|
314
|
+
<div class="form-group">
|
|
315
|
+
<label for="ha_url">Home Assistant URL</label>
|
|
316
|
+
<input
|
|
317
|
+
type="url"
|
|
318
|
+
id="ha_url"
|
|
319
|
+
name="ha_url"
|
|
320
|
+
placeholder="https://homeassistant.local:8123"
|
|
321
|
+
required
|
|
322
|
+
autocomplete="url"
|
|
323
|
+
>
|
|
324
|
+
<p class="help-text">
|
|
325
|
+
The URL of your Home Assistant instance (e.g., http://homeassistant.local:8123)
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div class="form-group">
|
|
330
|
+
<label for="ha_token">Long-Lived Access Token</label>
|
|
331
|
+
<input
|
|
332
|
+
type="password"
|
|
333
|
+
id="ha_token"
|
|
334
|
+
name="ha_token"
|
|
335
|
+
placeholder="Enter your access token"
|
|
336
|
+
required
|
|
337
|
+
autocomplete="off"
|
|
338
|
+
>
|
|
339
|
+
<p class="help-text">
|
|
340
|
+
Create a token at: Home Assistant → Profile →
|
|
341
|
+
<a href="https://www.home-assistant.io/docs/authentication/#your-account-profile" target="_blank" rel="noopener">
|
|
342
|
+
Long-Lived Access Tokens
|
|
343
|
+
</a>
|
|
344
|
+
</p>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="button-group">
|
|
348
|
+
<button type="button" class="btn-secondary" onclick="window.close(); history.back();">
|
|
349
|
+
Cancel
|
|
350
|
+
</button>
|
|
351
|
+
<button type="submit" class="btn-primary" id="submit-btn">
|
|
352
|
+
<span class="loading" id="loading">
|
|
353
|
+
<span class="spinner"></span>
|
|
354
|
+
</span>
|
|
355
|
+
<span id="btn-text">Authorize</span>
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
</form>
|
|
359
|
+
|
|
360
|
+
<div class="security-note">
|
|
361
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
362
|
+
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
|
|
363
|
+
</svg>
|
|
364
|
+
Your credentials are validated directly with your Home Assistant instance
|
|
365
|
+
and stored securely for this session only.
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<script>
|
|
370
|
+
document.getElementById('consent-form').addEventListener('submit', function(e) {{
|
|
371
|
+
var btn = document.getElementById('submit-btn');
|
|
372
|
+
var loading = document.getElementById('loading');
|
|
373
|
+
var btnText = document.getElementById('btn-text');
|
|
374
|
+
|
|
375
|
+
btn.disabled = true;
|
|
376
|
+
loading.classList.add('active');
|
|
377
|
+
btnText.textContent = 'Validating...';
|
|
378
|
+
}});
|
|
379
|
+
|
|
380
|
+
// Try to detect Home Assistant URL from common patterns
|
|
381
|
+
(function() {{
|
|
382
|
+
var savedUrl = localStorage.getItem('ha_mcp_url');
|
|
383
|
+
if (savedUrl) {{
|
|
384
|
+
document.getElementById('ha_url').value = savedUrl;
|
|
385
|
+
}}
|
|
386
|
+
}})();
|
|
387
|
+
|
|
388
|
+
// Save URL on successful form navigation
|
|
389
|
+
document.getElementById('ha_url').addEventListener('change', function(e) {{
|
|
390
|
+
if (e.target.value) {{
|
|
391
|
+
localStorage.setItem('ha_mcp_url', e.target.value);
|
|
392
|
+
}}
|
|
393
|
+
}});
|
|
394
|
+
</script>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def create_error_html(error: str, error_description: str) -> str:
|
|
401
|
+
"""
|
|
402
|
+
Generate HTML error page for OAuth errors.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
error: OAuth error code
|
|
406
|
+
error_description: Human-readable error description
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
HTML string for the error page
|
|
410
|
+
"""
|
|
411
|
+
return f"""
|
|
412
|
+
<!DOCTYPE html>
|
|
413
|
+
<html lang="en">
|
|
414
|
+
<head>
|
|
415
|
+
<meta charset="UTF-8">
|
|
416
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
417
|
+
<title>Authentication Error</title>
|
|
418
|
+
<style>
|
|
419
|
+
:root {{
|
|
420
|
+
--error-color: #f44336;
|
|
421
|
+
--text-color: #212121;
|
|
422
|
+
--text-secondary: #757575;
|
|
423
|
+
--bg-color: #fafafa;
|
|
424
|
+
--card-bg: #ffffff;
|
|
425
|
+
}}
|
|
426
|
+
|
|
427
|
+
@media (prefers-color-scheme: dark) {{
|
|
428
|
+
:root {{
|
|
429
|
+
--error-color: #ef5350;
|
|
430
|
+
--text-color: #e0e0e0;
|
|
431
|
+
--text-secondary: #9e9e9e;
|
|
432
|
+
--bg-color: #121212;
|
|
433
|
+
--card-bg: #1e1e1e;
|
|
434
|
+
}}
|
|
435
|
+
}}
|
|
436
|
+
|
|
437
|
+
body {{
|
|
438
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
439
|
+
background-color: var(--bg-color);
|
|
440
|
+
color: var(--text-color);
|
|
441
|
+
min-height: 100vh;
|
|
442
|
+
display: flex;
|
|
443
|
+
align-items: center;
|
|
444
|
+
justify-content: center;
|
|
445
|
+
padding: 20px;
|
|
446
|
+
margin: 0;
|
|
447
|
+
}}
|
|
448
|
+
|
|
449
|
+
.container {{
|
|
450
|
+
background: var(--card-bg);
|
|
451
|
+
border-radius: 16px;
|
|
452
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
453
|
+
max-width: 440px;
|
|
454
|
+
width: 100%;
|
|
455
|
+
padding: 32px;
|
|
456
|
+
text-align: center;
|
|
457
|
+
}}
|
|
458
|
+
|
|
459
|
+
.error-icon {{
|
|
460
|
+
width: 64px;
|
|
461
|
+
height: 64px;
|
|
462
|
+
margin-bottom: 16px;
|
|
463
|
+
color: var(--error-color);
|
|
464
|
+
}}
|
|
465
|
+
|
|
466
|
+
h1 {{
|
|
467
|
+
font-size: 24px;
|
|
468
|
+
font-weight: 600;
|
|
469
|
+
margin-bottom: 8px;
|
|
470
|
+
color: var(--error-color);
|
|
471
|
+
}}
|
|
472
|
+
|
|
473
|
+
.error-code {{
|
|
474
|
+
font-family: monospace;
|
|
475
|
+
background: var(--bg-color);
|
|
476
|
+
padding: 4px 8px;
|
|
477
|
+
border-radius: 4px;
|
|
478
|
+
font-size: 14px;
|
|
479
|
+
margin-bottom: 16px;
|
|
480
|
+
display: inline-block;
|
|
481
|
+
}}
|
|
482
|
+
|
|
483
|
+
p {{
|
|
484
|
+
color: var(--text-secondary);
|
|
485
|
+
font-size: 14px;
|
|
486
|
+
line-height: 1.6;
|
|
487
|
+
}}
|
|
488
|
+
</style>
|
|
489
|
+
</head>
|
|
490
|
+
<body>
|
|
491
|
+
<div class="container">
|
|
492
|
+
<svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
493
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
494
|
+
</svg>
|
|
495
|
+
<h1>Authentication Error</h1>
|
|
496
|
+
<div class="error-code">{error}</div>
|
|
497
|
+
<p>{error_description}</p>
|
|
498
|
+
</div>
|
|
499
|
+
</body>
|
|
500
|
+
</html>
|
|
501
|
+
"""
|