Compare commits

..

3 Commits

Author SHA1 Message Date
4262d4d017 fix: replaced remnawave_sdk to local gitea 2026-09-11 21:55:28 +07:00
429c961ace feat: /signup returns access_token 2026-09-04 21:21:20 +07:00
27e2f58956 feat: local testing compose file 2026-09-04 21:02:08 +07:00
8 changed files with 36 additions and 24 deletions

4
compose.local.yml Normal file
View File

@@ -0,0 +1,4 @@
services:
postgres:
ports: !override
- "5432:5432"

View File

@@ -5,7 +5,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env") model_config = SettingsConfigDict(env_file=".env", extra="ignore")
### Internal settings ### ### Internal settings ###
postgres_user: str = Field() postgres_user: str = Field()

View File

@@ -19,8 +19,6 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
ports:
- "5432:5432"
volumes: volumes:
postgres_data: postgres_data:

View File

@@ -8,7 +8,10 @@ asyncpg>=0.31.0
alembic>=1.18.0 alembic>=1.18.0
aiohttp>=3.14.0 aiohttp>=3.14.0
python-multipart==0.0.32 python-multipart==0.0.32
remnawave>=2.6.1 # Private remnawave SDK: bare URL without creds (safe to commit).
# Local install: export REMNAWAVE_SDK_TOKEN=xxx && git config --global url."https://agony:${REMNAWAVE_SDK_TOKEN}@git.mdevs.lat/".insteadOf "https://git.mdevs.lat/" && pip install -r requirements.txt
# Without the token git clone fails with 401/403 (repo is private, login is hardcoded to 'agony').
remnawave @ git+https://git.mdevs.lat/agony/remnawave-sdk.git
zxcvbn>=4.5.0 zxcvbn>=4.5.0
httpx2>=2.11.0 httpx2>=2.11.0
pytest>=9.1.0 pytest>=9.1.0

View File

@@ -1,5 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from core.secrets import ( from core.secrets import (
estimate_password_strength, estimate_password_strength,
@@ -10,7 +9,7 @@ from core.secrets import (
from db.session import UnitOfWork, get_uow from db.session import UnitOfWork, get_uow
from repositories.sessions import SessionsRepository from repositories.sessions import SessionsRepository
from repositories.users import UserRepository from repositories.users import UserRepository
from schemas.login import UserLogin, UserLoginData, UserTokens from schemas.login import AuthenticatedUser, UserLoginData, UserTokens
from schemas.providers import ProvidersType from schemas.providers import ProvidersType
from schemas.registration import UserRegistration from schemas.registration import UserRegistration
from schemas.user import UserInfo from schemas.user import UserInfo
@@ -20,9 +19,10 @@ from services.users import authorize_user
router = APIRouter(prefix="/auth") router = APIRouter(prefix="/auth")
@router.post("/signup") @router.post("/signup", response_model=AuthenticatedUser)
async def signup(req: UserRegistration, uow: UnitOfWork = Depends(get_uow)): async def signup(req: UserRegistration, uow: UnitOfWork = Depends(get_uow)) -> AuthenticatedUser:
users_repo = UserRepository(uow) users_repo = UserRepository(uow)
sessions_repo = SessionsRepository(uow)
if req.provider == "credentials": if req.provider == "credentials":
if not req.username or not req.password: if not req.username or not req.password:
@@ -44,19 +44,24 @@ async def signup(req: UserRegistration, uow: UnitOfWork = Depends(get_uow)):
username=req.username, hashed_password=password_hash, referal_id=referal_id username=req.username, hashed_password=password_hash, referal_id=referal_id
) )
await uow.commit() await uow.commit()
return JSONResponse(
UserInfo( info = UserInfo(
username=user.username, username=user.username,
telegram_id=user.telegram_id, telegram_id=user.telegram_id,
referal_code=user.referal_code, referal_code=user.referal_code,
bonus_balance=user.balance, bonus_balance=user.balance,
).model_dump(), )
status_code=201, user_login = await authorize_user(sessions_repo, user, req.provider)
return AuthenticatedUser(
access_token=user_login.access_token,
refresh_token=user_login.refresh_token,
expires_at=user_login.expires_at,
user=info,
) )
raise HTTPException(status_code=400, detail="Unsupported provider") raise HTTPException(status_code=400, detail="Unsupported provider")
@router.post("/login", response_model=UserLogin) @router.post("/login", response_model=AuthenticatedUser)
async def login(req: UserLoginData, uow: UnitOfWork = Depends(get_uow)): async def login(req: UserLoginData, uow: UnitOfWork = Depends(get_uow)):
users_repo = UserRepository(uow) users_repo = UserRepository(uow)
sessions_repo = SessionsRepository(uow) sessions_repo = SessionsRepository(uow)

View File

@@ -16,7 +16,7 @@ class UserLoginData(BaseModel):
telegram: TelegramData | None = None telegram: TelegramData | None = None
class UserLogin(BaseModel): class AuthenticatedUser(BaseModel):
access_token: str access_token: str
refresh_token: str refresh_token: str
expires_at: float expires_at: float

View File

@@ -1,21 +1,21 @@
from core.secrets import generate_pair, hash_refresh_token from core.secrets import generate_pair, hash_refresh_token
from db.models.users import User from db.models.users import User
from repositories.sessions import SessionsRepository from repositories.sessions import SessionsRepository
from schemas.login import UserLogin from schemas.login import AuthenticatedUser
from schemas.providers import ProvidersType from schemas.providers import ProvidersType
from schemas.user import UserInfo from schemas.user import UserInfo
async def authorize_user( async def authorize_user(
sessions_repo: SessionsRepository, user: User, iss: ProvidersType sessions_repo: SessionsRepository, user: User, iss: ProvidersType
) -> UserLogin: ) -> AuthenticatedUser:
key_pair = generate_pair(user.id, iss) key_pair = generate_pair(user.id, iss)
refresh_token_hash = hash_refresh_token(key_pair.refresh_token) refresh_token_hash = hash_refresh_token(key_pair.refresh_token)
await sessions_repo.create(user_id=user.id, refresh_token_hash=refresh_token_hash, iss=iss) await sessions_repo.create(user_id=user.id, refresh_token_hash=refresh_token_hash, iss=iss)
return UserLogin( return AuthenticatedUser(
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( user=UserInfo(

View File

@@ -49,15 +49,17 @@ def test_signup_rejects_weak_password(client):
def test_signup_accepts_unknown_referral_code(client): def test_signup_accepts_unknown_referral_code(client):
created_user = SimpleNamespace( created_user = SimpleNamespace(
username="alice", telegram_id=None, referal_code="new-code", balance=0 id=1, username="alice", telegram_id=None, referal_code="new-code", balance=0
) )
repository = SimpleNamespace( repository = SimpleNamespace(
get_user_by_username=AsyncMock(return_value=None), get_user_by_username=AsyncMock(return_value=None),
get_user_by_ref_code=AsyncMock(return_value=None), get_user_by_ref_code=AsyncMock(return_value=None),
create=AsyncMock(return_value=created_user), create=AsyncMock(return_value=created_user),
) )
sessions_repository = SimpleNamespace(create=AsyncMock(return_value=None))
with ( with (
patch("routes.auth.UserRepository", return_value=repository), patch("routes.auth.UserRepository", return_value=repository),
patch("routes.auth.SessionsRepository", return_value=sessions_repository),
patch("routes.auth.estimate_password_strength", return_value=True), patch("routes.auth.estimate_password_strength", return_value=True),
patch("routes.auth.hash_password", return_value="hashed"), patch("routes.auth.hash_password", return_value="hashed"),
): ):
@@ -71,11 +73,11 @@ def test_signup_accepts_unknown_referral_code(client):
}, },
) )
assert response.status_code == 201 assert response.status_code == 200
repository.create.assert_awaited_once_with( repository.create.assert_awaited_once_with(
username="alice", hashed_password="hashed", referal_id=None username="alice", hashed_password="hashed", referal_id=None
) )
assert response.json()["referal_code"] == "new-code" assert response.json()["user"]["referal_code"] == "new-code"
def test_login_distinguishes_unknown_user_and_bad_password(client): def test_login_distinguishes_unknown_user_and_bad_password(client):