From d59f96473d524ccd9113180a2cc3b982a0752559 Mon Sep 17 00:00:00 2001 From: hexdev Date: Fri, 24 Jul 2026 13:25:04 +0700 Subject: [PATCH] feat(auth): include expires_at in token responses and KeyPair DTO --- ...27aeb65f28_create_pricing_configs_table.py | 32 +++++++++++++ ...bdd_create_pricing_configs_table_frn_ow.py | 47 +++++++++++++++++++ core/secrets.py | 2 +- db/models/__init__.py | 3 +- db/models/pricing.py | 19 ++++++++ routes/auth.py | 6 ++- schemas/dto.py | 1 + schemas/login.py | 2 + services/users.py | 1 + 9 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 alembic/versions/2e27aeb65f28_create_pricing_configs_table.py create mode 100644 alembic/versions/74bd9cedebdd_create_pricing_configs_table_frn_ow.py create mode 100644 db/models/pricing.py diff --git a/alembic/versions/2e27aeb65f28_create_pricing_configs_table.py b/alembic/versions/2e27aeb65f28_create_pricing_configs_table.py new file mode 100644 index 0000000..341c5c3 --- /dev/null +++ b/alembic/versions/2e27aeb65f28_create_pricing_configs_table.py @@ -0,0 +1,32 @@ +"""create pricing_configs table + +Revision ID: 2e27aeb65f28 +Revises: 086c8d206446 +Create Date: 2026-07-24 13:22:30.845968 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '2e27aeb65f28' +down_revision: Union[str, Sequence[str], None] = '086c8d206446' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/versions/74bd9cedebdd_create_pricing_configs_table_frn_ow.py b/alembic/versions/74bd9cedebdd_create_pricing_configs_table_frn_ow.py new file mode 100644 index 0000000..e13c128 --- /dev/null +++ b/alembic/versions/74bd9cedebdd_create_pricing_configs_table_frn_ow.py @@ -0,0 +1,47 @@ +"""create pricing_configs table frn ow + +Revision ID: 74bd9cedebdd +Revises: 2e27aeb65f28 +Create Date: 2026-07-24 13:23:52.427837 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '74bd9cedebdd' +down_revision: Union[str, Sequence[str], None] = '2e27aeb65f28' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pricing_configs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('device_price', sa.FLOAT(), nullable=False), + sa.Column('updated_at', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False), + sa.CheckConstraint('id = 1', name='single_row_check'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + # ### end Alembic commands ### + + op.execute( + """ + INSERT INTO pricing_configs (id, device_price) + VALUES (1, 50.00) + ON CONFLICT (id) DO NOTHING; + """ + ) + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pricing_configs') + # ### end Alembic commands ### diff --git a/core/secrets.py b/core/secrets.py index 0c74e78..100607a 100644 --- a/core/secrets.py +++ b/core/secrets.py @@ -47,7 +47,7 @@ def generate_pair(user_id: int, iss: ProvidersType) -> KeyPair: access_token = generate_jwt(payload.model_dump()) refresh_token = secrets.token_urlsafe(32) - return KeyPair(access_token=access_token, refresh_token=refresh_token) + return KeyPair(access_token=access_token, refresh_token=refresh_token, expires_at=payload.exp) def hash_refresh_token(token: str): diff --git a/db/models/__init__.py b/db/models/__init__.py index dfdd56f..1bf849c 100644 --- a/db/models/__init__.py +++ b/db/models/__init__.py @@ -1,4 +1,5 @@ +from .pricing import PricingConfig from .sessions import Session from .users import User -__all__ = ["Session", "User"] +__all__ = ["PricingConfig", "Session", "User"] diff --git a/db/models/pricing.py b/db/models/pricing.py new file mode 100644 index 0000000..1122d01 --- /dev/null +++ b/db/models/pricing.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from sqlalchemy import FLOAT, INTEGER, TIMESTAMP, CheckConstraint, func +from sqlalchemy.orm import Mapped, mapped_column + +from db.base import Base + + +class PricingConfig(Base): + __tablename__ = "pricing_configs" + + __table_args__ = ( + CheckConstraint("id = 1", name="single_row_check"), + ) + + id: Mapped[int] = mapped_column(INTEGER, autoincrement=True, unique=True, nullable=False, primary_key=True) + device_price: Mapped[float] = mapped_column(FLOAT, nullable=False) + + updated_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now(), onupdate=func.now()) diff --git a/routes/auth.py b/routes/auth.py index 01ce7eb..7419d70 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -72,4 +72,8 @@ async def refresh(refresh_token: str, iss: ProvidersType, session: AsyncSession raise HTTPException(status_code=401, detail="Refresh token is invalid.") key_pair = await refresh_token_rotation(sessions_repo, token_entry, iss) - return UserTokens(access_token=key_pair.access_token, refresh_token=key_pair.refresh_token) + return UserTokens( + access_token=key_pair.access_token, + refresh_token=key_pair.refresh_token, + expires_at=key_pair.expires_at, + ) diff --git a/schemas/dto.py b/schemas/dto.py index 2984c22..cb09e30 100644 --- a/schemas/dto.py +++ b/schemas/dto.py @@ -5,3 +5,4 @@ from dataclasses import dataclass class KeyPair: access_token: str refresh_token: str + expires_at: float diff --git a/schemas/login.py b/schemas/login.py index f742f9e..df76fae 100644 --- a/schemas/login.py +++ b/schemas/login.py @@ -19,9 +19,11 @@ class UserLoginData(BaseModel): class UserLogin(BaseModel): access_token: str refresh_token: str + expires_at: float user: UserInfo class UserTokens(BaseModel): access_token: str refresh_token: str + expires_at: float diff --git a/services/users.py b/services/users.py index fee58bc..69c8b3f 100644 --- a/services/users.py +++ b/services/users.py @@ -19,4 +19,5 @@ async def authorize_user( access_token=key_pair.access_token, refresh_token=key_pair.refresh_token, user=UserInfo(username=user.username, telegram_id=user.telegram_id), + expires_at=key_pair.expires_at, )