feat(auth): include expires_at in token responses and KeyPair DTO

This commit is contained in:
2026-07-24 13:25:04 +07:00
parent 9187fbb970
commit d59f96473d
9 changed files with 110 additions and 3 deletions

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -47,7 +47,7 @@ def generate_pair(user_id: int, iss: ProvidersType) -> KeyPair:
access_token = generate_jwt(payload.model_dump()) access_token = generate_jwt(payload.model_dump())
refresh_token = secrets.token_urlsafe(32) 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): def hash_refresh_token(token: str):

View File

@@ -1,4 +1,5 @@
from .pricing import PricingConfig
from .sessions import Session from .sessions import Session
from .users import User from .users import User
__all__ = ["Session", "User"] __all__ = ["PricingConfig", "Session", "User"]

19
db/models/pricing.py Normal file
View File

@@ -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())

View File

@@ -72,4 +72,8 @@ async def refresh(refresh_token: str, iss: ProvidersType, session: AsyncSession
raise HTTPException(status_code=401, detail="Refresh token is invalid.") raise HTTPException(status_code=401, detail="Refresh token is invalid.")
key_pair = await refresh_token_rotation(sessions_repo, token_entry, iss) 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,
)

View File

@@ -5,3 +5,4 @@ from dataclasses import dataclass
class KeyPair: class KeyPair:
access_token: str access_token: str
refresh_token: str refresh_token: str
expires_at: float

View File

@@ -19,9 +19,11 @@ class UserLoginData(BaseModel):
class UserLogin(BaseModel): class UserLogin(BaseModel):
access_token: str access_token: str
refresh_token: str refresh_token: str
expires_at: float
user: UserInfo user: UserInfo
class UserTokens(BaseModel): class UserTokens(BaseModel):
access_token: str access_token: str
refresh_token: str refresh_token: str
expires_at: float

View File

@@ -19,4 +19,5 @@ async def authorize_user(
access_token=key_pair.access_token, access_token=key_pair.access_token,
refresh_token=key_pair.refresh_token, refresh_token=key_pair.refresh_token,
user=UserInfo(username=user.username, telegram_id=user.telegram_id), user=UserInfo(username=user.username, telegram_id=user.telegram_id),
expires_at=key_pair.expires_at,
) )