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