Skip to content

Commit 8354ca8

Browse files
rustyconoverclaude
andcommitted
Theme OAuth error and 404 pages to match homepage styling with VGI logo
Extract shared HTML style constants (_FONT_IMPORTS, _ERROR_PAGE_STYLE, _VGI_LOGO_HTML) into _common.py and apply the branded theme (Inter/JetBrains Mono fonts, green color scheme, circular logo, footer) to the OAuth error page and 404 page, replacing the previous plain system-font styling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 06ca51e commit 8354ca8

File tree

3 files changed

+80
-47
lines changed

3 files changed

+80
-47
lines changed

vgi_rpc/http/_common.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,44 @@ def _decompress_body(data: bytes) -> bytes:
6666
return zstandard.ZstdDecompressor().decompress(data)
6767

6868

69+
# ---------------------------------------------------------------------------
70+
# Shared HTML styles for error and landing pages
71+
# ---------------------------------------------------------------------------
72+
73+
_FONT_IMPORTS = (
74+
'<link rel="preconnect" href="https://fonts.googleapis.com">'
75+
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
76+
'<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">' # noqa: E501
77+
)
78+
79+
_ERROR_PAGE_STYLE = """\
80+
<style>
81+
body {{ font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 600px;
82+
margin: 0 auto; padding: 60px 20px 0; color: #2c2c1e; text-align: center;
83+
background: #faf8f0; }}
84+
.logo {{ margin-bottom: 24px; }}
85+
.logo img {{ width: 120px; height: 120px; border-radius: 50%;
86+
box-shadow: 0 4px 24px rgba(0,0,0,0.12); }}
87+
h1 {{ color: #2d5016; margin-bottom: 8px; font-weight: 700; }}
88+
code {{ font-family: 'JetBrains Mono', monospace; background: #f0ece0;
89+
padding: 2px 6px; border-radius: 3px; font-size: 0.9em; color: #2c2c1e; }}
90+
a {{ color: #2d5016; text-decoration: none; }}
91+
a:hover {{ color: #4a7c23; }}
92+
p {{ line-height: 1.7; color: #6b6b5a; }}
93+
.detail {{ margin-top: 12px; padding: 12px 16px; background: #f0ece0;
94+
border-radius: 6px; font-size: 0.9em; color: #6b6b5a; }}
95+
footer {{ margin-top: 48px; padding: 20px 0; border-top: 1px solid #f0ece0;
96+
color: #6b6b5a; font-size: 0.85em; line-height: 1.8; }}
97+
footer a {{ color: #2d5016; font-weight: 600; }}
98+
footer a:hover {{ color: #4a7c23; }}
99+
</style>"""
100+
101+
_VGI_LOGO_HTML = """\
102+
<div class="logo">
103+
<img src="https://vgi-rpc-python.query.farm/assets/logo-hero.png" alt="vgi-rpc logo">
104+
</div>"""
105+
106+
69107
class _RpcHttpError(Exception):
70108
"""Internal exception for HTTP-layer errors with status codes."""
71109

vgi_rpc/http/_oauth_pkce.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
from vgi_rpc.rpc import AuthContext
3838

39+
from ._common import _ERROR_PAGE_STYLE, _FONT_IMPORTS, _VGI_LOGO_HTML
40+
3941
logger = logging.getLogger(__name__)
4042

4143
# ---------------------------------------------------------------------------
@@ -71,6 +73,24 @@ def _generate_state_nonce() -> str:
7173
return secrets.token_urlsafe(24)
7274

7375

76+
def _is_jwt_expired(token: str) -> bool:
77+
"""Check whether a JWT's exp claim is in the past. Returns False if not a JWT or can't decode."""
78+
try:
79+
parts = token.split(".")
80+
if len(parts) < 2:
81+
return False
82+
# Add padding for base64url
83+
payload_b64 = parts[1]
84+
payload_b64 += "=" * (-len(payload_b64) % 4)
85+
payload = json.loads(base64.urlsafe_b64decode(payload_b64))
86+
exp = payload.get("exp")
87+
if exp is None:
88+
return False
89+
return int(time.time()) >= int(exp)
90+
except Exception:
91+
return False
92+
93+
7494
# ---------------------------------------------------------------------------
7595
# Derived HMAC key — prevents cross-protocol forgery with stream state tokens
7696
# ---------------------------------------------------------------------------
@@ -367,30 +387,33 @@ def _validate_return_to(url: str, allowed_origins: frozenset[str] = frozenset())
367387
# Error HTML page
368388
# ---------------------------------------------------------------------------
369389

370-
_OAUTH_ERROR_HTML = """\
390+
_OAUTH_ERROR_HTML = (
391+
"""\
371392
<!DOCTYPE html>
372393
<html lang="en">
373394
<head>
374395
<meta charset="utf-8">
375396
<meta name="viewport" content="width=device-width, initial-scale=1">
376-
<title>Authentication Error</title>
377-
<style>
378-
body {{ font-family: system-ui, -apple-system, sans-serif; max-width: 600px;
379-
margin: 0 auto; padding: 60px 20px; color: #2c2c1e; text-align: center;
380-
background: #faf8f0; }}
381-
h1 {{ color: #8b0000; }}
382-
.detail {{ background: #f0ece0; padding: 12px 20px; border-radius: 6px;
383-
font-family: monospace; margin: 20px 0; text-align: left; }}
384-
a {{ color: #2d5016; }}
385-
</style>
397+
<title>Authentication Error &mdash; vgi-rpc</title>
398+
"""
399+
+ _FONT_IMPORTS
400+
+ _ERROR_PAGE_STYLE
401+
+ """
386402
</head>
387403
<body>
404+
"""
405+
+ _VGI_LOGO_HTML
406+
+ """
388407
<h1>Authentication Error</h1>
389408
<p>{message}</p>
390409
{detail}
391410
<p><a href="{retry_url}">Try again</a></p>
411+
<footer>
412+
Powered by <a href="https://vgi-rpc.query.farm"><code>vgi-rpc</code></a>
413+
</footer>
392414
</body>
393415
</html>"""
416+
)
394417

395418

396419
def _oauth_error_page(message: str, detail: str | None, retry_url: str) -> bytes:
@@ -700,10 +723,9 @@ def __init__(
700723
def process_request(self, req: falcon.Request, resp: falcon.Response) -> None:
701724
"""If the user is already authenticated and has _vgi_return_to, redirect immediately.
702725
703-
This relies on _AuthMiddleware running first (Falcon processes
704-
process_request in middleware list order). If the cookie token is
705-
expired or invalid, _AuthMiddleware raises 401 *before* we get here,
706-
so we only redirect when the token is genuinely valid.
726+
The landing page path is exempt from _AuthMiddleware, so we must
727+
check the JWT exp claim ourselves to avoid redirecting with an
728+
expired token (which would cause an infinite redirect loop).
707729
"""
708730
if req.method != "GET":
709731
return
@@ -714,6 +736,9 @@ def process_request(self, req: falcon.Request, resp: falcon.Response) -> None:
714736
token = req.cookies.get(_AUTH_COOKIE_NAME)
715737
if not token:
716738
return # Not authenticated — let normal flow handle it
739+
# Don't redirect with an expired token — let the OAuth flow run again
740+
if _is_jwt_expired(token):
741+
return
717742
# Already authenticated with a return_to — redirect back with the token
718743
separator = "#" if "#" not in return_to else "&"
719744
fragment_params = [f"token={token}"]

vgi_rpc/http/_server.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383

8484
from ._common import (
8585
_ARROW_CONTENT_TYPE,
86+
_ERROR_PAGE_STYLE,
87+
_FONT_IMPORTS,
8688
_MAX_UPLOAD_URL_COUNT,
8789
_UPLOAD_URL_METHOD,
8890
_UPLOAD_URL_SCHEMA,
@@ -1101,42 +1103,10 @@ def _unpack_and_recover_state(
11011103
return state_obj, output_schema, input_schema
11021104

11031105

1104-
# ---------------------------------------------------------------------------
1105-
# Shared HTML styles for error and landing pages
1106-
# ---------------------------------------------------------------------------
1107-
1108-
_FONT_IMPORTS = (
1109-
'<link rel="preconnect" href="https://fonts.googleapis.com">'
1110-
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
1111-
'<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">' # noqa: E501
1112-
)
1113-
11141106
# ---------------------------------------------------------------------------
11151107
# 404 sink for unmatched routes
11161108
# ---------------------------------------------------------------------------
11171109

1118-
_ERROR_PAGE_STYLE = """\
1119-
<style>
1120-
body {{ font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 600px;
1121-
margin: 0 auto; padding: 60px 20px 0; color: #2c2c1e; text-align: center;
1122-
background: #faf8f0; }}
1123-
.logo {{ margin-bottom: 24px; }}
1124-
.logo img {{ width: 120px; height: 120px; border-radius: 50%;
1125-
box-shadow: 0 4px 24px rgba(0,0,0,0.12); }}
1126-
h1 {{ color: #2d5016; margin-bottom: 8px; font-weight: 700; }}
1127-
code {{ font-family: 'JetBrains Mono', monospace; background: #f0ece0;
1128-
padding: 2px 6px; border-radius: 3px; font-size: 0.9em; color: #2c2c1e; }}
1129-
a {{ color: #2d5016; text-decoration: none; }}
1130-
a:hover {{ color: #4a7c23; }}
1131-
p {{ line-height: 1.7; color: #6b6b5a; }}
1132-
.detail {{ margin-top: 12px; padding: 12px 16px; background: #f0ece0;
1133-
border-radius: 6px; font-size: 0.9em; color: #6b6b5a; }}
1134-
footer {{ margin-top: 48px; padding: 20px 0; border-top: 1px solid #f0ece0;
1135-
color: #6b6b5a; font-size: 0.85em; line-height: 1.8; }}
1136-
footer a {{ color: #2d5016; font-weight: 600; }}
1137-
footer a:hover {{ color: #4a7c23; }}
1138-
</style>"""
1139-
11401110
_NOT_FOUND_HTML_TEMPLATE = (
11411111
"""\
11421112
<!DOCTYPE html>

0 commit comments

Comments
 (0)