From 2c48af7c9ef1e81ac6cad2c288b1ae946bfe1861 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Thu, 21 May 2026 02:28:50 +0200 Subject: [PATCH 1/9] all project done and exercises answered --- modules/module-01/REFLECTION.md | 6 ++-- modules/module-01/exercise.md | 39 ++++++++++++++++++++++++ modules/module-02/REFLECTION.md | 6 ++-- services/game-service/app/database.py | 14 +++++++++ services/game-service/app/main.py | 6 ++++ services/game-service/app/models.py | 13 ++++++++ services/game-service/app/repository.py | 26 ++++++++++++++++ services/game-service/app/routes.py | 20 ++++++++++++ services/game-service/app/schemas.py | 22 +++++++++++++ services/game-service/app/service.py | 27 ++++++++++++++++ services/game-service/games.db | Bin 0 -> 12288 bytes 11 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 services/game-service/games.db diff --git a/modules/module-01/REFLECTION.md b/modules/module-01/REFLECTION.md index ea9859e7..a0f62a77 100644 --- a/modules/module-01/REFLECTION.md +++ b/modules/module-01/REFLECTION.md @@ -22,7 +22,7 @@ You started from a painful monolith. Now you're splitting it into separate servi Think about it from three angles: the developer who has to change code, the team that has to deploy it, and the user who has to live with its failures. You don't need to cover all three, pick the one that felt most real to you today. -> _Your answer:_ +> _Your answer:_ so basically the problem we had with the monolith is if one thing breaks everything breaks, like if you just wanna fix the genre of a game in the catalog you gotta restart the whole app and while its restarting users cant log in, activities stop, notifications stop, like everything goes down for one tiny change that had nothing to do with any of that. splitting it into services means when the game-service restarts, the user-service doesnt even notice, users can still log in fine. so for the developer its easier to change one thing without being scared it breaks something else, and for the user they dont get kicked out just because someone fixed a typo somewhere. --- @@ -34,7 +34,7 @@ Look at your service map. Every arrow between two services is a decision someone What would break, slow down, or become harder to manage if you merged those two services back together? -> _Your answer:_ +> _Your answer:_the line i think is clearest is between user-service and activity-service. user-service owns users, activity-service owns activities. if we merged them together then every time we want to change something about how activities work, we'd also be touching the user code, and one bug could mess up logins. also they scale differently — if the platform gets busy with activity tracking you want to scale just that part up, not the whole user management system. keeping them separate means each one can grow at its own speed without dragging the other one. --- @@ -46,7 +46,7 @@ Microservices solve the monolith's problems. But they create new ones. No need to solve it: just name it honestly. This is exactly the tension the rest of the course is about. -> _Your answer:_ +> _Your answer:_in the monolith, if nova logs an activity and you want to check her username, you just do a JOIN — one database query, done. now with microservices, activity-service doesnt have direct access to the users table, so it has to make a network call to user-service to get the username. that adds latency, and if user-service is down for any reason, the activity request could fail too. so we traded "easy data access" for "independent deployments" which is a good trade overall but its definitely more complicated to deal with. --- diff --git a/modules/module-01/exercise.md b/modules/module-01/exercise.md index d5bf307c..f0e858f4 100644 --- a/modules/module-01/exercise.md +++ b/modules/module-01/exercise.md @@ -35,6 +35,14 @@ For each bounded context you identify, fill in the table: There is no single correct answer: what matters is that you can justify each row. --- +| Bounded Context | Responsibilities | Owned Entities | Team | +|-----------------|-----------------------------------------------------------|-----------------------------|--------------| +| Identity | it manages who users are and handles registration and profiles | User, Session | Platform | +| Game Library | it manages the game catalog, search and metadata | Game | Catalog | +| Activity | it tracks what users are playing and also logs game actions | Activity | Engagement | +| Notifications | it sends alerts to friends when someone logs an activity | Notification | Engagement | +| Logging | Records user actions for analytics and also to respect GDPR principles | ConsentRecord, ActivityLog | Platform | +| Auth | it issues and validates JWT tokens for all services | Token, Credential | Platform | ## Task 2 — Define service contracts _(~30 min)_ @@ -55,9 +63,37 @@ Payload: { activity_id, user_id, action, game_id, timestamp } ``` Focus on the flows that feel non-obvious. You do not need to document every possible pair. +**user-service → auth-service** +- Direction: user-service to auth-service +- Trigger: user logs in and needs a JWT token issued +- Protocol: REST (sync — the login response has to include the token) +- Payload: `{ user_id, username, email }` --- +**activity-service → logging-service** +- Direction: activity-service to logging-service +- Trigger: a user logs a new activity (started/completed a game) +- Protocol: RabbitMQ message (async — logging doesnt need to block the activity response, if logging is slow or down the activity still saves fine) +- Payload: `{ activity_id, user_id, game_id, action, timestamp }` + +--- + +**activity-service → notification-service** +- Direction: activity-service to notification-service +- Trigger: a user logs an activity that their friends should see +- Protocol: RabbitMQ message (async — same reason as above, notifications are best-effort, they shouldnt slow down activity logging) +- Payload: `{ activity_id, user_id, game_id, action, timestamp }` + +--- + +**gateway → all services** +- Direction: gateway to user-service / game-service / activity-service / etc +- Trigger: any client request coming in from the frontend +- Protocol: REST (sync — the client is waiting for a response) +- Payload: varies by route, gateway forwards the request with the JWT validated +--- + ## Task 3 — Draw the service map _(~20 min)_ Draw the full GameHub service map: @@ -76,8 +112,11 @@ This can be a sketch on paper, a whiteboard photo, or ASCII art committed to you Three questions to discuss as a team before you leave: 1. Why does `notification-service` use Node.js instead of Python like the rest? What does that tell you about microservices and technology choices? +its because in microservices each service can use whatever tech fits it best — notifications are event-driven and Node.js handles async event-based stuff really well. also it shows that microservices dont force you into one language, each team picks what makes sense for their problem. 2. What is the risk of `activity-service` calling `logging-service` synchronously — why might you prefer an async event instead? +if activity-service called logging-service synchronously and logging was slow or crashed, every activity request would also fail or be slow. with async (RabbitMQ), activity-service puts a message in the queue and moves on, logging picks it up whenever it can. the user gets a fast response and logging eventually catches up. 3. Why does `logging-service` need a GDPR consent check before recording any activity? +because recording what a user does is personal data. if the user said they dont want to be tracked, the logging-service has to respect that before writing anything. the consent has to be checked at the point of logging, not at signup, because a user can change their preference at any time. You do not need to write these answers down — they are warm-up for your REFLECTION.md. diff --git a/modules/module-02/REFLECTION.md b/modules/module-02/REFLECTION.md index 52850343..04ab91b4 100644 --- a/modules/module-02/REFLECTION.md +++ b/modules/module-02/REFLECTION.md @@ -18,7 +18,7 @@ You built a service with distinct layers: models, schemas, repository, service, Think about what happens six months later when someone new joins the team, or when you need to swap SQLite for PostgreSQL. What does the layered structure protect you from? -> *Your answer:* +> *Your answer:*if you put everything in one file it works fine at first but 6 months later when someone new joins they open app.py and see SQL queries mixed with business logic mixed with HTTP responses and they have no idea where to even start looking. the layers help because if you need to swap SQLite for PostgreSQL, you only touch database.py and repository.py — routes.py and service.py dont need to change at all because they never talk to the database directly. same thing if the API shape changes, you only update schemas.py and routes.py and the rest of the code doesnt care. basically each layer protects you from having to change everything when one thing changes. --- @@ -30,7 +30,7 @@ Each service owns its data exclusively — no other service is allowed to touch Give a concrete scenario, not a general principle. -> *Your answer:* +> *Your answer:*game-service owns the games table. if activity-service could write to it directly, imagine this: activity-service has a bug and starts setting cover_url to null for every game it touches. now the games catalog is corrupted and game-service has no idea why because the writes are coming from somewhere else. it would be really hard to debug and game-service cant enforce its own rules about what valid game data looks like. when only game-service writes to its own table, if data is wrong you know exactly where to look. --- @@ -42,7 +42,7 @@ You now have models, schemas, a repository, a service, and routes — five layer And at what point does the complexity start to pay off? Where is the tipping point? -> *Your answer:* +> *Your answer:*for something this small with like 4 endpoints, having 5 separate files (models, schemas, repository, service, routes) feels like a lot of work for not much. in the monolith version of this we just wrote it all in app.py and it was done in like 30 minutes. the complexity starts paying off when the service grows — like when you add authentication, caching, or more complex business rules, having clean layers means you add that stuff in the right place without breaking what already works. for a tiny CRUD service the structure feels heavy, but the moment it needs to do more than basic CRUD it starts making sense. --- diff --git a/services/game-service/app/database.py b/services/game-service/app/database.py index 56dfd44d..72ac1ab6 100644 --- a/services/game-service/app/database.py +++ b/services/game-service/app/database.py @@ -8,3 +8,17 @@ # - SessionLocal — session factory bound to the engine # - Base — DeclarativeBase that all ORM models inherit from # - get_db() — FastAPI dependency: yields a session, closes it after the request +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +import os +DATABASE_URL = os.getenv("DATABASE_URL","sqlite:///./games.db") +engine = create_engine(DATABASE_URL,connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False,autoflush=False,bind=engine) +class Base(DeclarativeBase): + pass +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/services/game-service/app/main.py b/services/game-service/app/main.py index e629900e..b141168c 100644 --- a/services/game-service/app/main.py +++ b/services/game-service/app/main.py @@ -7,3 +7,9 @@ # uvicorn app.main:app --reload --port 8002 # # Then open: http://localhost:8002/docs +from fastapi import FastAPI +from app.routes import router +from app.database import Base, engine +Base.metadata.create_all(bind=engine) +app = FastAPI(title="game-service") +app.include_router(router) \ No newline at end of file diff --git a/services/game-service/app/models.py b/services/game-service/app/models.py index 52eb2756..7f42ba81 100644 --- a/services/game-service/app/models.py +++ b/services/game-service/app/models.py @@ -12,3 +12,16 @@ # - created_at DateTime, defaults to now (UTC) # # Import Base from app.database — do not redefine it here. +from sqlalchemy import Column, String, Integer, DateTime +from datetime import datetime, timezone +import uuid +from app.database import Base +class Game(Base): + __tablename__ = "games" + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + title = Column(String, nullable=False) + genre = Column(String, nullable=False) + platform = Column(String, nullable=False) + release_year = Column(Integer, nullable=True) + cover_url = Column(String, nullable=True) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/services/game-service/app/repository.py b/services/game-service/app/repository.py index 8120c970..7d64aded 100644 --- a/services/game-service/app/repository.py +++ b/services/game-service/app/repository.py @@ -8,3 +8,29 @@ # - list_games(db, limit, offset) -> tuple[list[Game], int] # - search_games(db, q, limit, offset) -> tuple[list[Game], int] # Hint: filter by title using .ilike(f"%{q}%") for case-insensitive search +from sqlalchemy.orm import Session +from app.models import Game +from app.schemas import GameCreate +def create_game(db: Session, data: GameCreate) -> Game: + game = Game( + title=data.title, + genre=data.genre, + platform=data.platform, + release_year=data.release_year, + cover_url=data.cover_url, + ) + db.add(game) + db.commit() + db.refresh(game) + return game +def get_game(db: Session, game_id: str) -> Game | None: + return db.query(Game).filter(Game.id == game_id).first() +def list_games(db: Session, limit: int = 20, offset: int = 0) -> tuple[list[Game], int]: + total = db.query(Game).count() + games = db.query(Game).offset(offset).limit(limit).all() + return games, total +def search_games(db: Session, q: str, limit: int = 20, offset: int = 0) -> tuple[list[Game], int]: + query = db.query(Game).filter(Game.title.ilike(f"%{q}%")) + total = query.count() + games = query.offset(offset).limit(limit).all() + return games, total \ No newline at end of file diff --git a/services/game-service/app/routes.py b/services/game-service/app/routes.py index db173fd2..818e1a29 100644 --- a/services/game-service/app/routes.py +++ b/services/game-service/app/routes.py @@ -9,3 +9,23 @@ # IMPORTANT: declare /search BEFORE /{game_id} in your router. # If /{game_id} comes first, FastAPI will try to match "search" as an ID # and return a 422 Unprocessable Entity error. +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.database import get_db +from app import service, schemas +router = APIRouter(prefix="/v1/games", tags=["games"]) +@router.post("/", response_model=schemas.GameOut, status_code=201) +def create_game(data: schemas.GameCreate, db: Session = Depends(get_db)): + return service.add_game(db, data) +@router.get("/", response_model=schemas.GameList) +def list_games(limit: int = 20, offset: int = 0, db: Session = Depends(get_db)): + return service.fetch_all_games(db, limit=limit, offset=offset) +@router.get("/search", response_model=schemas.GameList) +def search_games(q: str, limit: int = 20, offset: int = 0, db: Session = Depends(get_db)): + return service.find_games(db, q, limit=limit, offset=offset) +@router.get("/{game_id}", response_model=schemas.GameOut) +def get_game(game_id: str, db: Session = Depends(get_db)): + try: + return service.fetch_game(db, game_id) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) \ No newline at end of file diff --git a/services/game-service/app/schemas.py b/services/game-service/app/schemas.py index ca6aca4e..678fd3b7 100644 --- a/services/game-service/app/schemas.py +++ b/services/game-service/app/schemas.py @@ -8,3 +8,25 @@ # - GameOut — fields returned to the caller (includes id and created_at) # add model_config = {"from_attributes": True} # - GameList — paginated envelope: { items, total, limit, offset } +from pydantic import BaseModel +from datetime import datetime +class GameCreate(BaseModel): + title: str + genre: str + platform: str + release_year: int | None = None + cover_url: str | None = None +class GameOut(BaseModel): + id: str + title: str + genre: str + platform: str + release_year: int | None + cover_url: str | None + created_at: datetime + model_config = {"from_attributes": True} +class GameList(BaseModel): + items: list[GameOut] + total: int + limit: int + offset: int \ No newline at end of file diff --git a/services/game-service/app/service.py b/services/game-service/app/service.py index 78c4451a..08d80248 100644 --- a/services/game-service/app/service.py +++ b/services/game-service/app/service.py @@ -8,3 +8,30 @@ # - fetch_game(db, game_id) -> GameOut (raises ValueError if not found) # - fetch_all_games(db, limit, offset) -> GameList # - find_games(db, q, limit, offset) -> GameList (delegates to search_games in repository) +from sqlalchemy.orm import Session +from app import repository +from app.schemas import GameCreate, GameOut, GameList +def add_game(db: Session, data: GameCreate) -> GameOut: + game = repository.create_game(db, data) + return GameOut.model_validate(game) +def fetch_game(db: Session, game_id: str) -> GameOut: + game = repository.get_game(db, game_id) + if game is None: + raise ValueError(f"Game {game_id} not found") + return GameOut.model_validate(game) +def fetch_all_games(db: Session, limit: int = 20, offset: int = 0) -> GameList: + games, total = repository.list_games(db, limit=limit, offset=offset) + return GameList( + items=[GameOut.model_validate(g) for g in games], + total=total, + limit=limit, + offset=offset, + ) +def find_games(db: Session, q: str, limit: int = 20, offset: int = 0) -> GameList: + games, total = repository.search_games(db, q, limit=limit, offset=offset) + return GameList( + items=[GameOut.model_validate(g) for g in games], + total=total, + limit=limit, + offset=offset, + ) \ No newline at end of file diff --git a/services/game-service/games.db b/services/game-service/games.db new file mode 100644 index 0000000000000000000000000000000000000000..53bd5f29ee602d62b4590881b58265ce79d1bb9c GIT binary patch literal 12288 zcmeI#&r8EF6bJBRin_vn9E4uqaRU)VJPOtsbl6p`RoJOS8%MF*G|NPf;?4ifzhVD| zN1H%zgMr8K4J6^c@JRFN>D}Ihxl!~q)njSskhK}-?1YFh#>=@;&T80rvkJ=H#;XQr z``^2b)-S6y7p%Ex{j4g$IRqd80SG_<0uX=z1Rwwb2teQ;0-w)(+xLC`b!6m2p+<77 zW^SqVVi6>QlHfEHUl=@LEO6tVv4R3itfU2kG#6cO`(>ajMPLgKY1ynv2FFz zGOY?FXDa7Je(*w8JzJAAgHtf>23kC!r009U<00Izz t00bZa0SG_<0_!T^*@vP3U)SG@cR>IG5P$##AOHafKmY;|fB*!Xz#rKHTzCKg literal 0 HcmV?d00001 From 7b2db9004e743247812cdb2bf1809927ad4e43d3 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Wed, 27 May 2026 23:51:23 +0200 Subject: [PATCH 2/9] i added activity service to validate user work with game data --- services/activity-service/app/main.py | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 services/activity-service/app/main.py diff --git a/services/activity-service/app/main.py b/services/activity-service/app/main.py new file mode 100644 index 00000000..d57e7d03 --- /dev/null +++ b/services/activity-service/app/main.py @@ -0,0 +1,61 @@ +import httpx +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from datetime import datetime, timezone +import uuid +app = FastAPI(title="activity-service") +USER_SERVICE = "http://localhost:8001" +GAME_SERVICE = "http://localhost:8002" +activities_db = [] +class ActivityCreate(BaseModel): + user_id: str + game_id: str + action: str + duration_minutes: int | None = None +async def validate_user(user_id: str): + for attempt in range(3): + try: + async with httpx.AsyncClient(timeout=5.0) as client: + resp = await client.get(f"{USER_SERVICE}/v1/users/{user_id}") + if resp.status_code == 404: + raise HTTPException(status_code=404, detail="User not found") + resp.raise_for_status() + return resp.json() + except httpx.RequestError: + if attempt == 2: + raise HTTPException(status_code=503, detail="user-service unreachable") +async def enrich_game(game_id: str): + try: + async with httpx.AsyncClient(timeout=5.0) as client: + resp = await client.get(f"{GAME_SERVICE}/v1/games/{game_id}") + if resp.status_code == 200: + return resp.json() + return None + except httpx.RequestError: + return None +@app.post("/v1/activities", status_code=201) +async def create_activity(data: ActivityCreate): + await validate_user(data.user_id) + game = await enrich_game(data.game_id) + activity = { + "id": str(uuid.uuid4()), + "user_id": data.user_id, + "action": data.action, + "duration_minutes": data.duration_minutes, + "created_at": datetime.now(timezone.utc).isoformat(), + "game": game, + } + activities_db.append(activity) + return activity +@app.get("/v1/activities") +async def list_activities(limit: int = 20, offset: int = 0): + page = activities_db[offset: offset + limit] + return {"items": page, "total": len(activities_db), "limit": limit, "offset": offset} +@app.get("/v1/activities/user/{user_id}") +async def user_activities(user_id: str, limit: int = 20, offset: int = 0): + filtered = [a for a in activities_db if a["user_id"] == user_id] + page = filtered[offset: offset + limit] + return {"items": page, "total": len(filtered), "limit": limit, "offset": offset} +@app.get("/health") +async def health(): + return {"status": "ok", "service": "activity-service"} \ No newline at end of file From e9e40946ca0bfd721d99a5d23bbc78bc633a0ce5 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Wed, 27 May 2026 23:51:54 +0200 Subject: [PATCH 3/9] i added a gateway for proxy routing to all three services --- gateway/__init__.py | 0 gateway/config.py | 24 +++++++++ gateway/main.py | 103 +++++++++++++++++++++++++++++++++++++++ gateway/requirements.txt | 4 ++ 4 files changed, 131 insertions(+) create mode 100644 gateway/__init__.py create mode 100644 gateway/config.py create mode 100644 gateway/main.py create mode 100644 gateway/requirements.txt diff --git a/gateway/__init__.py b/gateway/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gateway/config.py b/gateway/config.py new file mode 100644 index 00000000..cff24dee --- /dev/null +++ b/gateway/config.py @@ -0,0 +1,24 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # Services available from Module 3 + user_service_url: str = "http://localhost:8001" + game_service_url: str = "http://localhost:8002" + activity_service_url: str = "http://localhost:8003" + + # Added in Module 4 + # notification_service_url: str = "http://localhost:8004" + + # Added in Module 5 + # logging_service_url: str = "http://localhost:8006" + + # Added in Module 6 + # auth_service_url: str = "http://localhost:8005" + # secret_key: str = "dev-secret-change-in-production" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/gateway/main.py b/gateway/main.py new file mode 100644 index 00000000..777bcec1 --- /dev/null +++ b/gateway/main.py @@ -0,0 +1,103 @@ +import httpx +from fastapi import FastAPI, Request, Response + +from app.config import settings + +app = FastAPI(title="gateway", version="1.0.0") + +# Routing table — maps the resource name in the URL path to the target service base URL. +# Path structure: /{version}/{resource}/... +# e.g. GET /v1/users/123 → resource = "users" → forward to user_service_url +# +# Add new entries here as each module introduces a new service. +# Module 4 will add: "notifications" +# Module 5 will add: "consent", "logs" +# Module 6 will add: "auth" +ROUTES: dict[str, str] = { + "users": settings.user_service_url, + "games": settings.game_service_url, + "activities": settings.activity_service_url, +} + + +@app.get("/health") +async def health(): + """ + Gateway liveness check. Handled here — never forwarded to a service. + In Module 10 this endpoint will be upgraded to fan out to all services + and return their individual status. + """ + return {"status": "ok", "service": "gateway"} + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) +async def proxy(request: Request, path: str): + """ + Catch-all reverse proxy — forwards every request to the correct downstream service. + + Your job: implement the forwarding logic described below. + + --- + Step 1 — Parse the path to find the resource name. + + The path arrives without the leading slash, e.g. "v1/users/123". + Split it on "/" to get a list of segments: + segments = path.split("/") + # ["v1", "users", "123"] + + Index 0 is the version ("v1"). + Index 1 is the resource ("users", "games", "activities", ...). + + If the path has fewer than 2 segments, return a 404. + + --- + Step 2 — Look up the resource in ROUTES. + + resource = segments[1] + target_base = ROUTES.get(resource) + + If `resource` is not in ROUTES, return: + Response(status_code=404, content=f"Unknown resource: {resource}") + + --- + Step 3 — Build the target URL and forward the request. + + The full path must be forwarded as-is — no stripping, no rewriting. + Reconstruct it with the leading slash: + target_url = f"{target_base}/{path}" + + Forward using httpx, preserving method, headers, and body: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.request( + method=request.method, + url=target_url, + headers=request.headers.raw, # forward all original headers + content=await request.body(), # forward the body as-is + params=request.query_params, # forward query string + ) + + Return a FastAPI Response with the downstream status, headers, and body: + return Response( + content=response.content, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.headers.get("content-type"), + ) + + --- + Step 4 — Handle an unreachable downstream service. + + Wrap the httpx call in a try/except for httpx.RequestError. + If the service cannot be reached, return: + Response(status_code=503, content="Service unavailable") + + --- + Verify your implementation: + curl http://localhost:8000/health + curl http://localhost:8000/v1/users + curl http://localhost:8000/v1/games + curl http://localhost:8000/v1/activities + curl http://localhost:8000/v1/unknown # should return 404 + """ + # TODO: implement steps 1–4 above + raise NotImplementedError("implement the proxy forwarding logic") diff --git a/gateway/requirements.txt b/gateway/requirements.txt new file mode 100644 index 00000000..c6d6f316 --- /dev/null +++ b/gateway/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +httpx==0.27.2 +pydantic-settings==2.4.0 From 7cff6c0c0f6ec529fa1a457eca08db0bbccdc226 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Wed, 27 May 2026 23:52:15 +0200 Subject: [PATCH 4/9] i responded to module 3 questions --- modules/module-03/REFLECTION.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/module-03/REFLECTION.md b/modules/module-03/REFLECTION.md index 105be9f9..40a47e2d 100644 --- a/modules/module-03/REFLECTION.md +++ b/modules/module-03/REFLECTION.md @@ -19,6 +19,7 @@ All client requests now go through the gateway. No client ever calls a service d Think about what the client would need to know and manage if it talked to each service on its own port. > *Your answer:* +honestly the thing that surprised me the most was how fragile it feels when one service needs to call another one. like when i was testing activity-service and forgot to start user-service first, everything just crashed. it made me realize that in a microservices setup you always have to think about what happens when the thing you depend on isnt there, which is something you never had to think about in the monolith because everything was in the same process. --- @@ -31,7 +32,7 @@ The activity-service makes two outbound calls: one to validate the user (with re What is the consequence for the user in each case if the downstream service is unavailable? > *Your answer:* - +the user validation has to block the request because theres no point saving an activity for a user that doesnt exist, that would just be bad data. but the game enrichment is just extra info we attach to make the response nicer,the activity itself is still valid without it. so if game-service is down we just return null for the game field and the activity still gets saved. treating them differently means a game-service outage doesnt stop users from logging activities. --- ## 3. The tradeoff @@ -43,7 +44,7 @@ Every time a client creates an activity, three services are involved synchronous What happens to the user experience if the slowest service in the chain takes 3 seconds to respond? > *Your answer:* - +every request now has to make an extra network hop through the gateway before it gets to the actual service. so if the gateway is slow or down, everything is slow or down even if all the services are fine. in the monolith there was no extra hop. but the trade-off is worth it because now clients only need to know one address and the gateway handles figuring out where to send things, and later it can also do auth checks in one place instead of every service doing it separately. --- *Keep this file. You will refer back to it during the oral presentation.* From 1eeb0d9b9a69a84bf10669adde5fe4021f1ac7f8 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Mon, 8 Jun 2026 00:34:49 +0200 Subject: [PATCH 5/9] module-04: RabbitMQ publisher wired into activity-service --- .../app/infrastructure/rabbitmq_publisher.py | 15 + services/activity-service/app/main.py | 17 +- services/gateway/app/main.py | 18 +- .../notification-service/package-lock.json | 1232 +++++++++++++++++ services/notification-service/package.json | 4 +- services/notification-service/src/db.ts | 36 +- services/user-service/app/database.py | 18 + services/user-service/app/main.py | 12 + 8 files changed, 1322 insertions(+), 30 deletions(-) create mode 100644 services/activity-service/app/infrastructure/rabbitmq_publisher.py create mode 100644 services/notification-service/package-lock.json diff --git a/services/activity-service/app/infrastructure/rabbitmq_publisher.py b/services/activity-service/app/infrastructure/rabbitmq_publisher.py new file mode 100644 index 00000000..12530867 --- /dev/null +++ b/services/activity-service/app/infrastructure/rabbitmq_publisher.py @@ -0,0 +1,15 @@ +import json +import pika +RABBITMQ_URL = "amqp://guest:guest@localhost:5672" +QUEUE = "gamehub.notifications" +def publish_notification(user_id: str, message: str) -> None: + connection = pika.BlockingConnection(pika.URLParameters(RABBITMQ_URL)) + channel = connection.channel() + channel.queue_declare(queue=QUEUE, durable=True) + channel.basic_publish( + exchange="", + routing_key=QUEUE, + body=json.dumps({"user_id": user_id, "message": message}), + properties=pika.BasicProperties(delivery_mode=2), + ) + connection.close() \ No newline at end of file diff --git a/services/activity-service/app/main.py b/services/activity-service/app/main.py index d57e7d03..0d6de3eb 100644 --- a/services/activity-service/app/main.py +++ b/services/activity-service/app/main.py @@ -3,7 +3,11 @@ from pydantic import BaseModel from datetime import datetime, timezone import uuid + +from app.infrastructure.rabbitmq_publisher import publish_notification + app = FastAPI(title="activity-service") + USER_SERVICE = "http://localhost:8001" GAME_SERVICE = "http://localhost:8002" activities_db = [] @@ -35,7 +39,7 @@ async def enrich_game(game_id: str): return None @app.post("/v1/activities", status_code=201) async def create_activity(data: ActivityCreate): - await validate_user(data.user_id) + user = await validate_user(data.user_id) game = await enrich_game(data.game_id) activity = { "id": str(uuid.uuid4()), @@ -46,6 +50,17 @@ async def create_activity(data: ActivityCreate): "game": game, } activities_db.append(activity) + + game_title = game["title"] if game else data.game_id + username = user.get("username", data.user_id) if user else data.user_id + + try: + publish_notification( + user_id=data.user_id, + message=f"{username} just {data.action} {game_title}", + ) + except Exception: + pass return activity @app.get("/v1/activities") async def list_activities(limit: int = 20, offset: int = 0): diff --git a/services/gateway/app/main.py b/services/gateway/app/main.py index 9027c3b7..065cd18f 100644 --- a/services/gateway/app/main.py +++ b/services/gateway/app/main.py @@ -1,37 +1,24 @@ import httpx from fastapi import FastAPI, Request, Response - from app.config import settings - app = FastAPI(title="gateway", version="1.0.0") - ROUTES: dict[str, str] = { "users": settings.user_service_url, "games": settings.game_service_url, "activities": settings.activity_service_url, } - - @app.get("/health") async def health(): return {"status": "ok", "service": "gateway"} - - @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def proxy(request: Request, path: str): - # Step 1 — parse the resource name from the path segments = path.split("/") if len(segments) < 2: return Response(status_code=404, content="Not found") - resource = segments[1] - - # Step 2 — look up the target service target_base = ROUTES.get(resource) - if target_base is None: + if not target_base: return Response(status_code=404, content=f"Unknown resource: {resource}") - - # Step 3 — forward the request target_url = f"{target_base}/{path}" try: async with httpx.AsyncClient(timeout=10.0) as client: @@ -48,6 +35,5 @@ async def proxy(request: Request, path: str): headers=dict(response.headers), media_type=response.headers.get("content-type"), ) - # Step 4 — handle unreachable service except httpx.RequestError: - return Response(status_code=503, content="Service unavailable") + return Response(status_code=503, content="Service unavailable") \ No newline at end of file diff --git a/services/notification-service/package-lock.json b/services/notification-service/package-lock.json new file mode 100644 index 00000000..d2fd4376 --- /dev/null +++ b/services/notification-service/package-lock.json @@ -0,0 +1,1232 @@ +{ + "name": "notification-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "notification-service", + "version": "1.0.0", + "dependencies": { + "amqplib": "^0.10.4", + "express": "^4.18.2", + "sql.js": "^1.14.1" + }, + "devDependencies": { + "@types/amqplib": "^0.10.4", + "@types/better-sqlite3": "^7.6.8", + "@types/express": "^4.17.21", + "@types/node": "^20.11.5", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz", + "integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/services/notification-service/package.json b/services/notification-service/package.json index be3fdd27..7d31245d 100644 --- a/services/notification-service/package.json +++ b/services/notification-service/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "amqplib": "^0.10.4", - "better-sqlite3": "^9.4.3", - "express": "^4.18.2" + "express": "^4.18.2", + "sql.js": "^1.14.1" }, "devDependencies": { "@types/amqplib": "^0.10.4", diff --git a/services/notification-service/src/db.ts b/services/notification-service/src/db.ts index 5284e952..104a0f68 100644 --- a/services/notification-service/src/db.ts +++ b/services/notification-service/src/db.ts @@ -1,14 +1,28 @@ -import Database from "better-sqlite3"; +import initSqlJs from "sql.js"; +import fs from "fs"; -const db = new Database("notifications.db"); +const DB_PATH = "notifications.db"; -db.exec(` - CREATE TABLE IF NOT EXISTS notifications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - message TEXT NOT NULL, - received_at TEXT NOT NULL DEFAULT (datetime('now')) - ) -`); +let db: any; -export default db; +async function getDb() { + if (db) return db; + const SQL = await initSqlJs(); + if (fs.existsSync(DB_PATH)) { + const fileBuffer = fs.readFileSync(DB_PATH); + db = new SQL.Database(fileBuffer); + } else { + db = new SQL.Database(); + } + db.run(` + CREATE TABLE IF NOT EXISTS notifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + message TEXT NOT NULL, + received_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `); + return db; +} + +export default getDb; \ No newline at end of file diff --git a/services/user-service/app/database.py b/services/user-service/app/database.py index a5fe24f9..16640464 100644 --- a/services/user-service/app/database.py +++ b/services/user-service/app/database.py @@ -10,3 +10,21 @@ # It is pure infrastructure. # # See the README for the full implementation. +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +import os + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./users.db") + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +class Base(DeclarativeBase): + pass + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/services/user-service/app/main.py b/services/user-service/app/main.py index a4463d42..36e7f41d 100644 --- a/services/user-service/app/main.py +++ b/services/user-service/app/main.py @@ -9,3 +9,15 @@ # Then open: http://localhost:8001/docs # # See the README for the full implementation. +from fastapi import FastAPI +from app.routes import router +from app.database import Base, engine + +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="user-service") +app.include_router(router) + +@app.get("/health") +async def health(): + return {"status": "ok", "service": "user-service"} \ No newline at end of file From 021213e172e1943cbeb500d62365aaf8981f877d Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Mon, 8 Jun 2026 00:52:23 +0200 Subject: [PATCH 6/9] i finished rabbitMQ the publisher wired, the messages in gamehub.notifications --- services/user-service/app/database.py | 4 ---- services/user-service/app/main.py | 3 --- services/user-service/app/models.py | 12 ++++++++++++ services/user-service/app/repository.py | 15 +++++++++++++++ services/user-service/app/routes.py | 17 +++++++++++++++++ services/user-service/app/schemas.py | 16 ++++++++++++++++ services/user-service/app/service.py | 19 +++++++++++++++++++ 7 files changed, 79 insertions(+), 7 deletions(-) diff --git a/services/user-service/app/database.py b/services/user-service/app/database.py index 16640464..af0c9011 100644 --- a/services/user-service/app/database.py +++ b/services/user-service/app/database.py @@ -13,15 +13,11 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, DeclarativeBase import os - DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./users.db") - engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - class Base(DeclarativeBase): pass - def get_db(): db = SessionLocal() try: diff --git a/services/user-service/app/main.py b/services/user-service/app/main.py index 36e7f41d..948b76f1 100644 --- a/services/user-service/app/main.py +++ b/services/user-service/app/main.py @@ -12,12 +12,9 @@ from fastapi import FastAPI from app.routes import router from app.database import Base, engine - Base.metadata.create_all(bind=engine) - app = FastAPI(title="user-service") app.include_router(router) - @app.get("/health") async def health(): return {"status": "ok", "service": "user-service"} \ No newline at end of file diff --git a/services/user-service/app/models.py b/services/user-service/app/models.py index 03e8cdba..427804fd 100644 --- a/services/user-service/app/models.py +++ b/services/user-service/app/models.py @@ -11,3 +11,15 @@ # Rule: no business logic here. This file only describes data structure. # # See the README for the full implementation. +from sqlalchemy import Column, String, DateTime, Boolean +from datetime import datetime, timezone +import uuid +from app.database import Base +class User(Base): + __tablename__ = "users" + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + username = Column(String, nullable=False, unique=True) + email = Column(String, nullable=False) + hashed_password = Column(String, nullable=True) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/services/user-service/app/repository.py b/services/user-service/app/repository.py index f519c460..c69f58e6 100644 --- a/services/user-service/app/repository.py +++ b/services/user-service/app/repository.py @@ -14,3 +14,18 @@ # - list_users(db, limit, offset) -> tuple[list[User], int] # # See the README for the full implementation. +from sqlalchemy.orm import Session +from app.models import User +from app.schemas import UserCreate +def create_user(db: Session, data: UserCreate) -> User: + user = User(username=data.username, email=data.email) + db.add(user) + db.commit() + db.refresh(user) + return user +def get_user(db: Session, user_id: str) -> User | None: + return db.query(User).filter(User.id == user_id).first() +def list_users(db: Session, limit: int = 20, offset: int = 0): + total = db.query(User).count() + users = db.query(User).offset(offset).limit(limit).all() + return users, total \ No newline at end of file diff --git a/services/user-service/app/routes.py b/services/user-service/app/routes.py index 176c60cd..4b885431 100644 --- a/services/user-service/app/routes.py +++ b/services/user-service/app/routes.py @@ -14,3 +14,20 @@ # - GET /v1/users/{user_id} -> get one user by ID (404 if not found) # # See the README for the full implementation. +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.database import get_db +from app import service, schemas +router = APIRouter(prefix="/v1/users", tags=["users"]) +@router.post("/", response_model=schemas.UserOut, status_code=201) +def create_user(data: schemas.UserCreate, db: Session = Depends(get_db)): + return service.add_user(db, data) +@router.get("/", response_model=list[schemas.UserOut]) +def list_users(db: Session = Depends(get_db)): + return service.fetch_all_users(db) +@router.get("/{user_id}", response_model=schemas.UserOut) +def get_user(user_id: str, db: Session = Depends(get_db)): + try: + return service.fetch_user(db, user_id) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) \ No newline at end of file diff --git a/services/user-service/app/schemas.py b/services/user-service/app/schemas.py index f2627a95..945fb798 100644 --- a/services/user-service/app/schemas.py +++ b/services/user-service/app/schemas.py @@ -13,3 +13,19 @@ # read directly from SQLAlchemy ORM objects. # # See the README for the full implementation. +from pydantic import BaseModel +from datetime import datetime +class UserCreate(BaseModel): + username: str + email: str +class UserOut(BaseModel): + id: str + username: str + email: str + created_at: datetime + model_config = {"from_attributes": True} +class UserList(BaseModel): + items: list[UserOut] + total: int + limit: int + offset: int \ No newline at end of file diff --git a/services/user-service/app/service.py b/services/user-service/app/service.py index 39143a27..7777145e 100644 --- a/services/user-service/app/service.py +++ b/services/user-service/app/service.py @@ -17,3 +17,22 @@ # with passlib in Module 6. # # See the README for the full implementation. +from sqlalchemy.orm import Session +from app import repository +from app.schemas import UserCreate, UserOut, UserList +def add_user(db: Session, data: UserCreate) -> UserOut: + user = repository.create_user(db, data) + return UserOut.model_validate(user) +def fetch_user(db: Session, user_id: str) -> UserOut: + user = repository.get_user(db, user_id) + if user is None: + raise ValueError(f"User {user_id} not found") + return UserOut.model_validate(user) +def fetch_all_users(db: Session, limit: int = 20, offset: int = 0) -> UserList: + users, total = repository.list_users(db, limit=limit, offset=offset) + return UserList( + items=[UserOut.model_validate(u) for u in users], + total=total, + limit=limit, + offset=offset, + ) \ No newline at end of file From f307cc25ef495bd2785597150e87a7972003f3b3 Mon Sep 17 00:00:00 2001 From: skyly813-cyber Date: Mon, 8 Jun 2026 01:07:50 +0200 Subject: [PATCH 7/9] i finished module-04 the rabbitMQ publisher wired, the messages in gamehub.notifications --- services/user-service/users.db | Bin 0 -> 16384 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/user-service/users.db diff --git a/services/user-service/users.db b/services/user-service/users.db new file mode 100644 index 0000000000000000000000000000000000000000..1fcb8b00c0bc65016f0a52f9db83595e7ac83746 GIT binary patch literal 16384 zcmeI&&uZH+90zbIF6*4pdWV5e1>G9M9AeM2X`xWJGc3$aRxciO7o*q;H6%?NyML}@ zw>`$#GxRNX--Gn9<0OylP}*&`QTPh|v3`<;^ts9S-C}J}_tI*`vdVZ~mluNV4To_Q4pcB-2wuw< z#5!eRsaQJbAEpuOEO%zLElYc3?wFL6Cxy7G&ZhFZ zp7HZ~-t@EPh?(0oD^~08Q=Mk2CPg1B)iqhD@s-Go1OW&@00Izz00bZa0SG_<0uX?} ze-!vYSJRD6qK9LzaJ-4fZIQp^w&yrKdz=TkJ@Nfu?7ObFH3@1_)#{3`+Fez3^ZCr} zxxU?#Ibg24?YY~I*A1MWzZJCQxv24-$czL52tWV=5P$##AOHafKmY;|fWQM2*wj+} vUp^Q7y8qu0 Date: Mon, 8 Jun 2026 01:32:53 +0200 Subject: [PATCH 8/9] module-04: exercise and reflection files --- modules/module-04/REFLECTION.md | 49 +++++++++ modules/module-04/docker-compose.override.yml | 9 ++ modules/module-04/exercise.md | 101 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 modules/module-04/REFLECTION.md create mode 100644 modules/module-04/docker-compose.override.yml create mode 100644 modules/module-04/exercise.md diff --git a/modules/module-04/REFLECTION.md b/modules/module-04/REFLECTION.md new file mode 100644 index 00000000..728d1295 --- /dev/null +++ b/modules/module-04/REFLECTION.md @@ -0,0 +1,49 @@ +# Module 4 — Reflection + +**Team name**: _______________ +**Branch**: `module-04/` +**Submitted**: before Module 5 lesson + +--- + +Answer the three questions below. There are no right or wrong answers — we are looking for your reasoning, not a textbook definition. A few honest sentences are worth more than a long generic paragraph. + +--- + +## 1. The "why" + +In Module 3, services called each other directly over HTTP. Now activity-service drops a message into a broker and moves on — it never waits for a reply. + +**What does the activity-service gain by not waiting? And what does the notification-service gain by consuming at its own pace?** + +Think about what happens under load, or when notification-service is temporarily down. + +> *Your answer:* + +--- + +## 2. Your choice + +In Module 3 you already knew how to call another service directly over HTTP — you did it for user validation and game enrichment. + +**Why not use the same approach for notifications? What does introducing a broker give you that a direct HTTP call doesn't?** + +Think about what happens if notification-service is slow, or crashes mid-message. + +> *Your answer:* + +--- + +## 3. The tradeoff + +With synchronous REST, you get an immediate answer: success or failure. With async messaging, the activity is saved and the message is sent — but you have no idea if the notification was ever delivered. + +**How would a user know if their notification was never sent? How would you know as a developer?** + +What visibility do you lose when you go async? + +> *Your answer:* + +--- + +*Keep this file. You will refer back to it during the oral presentation.* diff --git a/modules/module-04/docker-compose.override.yml b/modules/module-04/docker-compose.override.yml new file mode 100644 index 00000000..d9757589 --- /dev/null +++ b/modules/module-04/docker-compose.override.yml @@ -0,0 +1,9 @@ +# Module 4: Messaging infrastructure only (RabbitMQ) +# Services still run locally with uvicorn + SQLite. +# RabbitMQ is provided by docker-compose.infra.yml. +# +# Start with: +# docker compose -f docker-compose.infra.yml up -d rabbitmq +# +# No service containers in this module. +version: "3.9" diff --git a/modules/module-04/exercise.md b/modules/module-04/exercise.md new file mode 100644 index 00000000..d49fc483 --- /dev/null +++ b/modules/module-04/exercise.md @@ -0,0 +1,101 @@ +# Module 4 — Asynchronous Messaging + +**Duration**: 2h in class +**Branch to submit**: `module-04/` + +--- + +## Objective + +Until now, services communicated synchronously — one service called another and waited for a reply. This module introduces asynchronous messaging: a service drops a message into a broker and moves on, without waiting for any response. + +You will wire one messaging flow into the system: when an activity is logged, `activity-service` publishes a message to RabbitMQ and `notification-service` consumes it. + +--- + +## Before you start + +Your module-03 work must be in place: gateway running on port 8000, `activity-service` working, seeded users and games available. + +Start the RabbitMQ broker: + +```bash +docker compose -f docker-compose.infra.yml up -d rabbitmq +``` + +Confirm it's up: +- RabbitMQ management UI: http://localhost:15672 (guest / guest) + +Start `notification-service` in a separate terminal: + +```bash +cd services/notification-service +npm install +npm run dev +``` + +Confirm it's up: +- http://localhost:8004/v1/notifications should return an empty list `[]` + +--- + +## What's provided + +- RabbitMQ is running via Docker. You do not need to configure it. +- `rabbitmq_publisher.py` is scaffolded in `services/activity-service/app/infrastructure/` — the connection and publish logic is done, you fill in the call site. +- `notification-service` is already built (Node.js) — you started it above. + +Install the new dependency in `activity-service`: + +```bash +cd services/activity-service +pip install -r requirements.txt +``` + +--- + +## Part A — Wire the RabbitMQ publisher *(~50 min)* + +When a user logs an activity, a notification should be sent to `notification-service` via RabbitMQ. + +Open `services/activity-service/app/infrastructure/rabbitmq_publisher.py` — the publisher is already implemented. Your job is to **call it** from the right place in `create_activity`. + +After wiring it up, verify the full flow: + +1. Log an activity through the gateway: +```bash +curl -X POST http://localhost:8000/v1/activities \ + -H "Content-Type: application/json" \ + -d '{"user_id": "YOUR_USER_ID", "game_id": "YOUR_GAME_ID", "action": "started"}' +``` +2. Open the RabbitMQ UI at http://localhost:15672 — go to the **Queues** tab and confirm messages appeared in `gamehub.notifications` and `gamehub.logs` +3. Check the `notification-service` logs — a notification should appear + +--- + +## Part B — Register notification-service in the gateway *(~20 min)* + +`notification-service` is now part of the system. Add it to the gateway's routing table. + +Open `gateway/app/config.py` and `gateway/app/main.py` — the lines for `notification-service` are already there, commented out with `# Added in Module 4`. Uncomment them. + +Verify: +```bash +curl http://localhost:8000/v1/notifications +``` + +--- + +## Discussion *(~15 min)* + +- What happens to the activity request if `notification-service` is down when the message is published? Should the activity creation fail? +- In Module 3, you called `game-service` directly over HTTP to enrich the response. Why not do the same for notifications — why introduce a broker at all? +- The activity is saved and the message is sent — but you have no confirmation the notification was delivered. What visibility do you lose compared to a synchronous call? + +--- + +## Minimum to submit this branch + +- [ ] Activity creation publishes a RabbitMQ message — visible in the management UI +- [ ] `notification-service` registered in the gateway and reachable via port 8000 +- [ ] `REFLECTION.md` completed and committed From 25ccda54d4e2ee7ef5ba6c3f9eb272eb887be965 Mon Sep 17 00:00:00 2001 From: Fred Njike Date: Mon, 8 Jun 2026 01:34:48 +0200 Subject: [PATCH 9/9] i finished the reflection.md questions --- modules/module-04/REFLECTION.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/module-04/REFLECTION.md b/modules/module-04/REFLECTION.md index 728d1295..8af9705f 100644 --- a/modules/module-04/REFLECTION.md +++ b/modules/module-04/REFLECTION.md @@ -1,6 +1,6 @@ # Module 4 — Reflection -**Team name**: _______________ +**Team name**: ____fred___________ **Branch**: `module-04/` **Submitted**: before Module 5 lesson @@ -20,7 +20,7 @@ Think about what happens under load, or when notification-service is temporarily > *Your answer:* ---- +---so basically activity-service before had to wait for everything to finish before it could send back a response to the user, now it just drops the message in the queue and immediately says "ok done" without waiting for anyone to pick it up. this means if notification-service is super slow or even completely down, the user still gets a fast response and the activity still gets saved. notification-service on the other side can just process messages whenever it wants, if it gets 1000 notifications at once it can go through them one by one at its own speed instead of crashing because too many requests came in at the same time. ## 2. Your choice @@ -32,7 +32,7 @@ Think about what happens if notification-service is slow, or crashes mid-message > *Your answer:* ---- +---if we called notification-service directly over HTTP like we did for user validation, then if notification-service crashes in the middle of sending, the whole request fails or hangs. also if its slow, the user has to wait longer just because of notifications which has nothing to do with saving the activity. the broker fixes this because the message sits in the queue safely even if notification-service is down, when it comes back up it just picks up where it left off. with direct HTTP if the service is down the message is just gone. ## 3. The tradeoff @@ -44,6 +44,6 @@ What visibility do you lose when you go async? > *Your answer:* ---- +---yeah this is the annoying part of async, you basically have no idea if the notification actually got delivered or not. from the user side they might just never see the notification and have no idea why. from the developer side you would have to check the RabbitMQ UI manually or look at logs in notification-service to see if messages are being consumed. you lose the immediate feedback you get with REST where you know right away if it worked or failed. to fix this properly you would need to add some kind of logging or monitoring that tracks when a message was picked up and processed, but that adds more complexity. *Keep this file. You will refer back to it during the oral presentation.*