diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py index e69de29..e9c9388 100644 --- a/backend/app/core/__init__.py +++ b/backend/app/core/__init__.py @@ -0,0 +1,21 @@ +from app.core.config import get_settings, Settings +from app.core.database import get_db, Base, engine, SessionLocal +from app.core.security import ( + verify_password, + get_password_hash, + create_access_token, + decode_access_token, +) + +__all__ = [ + "get_settings", + "Settings", + "get_db", + "Base", + "engine", + "SessionLocal", + "verify_password", + "get_password_hash", + "create_access_token", + "decode_access_token", +] diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..db1b4f4 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,34 @@ +""" +Application configuration using Pydantic Settings. +""" +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + # Application + app_name: str = "Galaxy-PO" + debug: bool = False + + # Database + database_url: str = "postgresql://galaxy:devpassword@localhost:5432/galaxy_po" + + # JWT + jwt_secret: str = "dev-jwt-secret-change-in-production" + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 60 * 24 # 24 hours + + # External APIs + kis_app_key: str = "" + kis_app_secret: str = "" + kis_account_no: str = "" + dart_api_key: str = "" + + class Config: + env_file = ".env" + case_sensitive = False + + +@lru_cache +def get_settings() -> Settings: + return Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..b7d952e --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,28 @@ +""" +Database connection and session management. +""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from app.core.config import get_settings + +settings = get_settings() + +engine = create_engine( + settings.database_url, + pool_pre_ping=True, + pool_size=10, + max_overflow=20, +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + """Dependency for getting database session.""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..2369817 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,51 @@ +""" +Security utilities for authentication. +""" +from datetime import datetime, timedelta, timezone +from typing import Optional + +from jose import JWTError, jwt +from passlib.context import CryptContext + +from app.core.config import get_settings + +settings = get_settings() + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash a password.""" + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create a JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta( + minutes=settings.access_token_expire_minutes + ) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm + ) + return encoded_jwt + + +def decode_access_token(token: str) -> Optional[dict]: + """Decode and verify a JWT access token.""" + try: + payload = jwt.decode( + token, settings.jwt_secret, algorithms=[settings.jwt_algorithm] + ) + return payload + except JWTError: + return None