diff options
| author | 2026-07-22 17:37:10 +0200 | |
|---|---|---|
| committer | 2026-07-22 17:37:10 +0200 | |
| commit | f28781dd1dd718f45bf1cef82d1ce6eb4792b4ca (patch) | |
| tree | ed5bd923bca11cc75e0568245ea3518a6713041b | |
| parent | 498c79f434856aa68e9a883247ea69a22256fce6 (diff) | |
| download | server-f28781dd1dd718f45bf1cef82d1ce6eb4792b4ca.tar.gz server-f28781dd1dd718f45bf1cef82d1ce6eb4792b4ca.tar.bz2 server-f28781dd1dd718f45bf1cef82d1ce6eb4792b4ca.zip | |
Added management interface
| -rw-r--r-- | authentication/endpoints.py | 99 | ||||
| -rw-r--r-- | authentication/templates/base.html | 65 | ||||
| -rw-r--r-- | authentication/templates/register.html | 24 | ||||
| -rw-r--r-- | management/__init__.py | 0 | ||||
| -rw-r--r-- | management/endpoints.py | 50 | ||||
| -rw-r--r-- | management/templates/base.html | 7 | ||||
| -rw-r--r-- | management/templates/dashboard.html | 9 | ||||
| -rw-r--r-- | management/templates/users.html | 11 | ||||
| -rw-r--r-- | templates/global.html | 60 | ||||
| -rw-r--r-- | ttun_server/__init__.py | 4 |
10 files changed, 254 insertions, 75 deletions
diff --git a/authentication/endpoints.py b/authentication/endpoints.py index f94d55b..18f3ba6 100644 --- a/authentication/endpoints.py +++ b/authentication/endpoints.py | |||
| @@ -1,12 +1,11 @@ | |||
| 1 | import json | 1 | import json |
| 2 | from base64 import b64encode | 2 | from base64 import b64encode |
| 3 | from functools import partial | 3 | from functools import partial |
| 4 | from typing import Annotated | 4 | from typing import Annotated, Self |
| 5 | from uuid import uuid7 | 5 | from uuid import uuid7 |
| 6 | 6 | ||
| 7 | from alembic.testing import requirements | ||
| 8 | from fastapi import FastAPI, Request, Form, Query | 7 | from fastapi import FastAPI, Request, Form, Query |
| 9 | from pydantic import BaseModel, ValidationError | 8 | from pydantic import BaseModel, ValidationError, model_validator, EmailStr |
| 10 | from sqlmodel import select | 9 | from sqlmodel import select |
| 11 | from starlette.authentication import requires | 10 | from starlette.authentication import requires |
| 12 | from starlette.middleware.authentication import AuthenticationMiddleware | 11 | from starlette.middleware.authentication import AuthenticationMiddleware |
| @@ -31,24 +30,32 @@ oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | |||
| 31 | auth_server = AuthorizationServer() | 30 | auth_server = AuthorizationServer() |
| 32 | code_grant = partial(AuthorizationCodeGrant, server=auth_server) | 31 | code_grant = partial(AuthorizationCodeGrant, server=auth_server) |
| 33 | 32 | ||
| 34 | templates = Jinja2Templates(directory='authentication/templates') | 33 | templates = Jinja2Templates(directory=[ |
| 34 | 'templates', | ||
| 35 | 'authentication/templates' | ||
| 36 | ]) | ||
| 37 | |||
| 35 | 38 | ||
| 36 | class LoginForm(BaseModel): | 39 | class LoginForm(BaseModel): |
| 37 | username: str | 40 | username: str |
| 38 | password: str | 41 | password: str |
| 39 | 42 | ||
| 43 | |||
| 40 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): | 44 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): |
| 41 | return templates.TemplateResponse(request, 'login.html', context={ | 45 | return templates.TemplateResponse(request, 'login.html', context={ |
| 42 | 'form': form, | 46 | 'form': form, |
| 43 | 'errors': errors, | 47 | 'errors': errors, |
| 44 | }) | 48 | }) |
| 45 | 49 | ||
| 50 | |||
| 46 | @authentication.get('/login/', response_class=HTMLResponse) | 51 | @authentication.get('/login/', response_class=HTMLResponse) |
| 47 | async def get_login(request: Request, next: Annotated[str, Query()] = None): | 52 | async def get_login(request: Request, next: Annotated[str, Query()] = None): |
| 48 | return await base_login(request) | 53 | return await base_login(request) |
| 49 | 54 | ||
| 55 | |||
| 50 | @authentication.post('/login/', response_class=HTMLResponse) | 56 | @authentication.post('/login/', response_class=HTMLResponse) |
| 51 | async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): | 57 | async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', |
| 58 | next: Annotated[str, Query()] = None): | ||
| 52 | form = None | 59 | form = None |
| 53 | errors = {} | 60 | errors = {} |
| 54 | 61 | ||
| @@ -70,35 +77,101 @@ async def post_login(request: Request, username: Annotated[str, Form()] = '', pa | |||
| 70 | request.session.update({'id': str(session.id)}) | 77 | request.session.update({'id': str(session.id)}) |
| 71 | 78 | ||
| 72 | if next is not None: | 79 | if next is not None: |
| 73 | return RedirectResponse(next) | 80 | return RedirectResponse(next, status_code=303) |
| 74 | 81 | ||
| 75 | except ValidationError as exc: | 82 | except ValidationError as exc: |
| 76 | errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) | 83 | errors.update({e['loc'][0]: e['msg'] for e in exc.errors()}) |
| 77 | 84 | ||
| 78 | return await base_login(request, form, errors) | 85 | return await base_login(request, form, errors) |
| 79 | 86 | ||
| 80 | async def base_authorize(request: Request, user: User | None = None): | 87 | |
| 81 | if not conf.IS_LOCAL: | 88 | @authentication.get('/logout/') |
| 89 | def logout(request: Request): | ||
| 90 | with get_session_context() as db_session: | ||
| 91 | db_session.query(Session).filter(Session.id == request.session.get('id')).delete() | ||
| 92 | |||
| 93 | request.session.clear() | ||
| 94 | return RedirectResponse(request.headers.get('Referer', '/management/dashboard/'), status_code=303) | ||
| 95 | |||
| 96 | |||
| 97 | class RegisterForm(BaseModel): | ||
| 98 | email: EmailStr | ||
| 99 | password: str | ||
| 100 | verify_password: str | ||
| 101 | |||
| 102 | @model_validator(mode='after') | ||
| 103 | def check_passwords_match(self) -> Self: | ||
| 104 | if self.password != self.verify_password: | ||
| 105 | raise ValueError('Passwords do not match') | ||
| 106 | return self | ||
| 107 | |||
| 108 | |||
| 109 | async def base_register(request: Request, form: RegisterForm | None = None, errors: dict[str, str] | None = None): | ||
| 110 | return templates.TemplateResponse(request, 'register.html', context={ | ||
| 111 | 'form': form, | ||
| 112 | 'errors': errors, | ||
| 113 | }) | ||
| 114 | |||
| 115 | |||
| 116 | @authentication.get('/register/', response_class=HTMLResponse) | ||
| 117 | async def get_register(request: Request, next: Annotated[str, Query()] = None): | ||
| 118 | return await base_register(request) | ||
| 119 | |||
| 120 | |||
| 121 | @authentication.post('/register/', response_class=HTMLResponse) | ||
| 122 | async def post_register(request: Request, email: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', | ||
| 123 | verify_password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): | ||
| 124 | form = None | ||
| 125 | errors = {} | ||
| 126 | |||
| 127 | try: | ||
| 128 | form = RegisterForm.model_validate({ | ||
| 129 | 'email': email, | ||
| 130 | 'password': password, | ||
| 131 | 'verify_password': verify_password | ||
| 132 | }) | ||
| 133 | |||
| 82 | with get_session_context() as db_session: | 134 | with get_session_context() as db_session: |
| 83 | db_session.query(Session).filter(Session.id == request.session.get('id')).delete() | 135 | user = User(username=email, email=email) |
| 136 | user.set_password(password) | ||
| 137 | db_session.add(user) | ||
| 138 | |||
| 139 | session = Session(id=uuid7(), user=user) | ||
| 140 | db_session.add(session) | ||
| 141 | |||
| 142 | request.session.update({'id': str(session.id)}) | ||
| 143 | |||
| 144 | if next is not None: | ||
| 145 | return RedirectResponse(next, status_code=303) | ||
| 84 | 146 | ||
| 147 | except ValidationError as exc: | ||
| 148 | errors.update({e['loc'][0]: e['msg'] for e in exc.errors()}) | ||
| 149 | |||
| 150 | return await base_register(request, form, errors) | ||
| 151 | |||
| 152 | |||
| 153 | async def base_authorize(request: Request, user: User | None = None): | ||
| 85 | return auth_server.create_authorization_response(request, request.user, grant=None) | 154 | return auth_server.create_authorization_response(request, request.user, grant=None) |
| 86 | 155 | ||
| 156 | |||
| 87 | @oauth.get('/authorize/') | 157 | @oauth.get('/authorize/') |
| 88 | @requires(['authenticated', 'session'], redirect='get_login') | 158 | @requires(['authenticated', 'session'], redirect='get_login') |
| 89 | async def get_authorize(request: Request): | 159 | async def get_authorize(request: Request): |
| 90 | return await base_authorize(request, request.user) | 160 | return await base_authorize(request, request.user) |
| 91 | 161 | ||
| 162 | |||
| 92 | @oauth.post('/authorize/') | 163 | @oauth.post('/authorize/') |
| 93 | @requires(['authenticated', 'session'], redirect='get_login') | 164 | @requires(['authenticated', 'session'], redirect='get_login') |
| 94 | async def post_authorize(request: Request): | 165 | async def post_authorize(request: Request): |
| 95 | return await base_authorize(request, request.user) | 166 | return await base_authorize(request, request.user) |
| 96 | 167 | ||
| 168 | |||
| 97 | @oauth.post('/token/') | 169 | @oauth.post('/token/') |
| 98 | async def post_token(request: Request): | 170 | async def post_token(request: Request): |
| 99 | await prepare_oauth_request(request) | 171 | await prepare_oauth_request(request) |
| 100 | return auth_server.create_token_response(request) | 172 | return auth_server.create_token_response(request) |
| 101 | 173 | ||
| 174 | |||
| 102 | @authentication.get('/connect/') | 175 | @authentication.get('/connect/') |
| 103 | async def connect(request: Request): | 176 | async def connect(request: Request): |
| 104 | origin = request.url.scheme + '://' + request.url.netloc | 177 | origin = request.url.scheme + '://' + request.url.netloc |
| @@ -107,16 +180,18 @@ async def connect(request: Request): | |||
| 107 | app = session.exec(select(ClientApplication)).first() | 180 | app = session.exec(select(ClientApplication)).first() |
| 108 | 181 | ||
| 109 | return RedirectResponse( | 182 | return RedirectResponse( |
| 110 | '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { | 183 | '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { |
| 111 | 'client_id': app.client_id, | 184 | 'client_id': app.client_id, |
| 112 | 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), | 185 | 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), |
| 113 | 'response_type': 'code', | 186 | 'response_type': 'code', |
| 114 | 'state': b64encode(json.dumps({ | 187 | 'state': b64encode(json.dumps({ |
| 115 | 'client_id': app.client_id, | 188 | 'client_id': app.client_id, |
| 116 | }).encode()).decode(), | 189 | }).encode()).decode(), |
| 117 | }) | 190 | }), |
| 191 | status_code=303, | ||
| 118 | ) | 192 | ) |
| 119 | 193 | ||
| 194 | |||
| 120 | @oauth.get('/config/') | 195 | @oauth.get('/config/') |
| 121 | async def get_config(request: Request): | 196 | async def get_config(request: Request): |
| 122 | with get_session_context() as session: | 197 | with get_session_context() as session: |
diff --git a/authentication/templates/base.html b/authentication/templates/base.html index 47d007e..9874313 100644 --- a/authentication/templates/base.html +++ b/authentication/templates/base.html | |||
| @@ -1,63 +1,4 @@ | |||
| 1 | <!DOCTYP | ||
