feat: multi-provider OAuth callback + GitHub config + login buttons
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Callback now supports any provider (Gitea/GitHub) via providers.get_provider()
- OAuth tokens stored in user_oauth_tokens table per user + provider
- Login page shows both Gitea and GitHub OAuth buttons
- config.py: github_client_id + github_client_secret
- Auth flow no longer depends on gitea_oauth import — fully abstracted
- Fallback admin user now sets is_active=1 and admin@localhost email
This commit is contained in:
2026-07-10 15:44:19 -04:00
parent d4dcb06aee
commit 1f26d61997
2 changed files with 42 additions and 19 deletions
+4
View File
@@ -17,6 +17,10 @@ class Settings(BaseSettings):
gitea_oauth_client_secret: str = ""
gitea_webhook_secret: str = ""
# GitHub
github_client_id: str = ""
github_client_secret: str = ""
# OAuth2
oauth_redirect_uri: str = "http://localhost:8080/auth/callback"
+38 -19
View File
@@ -8,7 +8,6 @@ from fastapi import APIRouter, Request, Query
from fastapi.responses import RedirectResponse, HTMLResponse
from app.auth.session import SessionManager
from app.auth.oauth import gitea_oauth
from app.config import settings
logger = logging.getLogger(__name__)
@@ -62,7 +61,8 @@ body{background:#191919;color:#fff;font-family:-apple-system,BlinkMacSystemFont,
<button type="submit" class="btn btn-primary" id="submit-btn">Login</button>
</form>
<div class="oauth-section">
<button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'">🔗 Login with Gitea</button>
<button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'" style="margin-bottom:8px">🔗 Login with Gitea</button>
<button class="oauth-btn" onclick="window.location='/auth/login?provider=github'">🐙 Login with GitHub</button>
</div>
</div>
<script>
@@ -82,14 +82,19 @@ async def login(request: Request, provider: str = Query("gitea")):
from fastapi.responses import HTMLResponse
return HTMLResponse(LOCAL_LOGIN_HTML, status_code=200)
# OAuth flow
if not gitea_oauth.enabled:
# Fallback: use global token, create a fake session
# OAuth flow — check if provider is configured
from app.auth.providers import get_provider
oauth_provider = get_provider(provider)
if not oauth_provider:
# Fallback: no OAuth configured, create admin session
from app.db import get_conn
with get_conn() as conn:
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
if not user:
conn.execute("INSERT INTO users (login, full_name, email, avatar_url) VALUES ('admin', 'Admin', '', '')")
conn.execute(
"INSERT INTO users (login, full_name, email, avatar_url, is_active) "
"VALUES ('admin', 'Admin', 'admin@localhost', '', 1)"
)
conn.commit()
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
user_data = dict(user)
@@ -100,7 +105,8 @@ async def login(request: Request, provider: str = Query("gitea")):
state = secrets.token_hex(32)
request.session["oauth_state"] = state
auth_url = gitea_oauth.get_authorize_url(state)
request.session["oauth_provider"] = provider
auth_url = oauth_provider.get_authorize_url(state)
return RedirectResponse(url=auth_url, status_code=302)
@@ -207,11 +213,17 @@ async def callback(
"""Handle OAuth2 callback from Gitea."""
# Validate state
expected_state = request.session.get("oauth_state", "")
provider_name = request.session.get("oauth_provider", "gitea")
if not expected_state or state != expected_state:
return HTMLResponse("<h1>Invalid state</h1>", status_code=400)
from app.auth.providers import get_provider
oauth_provider = get_provider(provider_name)
if not oauth_provider:
return HTMLResponse(f"<h1>Unknown provider: {provider_name}</h1>", status_code=400)
# Exchange code for token
token_data = await gitea_oauth.exchange_code(code)
token_data = await oauth_provider.exchange_code(code)
if not token_data:
return HTMLResponse("<h1>Token exchange failed</h1>", status_code=400)
@@ -220,29 +232,36 @@ async def callback(
return HTMLResponse("<h1>No access token</h1>", status_code=400)
# Get user info
user = await gitea_oauth.get_user(access_token)
if not user:
oauth_user = await oauth_provider.get_user(access_token)
if not oauth_user:
return HTMLResponse("<h1>Failed to get user</h1>", status_code=400)
# Store user in DB
from app.db import get_conn
login_id = f"{provider_name}_{oauth_user['login']}"
with get_conn() as conn:
conn.execute(
"""INSERT INTO users (login, full_name, email, avatar_url)
VALUES (?, ?, ?, ?)
ON CONFLICT(login)
DO UPDATE SET full_name=excluded.full_name, email=excluded.email, avatar_url=excluded.avatar_url""",
(user["login"], user.get("full_name", ""), user.get("email", ""), user.get("avatar_url", "")),
(login_id, oauth_user.get("full_name", ""), oauth_user.get("email", ""), oauth_user.get("avatar_url", "")),
)
conn.commit()
# Store token
SessionManager.store_token(user["id"], access_token)
# Also store user in local DB
with get_conn() as conn:
db_user = conn.execute("SELECT * FROM users WHERE login=?", (user["login"],)).fetchone()
user_data = dict(db_user) if db_user else user
# Store OAuth token
uid = conn.execute("SELECT id FROM users WHERE login=?", (login_id,)).fetchone()
if uid:
conn.execute(
"""INSERT OR REPLACE INTO user_oauth_tokens
(user_id, provider, access_token, refresh_token, expires_at, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)""",
(uid["id"], provider_name, access_token, token_data.get("refresh_token"), token_data.get("expires_at")),
)
conn.commit()
user = conn.execute("SELECT * FROM users WHERE id=?", (uid["id"],)).fetchone()
else:
user = conn.execute("SELECT * FROM users WHERE login=?", (login_id,)).fetchone()
user_data = dict(user) if user else oauth_user
# Create session
session = SessionManager.create_session(user_data)