diff options
Diffstat (limited to 'management')
| -rw-r--r-- | management/endpoints.py | 22 | ||||
| -rw-r--r-- | management/templates/base.html | 17 | ||||
| -rw-r--r-- | management/templates/dashboard.html | 16 | ||||
| -rw-r--r-- | management/templates/users.html | 32 |
4 files changed, 67 insertions, 20 deletions
diff --git a/management/endpoints.py b/management/endpoints.py index 47602d6..ef44114 100644 --- a/management/endpoints.py +++ b/management/endpoints.py | |||
| @@ -1,7 +1,9 @@ | |||
| 1 | from functools import wraps | ||
| 2 | from urllib.parse import urlencode | ||
| 3 | |||
| 1 | from fastapi import FastAPI, Request | 4 | from fastapi import FastAPI, Request |
| 2 | from sqlalchemy import func | 5 | from sqlalchemy import func |
| 3 | from sqlmodel import select | 6 | from sqlmodel import select |
| 4 | from starlette.authentication import requires | ||
| 5 | from starlette.middleware.authentication import AuthenticationMiddleware | 7 | from starlette.middleware.authentication import AuthenticationMiddleware |
| 6 | from starlette.middleware.sessions import SessionMiddleware | 8 | from starlette.middleware.sessions import SessionMiddleware |
| 7 | from starlette.responses import RedirectResponse | 9 | from starlette.responses import RedirectResponse |
| @@ -21,6 +23,20 @@ templates = Jinja2Templates(directory=[ | |||
| 21 | 'management/templates' | 23 | 'management/templates' |
| 22 | ]) | 24 | ]) |
| 23 | 25 | ||
| 26 | |||
| 27 | def requires_session(func): | ||
| 28 | """Redirects to an absolute login URL instead of Starlette's `requires(redirect=...)`, | ||
| 29 | which resolves the redirect target via `request.url_for()` against whichever router is | ||
| 30 | bound into `request.scope` -- a lookup that only reaches `authentication`'s routes when | ||
| 31 | `management` happens to be mounted under a shared root app.""" | ||
| 32 | @wraps(func) | ||
| 33 | async def wrapper(request: Request, *args, **kwargs): | ||
| 34 | if not {'authenticated', 'session'}.issubset(request.auth.scopes): | ||
| 35 | next_param = urlencode({'next': str(request.url)}) | ||
| 36 | return RedirectResponse(f'/auth/login/?{next_param}', status_code=303) | ||
| 37 | return await func(request, *args, **kwargs) | ||
| 38 | return wrapper | ||
| 39 | |||
| 24 | @management.get('/') | 40 | @management.get('/') |
| 25 | async def index(request: Request): | 41 | async def index(request: Request): |
| 26 | with get_session_context() as session: | 42 | with get_session_context() as session: |
| @@ -34,12 +50,12 @@ async def index(request: Request): | |||
| 34 | 50 | ||
| 35 | 51 | ||
| 36 | @management.get('/dashboard/') | 52 | @management.get('/dashboard/') |
| 37 | @requires(['authenticated', 'session'], redirect='get_login') | 53 | @requires_session |
| 38 | async def dashboard(request: Request): | 54 | async def dashboard(request: Request): |
| 39 | return templates.TemplateResponse(request, 'dashboard.html') | 55 | return templates.TemplateResponse(request, 'dashboard.html') |
| 40 | 56 | ||
| 41 | @management.get('/users/') | 57 | @management.get('/users/') |
| 42 | @requires(['authenticated', 'session'], redirect='get_login') | 58 | @requires_session |
| 43 | async def users(request: Request): | 59 | async def users(request: Request): |
| 44 | with get_session_context() as session: | 60 | with get_session_context() as session: |
| 45 | query = select(User) | 61 | query = select(User) |
diff --git a/management/templates/base.html b/management/templates/base.html index 0b9da11..7ba0adb 100644 --- a/management/templates/base.html +++ b/management/templates/base.html | |||
| @@ -1,7 +1,18 @@ | |||
| 1 | {% extends 'global.html' %} | 1 | {% extends 'global.html' %} |
| 2 | {% import 'macros.html' as ui %} | ||
| 2 | {% block template %} | 3 | {% block template %} |
| 3 | <nav> | 4 | <nav class="nav"> |
| 4 | <a href="/auth/logout/">Logout</a> | 5 | <div class="nav__brand">{{ ui.connector_icon(18) }} ttun</div> |
| 6 | <div class="nav__links"> | ||
| 7 | <a class="nav__link{% if request.url.path == '/management/dashboard/' %} nav__link--active{% endif %}" href="/management/dashboard/">Dashboard</a> | ||
| 8 | <a class="nav__link{% if request.url.path == '/management/users/' %} nav__link--active{% endif %}" href="/management/users/">Users</a> | ||
| 9 | </div> | ||
| 10 | <div class="nav__actions"> | ||
| 11 | {{ ui.status_badge('Session active', 'good') }} | ||
| 12 | <a class="nav__link" href="/auth/logout/">Sign out</a> | ||
| 13 | </div> | ||
| 5 | </nav> | 14 | </nav> |
| 6 | {% block content %}{% endblock %} | 15 | <div class="content"> |
| 16 | {% block content %}{% endblock %} | ||
| 17 | </div> | ||
| 7 | {% endblock %} | 18 | {% endblock %} |
diff --git a/management/templates/dashboard.html b/management/templates/dashboard.html index 201aa48..4babba9 100644 --- a/management/templates/dashboard.html +++ b/management/templates/dashboard.html | |||
| @@ -1,9 +1,13 @@ | |||
| 1 | {% extends 'base.html' %} | 1 | {% extends 'base.html' %} |
| 2 | |||
| 3 | {% block content %} | 2 | {% block content %} |
| 4 | <div> | 3 | <div class="content__header"> |
| 5 | <ul> | 4 | <span class="eyebrow">Dashboard</span> |
| 6 | <li><a href="/management/users/">Users</a></li> | 5 | <h1>Manage this server</h1> |
| 7 | </ul> | 6 | </div> |
| 8 | </div> | 7 | <div class="module-grid"> |
| 8 | <a class="module-card" href="/management/users/"> | ||
| 9 | <div class="module-card__title">Users</div> | ||
| 10 | <p class="module-card__desc">Manage who can sign in to this server.</p> | ||
| 11 | </a> | ||
| 12 | </div> | ||
| 9 | {% endblock %} | 13 | {% endblock %} |
diff --git a/management/templates/users.html b/management/templates/users.html index 272d2b4..3cf9103 100644 --- a/management/templates/users.html +++ b/management/templates/users.html | |||
| @@ -1,11 +1,27 @@ | |||
| 1 | {% extends 'base.html' %} | 1 | {% extends 'base.html' %} |
| 2 | |||
| 3 | {% block content %} | 2 | {% block content %} |
| 4 | <div> | 3 | <div class="content__header"> |
| 5 | <ul> | 4 | <span class="eyebrow">{{ users|length }} account{{ 's' if users|length != 1 else '' }}</span> |
| 6 | {% for user in users %} | 5 | <h1>Users</h1> |
| 7 | <li>{{ user.username }}</li> | 6 | </div> |
| 8 | {% endfor %} | 7 | <div class="table-wrap"> |
| 9 | </ul> | 8 | <table> |
| 10 | </div> | 9 | <thead> |
| 10 | <tr> | ||
| 11 | <th>Username</th> | ||
| 12 | <th>Email</th> | ||
| 13 | <th>Created</th> | ||
| 14 | </tr> | ||
| 15 | </thead> | ||
| 16 | <tbody> | ||
| 17 | {% for user in users %} | ||
| 18 | <tr> | ||
| 19 | <td>{{ user.username }}</td> | ||
| 20 | <td>{{ user.email }}</td> | ||
| 21 | <td class="muted">{{ user.created.strftime('%Y-%m-%d %H:%M') }}</td> | ||
| 22 | </tr> | ||
| 23 | {% endfor %} | ||
| 24 | </tbody> | ||
| 25 | </table> | ||
| 26 | </div> | ||
| 11 | {% endblock %} | 27 | {% endblock %} |
