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

68
shared/auth.py Normal file
View File

@@ -0,0 +1,68 @@
"""
Authentication utilities for all services.
This module provides common authentication functionality to avoid circular imports.
"""
from datetime import datetime, timedelta
from typing import Optional
import jwt
from jwt.exceptions import InvalidTokenError
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext
from shared.config import settings
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Bearer token scheme
security = HTTPBearer()
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Get password hash."""
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[dict]:
"""Verify and decode JWT token."""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
return None
return {"user_id": int(user_id), "email": payload.get("email")}
except InvalidTokenError:
return None
async def get_current_user_from_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
"""Get current user from JWT token."""
token = credentials.credentials
user_data = verify_token(token)
if user_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user_data

42
shared/cache.py Normal file
View File

@@ -0,0 +1,42 @@
import redis.asyncio as redis
from shared.config import settings
# Redis connection
redis_client = redis.from_url(settings.REDIS_URL)
class CacheService:
@staticmethod
async def set(key: str, value: str, expire: int = 3600):
"""Set cache with expiration"""
await redis_client.set(key, value, ex=expire)
@staticmethod
async def get(key: str) -> str:
"""Get cache value"""
return await redis_client.get(key)
@staticmethod
async def delete(key: str):
"""Delete cache key"""
await redis_client.delete(key)
@staticmethod
async def exists(key: str) -> bool:
"""Check if key exists"""
return await redis_client.exists(key)
@staticmethod
async def set_location(user_id: int, latitude: float, longitude: float, expire: int = 300):
"""Cache user location with expiration (5 minutes default)"""
location_data = f"{latitude},{longitude}"
await redis_client.set(f"location:{user_id}", location_data, ex=expire)
@staticmethod
async def get_location(user_id: int) -> tuple[float, float] | None:
"""Get cached user location"""
location_data = await redis_client.get(f"location:{user_id}")
if location_data:
lat, lng = location_data.decode().split(',')
return float(lat), float(lng)
return None

53
shared/config.py Normal file
View File

@@ -0,0 +1,53 @@
import os
from pydantic_settings import BaseSettings
from typing import Optional
# Load .env file manually from project root
from dotenv import load_dotenv
# Find and load .env file
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir) # Go up one level from shared/
env_path = os.path.join(project_root, ".env")
if os.path.exists(env_path):
load_dotenv(env_path)
print(f"✅ Loaded .env from: {env_path}")
else:
print(f"⚠️ .env not found at: {env_path}")
class Settings(BaseSettings):
# Database
DATABASE_URL: str = "postgresql+asyncpg://admin:password@localhost:5432/women_safety"
# Redis
REDIS_URL: str = "redis://localhost:6379/0"
# Kafka
KAFKA_BOOTSTRAP_SERVERS: str = "localhost:9092"
# JWT
SECRET_KEY: str = "your-secret-key-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# App
APP_NAME: str = "Women Safety App"
DEBUG: bool = True
API_V1_STR: str = "/api/v1"
# External Services
FCM_SERVER_KEY: Optional[str] = None
# Security
CORS_ORIGINS: list = ["*"] # Change in production
# Location
MAX_EMERGENCY_RADIUS_KM: float = 1.0
class Config:
env_file = ".env"
settings = Settings()

57
shared/database.py Normal file
View File

@@ -0,0 +1,57 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import Column, Integer, DateTime, Boolean
from sqlalchemy.sql import func
from shared.config import settings
# Database setup
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
future=True,
pool_size=20,
max_overflow=30,
pool_pre_ping=True,
)
AsyncSessionLocal = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
Base = declarative_base()
class BaseModel(Base):
"""Base model with common fields"""
__abstract__ = True
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
is_active = Column(Boolean, default=True)
async def get_db() -> AsyncSession:
"""Database session dependency"""
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def init_db():
"""Initialize database"""
async with engine.begin() as conn:
# Import all models here to ensure they are registered
from services.user_service.models import User
from services.emergency_service.models import EmergencyAlert
from services.location_service.models import UserLocation
from services.calendar_service.models import CalendarEntry
await conn.run_sync(Base.metadata.create_all)