feat: multi-provider OAuth2 login page and generic routes
Replace hardcoded Gitea OAuth handlers with generic {name}-parameterized
routes and update the login page to render a button for each configured
provider via oauth_mod.get_providers().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+44
-28
@@ -625,15 +625,18 @@ async def start(
|
||||
elif request.rel_url.query.get("error"):
|
||||
error = "Sign-in failed. Please try again."
|
||||
|
||||
gitea_button = ""
|
||||
if oauth_mod.is_enabled(config):
|
||||
logo_url = config.get("oauth", {}).get("gitea", {}).get("logo", "")
|
||||
logo_img = f'<img src="{logo_url}" alt="" class="gitea-logo">' if logo_url else ""
|
||||
gitea_button = f"""
|
||||
<div class="divider">or</div>
|
||||
<a href="/login/oauth/gitea" class="gitea-btn">
|
||||
{logo_img}Sign in with Gitea
|
||||
oauth_buttons = ""
|
||||
_providers = oauth_mod.get_providers(config)
|
||||
if _providers:
|
||||
buttons_html = ""
|
||||
for _p in _providers:
|
||||
_logo = f'<img src="{_p.logo}" alt="" class="oauth-logo">' if _p.logo else ""
|
||||
buttons_html += f"""
|
||||
<a href="/login/oauth/{_p.name}" class="oauth-btn">
|
||||
{_logo}{_p.label}
|
||||
</a>"""
|
||||
oauth_buttons = f"""
|
||||
<div class="divider">or</div>{buttons_html}"""
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -656,12 +659,12 @@ async def start(
|
||||
.field {{ margin-bottom: .9em; }}
|
||||
.divider {{ text-align: center; margin: 1.2em 0 .8em; color: #999;
|
||||
font-size: .85em; border-top: 1px solid #eee; padding-top: .8em; }}
|
||||
.gitea-btn {{ display: flex; align-items: center; justify-content: center;
|
||||
.oauth-btn {{ display: flex; align-items: center; justify-content: center;
|
||||
gap: .5em; width: 100%; padding: .6em; background: #16191d;
|
||||
color: #fff; border-radius: 4px; font-size: 1em; text-align: center;
|
||||
text-decoration: none; box-sizing: border-box; }}
|
||||
.gitea-btn:hover {{ background: #4e7d1e; }}
|
||||
.gitea-logo {{ height: 1.2em; width: auto; vertical-align: middle; }}
|
||||
text-decoration: none; box-sizing: border-box; margin-top: .5em; }}
|
||||
.oauth-btn:hover {{ background: #444; }}
|
||||
.oauth-logo {{ height: 1.2em; width: auto; vertical-align: middle; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -672,7 +675,7 @@ async def start(
|
||||
<div class="field"><label>Username</label><input name="username" autofocus></div>
|
||||
<div class="field"><label>Password</label><input name="password" type="password"></div>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>{gitea_button}
|
||||
</form>{oauth_buttons}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -918,21 +921,34 @@ async def start(
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
def _oauth_redirect_uri(request) -> str:
|
||||
def _oauth_redirect_uri(request, provider_name: str) -> str:
|
||||
base = config.get("base_url", "").rstrip("/") or str(request.url.origin())
|
||||
return f"{base}/login/oauth/gitea/callback"
|
||||
return f"{base}/login/oauth/{provider_name}/callback"
|
||||
|
||||
async def oauth_gitea_redirect(request):
|
||||
"""GET /login/oauth/gitea — kick off the Gitea OAuth2 flow."""
|
||||
if not oauth_mod.is_enabled(config):
|
||||
return web.Response(status=404, text="OAuth not configured")
|
||||
def _get_oauth_provider(name: str):
|
||||
"""Return the ResolvedProvider for *name*, or None if not found."""
|
||||
return next(
|
||||
(p for p in oauth_mod.get_providers(config) if p.name == name),
|
||||
None,
|
||||
)
|
||||
|
||||
async def oauth_redirect(request):
|
||||
"""GET /login/oauth/{name} — kick off the OAuth2 flow for the named provider."""
|
||||
name = request.match_info["name"]
|
||||
provider = _get_oauth_provider(name)
|
||||
if provider is None:
|
||||
return web.Response(status=404, text="OAuth provider not found")
|
||||
state = oauth_mod.make_state()
|
||||
raise web.HTTPFound(oauth_mod.authorization_url(config, state, _oauth_redirect_uri(request)))
|
||||
raise web.HTTPFound(
|
||||
oauth_mod.build_auth_url(provider, state, _oauth_redirect_uri(request, name))
|
||||
)
|
||||
|
||||
async def oauth_gitea_callback(request):
|
||||
"""GET /login/oauth/gitea/callback — handle Gitea's redirect back."""
|
||||
if not oauth_mod.is_enabled(config):
|
||||
return web.Response(status=404, text="OAuth not configured")
|
||||
async def oauth_callback(request):
|
||||
"""GET /login/oauth/{name}/callback — handle the provider's redirect back."""
|
||||
name = request.match_info["name"]
|
||||
provider = _get_oauth_provider(name)
|
||||
if provider is None:
|
||||
return web.Response(status=404, text="OAuth provider not found")
|
||||
code = request.rel_url.query.get("code", "")
|
||||
state = request.rel_url.query.get("state", "")
|
||||
if not code or not state:
|
||||
@@ -941,8 +957,8 @@ async def start(
|
||||
logger.warning("OAuth: invalid or expired state token from %s", request.remote)
|
||||
raise web.HTTPFound("/login?error=1")
|
||||
try:
|
||||
token = await oauth_mod.exchange_code(config, code, _oauth_redirect_uri(request))
|
||||
profile = await oauth_mod.fetch_user(config, token)
|
||||
token = await oauth_mod.exchange_code(provider, code, _oauth_redirect_uri(request, name))
|
||||
profile = await oauth_mod.fetch_user(provider, token)
|
||||
except oauth_mod.OAuthError as exc:
|
||||
logger.warning("OAuth error: %s", exc)
|
||||
raise web.HTTPFound("/login?error=1")
|
||||
@@ -973,8 +989,8 @@ async def start(
|
||||
web.get("/logout", web_logout),
|
||||
web.post("/api/0/auth/login", api_login),
|
||||
web.post("/api/0/auth/logout", api_logout),
|
||||
web.get("/login/oauth/gitea", oauth_gitea_redirect),
|
||||
web.get("/login/oauth/gitea/callback", oauth_gitea_callback),
|
||||
web.get("/login/oauth/{name}", oauth_redirect),
|
||||
web.get("/login/oauth/{name}/callback", oauth_callback),
|
||||
# Users
|
||||
web.get("/api/0/users", api_users),
|
||||
web.get("/api/0/users/me", api_user_self),
|
||||
|
||||
Reference in New Issue
Block a user