FossilRepo
Global OAuth callback URLs for GitHub/GitLab Callback URLs are now top-level (not per-project): - https://fossilrepo.io/oauth/callback/github/ - https://fossilrepo.io/oauth/callback/gitlab/ The state parameter carries the project slug so the callback redirects back to the correct project's Git mirror config page. This means one OAuth App configuration works for all projects.
Commit
9460009c81996484d6dbe1d49e46752ad5f30ac35c10f34e2cbe5d9d408f7cc9
Parent
be7265dce42fcd5…
2 files changed
+33
+3
-3
+33
| --- config/urls.py | ||
| +++ config/urls.py | ||
| @@ -2,12 +2,43 @@ | ||
| 2 | 2 | from datetime import UTC, datetime |
| 3 | 3 | |
| 4 | 4 | from django.conf import settings |
| 5 | 5 | from django.contrib import admin |
| 6 | 6 | from django.http import HttpResponse, JsonResponse |
| 7 | +from django.shortcuts import redirect as _redirect | |
| 7 | 8 | from django.urls import include, path |
| 8 | 9 | from django.views.generic import RedirectView |
| 10 | + | |
| 11 | + | |
| 12 | +def _oauth_github_callback(request): | |
| 13 | + """Global GitHub OAuth callback. Extracts slug from state param and delegates.""" | |
| 14 | + state = request.GET.get("state", "") | |
| 15 | + slug = state.split(":")[0] if ":" in state else "" | |
| 16 | + if not slug: | |
| 17 | + return _redirect("/dashboard/") | |
| 18 | + from fossil.oauth import github_exchange_token | |
| 19 | + | |
| 20 | + result = github_exchange_token(request, slug) | |
| 21 | + if result.get("token"): | |
| 22 | + request.session["github_oauth_token"] = result["token"] | |
| 23 | + request.session["github_oauth_user"] = result.get("username", "") | |
| 24 | + return _redirect(f"/projects/{slug}/fossil/sync/git/") | |
| 25 | + | |
| 26 | + | |
| 27 | +def _oauth_gitlab_callback(request): | |
| 28 | + """Global GitLab OAuth callback. Extracts slug from state param and delegates.""" | |
| 29 | + state = request.GET.get("state", "") | |
| 30 | + slug = state.split(":")[0] if ":" in state else "" | |
| 31 | + if not slug: | |
| 32 | + return _redirect("/dashboard/") | |
| 33 | + from fossil.oauth import gitlab_exchange_token | |
| 34 | + | |
| 35 | + result = gitlab_exchange_token(request, slug) | |
| 36 | + if result.get("token"): | |
| 37 | + request.session["gitlab_oauth_token"] = result["token"] | |
| 38 | + return _redirect(f"/projects/{slug}/fossil/sync/git/") | |
| 39 | + | |
| 9 | 40 | |
| 10 | 41 | admin.site.site_header = settings.ADMIN_SITE_HEADER |
| 11 | 42 | admin.site.site_title = settings.ADMIN_SITE_TITLE |
| 12 | 43 | admin.site.index_title = "Welcome to Fossilrepo" |
| 13 | 44 | |
| @@ -193,8 +224,10 @@ | ||
| 193 | 224 | path("settings/", include("organization.urls")), |
| 194 | 225 | path("projects/", include("projects.urls")), |
| 195 | 226 | path("projects/<slug:slug>/fossil/", include("fossil.urls")), |
| 196 | 227 | path("kb/", include("pages.urls")), |
| 197 | 228 | path("items/", include("items.urls")), |
| 229 | + path("oauth/callback/github/", _oauth_github_callback, name="oauth_github_callback_global"), | |
| 230 | + path("oauth/callback/gitlab/", _oauth_gitlab_callback, name="oauth_gitlab_callback_global"), | |
| 198 | 231 | path("admin/", admin.site.urls), |
| 199 | 232 | path("health/", health_check, name="health"), |
| 200 | 233 | ] |
| 201 | 234 |
| --- config/urls.py | |
| +++ config/urls.py | |
| @@ -2,12 +2,43 @@ | |
| 2 | from datetime import UTC, datetime |
| 3 | |
| 4 | from django.conf import settings |
| 5 | from django.contrib import admin |
| 6 | from django.http import HttpResponse, JsonResponse |
| 7 | from django.urls import include, path |
| 8 | from django.views.generic import RedirectView |
| 9 | |
| 10 | admin.site.site_header = settings.ADMIN_SITE_HEADER |
| 11 | admin.site.site_title = settings.ADMIN_SITE_TITLE |
| 12 | admin.site.index_title = "Welcome to Fossilrepo" |
| 13 | |
| @@ -193,8 +224,10 @@ | |
| 193 | path("settings/", include("organization.urls")), |
| 194 | path("projects/", include("projects.urls")), |
| 195 | path("projects/<slug:slug>/fossil/", include("fossil.urls")), |
| 196 | path("kb/", include("pages.urls")), |
| 197 | path("items/", include("items.urls")), |
| 198 | path("admin/", admin.site.urls), |
| 199 | path("health/", health_check, name="health"), |
| 200 | ] |
| 201 |
| --- config/urls.py | |
| +++ config/urls.py | |
| @@ -2,12 +2,43 @@ | |
| 2 | from datetime import UTC, datetime |
| 3 | |
| 4 | from django.conf import settings |
| 5 | from django.contrib import admin |
| 6 | from django.http import HttpResponse, JsonResponse |
| 7 | from django.shortcuts import redirect as _redirect |
| 8 | from django.urls import include, path |
| 9 | from django.views.generic import RedirectView |
| 10 | |
| 11 | |
| 12 | def _oauth_github_callback(request): |
| 13 | """Global GitHub OAuth callback. Extracts slug from state param and delegates.""" |
| 14 | state = request.GET.get("state", "") |
| 15 | slug = state.split(":")[0] if ":" in state else "" |
| 16 | if not slug: |
| 17 | return _redirect("/dashboard/") |
| 18 | from fossil.oauth import github_exchange_token |
| 19 | |
| 20 | result = github_exchange_token(request, slug) |
| 21 | if result.get("token"): |
| 22 | request.session["github_oauth_token"] = result["token"] |
| 23 | request.session["github_oauth_user"] = result.get("username", "") |
| 24 | return _redirect(f"/projects/{slug}/fossil/sync/git/") |
| 25 | |
| 26 | |
| 27 | def _oauth_gitlab_callback(request): |
| 28 | """Global GitLab OAuth callback. Extracts slug from state param and delegates.""" |
| 29 | state = request.GET.get("state", "") |
| 30 | slug = state.split(":")[0] if ":" in state else "" |
| 31 | if not slug: |
| 32 | return _redirect("/dashboard/") |
| 33 | from fossil.oauth import gitlab_exchange_token |
| 34 | |
| 35 | result = gitlab_exchange_token(request, slug) |
| 36 | if result.get("token"): |
| 37 | request.session["gitlab_oauth_token"] = result["token"] |
| 38 | return _redirect(f"/projects/{slug}/fossil/sync/git/") |
| 39 | |
| 40 | |
| 41 | admin.site.site_header = settings.ADMIN_SITE_HEADER |
| 42 | admin.site.site_title = settings.ADMIN_SITE_TITLE |
| 43 | admin.site.index_title = "Welcome to Fossilrepo" |
| 44 | |
| @@ -193,8 +224,10 @@ | |
| 224 | path("settings/", include("organization.urls")), |
| 225 | path("projects/", include("projects.urls")), |
| 226 | path("projects/<slug:slug>/fossil/", include("fossil.urls")), |
| 227 | path("kb/", include("pages.urls")), |
| 228 | path("items/", include("items.urls")), |
| 229 | path("oauth/callback/github/", _oauth_github_callback, name="oauth_github_callback_global"), |
| 230 | path("oauth/callback/gitlab/", _oauth_gitlab_callback, name="oauth_gitlab_callback_global"), |
| 231 | path("admin/", admin.site.urls), |
| 232 | path("health/", health_check, name="health"), |
| 233 | ] |
| 234 |
+3
-3
| --- fossil/oauth.py | ||
| +++ fossil/oauth.py | ||
| @@ -24,11 +24,11 @@ | ||
| 24 | 24 | |
| 25 | 25 | client_id = config.GITHUB_OAUTH_CLIENT_ID |
| 26 | 26 | if not client_id: |
| 27 | 27 | return None |
| 28 | 28 | |
| 29 | - callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/github/") | |
| 29 | + callback = request.build_absolute_uri("/oauth/callback/github/") | |
| 30 | 30 | state = f"{slug}:{mirror_id or 'new'}" |
| 31 | 31 | |
| 32 | 32 | return f"{GITHUB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&scope=repo&state={state}" |
| 33 | 33 | |
| 34 | 34 | |
| @@ -71,11 +71,11 @@ | ||
| 71 | 71 | |
| 72 | 72 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 73 | 73 | if not client_id: |
| 74 | 74 | return None |
| 75 | 75 | |
| 76 | - callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/gitlab/") | |
| 76 | + callback = request.build_absolute_uri("/oauth/callback/gitlab/") | |
| 77 | 77 | state = f"{slug}:{mirror_id or 'new'}" |
| 78 | 78 | |
| 79 | 79 | return f"{GITLAB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&response_type=code&scope=api&state={state}" |
| 80 | 80 | |
| 81 | 81 | |
| @@ -87,11 +87,11 @@ | ||
| 87 | 87 | if not code: |
| 88 | 88 | return {"token": "", "error": "No code received"} |
| 89 | 89 | |
| 90 | 90 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 91 | 91 | client_secret = config.GITLAB_OAUTH_CLIENT_SECRET |
| 92 | - callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/gitlab/") | |
| 92 | + callback = request.build_absolute_uri("/oauth/callback/gitlab/") | |
| 93 | 93 | |
| 94 | 94 | try: |
| 95 | 95 | resp = requests.post( |
| 96 | 96 | GITLAB_TOKEN_URL, |
| 97 | 97 | data={ |
| 98 | 98 |
| --- fossil/oauth.py | |
| +++ fossil/oauth.py | |
| @@ -24,11 +24,11 @@ | |
| 24 | |
| 25 | client_id = config.GITHUB_OAUTH_CLIENT_ID |
| 26 | if not client_id: |
| 27 | return None |
| 28 | |
| 29 | callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/github/") |
| 30 | state = f"{slug}:{mirror_id or 'new'}" |
| 31 | |
| 32 | return f"{GITHUB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&scope=repo&state={state}" |
| 33 | |
| 34 | |
| @@ -71,11 +71,11 @@ | |
| 71 | |
| 72 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 73 | if not client_id: |
| 74 | return None |
| 75 | |
| 76 | callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/gitlab/") |
| 77 | state = f"{slug}:{mirror_id or 'new'}" |
| 78 | |
| 79 | return f"{GITLAB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&response_type=code&scope=api&state={state}" |
| 80 | |
| 81 | |
| @@ -87,11 +87,11 @@ | |
| 87 | if not code: |
| 88 | return {"token": "", "error": "No code received"} |
| 89 | |
| 90 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 91 | client_secret = config.GITLAB_OAUTH_CLIENT_SECRET |
| 92 | callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/gitlab/") |
| 93 | |
| 94 | try: |
| 95 | resp = requests.post( |
| 96 | GITLAB_TOKEN_URL, |
| 97 | data={ |
| 98 |
| --- fossil/oauth.py | |
| +++ fossil/oauth.py | |
| @@ -24,11 +24,11 @@ | |
| 24 | |
| 25 | client_id = config.GITHUB_OAUTH_CLIENT_ID |
| 26 | if not client_id: |
| 27 | return None |
| 28 | |
| 29 | callback = request.build_absolute_uri("/oauth/callback/github/") |
| 30 | state = f"{slug}:{mirror_id or 'new'}" |
| 31 | |
| 32 | return f"{GITHUB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&scope=repo&state={state}" |
| 33 | |
| 34 | |
| @@ -71,11 +71,11 @@ | |
| 71 | |
| 72 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 73 | if not client_id: |
| 74 | return None |
| 75 | |
| 76 | callback = request.build_absolute_uri("/oauth/callback/gitlab/") |
| 77 | state = f"{slug}:{mirror_id or 'new'}" |
| 78 | |
| 79 | return f"{GITLAB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&response_type=code&scope=api&state={state}" |
| 80 | |
| 81 | |
| @@ -87,11 +87,11 @@ | |
| 87 | if not code: |
| 88 | return {"token": "", "error": "No code received"} |
| 89 | |
| 90 | client_id = config.GITLAB_OAUTH_CLIENT_ID |
| 91 | client_secret = config.GITLAB_OAUTH_CLIENT_SECRET |
| 92 | callback = request.build_absolute_uri("/oauth/callback/gitlab/") |
| 93 | |
| 94 | try: |
| 95 | resp = requests.post( |
| 96 | GITLAB_TOKEN_URL, |
| 97 | data={ |
| 98 |