diff options
| author | 2026-09-04 17:09:24 +0200 | |
|---|---|---|
| committer | 2026-09-04 17:09:24 +0200 | |
| commit | 6b6f20a032eb31d747366c8bc270ca81731eaf6a (patch) | |
| tree | ada48080495267bebde4ee1251bbbe77d629745f /authentication/endpoints.py | |
| parent | 2d22158dd95f76399f5716d556be60c77da04a46 (diff) | |
| download | server-6b6f20a032eb31d747366c8bc270ca81731eaf6a.tar.gz server-6b6f20a032eb31d747366c8bc270ca81731eaf6a.tar.bz2 server-6b6f20a032eb31d747366c8bc270ca81731eaf6a.zip | |
Fixed pre-commit issuesv3
Diffstat (limited to 'authentication/endpoints.py')
| -rw-r--r-- | authentication/endpoints.py | 177 |
1 files changed, 111 insertions, 66 deletions
diff --git a/authentication/endpoints.py b/authentication/endpoints.py index 3af49fc..17d49d7 100644 --- a/authentication/endpoints.py +++ b/authentication/endpoints.py | |||
| @@ -4,8 +4,8 @@ from functools import partial | |||
| 4 | from typing import Annotated, Self | 4 | from typing import Annotated, Self |
| 5 | from uuid import uuid7 | 5 | from uuid import uuid7 |
| 6 | 6 | ||
| 7 | from fastapi import FastAPI, Request, Form, Query | 7 | from fastapi import FastAPI, Form, Query, Request |
| 8 | from pydantic import BaseModel, ValidationError, model_validator, EmailStr | 8 | from pydantic import BaseModel, EmailStr, ValidationError, model_validator |
| 9 | from sqlalchemy import func | 9 | from sqlalchemy import func |
| 10 | from sqlmodel import select | 10 | from sqlmodel import select |
| 11 | from starlette.authentication import requires | 11 | from starlette.authentication import requires |
| @@ -15,10 +15,14 @@ from starlette.responses import HTMLResponse, RedirectResponse | |||
| 15 | from starlette.templating import Jinja2Templates | 15 | from starlette.templating import Jinja2Templates |
| 16 | 16 | ||
| 17 | import conf | 17 | import conf |
| 18 | from authentication.authlib import AuthorizationServer, AuthorizationCodeGrant, prepare_oauth_request | 18 | from authentication.authlib import ( |
| 19 | AuthorizationCodeGrant, | ||
| 20 | AuthorizationServer, | ||
| 21 | prepare_oauth_request, | ||
| 22 | ) | ||
| 19 | from authentication.backend import SessionAuthBackend | 23 | from authentication.backend import SessionAuthBackend |
| 20 | from authentication.utils import bcrypt_sha256_verify, get_url_path_with_query | 24 | from authentication.utils import bcrypt_sha256_verify, get_url_path_with_query |
| 21 | from db.models import User, Session, ClientApplication | 25 | from db.models import ClientApplication, Session, User |
| 22 | from db.session import get_session_context | 26 | from db.session import get_session_context |
| 23 | 27 | ||
| 24 | authentication = FastAPI() | 28 | authentication = FastAPI() |
| @@ -31,10 +35,7 @@ oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | |||
| 31 | auth_server = AuthorizationServer() | 35 | auth_server = AuthorizationServer() |
| 32 | code_grant = partial(AuthorizationCodeGrant, server=auth_server) | 36 | code_grant = partial(AuthorizationCodeGrant, server=auth_server) |
| 33 | 37 | ||
| 34 | templates = Jinja2Templates(directory=[ | 38 | templates = Jinja2Templates(directory=["templates", "authentication/templates"]) |
| 35 | 'templates', | ||
| 36 | 'authentication/templates' | ||
| 37 | ]) | ||
| 38 | 39 | ||
| 39 | 40 | ||
| 40 | class LoginForm(BaseModel): | 41 | class LoginForm(BaseModel): |
| @@ -42,57 +43,75 @@ class LoginForm(BaseModel): | |||
| 42 | password: str | 43 | password: str |
| 43 | 44 | ||
| 44 | 45 | ||
| 45 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): | 46 | async def base_login( |
| 46 | return templates.TemplateResponse(request, 'login.html', context={ | 47 | request: Request, |
| 47 | 'form': form, | 48 | form: LoginForm | None = None, |
| 48 | 'errors': errors, | 49 | errors: dict[str, str] | None = None, |
| 49 | }) | 50 | ): |
| 51 | return templates.TemplateResponse( | ||
| 52 | request, | ||
| 53 | "login.html", | ||
| 54 | context={ | ||
| 55 | "form": form, | ||
| 56 | "errors": errors, | ||
| 57 | }, | ||
| 58 | ) | ||
| 50 | 59 | ||
| 51 | 60 | ||
| 52 | @authentication.get('/login/', response_class=HTMLResponse) | 61 | @authentication.get("/login/", response_class=HTMLResponse) |
| 53 | async def get_login(request: Request, next: Annotated[str, Query()] = None): | 62 | async def get_login(request: Request, next: Annotated[str | None, Query()] = None): |
| 54 | return await base_login(request) | 63 | return await base_login(request) |
| 55 | 64 | ||
| 56 | 65 | ||
| 57 | @authentication.post('/login/', response_class=HTMLResponse) | 66 | @authentication.post("/login/", response_class=HTMLResponse) |
| 58 | async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', | 67 | async def post_login( |
| 59 | next: Annotated[str, Query()] = None): | 68 | request: Request, |
| 69 | username: Annotated[str, Form()] = "", | ||
| 70 | password: Annotated[str, Form()] = "", | ||
| 71 | next: Annotated[str | None, Query()] = None, | ||
| 72 | ): | ||
| 60 | form = None | 73 | form = None |
| 61 | errors = {} | 74 | errors = {} |
| 62 | 75 | ||
| 63 | try: | 76 | try: |
| 64 | form = LoginForm.model_validate({ | 77 | form = LoginForm.model_validate({"username": username, "password": password}) |
| 65 | 'username': username, | ||
| 66 | 'password': password | ||
| 67 | }) | ||
| 68 | 78 | ||
| 69 | with get_session_context() as db_session: | 79 | with get_session_context() as db_session: |
| 70 | user = db_session.query(User).filter(User.username == form.username).first() | 80 | user = db_session.query(User).filter(User.username == form.username).first() |
| 71 | 81 | ||
| 72 | if not bcrypt_sha256_verify(password, user.password if user is not None else '') or user is None: | 82 | if ( |
| 73 | errors.update({'password': 'Invalid username or password'}) | 83 | not bcrypt_sha256_verify( |
| 84 | password, user.password if user is not None else "" | ||
| 85 | ) | ||
| 86 | or user is None | ||
| 87 | ): | ||
| 88 | errors.update({"password": "Invalid username or password"}) | ||
| 74 | else: | 89 | else: |
| 75 | with get_session_context() as db_session: | 90 | with get_session_context() as db_session: |
| 76 | session = Session(id=uuid7(), user=user) | 91 | session = Session(id=uuid7(), user=user) |
| 77 | db_session.add(session) | 92 | db_session.add(session) |
| 78 | 93 | ||
| 79 | request.session.update({'id': str(session.id)}) | 94 | request.session.update({"id": str(session.id)}) |
| 80 | 95 | ||
| 81 | if next is not None: | 96 | if next is not None: |
| 82 | return RedirectResponse(next, status_code=303) | 97 | return RedirectResponse(next, status_code=303) |
| 83 | except ValidationError as exc: | 98 | except ValidationError as exc: |
| 84 | errors.update({e['loc'][0]: e['msg'] for e in exc.errors()}) | 99 | errors.update({e["loc"][0]: e["msg"] for e in exc.errors()}) |
| 85 | 100 | ||
| 86 | return await base_login(request, form, errors) | 101 | return await base_login(request, form, errors) |
| 87 | 102 | ||
| 88 | 103 | ||
| 89 | @authentication.get('/logout/') | 104 | @authentication.get("/logout/") |
| 90 | def logout(request: Request): | 105 | def logout(request: Request): |
| 91 | with get_session_context() as db_session: | 106 | with get_session_context() as db_session: |
| 92 | db_session.query(Session).filter(Session.id == request.session.get('id')).delete() | 107 | db_session.query(Session).filter( |
| 108 | Session.id == request.session.get("id") | ||
| 109 | ).delete() | ||
| 93 | 110 | ||
| 94 | request.session.clear() | 111 | request.session.clear() |
| 95 | return RedirectResponse(request.headers.get('Referer', '/management/dashboard/'), status_code=303) | 112 | return RedirectResponse( |
| 113 | request.headers.get("Referer", "/management/dashboard/"), status_code=303 | ||
| 114 | ) | ||
| 96 | 115 | ||
| 97 | 116 | ||
| 98 | class RegisterForm(BaseModel): | 117 | class RegisterForm(BaseModel): |
| @@ -100,10 +119,10 @@ class RegisterForm(BaseModel): | |||
| 100 | password: str | 119 | password: str |
| 101 | verify_password: str | 120 | verify_password: str |
| 102 | 121 | ||
| 103 | @model_validator(mode='after') | 122 | @model_validator(mode="after") |
| 104 | def check_passwords_match(self) -> Self: | 123 | def check_passwords_match(self) -> Self: |
| 105 | if self.password != self.verify_password: | 124 | if self.password != self.verify_password: |
| 106 | raise ValueError('Passwords do not match') | 125 | raise ValueError("Passwords do not match") |
| 107 | return self | 126 | return self |
| 108 | 127 | ||
| 109 | 128 | ||
| @@ -113,31 +132,42 @@ def is_first_run() -> bool: | |||
| 113 | return user_count.first() == 0 | 132 | return user_count.first() == 0 |
| 114 | 133 | ||
| 115 | 134 | ||
| 116 | async def base_register(request: Request, form: RegisterForm | None = None, errors: dict[str, str] | None = None): | 135 | async def base_register( |
| 117 | return templates.TemplateResponse(request, 'register.html', context={ | 136 | request: Request, |
| 118 | 'form': form, | 137 | form: RegisterForm | None = None, |
| 119 | 'errors': errors, | 138 | errors: dict[str, str] | None = None, |
| 120 | 'is_first_run': is_first_run(), | 139 | ): |
| 121 | }) | 140 | return templates.TemplateResponse( |
| 141 | request, | ||
| 142 | "register.html", | ||
| 143 | context={ | ||
| 144 | "form": form, | ||
| 145 | "errors": errors, | ||
| 146 | "is_first_run": is_first_run(), | ||
| 147 | }, | ||
| 148 | ) | ||
| 122 | 149 | ||
| 123 | 150 | ||
| 124 | @authentication.get('/register/', response_class=HTMLResponse) | 151 | @authentication.get("/register/", response_class=HTMLResponse) |
| 125 | async def get_register(request: Request, next: Annotated[str, Query()] = None): | 152 | async def get_register(request: Request, next: Annotated[str | None, Query()] = None): |
| 126 | return await base_register(request) | 153 | return await base_register(request) |
| 127 | 154 | ||
| 128 | 155 | ||
| 129 | @authentication.post('/register/', response_class=HTMLResponse) | 156 | @authentication.post("/register/", response_class=HTMLResponse) |
| 130 | async def post_register(request: Request, email: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', | 157 | async def post_register( |
| 131 | verify_password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): | 158 | request: Request, |
| 159 | email: Annotated[str, Form()] = "", | ||
| 160 | password: Annotated[str, Form()] = "", | ||
| 161 | verify_password: Annotated[str, Form()] = "", | ||
| 162 | next: Annotated[str | None, Query()] = None, | ||
| 163 | ): | ||
| 132 | form = None | 164 | form = None |
| 133 | errors = {} | 165 | errors = {} |
| 134 | 166 | ||
| 135 | try: | 167 | try: |
| 136 | form = RegisterForm.model_validate({ | 168 | form = RegisterForm.model_validate( |
| 137 | 'email': email, | 169 | {"email": email, "password": password, "verify_password": verify_password} |
| 138 | 'password': password, | 170 | ) |
| 139 | 'verify_password': verify_password | ||
| 140 | }) | ||
| 141 | 171 | ||
| 142 | with get_session_context() as db_session: | 172 | with get_session_context() as db_session: |
| 143 | user = User(username=email, email=email) | 173 | user = User(username=email, email=email) |
| @@ -147,13 +177,18 @@ async def post_register(request: Request, email: Annotated[str, Form()] = '', pa | |||
| 147 | session = Session(id=uuid7(), user=user) | 177 | session = Session(id=uuid7(), user=user) |
| 148 | db_session.add(session) | 178 | db_session.add(session) |
| 149 | 179 | ||
| 150 | request.session.update({'id': str(session.id)}) | 180 | request.session.update({"id": str(session.id)}) |
| 151 | 181 | ||
| 152 | if next is not None: | 182 | if next is not None: |
| 153 | return RedirectResponse(next, status_code=303) | 183 | return RedirectResponse(next, status_code=303) |
| 154 | 184 | ||
| 155 | except ValidationError as exc: | 185 | except ValidationError as exc: |
| 156 | errors.update({(e['loc'][0] if e['loc'] else 'verify_password'): e['msg'] for e in exc.errors()}) | 186 | errors.update( |
| 187 | { | ||
| 188 | (e["loc"][0] if e["loc"] else "verify_password"): e["msg"] | ||
| 189 | for e in exc.errors() | ||
| 190 | } | ||
| 191 | ) | ||
| 157 | 192 | ||
| 158 | return await base_register(request, form, errors) | 193 | return await base_register(request, form, errors) |
| 159 | 194 | ||
| @@ -162,49 +197,59 @@ async def base_authorize(request: Request, user: User | None = None): | |||
| 162 | return auth_server.create_authorization_response(request, request.user, grant=None) | 197 | return auth_server.create_authorization_response(request, request.user, grant=None) |
| 163 | 198 | ||
| 164 | 199 | ||
| 165 | @oauth.get('/authorize/') | 200 | @oauth.get("/authorize/") |
| 166 | @requires(['authenticated', 'session'], redirect='get_login') | 201 | @requires(["authenticated", "session"], redirect="get_login") |
| 167 | async def get_authorize(request: Request): | 202 | async def get_authorize(request: Request): |
| 168 | return await base_authorize(request, request.user) | 203 | return await base_authorize(request, request.user) |
| 169 | 204 | ||
| 170 | 205 | ||
| 171 | @oauth.post('/authorize/') | 206 | @oauth.post("/authorize/") |
| 172 | @requires(['authenticated', 'session'], redirect='get_login') | 207 | @requires(["authenticated", "session"], redirect="get_login") |
| 173 | async def post_authorize(request: Request): | 208 | async def post_authorize(request: Request): |
| 174 | return await base_authorize(request, request.user) | 209 | return await base_authorize(request, request.user) |
| 175 | 210 | ||
| 176 | 211 | ||
| 177 | @oauth.post('/token/') | 212 | @oauth.post("/token/") |
| 178 | async def post_token(request: Request): | 213 | async def post_token(request: Request): |
| 179 | await prepare_oauth_request(request) | 214 | await prepare_oauth_request(request) |
| 180 | return auth_server.create_token_response(request) | 215 | return auth_server.create_token_response(request) |
| 181 | 216 | ||
| 182 | 217 | ||
| 183 | @authentication.get('/connect/') | 218 | @authentication.get("/connect/") |
| 184 | async def connect(request: Request): | 219 | async def connect(request: Request): |
| 185 | origin = request.url.scheme + '://' + request.url.netloc | 220 | origin = request.url.scheme + "://" + request.url.netloc |
| 186 | 221 | ||
| 187 | with get_session_context() as session: | 222 | with get_session_context() as session: |
| 188 | app = session.exec(select(ClientApplication)).first() | 223 | app = session.exec(select(ClientApplication)).first() |
| 189 | 224 | ||
| 190 | return RedirectResponse( | 225 | return RedirectResponse( |
| 191 | '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { | 226 | "/oauth" |
| 192 | 'client_id': app.client_id, | 227 | + get_url_path_with_query( |
| 193 | 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), | 228 | oauth, |
| 194 | 'response_type': 'code', | 229 | "get_authorize", |
| 195 | 'state': b64encode(json.dumps({ | 230 | { |
| 196 | 'client_id': app.client_id, | 231 | "client_id": app.client_id, |
| 197 | }).encode()).decode(), | 232 | "redirect_uri": f"{origin}/oauth" |
| 198 | }), | 233 | + get_url_path_with_query(oauth, "callback"), |
| 234 | "response_type": "code", | ||
| 235 | "state": b64encode( | ||
| 236 | json.dumps( | ||
| 237 | { | ||
| 238 | "client_id": app.client_id, | ||
| 239 | } | ||
| 240 | ).encode() | ||
| 241 | ).decode(), | ||
| 242 | }, | ||
| 243 | ), | ||
| 199 | status_code=303, | 244 | status_code=303, |
| 200 | ) | 245 | ) |
| 201 | 246 | ||
| 202 | 247 | ||
| 203 | @oauth.get('/config/') | 248 | @oauth.get("/config/") |
| 204 | async def get_config(request: Request): | 249 | async def get_config(request: Request): |
| 205 | with get_session_context() as session: | 250 | with get_session_context() as session: |
| 206 | app = session.exec(select(ClientApplication)).first() | 251 | app = session.exec(select(ClientApplication)).first() |
| 207 | 252 | ||
| 208 | return { | 253 | return { |
| 209 | 'client_id': app.client_id, | 254 | "client_id": app.client_id, |
| 210 | } | 255 | } |
