Skip to content

Commit fe69523

Browse files
JihaoXinclaude
andcommitted
Add Arabic language support and globe dropdown to portal
- app.html: replace lang toggle button with 🌐 dropdown (EN / 中文 / العربية) - Add full Arabic (ar) translation for all UI strings - RTL support: dir="rtl" + Cairo font when Arabic is selected - Date locale switches to ar-SA for Arabic - Login logo: use logo_ark_transparent.png (120px, no background) - Add Google OAuth login UI (btn-google, login-divider) - Update submodule pointer (webpage: logo, spacing, paper meta fixes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 145726d commit fe69523

File tree

4 files changed

+327
-32
lines changed

4 files changed

+327
-32
lines changed

ark/webapp/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def _env_file() -> Path:
2828
"SLURM_PARTITION": "",
2929
"SLURM_ACCOUNT": "",
3030
"SLURM_CONDA_ENV": "ark",
31+
"GOOGLE_CLIENT_ID": "",
32+
"GOOGLE_CLIENT_SECRET": "",
3133
}
3234

3335

@@ -70,6 +72,11 @@ def _write_default_env():
7072
# Admin emails (comma-separated). Admins can disable/enable the webapp and kill all jobs.
7173
ADMIN_EMAILS=
7274
75+
# Google OAuth (optional). Get credentials at console.cloud.google.com → APIs & Services → Credentials.
76+
# Redirect URI to register: {BASE_URL}/auth/google/callback
77+
GOOGLE_CLIENT_ID=
78+
GOOGLE_CLIENT_SECRET=
79+
7380
PROJECTS_ROOT={_root / 'ark_webapp' / 'projects'}
7481
SECRET_KEY={secrets.token_hex(32)}
7582
DB_PATH={_root / 'ark_webapp' / 'webapp.db'}
@@ -117,6 +124,9 @@ def __init__(self):
117124
raw_admins = merged.get("ADMIN_EMAILS", "")
118125
self.admin_emails: list[str] = [e.strip().lower() for e in raw_admins.split(",") if e.strip()]
119126

127+
self.google_client_id: str = merged.get("GOOGLE_CLIENT_ID", "")
128+
self.google_client_secret: str = merged.get("GOOGLE_CLIENT_SECRET", "")
129+
120130
self.projects_root.mkdir(parents=True, exist_ok=True)
121131

122132

ark/webapp/routes.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,32 @@ def _disabled_flag() -> _Path:
3434
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
3535
from starlette.requests import Request
3636

37+
from authlib.integrations.starlette_client import OAuth as _OAuth
38+
3739
from .auth import make_token, verify_token
3840
from .config import get_settings
41+
42+
# Lazy-initialized Google OAuth client
43+
_google_oauth: _OAuth | None = None
44+
45+
46+
def _get_google_oauth() -> _OAuth | None:
47+
"""Return authlib OAuth client if Google credentials are configured."""
48+
global _google_oauth
49+
if _google_oauth is not None:
50+
return _google_oauth
51+
settings = get_settings()
52+
if not settings.google_client_id or not settings.google_client_secret:
53+
return None
54+
_google_oauth = _OAuth()
55+
_google_oauth.register(
56+
name="google",
57+
client_id=settings.google_client_id,
58+
client_secret=settings.google_client_secret,
59+
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
60+
client_kwargs={"scope": "openid email profile"},
61+
)
62+
return _google_oauth
3963
from .db import (
4064
Project,
4165
User,
@@ -363,6 +387,86 @@ async def auth_logout(request: Request):
363387
return RedirectResponse("/")
364388

365389

390+
_GOOGLE_REDIRECT_URI = "https://kaust-ark.github.io/oauth-callback"
391+
392+
393+
@router.get("/auth/google")
394+
async def auth_google(request: Request):
395+
oauth = _get_google_oauth()
396+
if not oauth:
397+
raise HTTPException(400, "Google login is not configured on this server.")
398+
return await oauth.google.authorize_redirect(request, _GOOGLE_REDIRECT_URI)
399+
400+
401+
@router.get("/auth/google/callback")
402+
async def auth_google_callback(request: Request):
403+
oauth = _get_google_oauth()
404+
if not oauth:
405+
raise HTTPException(400, "Google login is not configured on this server.")
406+
settings = get_settings()
407+
try:
408+
token = await oauth.google.authorize_access_token(request)
409+
except Exception as exc:
410+
logger.warning(f"Google OAuth error: {exc}")
411+
return RedirectResponse("/?google_error=1")
412+
413+
userinfo = token.get("userinfo") or {}
414+
email = (userinfo.get("email") or "").strip().lower()
415+
if not email:
416+
return RedirectResponse("/?google_error=1")
417+
418+
# Apply same allow-list checks as magic link
419+
denied = False
420+
if settings.allowed_emails:
421+
if email not in settings.allowed_emails:
422+
denied = True
423+
elif settings.email_domains:
424+
if email.split("@")[-1] not in settings.email_domains:
425+
denied = True
426+
427+
if denied:
428+
return HTMLResponse(
429+
f"""<!DOCTYPE html>
430+
<html>
431+
<head>
432+
<meta charset="utf-8" />
433+
<title>Access Denied — ARK</title>
434+
<style>
435+
body {{ font-family: sans-serif; display: flex; align-items: center; justify-content: center;
436+
min-height: 100vh; margin: 0; background: #f0fdfa; }}
437+
.card {{ background: #fff; border-radius: 16px; padding: 48px 52px; max-width: 420px;
438+
box-shadow: 0 4px 24px rgba(0,0,0,.08); text-align: center; }}
439+
h2 {{ color: #991b1b; margin-bottom: 12px; }}
440+
p {{ color: #555; line-height: 1.6; }}
441+
a {{ color: #0d9488; }}
442+
.back {{ margin-top: 24px; display: inline-block; color: #0d9488; font-size: .9rem; }}
443+
</style>
444+
</head>
445+
<body>
446+
<div class="card">
447+
<h2>Access Denied</h2>
448+
<p>Your Google account (<strong>{email}</strong>) is not authorized to access ARK.</p>
449+
<p>To request access, contact<br/>
450+
<a href="mailto:jihao.xin@kaust.edu.sa">jihao.xin@kaust.edu.sa</a></p>
451+
<a class="back" href="/">← Back to login</a>
452+
</div>
453+
</body>
454+
</html>""",
455+
status_code=403,
456+
)
457+
458+
with get_session(settings.db_path) as session:
459+
user = get_or_create_user_by_email(session, email)
460+
request.session["user_id"] = user.id
461+
return RedirectResponse("/")
462+
463+
464+
@router.get("/auth/google/enabled")
465+
async def auth_google_enabled():
466+
"""Frontend polls this to know whether to show Google button."""
467+
return JSONResponse({"enabled": _get_google_oauth() is not None})
468+
469+
366470
@router.get("/api/me")
367471
async def api_me(request: Request):
368472
user = _get_current_user(request)

0 commit comments

Comments
 (0)