mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 23:20:56 +01:00
feat(docs): codebase refactor - added src directory
This commit is contained in:
0
7project/src/backend/app/services/__init__.py
Normal file
0
7project/src/backend/app/services/__init__.py
Normal file
178
7project/src/backend/app/services/bank_scraper.py
Normal file
178
7project/src/backend/app/services/bank_scraper.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from os.path import dirname, join
|
||||
from time import strptime
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db import sync_session_maker
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OAUTH_DIR = join(dirname(__file__), "..", "oauth")
|
||||
CERTS = (
|
||||
join(OAUTH_DIR, "public_key.pem"),
|
||||
join(OAUTH_DIR, "private_key.key"),
|
||||
)
|
||||
|
||||
|
||||
def load_mock_bank_transactions(user_id: str) -> None:
|
||||
try:
|
||||
uid = UUID(str(user_id))
|
||||
except Exception:
|
||||
logger.error("Invalid user_id provided to bank_scraper (sync): %r", user_id)
|
||||
return
|
||||
|
||||
_load_mock_bank_transactions(uid)
|
||||
|
||||
|
||||
def load_all_mock_bank_transactions() -> None:
|
||||
with sync_session_maker() as session:
|
||||
users = session.execute(select(User)).unique().scalars().all()
|
||||
logger.info("[BankScraper] Starting Mock Bank scrape for all users | count=%d", len(users))
|
||||
|
||||
processed = 0
|
||||
for user in users:
|
||||
try:
|
||||
_load_mock_bank_transactions(user.id)
|
||||
processed += 1
|
||||
except Exception:
|
||||
logger.exception("[BankScraper] Error scraping for user id=%s email=%s", user.id,
|
||||
getattr(user, 'email', None))
|
||||
logger.info("[BankScraper] Finished Mock Bank scrape for all users | processed=%d", processed)
|
||||
|
||||
|
||||
def _load_mock_bank_transactions(user_id: UUID) -> None:
|
||||
with sync_session_maker() as session:
|
||||
user: User | None = session.execute(select(User).where(User.id == user_id)).unique().scalar_one_or_none()
|
||||
if user is None:
|
||||
logger.warning("User not found for id=%s", user_id)
|
||||
return
|
||||
|
||||
transactions = []
|
||||
with httpx.Client() as client:
|
||||
response = client.get(f"{os.getenv('APP_POD_URL')}/mock-bank/scrape")
|
||||
if response.status_code != httpx.codes.OK:
|
||||
return
|
||||
for transaction in response.json():
|
||||
transactions.append(
|
||||
Transaction(
|
||||
amount=transaction["amount"],
|
||||
description=transaction.get("description"),
|
||||
date=strptime(transaction["date"], "%Y-%m-%d"),
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
for transaction in transactions:
|
||||
session.add(transaction)
|
||||
session.commit()
|
||||
|
||||
|
||||
def load_ceska_sporitelna_transactions(user_id: str) -> None:
|
||||
try:
|
||||
uid = UUID(str(user_id))
|
||||
except Exception:
|
||||
logger.error("Invalid user_id provided to bank_scraper (sync): %r", user_id)
|
||||
return
|
||||
|
||||
_load_ceska_sporitelna_transactions(uid)
|
||||
|
||||
|
||||
def load_all_ceska_sporitelna_transactions() -> None:
|
||||
with sync_session_maker() as session:
|
||||
users = session.execute(select(User)).unique().scalars().all()
|
||||
logger.info("[BankScraper] Starting CSAS scrape for all users | count=%d", len(users))
|
||||
|
||||
processed = 0
|
||||
for user in users:
|
||||
try:
|
||||
_load_ceska_sporitelna_transactions(user.id)
|
||||
processed += 1
|
||||
except Exception:
|
||||
logger.exception("[BankScraper] Error scraping for user id=%s email=%s", user.id,
|
||||
getattr(user, 'email', None))
|
||||
logger.info("[BankScraper] Finished CSAS scrape for all users | processed=%d", processed)
|
||||
|
||||
|
||||
def _load_ceska_sporitelna_transactions(user_id: UUID) -> None:
|
||||
with sync_session_maker() as session:
|
||||
user: User | None = session.execute(select(User).where(User.id == user_id)).unique().scalar_one_or_none()
|
||||
if user is None:
|
||||
logger.warning("User not found for id=%s", user_id)
|
||||
return
|
||||
|
||||
cfg = user.config or {}
|
||||
if "csas" not in cfg:
|
||||
return
|
||||
|
||||
cfg = json.loads(cfg["csas"])
|
||||
if "access_token" not in cfg:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
try:
|
||||
with httpx.Client(cert=CERTS, timeout=httpx.Timeout(20.0)) as client:
|
||||
response = client.get(
|
||||
"https://webapi.developers.erstegroup.com/api/csas/sandbox/v4/account-information/my/accounts?size=10&page=0&sort=iban&order=desc",
|
||||
headers={
|
||||
"Authorization": f"Bearer {cfg['access_token']}",
|
||||
"WEB-API-key": "09fdc637-3c57-4242-95f2-c2205a2438f3",
|
||||
"user-involved": "false",
|
||||
},
|
||||
)
|
||||
if response.status_code != httpx.codes.OK:
|
||||
return
|
||||
|
||||
for account in response.json().get("accounts", []):
|
||||
accounts.append(account)
|
||||
|
||||
except (httpx.HTTPError,) as e:
|
||||
logger.exception("[BankScraper] HTTP error during CSAS request | user_id=%s", user_id)
|
||||
return
|
||||
|
||||
for account in accounts:
|
||||
acc_id = account.get("id")
|
||||
if not acc_id:
|
||||
continue
|
||||
|
||||
url = f"https://webapi.developers.erstegroup.com/api/csas/sandbox/v4/account-information/my/accounts/{acc_id}/transactions?size=100&page=0&sort=bookingdate&order=desc"
|
||||
with httpx.Client(cert=CERTS) as client:
|
||||
response = client.get(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {cfg['access_token']}",
|
||||
"WEB-API-key": "09fdc637-3c57-4242-95f2-c2205a2438f3",
|
||||
"user-involved": "false",
|
||||
},
|
||||
)
|
||||
if response.status_code != httpx.codes.OK:
|
||||
continue
|
||||
|
||||
transactions = response.json().get("transactions", [])
|
||||
|
||||
for transaction in transactions:
|
||||
description = transaction.get("entryDetails", {}).get("transactionDetails", {}).get(
|
||||
"additionalRemittanceInformation")
|
||||
date_str = transaction.get("bookingDate", {}).get("date")
|
||||
date = strptime(date_str, "%Y-%m-%d") if date_str else None
|
||||
amount = transaction.get("amount", {}).get("value")
|
||||
if transaction.get("creditDebitIndicator") == "DBIT" and amount is not None:
|
||||
amount = -abs(amount)
|
||||
|
||||
if amount is None:
|
||||
continue
|
||||
|
||||
obj = Transaction(
|
||||
amount=amount,
|
||||
description=description,
|
||||
date=date,
|
||||
user_id=user_id,
|
||||
)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
16
7project/src/backend/app/services/db.py
Normal file
16
7project/src/backend/app/services/db.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import AsyncGenerator
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
|
||||
from ..core.db import async_session_maker
|
||||
from ..models.user import User, OAuthAccount
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
|
||||
48
7project/src/backend/app/services/prometheus.py
Normal file
48
7project/src/backend/app/services/prometheus.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import Callable
|
||||
from prometheus_fastapi_instrumentator.metrics import Info
|
||||
from prometheus_client import Gauge
|
||||
from sqlalchemy import select, func
|
||||
|
||||
from app.core.db import async_session_maker
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def number_of_users() -> Callable[[Info], None]:
|
||||
METRIC = Gauge(
|
||||
"number_of_users_total",
|
||||
"Number of registered users.",
|
||||
labelnames=("users",)
|
||||
)
|
||||
|
||||
async def instrumentation(info: Info) -> None:
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(select(func.count(User.id)))
|
||||
user_count = result.scalar_one() or 0
|
||||
except Exception:
|
||||
# In case of DB errors, avoid crashing metrics endpoint
|
||||
user_count = 0
|
||||
METRIC.labels(users="total").set(user_count)
|
||||
|
||||
return instrumentation
|
||||
|
||||
|
||||
def number_of_transactions() -> Callable[[Info], None]:
|
||||
METRIC = Gauge(
|
||||
"number_of_transactions_total",
|
||||
"Number of transactions stored.",
|
||||
labelnames=("transactions",)
|
||||
)
|
||||
|
||||
async def instrumentation(info: Info) -> None:
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(select(func.count()).select_from(Transaction))
|
||||
transaction_count = result.scalar_one() or 0
|
||||
except Exception:
|
||||
# In case of DB errors, avoid crashing metrics endpoint
|
||||
transaction_count = 0
|
||||
METRIC.labels(transactions="total").set(transaction_count)
|
||||
|
||||
return instrumentation
|
||||
117
7project/src/backend/app/services/user_service.py
Normal file
117
7project/src/backend/app/services/user_service.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
|
||||
from fastapi_users.authentication import (
|
||||
AuthenticationBackend,
|
||||
BearerTransport,
|
||||
)
|
||||
from fastapi_users.authentication.strategy.jwt import JWTStrategy
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
from httpx_oauth.oauth2 import BaseOAuth2
|
||||
|
||||
from app.models.user import User
|
||||
from app.oauth.bank_id import BankID
|
||||
from app.oauth.csas import CSASOAuth
|
||||
from app.oauth.custom_openid import CustomOpenID
|
||||
from app.oauth.moje_id import MojeIDOAuth
|
||||
from app.services.db import get_user_db
|
||||
from app.core.queue import enqueue_email
|
||||
|
||||
SECRET = os.getenv("SECRET", "CHANGE_ME_SECRET")
|
||||
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
||||
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000")
|
||||
|
||||
providers = {
|
||||
"MojeID": MojeIDOAuth(
|
||||
os.getenv("MOJEID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"),
|
||||
os.getenv("MOJEID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"),
|
||||
),
|
||||
"BankID": BankID(
|
||||
os.getenv("BANKID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"),
|
||||
os.getenv("BANKID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_oauth_provider(name: str) -> Optional[BaseOAuth2]:
|
||||
if name not in providers:
|
||||
return None
|
||||
return providers[name]
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
reset_password_token_secret = SECRET
|
||||
verification_token_secret = SECRET
|
||||
|
||||
async def oauth_callback(self: "BaseUserManager[models.UOAP, models.ID]", oauth_name: str, access_token: str,
|
||||
account_id: str, account_email: str, expires_at: Optional[int] = None,
|
||||
refresh_token: Optional[str] = None, request: Optional[Request] = None, *,
|
||||
associate_by_email: bool = False, is_verified_by_default: bool = False) -> models.UOAP:
|
||||
|
||||
user = await super().oauth_callback(oauth_name, access_token, account_id, account_email, expires_at,
|
||||
refresh_token, request, associate_by_email=associate_by_email,
|
||||
is_verified_by_default=is_verified_by_default)
|
||||
|
||||
# set additional user info from the OAuth provider
|
||||
provider = get_oauth_provider(oauth_name)
|
||||
if provider is not None and isinstance(provider, CustomOpenID):
|
||||
update_dict = await provider.get_user_info(access_token)
|
||||
await self.user_db.update(user, update_dict)
|
||||
|
||||
return user
|
||||
|
||||
async def on_after_register(self, user: User, request: Optional[Request] = None):
|
||||
await self.request_verify(user, request)
|
||||
|
||||
async def on_after_forgot_password(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
async def on_after_request_verify(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
):
|
||||
verify_frontend_link = f"{FRONTEND_URL}/verify?token={token}"
|
||||
verify_backend_link = f"{BACKEND_URL}/auth/verify?token={token}"
|
||||
subject = "Ověření účtu"
|
||||
body = (
|
||||
"Ahoj,\n\n"
|
||||
"děkujeme za registraci. Prosíme, ověř svůj účet kliknutím na tento odkaz:\n"
|
||||
f"{verify_frontend_link}\n\n"
|
||||
"Pokud by odkaz nefungoval, můžeš použít i přímý odkaz na backend:\n"
|
||||
f"{verify_backend_link}\n\n"
|
||||
"Pokud jsi registraci neprováděl(a), tento email ignoruj.\n"
|
||||
)
|
||||
try:
|
||||
enqueue_email(to=user.email, subject=subject, body=body)
|
||||
except Exception as e:
|
||||
print("[Email Fallback] To:", user.email)
|
||||
print("[Email Fallback] Subject:", subject)
|
||||
print("[Email Fallback] Body:\n", body)
|
||||
|
||||
|
||||
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
||||
|
||||
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
|
||||
|
||||
def get_jwt_strategy() -> JWTStrategy:
|
||||
return JWTStrategy(secret=SECRET, lifetime_seconds=604800)
|
||||
|
||||
|
||||
auth_backend = AuthenticationBackend(
|
||||
name="jwt",
|
||||
transport=bearer_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
||||
|
||||
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])
|
||||
|
||||
current_active_user = fastapi_users.current_user(active=True)
|
||||
current_active_verified_user = fastapi_users.current_user(active=True, verified=True)
|
||||
Reference in New Issue
Block a user