diff options
| -rw-r--r-- | authentication/backend.py | 48 | ||||
| -rw-r--r-- | authentication/endpoints.py | 87 | ||||
| -rw-r--r-- | authentication/templates/authorize.html | 5 | ||||
| -rw-r--r-- | authentication/templates/base.html | 63 | ||||
| -rw-r--r-- | authentication/templates/login.html | 18 | ||||
| -rw-r--r-- | conf/__init__.py | 1 | ||||
| -rw-r--r-- | conf/local.py | 1 | ||||
| -rw-r--r-- | db/migrations/versions/202607031440-69d8b4938a12_added_more_fields_and_session.py | 56 | ||||
| -rw-r--r-- | db/models.py | 20 | ||||
| -rw-r--r-- | db/session.py | 2 | ||||
| -rw-r--r-- | pyproject.toml | 4 | ||||
| -rw-r--r-- | ttun_server/__init__.py | 7 | ||||
| -rw-r--r-- | ttun_server/endpoints.py | 5 | ||||
| -rw-r--r-- | uv.lock | 79 |
14 files changed, 387 insertions, 9 deletions
diff --git a/authentication/backend.py b/authentication/backend.py new file mode 100644 index 0000000..29408e3 --- /dev/null +++ b/authentication/backend.py | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | from datetime import datetime, UTC | ||
| 2 | from typing import Optional | ||
| 3 | |||
| 4 | from sqlalchemy import update | ||
| 5 | from sqlmodel import select | ||
| 6 | from starlette.authentication import AuthenticationBackend, AuthCredentials, BaseUser, UnauthenticatedUser | ||
| 7 | from starlette.requests import HTTPConnection | ||
| 8 | |||
| 9 | from db.models import Session, User | ||
| 10 | from db.session import get_session_context | ||
| 11 | |||
| 12 | |||
| 13 | class AuthBackend(AuthenticationBackend): | ||
| 14 | async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: | ||
| 15 | if "id" not in request.session: | ||
| 16 | return None | ||
| 17 | |||
| 18 | query = ( | ||
| 19 | select(Session) | ||
| 20 | .join(User) | ||
| 21 | .where( | ||
| 22 | Session.id == request.session['id'] | ||
| 23 | ) | ||
| 24 | ) | ||
| 25 | |||
| 26 | |||
| 27 | with get_session_context() as db_session: | ||
| 28 | session: Session = db_session.exec(query).first() | ||
| 29 | |||
| 30 | if session is not None: | ||
| 31 | db_session.execute( | ||
| 32 | update(Session).where(Session.id == session.id) | ||
| 33 | ) | ||
| 34 | db_session.commit() | ||
| 35 | |||
| 36 | return ( | ||
| 37 | AuthCredentials( | ||
| 38 | [ | ||
| 39 | "authenticated", | ||
| 40 | *[ | ||
| 41 | connection.type for connection in session.user.app_connections | ||
| 42 | ] | ||
| 43 | ] | ||
| 44 | if session is not None | ||
| 45 | else [] | ||
| 46 | ), | ||
| 47 | session.user if session is not None else UnauthenticatedUser() | ||
| 48 | ) | ||
diff --git a/authentication/endpoints.py b/authentication/endpoints.py new file mode 100644 index 0000000..ebdc7a1 --- /dev/null +++ b/authentication/endpoints.py | |||
| @@ -0,0 +1,87 @@ | |||
| 1 | from typing import Annotated | ||
| 2 | from uuid import uuid7 | ||
| 3 | |||
| 4 | from fastapi import FastAPI, Request, Form, Query, HTTPException | ||
| 5 | from pydantic import BaseModel, EmailStr, ValidationError | ||
| 6 | from starlette.middleware.authentication import AuthenticationMiddleware | ||
| 7 | from starlette.middleware.sessions import SessionMiddleware | ||
| 8 | from starlette.responses import HTMLResponse, RedirectResponse | ||
| 9 | from starlette.templating import Jinja2Templates | ||
| 10 | |||
| 11 | import conf | ||
| 12 | from authentication.backend import AuthBackend | ||
| 13 | from authentication.utils import bcrypt_sha256_verify | ||
| 14 | from db.models import User, Session, ClientApplication | ||
| 15 | from db.session import get_session_context | ||
| 16 | from proxy.utils import get_path_with_query_string | ||
| 17 | |||
| 18 | authentication = FastAPI() | ||
| 19 | authentication.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | ||
| 20 | |||
| 21 | oauth = FastAPI() | ||
| 22 | oauth.add_middleware(AuthenticationMiddleware, backend=AuthBackend()) | ||
| 23 | oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | ||
| 24 | |||
| 25 | templates = Jinja2Templates(directory='authentication/templates') | ||
| 26 | |||
| 27 | class LoginForm(BaseModel): | ||
| 28 | username: EmailStr | ||
| 29 | password: str | ||
| 30 | |||
| 31 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): | ||
| 32 | return templates.TemplateResponse(request, 'login.html', context={ | ||
| 33 | 'form': form, | ||
| 34 | 'errors': errors, | ||
| 35 | }) | ||
| 36 | |||
| 37 | @authentication.get('/login/', response_class=HTMLResponse) | ||
| 38 | async def login(request: Request): | ||
| 39 | return await base_login(request) | ||
| 40 | |||
| 41 | @authentication.post('/login/', response_class=HTMLResponse) | ||
| 42 | async def login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', redirect: Annotated[str, Query()] = None): | ||
| 43 | form = None | ||
| 44 | errors = {} | ||
| 45 | |||
| 46 | try: | ||
| 47 | form = LoginForm.model_validate({ | ||
| 48 | 'username': username, | ||
| 49 | 'password': password | ||
| 50 | }) | ||
| 51 | with get_session_context() as db_session: | ||
| 52 | user = db_session.query(User).filter(User.username == form.username).first() | ||
| 53 | |||
| 54 | if not bcrypt_sha256_verify(password, user.password if user is not None else '') or user is None: | ||
| 55 | errors.update({'password': 'Invalid username or password'}) | ||
| 56 | |||
| 57 | with get_session_context() as db_session: | ||
| 58 | session = Session(id=uuid7(), user=user) | ||
| 59 | db_session.add(session) | ||
| 60 | |||
| 61 | if redirect is not None: | ||
| 62 | return RedirectResponse(redirect) | ||
| 63 | |||
| 64 | except ValidationError as exc: | ||
| 65 | errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) | ||
| 66 | |||
| 67 | return await base_login(request, form, errors) | ||
| 68 | |||
| 69 | |||
| 70 | async def base_authorize(request: Request, client_id: str, redirect_uri: str): | ||
| 71 | with get_session_context() as session: | ||
| 72 | app = session.query(ClientApplication).where(ClientApplication.client_id == client_id).first() | ||
| 73 | |||
| 74 | if app is None: | ||
| 75 | raise HTTPException(status_code=404, detail='Invalid client id') | ||
| 76 | |||
| 77 | if not request.user.is_authenticated: | ||
| 78 | return RedirectResponse(f'/auth/login/?redirect={get_path_with_query_string(request)}') | ||
| 79 | |||
| 80 | |||
| 81 | @oauth.get('/authorize/') | ||
| 82 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | ||
| 83 | return await base_authorize(request, client_id, redirect_uri) | ||
| 84 | |||
| 85 | @oauth.post('/authorize/') | ||
| 86 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | ||
| 87 | return await base_authorize(request, client_id, redirect_uri) | ||
diff --git a/authentication/templates/authorize.html b/authentication/templates/authorize.html new file mode 100644 index 0000000..1ed6506 --- /dev/null +++ b/authentication/templates/authorize.html | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | {% extends "./base.html" %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <h1>AUTH</h1> | ||
| 5 | {% endblock %} | ||
diff --git a/authentication/templates/base.html b/authentication/templates/base.html new file mode 100644 index 0000000..47d007e --- /dev/null +++ b/authentication/templates/base.html | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="UTF-8"> | ||
| 5 | <title>TTUN</title> | ||
| 6 | <style> | ||
| 7 | /* http://meyerweb.com/eric/tools/css/reset/ | ||
| 8 | v2.0 | 20110126 | ||
| 9 | License: none (public domain) | ||
| 10 | */ | ||
| 11 | |||
| 12 | html, body, div, span, applet, object, iframe, | ||
| 13 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||
| 14 | a, abbr, acronym, address, big, cite, code, | ||
| 15 | del, dfn, em, img, ins, kbd, q, s, samp, | ||
| 16 | small, strike, strong, sub, sup, tt, var, | ||
| 17 | b, u, i, center, | ||
| 18 | dl, dt, dd, ol, ul, li, | ||
| 19 | fieldset, form, label, legend, | ||
| 20 | table, caption, tbody, tfoot, thead, tr, th, td, | ||
| 21 | article, aside, canvas, details, embed, | ||
| 22 | figure, figcaption, footer, header, hgroup, | ||
| 23 | menu, nav, output, ruby, section, summary, | ||
| 24 | time, mark, audio, video { | ||
| 25 | margin: 0; | ||
| 26 | padding: 0; | ||
| 27 | border: 0; | ||
| 28 | font-size: 100%; | ||
| 29 | font: inherit; | ||
| 30 | vertical-align: baseline; | ||
| 31 | } | ||
| 32 | /* HTML5 display-role reset for older browsers */ | ||
| 33 | article, aside, details, figcaption, figure, | ||
| 34 | footer, header, hgroup, menu, nav, section { | ||
| 35 | display: block; | ||
| 36 | } | ||
| 37 | body { | ||
| 38 | line-height: 1; | ||
| 39 | } | ||
| 40 | ol, ul { | ||
| 41 | list-style: none; | ||
| 42 | } | ||
| 43 | blockquote, q { | ||
| 44 | quotes: none; | ||
| 45 | } | ||
| 46 | blockquote:before, blockquote:after, | ||
| 47 | q:before, q:after { | ||
| 48 | content: ''; | ||
| 49 | content: none; | ||
| 50 | } | ||
| 51 | table { | ||
| 52 | border-collapse: collapse; | ||
| 53 | border-spacing: 0; | ||
| 54 | } | ||
| 55 | </style> | ||
| 56 | </head> | ||
| 57 | <body> | ||
| 58 | <div> | ||
| 59 | |||
| 60 | </div> | ||
| 61 | {% block content %}{% endblock %} | ||
| 62 | </body> | ||
| 63 | </html> | ||
diff --git a/authentication/templates/login.html b/authentication/templates/login.html new file mode 100644 index 0000000..fcba5cd --- /dev/null +++ b/authentication/templates/login.html | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | {% extends "./base.html" %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <form method="post"> | ||
| 5 | |||
| 6 | <input type="email" name="username" {% if form %}value="{{ form.username }}"{% endif %} placeholder="username" /> | ||
| 7 | {% if errors and errors.username %} | ||
| 8 | {{ errors.username }} | ||
| 9 | {% endif %} | ||
| 10 | |||
| 11 | |||
| 12 | <input type="password" name="password" /> | ||
| 13 | {% if errors and errors.password%} | ||
| 14 | {{ errors.password }} | ||
| 15 | {% endif %} | ||
| 16 | <button type="submit">Login</button> | ||
| 17 | </form> | ||
| 18 | {% endblock %} | ||
diff --git a/conf/__init__.py b/conf/__init__.py index c08dcee..bcdb9c5 100644 --- a/conf/__init__.py +++ b/conf/__init__.py | |||
| @@ -6,3 +6,4 @@ except ImportError: | |||
| 6 | pass | 6 | pass |
| 7 | 7 | ||
| 8 | DATABASE_URL = os.environ.get('DATABASE_URL') | 8 | DATABASE_URL = os.environ.get('DATABASE_URL') |
| 9 | SECRET_KEY = os.environ.get('SECRET_KEY') | ||
diff --git a/conf/local.py b/conf/local.py index 608e0e5..19b304e 100644 --- a/conf/local.py +++ b/conf/local.py | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | import os | 1 | import os |
| 2 | 2 | ||
| 3 | os.environ.setdefault('DATABASE_URL', 'postgresql:///ttun') | 3 | os.environ.setdefault('DATABASE_URL', 'postgresql:///ttun') |
| 4 | os.environ.setdefault('SECRET_KEY', 'secret') | ||
diff --git a/db/migrations/versions/202607031440-69d8b4938a12_added_more_fields_and_session.py b/db/migrations/versions/202607031440-69d8b4938a12_added_more_fields_and_session.py new file mode 100644 index 0000000..ec5031b --- /dev/null +++ b/db/migrations/versions/202607031440-69d8b4938a12_added_more_fields_and_session.py | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | """Added more fields and session | ||
| 2 | |||
| 3 | Revision ID: 69d8b4938a12 | ||
| 4 | Revises: ac4fbf1fea31 | ||
| 5 | Create Date: 2026-07-03 14:40:55.628816 | ||
| 6 | |||
| 7 | """ | ||
| 8 | from typing import Sequence, Union | ||
| 9 | |||
| 10 | from alembic import op | ||
| 11 | import sqlalchemy as sa | ||
| 12 | import sqlmodel | ||
| 13 | |||
| 14 | |||
| 15 | # revision identifiers, used by Alembic. | ||
| 16 | revision: str = '69d8b4938a12' | ||
| 17 | down_revision: Union[str, Sequence[str], None] = 'ac4fbf1fea31' | ||
| 18 | branch_labels: Union[str, Sequence[str], None] = None | ||
| 19 | depends_on: Union[str, Sequence[str], None] = None | ||
| 20 | |||
| 21 | |||
| 22 | def upgrade() -> None: | ||
| 23 | """Upgrade schema.""" | ||
| 24 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 25 | op.create_table('session', | ||
| 26 | sa.Column('id', sa.Uuid(), nullable=False), | ||
| 27 | sa.Column('created', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')), | ||
| 28 | sa.Column('updated', sa.DateTime(timezone=True), nullable=True), | ||
| 29 | sa.Column('user_id', sa.Uuid(), nullable=False), | ||
| 30 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), | ||
| 31 | sa.PrimaryKeyConstraint('id') | ||
| 32 | ) | ||
| 33 | op.add_column('authcode', sa.Column('created', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()'))) | ||
| 34 | op.add_column('authcode', sa.Column('updated', sa.DateTime(timezone=True), nullable=True)) | ||
| 35 | op.add_column('authtoken', sa.Column('created', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()'))) | ||
| 36 | op.add_column('authtoken', sa.Column('updated', sa.DateTime(timezone=True), nullable=True)) | ||
| 37 | op.add_column('clientapplication', sa.Column('created', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()'))) | ||
| 38 | op.add_column('clientapplication', sa.Column('updated', sa.DateTime(timezone=True), nullable=True)) | ||
| 39 | op.add_column('user', sa.Column('created', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()'))) | ||
| 40 | op.add_column('user', sa.Column('updated', sa.DateTime(timezone=True), nullable=True)) | ||
| 41 | # ### end Alembic commands ### | ||
| 42 | |||
| 43 | |||
| 44 | def downgrade() -> None: | ||
| 45 | """Downgrade schema.""" | ||
| 46 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 47 | op.drop_column('user', 'updated') | ||
| 48 | op.drop_column('user', 'created') | ||
| 49 | op.drop_column('clientapplication', 'updated') | ||
| 50 | op.drop_column('clientapplication', 'created') | ||
| 51 | op.drop_column('authtoken', 'updated') | ||
| 52 | op.drop_column('authtoken', 'created') | ||
| 53 | op.drop_column('authcode', 'updated') | ||
| 54 | op.drop_column('authcode', 'created') | ||
| 55 | op.drop_table('session') | ||
| 56 | # ### end Alembic commands ### | ||
diff --git a/db/models.py b/db/models.py index e758671..7e0afb0 100644 --- a/db/models.py +++ b/db/models.py | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | from datetime import datetime, UTC | ||
| 1 | from uuid import uuid4, UUID, uuid7 | 2 | from uuid import uuid4, UUID, uuid7 |
| 2 | 3 | ||
| 3 | from sqlmodel import SQLModel, Field | 4 | from sqlalchemy import DateTime |
| 5 | from sqlmodel import SQLModel, Field, Relationship | ||
| 4 | from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin, OAuth2AuthorizationCodeMixin, OAuth2TokenMixin | 6 | from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin, OAuth2AuthorizationCodeMixin, OAuth2TokenMixin |
| 5 | 7 | ||
| 6 | from authentication.utils import bcrypt_sha256_hash | 8 | from authentication.utils import bcrypt_sha256_hash |
| @@ -8,6 +10,19 @@ from authentication.utils import bcrypt_sha256_hash | |||
| 8 | 10 | ||
| 9 | class BaseModel(SQLModel): | 11 | class BaseModel(SQLModel): |
| 10 | id: UUID | None = Field(primary_key=True, default_factory=uuid7) | 12 | id: UUID | None = Field(primary_key=True, default_factory=uuid7) |
| 13 | created: datetime = Field( | ||
| 14 | default_factory=lambda: datetime.now(tz=UTC), | ||
| 15 | sa_type=DateTime(timezone=True), | ||
| 16 | ) | ||
| 17 | updated: datetime = Field( | ||
| 18 | default=None, | ||
| 19 | nullable=True, | ||
| 20 | sa_column_kwargs={ | ||
| 21 | 'onupdate': lambda: datetime.now(tz=UTC), | ||
| 22 | }, | ||
| 23 | sa_type=DateTime(timezone=True), | ||
| 24 | ) | ||
| 25 | |||
| 11 | 26 | ||
| 12 | class User(BaseModel, table=True): | 27 | class User(BaseModel, table=True): |
| 13 | username: str = Field(unique=True, index=True) | 28 | username: str = Field(unique=True, index=True) |
| @@ -18,6 +33,9 @@ class User(BaseModel, table=True): | |||
| 18 | def set_password(self, value): | 33 | def set_password(self, value): |
| 19 | self.password = bcrypt_sha256_hash(value) | 34 | self.password = bcrypt_sha256_hash(value) |
| 20 | 35 | ||
| 36 | class Session(BaseModel, table=True): | ||
| 37 | user_id: UUID = Field(foreign_key='user.id', exclude=True) | ||
| 38 | user: User = Relationship() | ||
| 21 | 39 | ||
| 22 | class ClientApplication(BaseModel, OAuth2ClientMixin, table=True): | 40 | class ClientApplication(BaseModel, OAuth2ClientMixin, table=True): |
| 23 | name: str | 41 | name: str |
diff --git a/db/session.py b/db/session.py index ec700bd..82c0211 100644 --- a/db/session.py +++ b/db/session.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | from contextlib import contextmanager | 1 | from contextlib import contextmanager |
| 2 | 2 | ||
| 3 | from sqlalchemy import create_engine | ||
| 4 | from sqlalchemy.orm import sessionmaker, scoped_session | 3 | from sqlalchemy.orm import sessionmaker, scoped_session |
| 4 | from sqlmodel import create_engine | ||
| 5 | from sqlmodel import Session | 5 | from sqlmodel import Session |
| 6 | 6 | ||
| 7 | from conf import DATABASE_URL | 7 | from conf import DATABASE_URL |
diff --git a/pyproject.toml b/pyproject.toml index 7355e22..6a3117f 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -6,9 +6,13 @@ dependencies = [ | |||
| 6 | "alembic>=1.18.5", | 6 | "alembic>=1.18.5", |
| 7 | "authlib>=1.7.2", | 7 | "authlib>=1.7.2", |
| 8 | "bcrypt>=5.0.0", | 8 | "bcrypt>=5.0.0", |
| 9 | "csrfmiddleware>=1.3", | ||
| 9 | "fastapi[standard]>=0.138.2", | 10 | "fastapi[standard]>=0.138.2", |
| 11 | "itsdangerous>=2.2.0", | ||
| 12 | "jinja2>=3.1.6", | ||
| 10 | "passlib>=1.7.4", | 13 | "passlib>=1.7.4", |
| 11 | "psycopg2>=2.9.12", | 14 | "psycopg2>=2.9.12", |
| 15 | "python-multipart>=0.0.32", | ||
| 12 | "redis[hiredis]~=7.4.0", | 16 | "redis[hiredis]~=7.4.0", |
| 13 | "sqlmodel>=0.0.39", | 17 | "sqlmodel>=0.0.39", |
| 14 | "uvicorn[standard]~=0.44.0", | 18 | "uvicorn[standard]~=0.44.0", |
diff --git a/ttun_server/__init__.py b/ttun_server/__init__.py index d54227a..4af6ca0 100644 --- a/ttun_server/__init__.py +++ b/ttun_server/__init__.py | |||
| @@ -2,10 +2,11 @@ import logging | |||
| 2 | import os | 2 | import os |
| 3 | 3 | ||
| 4 | from fastapi import FastAPI | 4 | from fastapi import FastAPI |
| 5 | from starlette.routing import Host, Route, WebSocketRoute | 5 | from starlette.routing import Host, Route, WebSocketRoute, Mount |
| 6 | 6 | ||
| 7 | from authentication.endpoints import authentication, oauth | ||
| 7 | from proxy import app as proxy_app | 8 | from proxy import app as proxy_app |
| 8 | from ttun_server.endpoints import health, base_endpoints | 9 | from ttun_server.endpoints import health |
| 9 | from ttun_server.websockets import tunnel | 10 | from ttun_server.websockets import tunnel |
| 10 | 11 | ||
| 11 | logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO'))) | 12 | logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO'))) |
| @@ -13,6 +14,8 @@ logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO'))) | |||
| 13 | app = FastAPI( | 14 | app = FastAPI( |
| 14 | debug=True, | 15 | debug=True, |
| 15 | routes=[ | 16 | routes=[ |
| 17 | Mount('/auth/', app=authentication), | ||
| 18 | Mount('/oauth/', app=oauth), | ||
| 16 | WebSocketRoute('/tunnel/', endpoint=tunnel), | 19 | WebSocketRoute('/tunnel/', endpoint=tunnel), |
| 17 | Route('/health/', endpoint=health), | 20 | Route('/health/', endpoint=health), |
| 18 | Host(f'{{subdomain}}.{os.environ['TUNNEL_DOMAIN']}', app=proxy_app) | 21 | Host(f'{{subdomain}}.{os.environ['TUNNEL_DOMAIN']}', app=proxy_app) |
diff --git a/ttun_server/endpoints.py b/ttun_server/endpoints.py index 51c17aa..61e3f21 100644 --- a/ttun_server/endpoints.py +++ b/ttun_server/endpoints.py | |||
| @@ -1,7 +1,2 @@ | |||
| 1 | from fastapi import FastAPI | ||
| 2 | |||
| 3 | base_endpoints = FastAPI() | ||
| 4 | |||
| 5 | @base_endpoints.get('/health/') | ||
| 6 | async def health(): | 1 | async def health(): |
| 7 | return 'OK' | 2 | return 'OK' |
| @@ -111,6 +111,15 @@ wheels = [ | |||
| 111 | ] | 111 | ] |
| 112 | 112 | ||
| 113 | [[package]] | 113 | [[package]] |
| 114 | name = "beaker" | ||
| 115 | version = "1.14.1" | ||
| 116 | source = { registry = "https://pypi.org/simple" } | ||
| 117 | sdist = { url = "https://files.pythonhosted.org/packages/83/6b/3cd3dcf40417e3be31a3a2257957144b0c058ffaf9ca32d2c83c85567cb6/beaker-1.14.1.tar.gz", hash = "sha256:886f52a51810703fdbc0a3e54fca40886288ff530b2070582edce72bf1945447", size = 39076, upload-time = "2026-05-30T12:02:22.975Z" } | ||
| 118 | wheels = [ | ||
| 119 | { url = "https://files.pythonhosted.org/packages/1e/32/9ec40a82a4831ec8c280756e3312b3bbd95754a74a69574a240274318f35/beaker-1.14.1-py3-none-any.whl", hash = "sha256:b435a4f5ac67d0367e02351ef7ce8df89e74b6450f1f4c3fb981c6d8c4071213", size = 47639, upload-time = "2026-05-30T12:02:21.837Z" }, | ||
| 120 | ] | ||
| 121 | |||
| 122 | [[package]] | ||
| 114 | name = "certifi" | 123 | name = "certifi" |
| 115 | version = "2026.6.17" | 124 | version = "2026.6.17" |
| 116 | source = { registry = "https://pypi.org/simple" } | 125 | source = { registry = "https://pypi.org/simple" } |
| @@ -224,6 +233,17 @@ wheels = [ | |||
| 224 | ] | 233 | ] |
| 225 | 234 | ||
| 226 | [[package]] | 235 | [[package]] |
| 236 | name = "csrfmiddleware" | ||
| 237 | version = "1.3" | ||
| 238 | source = { registry = "https://pypi.org/simple" } | ||
| 239 | dependencies = [ | ||
| 240 | { name = "beaker" }, | ||
| 241 | { name = "paste" }, | ||
| 242 | { name = "webob" }, | ||
| 243 | ] | ||
| 244 | sdist = { url = "https://files.pythonhosted.org/packages/2c/cf/76b75cdb5ab7496c8ac24fd2fe6e1c4a7caa988cca19d74895d308e816db/csrfmiddleware-1.3.tar.gz", hash = "sha256:5315a188c72d99914f3899449479fa6ac62c55447ece9decdf7339bea08849e7", size = 3521, upload-time = "2009-02-20T20:45:43.803Z" } | ||
| 245 | |||
| 246 | [[package]] | ||
| 227 | name = "detect-installer" | 247 | name = "detect-installer" |
| 228 | version = "0.1.0" | 248 | version = "0.1.0" |
| 229 | source = { registry = "https://pypi.org/simple" } | 249 | source = { registry = "https://pypi.org/simple" } |
| @@ -498,6 +518,15 @@ wheels = [ | |||
| 498 | ] | 518 | ] |
| 499 | 519 | ||
| 500 | [[package]] | 520 | [[package]] |
| 521 | name = "itsdangerous" | ||
| 522 | version = "2.2.0" | ||
| 523 | source = { registry = "https://pypi.org/simple" } | ||
| 524 | sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } | ||
| 525 | wheels = [ | ||
| 526 | { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, | ||
| 527 | ] | ||
| 528 | |||
| 529 | [[package]] | ||
| 501 | name = "jinja2" | 530 | name = "jinja2" |
| 502 | version = "3.1.6" | 531 | version = "3.1.6" |
| 503 | source = { registry = "https://pypi.org/simple" } | 532 | source = { registry = "https://pypi.org/simple" } |
| @@ -522,6 +551,15 @@ wheels = [ | |||
| 522 | ] | 551 | ] |
| 523 | 552 | ||
| 524 | [[package]] | 553 | [[package]] |
| 554 | name = "legacy-cgi" | ||
| 555 | version = "2.6.4" | ||
| 556 | source = { registry = "https://pypi.org/simple" } | ||
| 557 | sdist = { url = "https://files.pythonhosted.org/packages/f4/9c/91c7d2c5ebbdf0a1a510bfa0ddeaa2fbb5b78677df5ac0a0aa51cf7125b0/legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577", size = 24603, upload-time = "2025-10-27T05:20:05.395Z" } | ||
| 558 | wheels = [ | ||
| 559 | { url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" }, | ||
| 560 | ] | ||
| 561 | |||
| 562 | [[package]] | ||
| 525 | name = "mako" | 563 | name = "mako" |
| 526 | version = "1.3.12" | 564 | version = "1.3.12" |
| 527 | source = { registry = "https://pypi.org/simple" } | 565 | source = { registry = "https://pypi.org/simple" } |
| @@ -594,6 +632,18 @@ wheels = [ | |||
| 594 | ] | 632 | ] |
| 595 | 633 | ||
| 596 | [[package]] | 634 | [[package]] |
| 635 | name = "paste" | ||
| 636 | version = "3.10.1" | ||
| 637 | source = { registry = "https://pypi.org/simple" } | ||
| 638 | dependencies = [ | ||
| 639 | { name = "setuptools" }, | ||
| 640 | ] | ||
| 641 | sdist = { url = "https://files.pythonhosted.org/packages/d7/1c/6bc9040bf9b4cfc9334f66d2738f952384c106c48882adf6097fed3da966/paste-3.10.1.tar.gz", hash = "sha256:1c3d12065a5e8a7a18c0c7be1653a97cf38cc3e9a5a0c8334a9dd992d3a05e4a", size = 652629, upload-time = "2024-05-01T11:41:08.536Z" } | ||
| 642 | wheels = [ | ||
| 643 | { url = "https://files.pythonhosted.org/packages/2e/14/032895c25726a859bf48b8ed68944c3efc7a3decd920533ed929f12f08a1/Paste-3.10.1-py3-none-any.whl", hash = "sha256:995e9994b6a94a2bdd8bd9654fb70ca3946ffab75442468bacf31b4d06481c3d", size = 289253, upload-time = "2024-05-01T11:41:05.31Z" }, | ||
| 644 | ] | ||
| 645 | |||
| 646 | [[package]] | ||
| 597 | name = "psycopg2" | 647 | name = "psycopg2" |
| 598 | version = "2.9.12" | 648 | version = "2.9.12" |
| 599 | source = { registry = "https://pypi.org/simple" } | 649 | source = { registry = "https://pypi.org/simple" } |
| @@ -845,6 +895,15 @@ wheels = [ | |||
| 845 | ] | 895 | ] |
| 846 | 896 | ||
| 847 | [[package]] | 897 | [[package]] |
| 898 | name = "setuptools" | ||
| 899 | version = "82.0.1" | ||
| 900 | source = { registry = "https://pypi.org/simple" } | ||
| 901 | sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } | ||
| 902 | wheels = [ | ||
| 903 | { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, | ||
| 904 | ] | ||
| 905 | |||
| 906 | [[package]] | ||
| 848 | name = "shellingham" | 907 | name = "shellingham" |
| 849 | version = "1.5.4" | 908 | version = "1.5.4" |
| 850 | source = { registry = "https://pypi.org/simple" } | 909 | source = { registry = "https://pypi.org/simple" } |
| @@ -914,9 +973,13 @@ dependencies = [ | |||
| 914 | { name = "alembic" }, | 973 | { name = "alembic" }, |
| 915 | { name = "authlib" }, | 974 | { name = "authlib" }, |
| 916 | { name = "bcrypt" }, | 975 | { name = "bcrypt" }, |
| 976 | { name = "csrfmiddleware" }, | ||
| 917 | { name = "fastapi", extra = ["standard"] }, | 977 | { name = "fastapi", extra = ["standard"] }, |
| 978 | { name = "itsdangerous" }, | ||
| 979 | { name = "jinja2" }, | ||
| 918 | { name = "passlib" }, | 980 | { name = "passlib" }, |
| 919 | { name = "psycopg2" }, | 981 | { name = "psycopg2" }, |
| 982 | { name = "python-multipart" }, | ||
| 920 | { name = "redis", extra = ["hiredis"] }, | 983 | { name = "redis", extra = ["hiredis"] }, |
| 921 | { name = "sqlmodel" }, | 984 | { name = "sqlmodel" }, |
| 922 | { name = "uvicorn", extra = ["standard"] }, | 985 | { name = "uvicorn", extra = ["standard"] }, |
| @@ -927,9 +990,13 @@ requires-dist = [ | |||
| 927 | { name = "alembic", specifier = ">=1.18.5" }, | 990 | { name = "alembic", specifier = ">=1.18.5" }, |
| 928 | { name = "authlib", specifier = ">=1.7.2" }, | 991 | { name = "authlib", specifier = ">=1.7.2" }, |
| 929 | { name = "bcrypt", specifier = ">=5.0.0" }, | 992 | { name = "bcrypt", specifier = ">=5.0.0" }, |
| 993 | { name = "csrfmiddleware", specifier = ">=1.3" }, | ||
| 930 | { name = "fastapi", extras = ["standard"], specifier = ">=0.138.2" }, | 994 | { name = "fastapi", extras = ["standard"], specifier = ">=0.138.2" }, |
| 995 | { name = "itsdangerous", specifier = ">=2.2.0" }, | ||
| 996 | { name = "jinja2", specifier = ">=3.1.6" }, | ||
| 931 | { name = "passlib", specifier = ">=1.7.4" }, | 997 | { name = "passlib", specifier = ">=1.7.4" }, |
| 932 | { name = "psycopg2", specifier = ">=2.9.12" }, | 998 | { name = "psycopg2", specifier = ">=2.9.12" }, |
| 999 | { name = "python-multipart", specifier = ">=0.0.32" }, | ||
| 933 | { name = "redis", extras = ["hiredis"], specifier = "~=7.4.0" }, | 1000 | { name = "redis", extras = ["hiredis"], specifier = "~=7.4.0" }, |
| 934 | { name = "sqlmodel", specifier = ">=0.0.39" }, | 1001 | { name = "sqlmodel", specifier = ">=0.0.39" }, |
| 935 | { name = "uvicorn", extras = ["standard"], specifier = "~=0.44.0" }, | 1002 | { name = "uvicorn", extras = ["standard"], specifier = "~=0.44.0" }, |
| @@ -1059,6 +1126,18 @@ wheels = [ | |||
| 1059 | ] | 1126 | ] |
| 1060 | 1127 | ||
| 1061 | [[package]] | 1128 | [[package]] |
| 1129 | name = "webob" | ||
| 1130 | version = "1.8.10" | ||
| 1131 | source = { registry = "https://pypi.org/simple" } | ||
| 1132 | dependencies = [ | ||
| 1133 | { name = "legacy-cgi" }, | ||
| 1134 | ] | ||
| 1135 | sdist = { url = "https://files.pythonhosted.org/packages/58/f9/974eafebfd0bd442b8848899fe7d30675c93f750c313e1a6fe61acbde1e3/webob-1.8.10.tar.gz", hash = "sha256:1c963a11f307bc3f624fbab9dde737701eae255f32981b7a5486a88db1767c2b", size = 280796, upload-time = "2026-06-02T19:56:47.268Z" } | ||
| 1136 | wheels = [ | ||
| 1137 | { url = "https://files.pythonhosted.org/packages/83/21/fce134877fb6fc6ad3c464e4a07ede0ee9219f705d26a981ae58ea36ca13/webob-1.8.10-py2.py3-none-any.whl", hash = "sha256:e68ad87fda378191081965ab02a185391c26e4e926adec855c3b0286a8369d49", size = 115825, upload-time = "2026-06-02T19:56:44.765Z" }, | ||
| 1138 | ] | ||
| 1139 | |||
| 1140 | [[package]] | ||
| 1062 | name = "websockets" | 1141 | name = "websockets" |
| 1063 | version = "16.0" | 1142 | version = "16.0" |
| 1064 | source = { registry = "https://pypi.org/simple" } | 1143 | source = { registry = "https://pypi.org/simple" } |
