FossilRepo

fossilrepo / config / urls.py
Source Blame History 262 lines
4ce269c… ragelink 1 import time
4ce269c… ragelink 2 from datetime import UTC, datetime
4ce269c… ragelink 3
4ce269c… ragelink 4 from django.conf import settings
4ce269c… ragelink 5 from django.contrib import admin
4ce269c… ragelink 6 from django.http import HttpResponse, JsonResponse
e33a0d1… ragelink 7 from django.shortcuts import redirect as _redirect
4ce269c… ragelink 8 from django.urls import include, path
4ce269c… ragelink 9 from django.views.generic import RedirectView
e33a0d1… ragelink 10
e33a0d1… ragelink 11
e33a0d1… ragelink 12 def _oauth_github_callback(request):
e33a0d1… ragelink 13 """Global GitHub OAuth callback. Extracts slug from state param and delegates."""
c588255… ragelink 14 from django.contrib import messages
c588255… ragelink 15
e33a0d1… ragelink 16 state = request.GET.get("state", "")
c588255… ragelink 17 parts = state.split(":")
c588255… ragelink 18 if len(parts) < 3:
e33a0d1… ragelink 19 return _redirect("/dashboard/")
c588255… ragelink 20
c588255… ragelink 21 slug = parts[0]
c588255… ragelink 22 nonce = parts[2]
c588255… ragelink 23
c588255… ragelink 24 expected_nonce = request.session.pop("oauth_state_nonce", "")
c588255… ragelink 25 if not nonce or nonce != expected_nonce:
c588255… ragelink 26 messages.error(request, "OAuth state mismatch. Please try again.")
c588255… ragelink 27 return _redirect(f"/projects/{slug}/fossil/sync/git/")
c588255… ragelink 28
e33a0d1… ragelink 29 from fossil.oauth import github_exchange_token
e33a0d1… ragelink 30
e33a0d1… ragelink 31 result = github_exchange_token(request, slug)
e33a0d1… ragelink 32 if result.get("token"):
e33a0d1… ragelink 33 request.session["github_oauth_token"] = result["token"]
e33a0d1… ragelink 34 request.session["github_oauth_user"] = result.get("username", "")
e33a0d1… ragelink 35 return _redirect(f"/projects/{slug}/fossil/sync/git/")
e33a0d1… ragelink 36
e33a0d1… ragelink 37
e33a0d1… ragelink 38 def _oauth_gitlab_callback(request):
e33a0d1… ragelink 39 """Global GitLab OAuth callback. Extracts slug from state param and delegates."""
c588255… ragelink 40 from django.contrib import messages
c588255… ragelink 41
e33a0d1… ragelink 42 state = request.GET.get("state", "")
c588255… ragelink 43 parts = state.split(":")
c588255… ragelink 44 if len(parts) < 3:
e33a0d1… ragelink 45 return _redirect("/dashboard/")
c588255… ragelink 46
c588255… ragelink 47 slug = parts[0]
c588255… ragelink 48 nonce = parts[2]
c588255… ragelink 49
c588255… ragelink 50 expected_nonce = request.session.pop("oauth_state_nonce", "")
c588255… ragelink 51 if not nonce or nonce != expected_nonce:
c588255… ragelink 52 messages.error(request, "OAuth state mismatch. Please try again.")
c588255… ragelink 53 return _redirect(f"/projects/{slug}/fossil/sync/git/")
c588255… ragelink 54
e33a0d1… ragelink 55 from fossil.oauth import gitlab_exchange_token
e33a0d1… ragelink 56
e33a0d1… ragelink 57 result = gitlab_exchange_token(request, slug)
e33a0d1… ragelink 58 if result.get("token"):
e33a0d1… ragelink 59 request.session["gitlab_oauth_token"] = result["token"]
e33a0d1… ragelink 60 return _redirect(f"/projects/{slug}/fossil/sync/git/")
e33a0d1… ragelink 61
4ce269c… ragelink 62
4ce269c… ragelink 63 admin.site.site_header = settings.ADMIN_SITE_HEADER
4ce269c… ragelink 64 admin.site.site_title = settings.ADMIN_SITE_TITLE
4ce269c… ragelink 65 admin.site.index_title = "Welcome to Fossilrepo"
4ce269c… ragelink 66
4ce269c… ragelink 67 _START_TIME = time.monotonic()
4ce269c… ragelink 68
4ce269c… ragelink 69
4ce269c… ragelink 70 def _uptime_str():
4ce269c… ragelink 71 secs = int(time.monotonic() - _START_TIME)
4ce269c… ragelink 72 h, rem = divmod(secs, 3600)
4ce269c… ragelink 73 m, s = divmod(rem, 60)
4ce269c… ragelink 74 if h:
4ce269c… ragelink 75 return f"{h}h {m}m {s}s"
4ce269c… ragelink 76 if m:
4ce269c… ragelink 77 return f"{m}m {s}s"
4ce269c… ragelink 78 return f"{s}s"
4ce269c… ragelink 79
4ce269c… ragelink 80
4ce269c… ragelink 81 def health_check(request):
4ce269c… ragelink 82 from django.db import connection
4ce269c… ragelink 83
4ce269c… ragelink 84 try:
4ce269c… ragelink 85 with connection.cursor() as cursor:
4ce269c… ragelink 86 cursor.execute("SELECT 1")
4ce269c… ragelink 87 db_ok = True
fcd8df3… ragelink 88 except Exception:
4ce269c… ragelink 89 return JsonResponse(
4ce269c… ragelink 90 {
4ce269c… ragelink 91 "service": "fossilrepo-django-htmx",
4ce269c… ragelink 92 "version": settings.VERSION,
4ce269c… ragelink 93 "status": "error",
4ce269c… ragelink 94 "uptime": _uptime_str(),
4ce269c… ragelink 95 "timestamp": datetime.now(UTC).isoformat(),
fcd8df3… ragelink 96 "checks": {"database": "error"},
4ce269c… ragelink 97 },
4ce269c… ragelink 98 status=503,
4ce269c… ragelink 99 )
4ce269c… ragelink 100
4ce269c… ragelink 101 return JsonResponse(
4ce269c… ragelink 102 {
4ce269c… ragelink 103 "service": "fossilrepo-django-htmx",
4ce269c… ragelink 104 "version": settings.VERSION,
4ce269c… ragelink 105 "status": "ok",
4ce269c… ragelink 106 "uptime": _uptime_str(),
4ce269c… ragelink 107 "timestamp": datetime.now(UTC).isoformat(),
4ce269c… ragelink 108 "checks": {"database": "ok" if db_ok else "error"},
4ce269c… ragelink 109 "links": {
4ce269c… ragelink 110 "app": "/dashboard/",
4ce269c… ragelink 111 "admin": "/admin/",
4ce269c… ragelink 112 "status": "/status/",
4ce269c… ragelink 113 "login": "/auth/login/",
4ce269c… ragelink 114 },
4ce269c… ragelink 115 }
4ce269c… ragelink 116 )
4ce269c… ragelink 117
4ce269c… ragelink 118
4ce269c… ragelink 119 def status_page(request):
4ce269c… ragelink 120 version = settings.VERSION
4ce269c… ragelink 121 env = getattr(settings, "DJANGO_CONFIGURATION", "Local")
4ce269c… ragelink 122 uptime = _uptime_str()
4ce269c… ragelink 123 now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
4ce269c… ragelink 124
4ce269c… ragelink 125 env_color = {
4ce269c… ragelink 126 "Local": "#22c55e",
4ce269c… ragelink 127 "Staging": "#f59e0b",
4ce269c… ragelink 128 "Production": "#ef4444",
4ce269c… ragelink 129 }.get(env, "#6b7280")
4ce269c… ragelink 130
4ce269c… ragelink 131 links = [
4ce269c… ragelink 132 ("App", "/dashboard/", "Django + HTMX application"),
4ce269c… ragelink 133 ("Admin", "/admin/", "Django admin — users, permissions, data"),
4ce269c… ragelink 134 ("Health", "/health/", "Service health checks (JSON)"),
4ce269c… ragelink 135 ("Login", "/auth/login/", "Session-based authentication"),
4ce269c… ragelink 136 ]
4ce269c… ragelink 137
4ce269c… ragelink 138 links_html = "\n".join(
4ce269c… ragelink 139 f"""<a href="{url}" class="link-card">
4ce269c… ragelink 140 <span class="link-title">{name}</span>
4ce269c… ragelink 141 <span class="link-desc">{desc}</span>
4ce269c… ragelink 142 <span class="link-arrow">&rarr;</span>
4ce269c… ragelink 143 </a>"""
4ce269c… ragelink 144 for name, url, desc in links
4ce269c… ragelink 145 )
4ce269c… ragelink 146
4ce269c… ragelink 147 html = f"""<!DOCTYPE html>
4ce269c… ragelink 148 <html lang="en">
4ce269c… ragelink 149 <head>
4ce269c… ragelink 150 <meta charset="UTF-8" />
4ce269c… ragelink 151 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
4ce269c… ragelink 152 <title>Fossilrepo Status</title>
4ce269c… ragelink 153 <style>
4ce269c… ragelink 154 *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
4ce269c… ragelink 155 :root {{
4ce269c… ragelink 156 --bg: #0a0a0a; --surface: #111111; --border: #1f1f1f;
4ce269c… ragelink 157 --text: #e5e5e5; --muted: #6b7280; --accent: #ffffff; --green: #22c55e;
4ce269c… ragelink 158 }}
4ce269c… ragelink 159 body {{
4ce269c… ragelink 160 background: var(--bg); color: var(--text);
4ce269c… ragelink 161 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4ce269c… ragelink 162 min-height: 100vh; display: flex; flex-direction: column;
4ce269c… ragelink 163 align-items: center; justify-content: center; padding: 2rem;
4ce269c… ragelink 164 }}
4ce269c… ragelink 165 .container {{ width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 2rem; }}
4ce269c… ragelink 166 .header {{ display: flex; flex-direction: column; gap: 0.5rem; }}
4ce269c… ragelink 167 .wordmark {{ font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; color: var(--accent); }}
4ce269c… ragelink 168 .tagline {{ font-size: 0.875rem; color: var(--muted); }}
4ce269c… ragelink 169 .status-bar {{
4ce269c… ragelink 170 background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem;
4ce269c… ragelink 171 padding: 1rem 1.25rem; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
4ce269c… ragelink 172 }}
4ce269c… ragelink 173 .status-dot {{
4ce269c… ragelink 174 width: 8px; height: 8px; border-radius: 50%;
4ce269c… ragelink 175 background: var(--green); box-shadow: 0 0 6px var(--green); flex-shrink: 0;
4ce269c… ragelink 176 }}
4ce269c… ragelink 177 .status-text {{ font-size: 0.875rem; font-weight: 500; flex: 1; }}
4ce269c… ragelink 178 .meta-pills {{ display: flex; gap: 0.5rem; flex-wrap: wrap; }}
4ce269c… ragelink 179 .pill {{
4ce269c… ragelink 180 font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 999px;
4ce269c… ragelink 181 border: 1px solid var(--border); color: var(--muted); white-space: nowrap;
4ce269c… ragelink 182 }}
4ce269c… ragelink 183 .pill-env {{ border-color: {env_color}33; color: {env_color}; }}
4ce269c… ragelink 184 .links {{ display: flex; flex-direction: column; gap: 0.5rem; }}
4ce269c… ragelink 185 .links-label {{
4ce269c… ragelink 186 font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
4ce269c… ragelink 187 letter-spacing: 0.08em; color: var(--muted);
4ce269c… ragelink 188 padding: 0 0.25rem; margin-bottom: 0.25rem;
4ce269c… ragelink 189 }}
4ce269c… ragelink 190 .link-card {{
4ce269c… ragelink 191 background: var(--surface); border: 1px solid var(--border); border-radius: 0.625rem;
4ce269c… ragelink 192 padding: 0.875rem 1.25rem; display: grid;
4ce269c… ragelink 193 grid-template-columns: 1fr auto; grid-template-rows: auto auto;
4ce269c… ragelink 194 gap: 0.125rem 0.5rem; text-decoration: none; color: inherit;
4ce269c… ragelink 195 transition: border-color 0.15s, background 0.15s;
4ce269c… ragelink 196 }}
4ce269c… ragelink 197 .link-card:hover {{ border-color: #2f2f2f; background: #161616; }}
4ce269c… ragelink 198 .link-title {{
4ce269c… ragelink 199 font-size: 0.875rem; font-weight: 500; color: var(--text);
4ce269c… ragelink 200 grid-column: 1; grid-row: 1;
4ce269c… ragelink 201 }}
4ce269c… ragelink 202 .link-desc {{ font-size: 0.75rem; color: var(--muted); grid-column: 1; grid-row: 2; }}
4ce269c… ragelink 203 .link-arrow {{
4ce269c… ragelink 204 font-size: 1rem; color: var(--muted); grid-column: 2; grid-row: 1 / 3;
4ce269c… ragelink 205 align-self: center; transition: color 0.15s, transform 0.15s;
4ce269c… ragelink 206 }}
4ce269c… ragelink 207 .link-card:hover .link-arrow {{ color: var(--text); transform: translateX(2px); }}
4ce269c… ragelink 208 .footer {{ font-size: 0.7rem; color: var(--muted); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem; }}
4ce269c… ragelink 209 </style>
4ce269c… ragelink 210 </head>
4ce269c… ragelink 211 <body>
4ce269c… ragelink 212 <div class="container">
4ce269c… ragelink 213 <div class="header">
4ce269c… ragelink 214 <div class="wordmark">Fossilrepo</div>
4ce269c… ragelink 215 <div class="tagline">Server-rendered Django + HTMX.</div>
4ce269c… ragelink 216 </div>
4ce269c… ragelink 217 <div class="status-bar">
4ce269c… ragelink 218 <div class="status-dot"></div>
4ce269c… ragelink 219 <div class="status-text">All systems operational</div>
4ce269c… ragelink 220 <div class="meta-pills">
4ce269c… ragelink 221 <span class="pill pill-env">{env}</span>
4ce269c… ragelink 222 <span class="pill">v{version}</span>
4ce269c… ragelink 223 <span class="pill">&uarr; {uptime}</span>
4ce269c… ragelink 224 </div>
4ce269c… ragelink 225 </div>
4ce269c… ragelink 226 <div class="links">
4ce269c… ragelink 227 <div class="links-label">Endpoints</div>
4ce269c… ragelink 228 {links_html}
4ce269c… ragelink 229 </div>
4ce269c… ragelink 230 <div class="footer">
4ce269c… ragelink 231 <span>fossilrepo-django-htmx</span>
4ce269c… ragelink 232 <span>{now}</span>
4ce269c… ragelink 233 </div>
4ce269c… ragelink 234 </div>
4ce269c… ragelink 235 </body>
4ce269c… ragelink 236 </html>"""
4ce269c… ragelink 237
4ce269c… ragelink 238 return HttpResponse(html)
4ce269c… ragelink 239
4ce269c… ragelink 240
c588255… ragelink 241 def _explore_view(request):
c588255… ragelink 242 from projects.views import explore
c588255… ragelink 243
c588255… ragelink 244 return explore(request)
c588255… ragelink 245
c588255… ragelink 246
4ce269c… ragelink 247 urlpatterns = [
c588255… ragelink 248 path("", lambda request: _redirect("/explore/") if not request.user.is_authenticated else _redirect("/dashboard/"), name="home"),
c588255… ragelink 249 path("profile/", RedirectView.as_view(pattern_name="accounts:profile", permanent=False)),
4ce269c… ragelink 250 path("status/", status_page, name="status"),
c588255… ragelink 251 path("explore/", _explore_view, name="explore"),
4ce269c… ragelink 252 path("dashboard/", include("core.urls")),
c588255… ragelink 253 path("auth/", include("accounts.urls")),
4ce269c… ragelink 254 path("settings/", include("organization.urls")),
4ce269c… ragelink 255 path("projects/", include("projects.urls")),
4ce269c… ragelink 256 path("projects/<slug:slug>/fossil/", include("fossil.urls")),
4ce269c… ragelink 257 path("kb/", include("pages.urls")),
e33a0d1… ragelink 258 path("oauth/callback/github/", _oauth_github_callback, name="oauth_github_callback_global"),
e33a0d1… ragelink 259 path("oauth/callback/gitlab/", _oauth_gitlab_callback, name="oauth_gitlab_callback_global"),
4ce269c… ragelink 260 path("admin/", admin.site.urls),
4ce269c… ragelink 261 path("health/", health_check, name="health"),
4ce269c… ragelink 262 ]

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button