Compare commits
3 Commits
a374501e4d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4262d4d017 | |||
| 429c961ace | |||
| 27e2f58956 |
4
compose.local.yml
Normal file
4
compose.local.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
ports: !override
|
||||||
|
- "5432:5432"
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user