FossilRepo

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

Keyboard Shortcuts

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