init commit

This commit is contained in:
2025-09-25 08:05:25 +09:00
commit 4d7551d4f1
56 changed files with 5977 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import timedelta
from shared.config import settings
from shared.database import get_db
from shared.auth import (
verify_password,
get_password_hash,
create_access_token,
get_current_user_from_token
)
from services.user_service.models import User
from services.user_service.schemas import UserCreate, UserResponse, UserLogin, Token, UserUpdate
app = FastAPI(title="User Service", version="1.0.0")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # В продакшене ограничить конкретными доменами
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
async def get_current_user(
user_data: dict = Depends(get_current_user_from_token),
db: AsyncSession = Depends(get_db)
):
"""Get current user from token via auth dependency."""
# Get full user object from database
result = await db.execute(select(User).filter(User.id == user_data["user_id"]))
user = result.scalars().first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "user_service"}
@app.post("/api/v1/register", response_model=UserResponse)
async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
"""Register a new user"""
# Check if user already exists
result = await db.execute(select(User).filter(User.email == user_data.email))
if result.scalars().first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Create new user
hashed_password = get_password_hash(user_data.password)
db_user = User(
email=user_data.email,
phone=user_data.phone,
password_hash=hashed_password,
first_name=user_data.first_name,
last_name=user_data.last_name,
date_of_birth=user_data.date_of_birth,
bio=user_data.bio,
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return UserResponse.model_validate(db_user)
@app.post("/api/v1/login", response_model=Token)
async def login(user_credentials: UserLogin, db: AsyncSession = Depends(get_db)):
"""Authenticate user and return token"""
result = await db.execute(select(User).filter(User.email == user_credentials.email))
user = result.scalars().first()
if not user or not verify_password(user_credentials.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Account is deactivated",
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user.id), "email": user.email},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/api/v1/profile", response_model=UserResponse)
async def get_profile(current_user: User = Depends(get_current_user)):
"""Get current user profile"""
return UserResponse.model_validate(current_user)
@app.put("/api/v1/profile", response_model=UserResponse)
async def update_profile(
user_update: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update user profile"""
update_data = user_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(current_user, field, value)
await db.commit()
await db.refresh(current_user)
return UserResponse.model_validate(current_user)
@app.get("/api/v1/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "user-service"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)

View File

@@ -0,0 +1,39 @@
from sqlalchemy import Column, String, Integer, Date, Text, Boolean
from sqlalchemy.dialects.postgresql import UUID
from shared.database import BaseModel
import uuid
class User(BaseModel):
__tablename__ = "users"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
phone = Column(String, unique=True, index=True)
password_hash = Column(String, nullable=False)
# Profile information
first_name = Column(String(50), nullable=False)
last_name = Column(String(50), nullable=False)
date_of_birth = Column(Date)
avatar_url = Column(String)
bio = Column(Text)
# Emergency contacts
emergency_contact_1_name = Column(String(100))
emergency_contact_1_phone = Column(String(20))
emergency_contact_2_name = Column(String(100))
emergency_contact_2_phone = Column(String(20))
# Settings
location_sharing_enabled = Column(Boolean, default=True)
emergency_notifications_enabled = Column(Boolean, default=True)
push_notifications_enabled = Column(Boolean, default=True)
# Security
email_verified = Column(Boolean, default=False)
phone_verified = Column(Boolean, default=False)
is_blocked = Column(Boolean, default=False)
def __repr__(self):
return f"<User {self.email}>"

View File

@@ -0,0 +1,77 @@
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from datetime import date
from uuid import UUID
class UserBase(BaseModel):
email: EmailStr
phone: Optional[str] = None
first_name: str = Field(..., min_length=1, max_length=50)
last_name: str = Field(..., min_length=1, max_length=50)
date_of_birth: Optional[date] = None
bio: Optional[str] = Field(None, max_length=500)
class UserCreate(UserBase):
password: str = Field(..., min_length=8, max_length=100)
class UserUpdate(BaseModel):
first_name: Optional[str] = Field(None, min_length=1, max_length=50)
last_name: Optional[str] = Field(None, min_length=1, max_length=50)
phone: Optional[str] = None
date_of_birth: Optional[date] = None
bio: Optional[str] = Field(None, max_length=500)
avatar_url: Optional[str] = None
# Emergency contacts
emergency_contact_1_name: Optional[str] = Field(None, max_length=100)
emergency_contact_1_phone: Optional[str] = Field(None, max_length=20)
emergency_contact_2_name: Optional[str] = Field(None, max_length=100)
emergency_contact_2_phone: Optional[str] = Field(None, max_length=20)
# Settings
location_sharing_enabled: Optional[bool] = None
emergency_notifications_enabled: Optional[bool] = None
push_notifications_enabled: Optional[bool] = None
class UserResponse(UserBase):
id: int
uuid: str
avatar_url: Optional[str] = None
emergency_contact_1_name: Optional[str] = None
emergency_contact_1_phone: Optional[str] = None
emergency_contact_2_name: Optional[str] = None
emergency_contact_2_phone: Optional[str] = None
location_sharing_enabled: bool
emergency_notifications_enabled: bool
push_notifications_enabled: bool
email_verified: bool
phone_verified: bool
is_active: bool
@field_validator('uuid', mode='before')
@classmethod
def convert_uuid_to_str(cls, v):
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Optional[str] = None