From 9e8470875c8bc2b161d52adea8ae280304342da2 Mon Sep 17 00:00:00 2001 From: Bogdan Stojadinovic Date: Tue, 31 Mar 2026 17:06:51 +0200 Subject: [PATCH] Implement internal API key validation for gRPC services --- .env.example | 37 ++-- DEV_QUICKSTART.md | 96 ++++----- Glense.Server/DonationService/Program.cs | 14 +- .../Services/AccountServiceClient.cs | 6 + README.md | 192 +++++++++++------- docker-compose.yml | 87 ++++---- schema-Glense.svg | 1 - .../Controllers/NotificationController.cs | 4 +- .../Controllers/ProfileController.cs | 4 +- services/Glense.AccountService/Program.cs | 20 +- .../Services/InternalApiKeyInterceptor.cs | 38 ++++ services/Glense.ChatService/Program.cs | 21 +- services/Glense.ChatService/appsettings.json | 4 +- .../InternalApiKeyClientInterceptor.cs | 34 ++++ services/Glense.VideoCatalogue/Program.cs | 18 +- 15 files changed, 365 insertions(+), 211 deletions(-) delete mode 100644 schema-Glense.svg create mode 100644 services/Glense.AccountService/Services/InternalApiKeyInterceptor.cs create mode 100644 services/Glense.VideoCatalogue/GrpcClients/InternalApiKeyClientInterceptor.cs diff --git a/.env.example b/.env.example index f7aa09f..9d5eed2 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,33 @@ # Glense Environment Variables -# Copy this file to .env and fill in your values -# Usage: source .env before running services, or set in your shell profile +# Copy this file to .env and fill in your values. +# Usage: docker compose --env-file .env up # =================== -# DonationService +# Shared Secrets # =================== -# Neon PostgreSQL connection string -DONATION_DB_CONNECTION_STRING="Host=your-neon-host.neon.tech;Database=neondb;Username=your_user;Password=your_password;SslMode=Require" +JWT_SECRET_KEY=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm +JWT_ISSUER=GlenseAccountService +JWT_AUDIENCE=GlenseApp + +# Inter-service API key (used for service-to-service authentication) +INTERNAL_API_KEY=GlenseInternalServiceKey_ChangeMe_AtLeast32Chars! + +# =================== +# Database Credentials +# =================== +POSTGRES_USER=glense +POSTGRES_PASSWORD=changeme # =================== -# AccountService +# RabbitMQ Credentials # =================== -# PostgreSQL connection string -ACCOUNT_DB_CONNECTION_STRING="Host=postgres_account;Port=5432;Database=glense_account;Username=glense;Password=your_password" +RABBITMQ_USER=guest +RABBITMQ_PASS=guest -# JWT Configuration -JWT_SECRET_KEY="YourSecretKeyThatIsAtLeast32CharactersLong" -JWT_ISSUER="GlenseAccountService" -JWT_AUDIENCE="GlenseApp" +# =================== +# Connection Strings +# =================== +ACCOUNT_DB_CONNECTION_STRING=Host=postgres_account;Port=5432;Database=glense_account;Username=glense;Password=changeme +VIDEO_DB_CONNECTION_STRING=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=changeme +DONATION_DB_CONNECTION_STRING=Host=postgres_donation;Port=5432;Database=glense_donation;Username=glense;Password=changeme +CHAT_DB_CONNECTION_STRING=Host=postgres_chat;Port=5432;Database=glense_chat;Username=glense;Password=changeme diff --git a/DEV_QUICKSTART.md b/DEV_QUICKSTART.md index 613f9dd..60c7d21 100644 --- a/DEV_QUICKSTART.md +++ b/DEV_QUICKSTART.md @@ -2,46 +2,29 @@ ## Prerequisites -- .NET 8 SDK +- .NET 8 SDK (for running services outside Docker) - Node.js v22 -- Docker Desktop **or** Podman +- Docker Desktop or Podman -## Start everything - -### 1. Start databases + microservices +## Setup ```bash -# Docker Desktop (recommended): -docker compose up --build postgres_account postgres_donation account_service donation_service -d +# 1. Copy environment file +cp .env.example .env -# Or Podman: -podman machine start -# Copy the DOCKER_HOST export line from podman's output, then: -docker compose up --build postgres_account postgres_donation account_service donation_service -d -``` +# 2. Start infrastructure + services +docker compose up --build -d -### 2. Start the API Gateway (new terminal) +# 3. Start frontend +cd glense.client && npm install && npm run dev -```bash -cd Glense.Server -dotnet run --urls http://localhost:5050 -``` - -### 3. Start the frontend (new terminal) - -```bash -cd glense.client -npm install -npm run dev -``` - -### 4. Seed test users (new terminal) - -```bash +# 4. Seed test data ./scripts/seed.sh ``` -Creates 3 users (password for all: `Password123!`): +## Test users + +Password for all: `Password123!` | Username | Email | Type | Wallet | |----------|-------|------|--------| @@ -53,57 +36,60 @@ Creates 3 users (password for all: `Password123!`): | Service | Port | Notes | |---------|------|-------| -| Frontend (Vite) | 5173+ | Opens next free port | +| Frontend (Vite) | 5173 | Opens next free port if taken | | API Gateway | 5050 | All frontend requests go here | -| Account Service | 5001 | Auth, profiles, notifications | +| Account Service | 5001 (REST), 5003 (gRPC) | Auth, profiles, notifications | +| Video Catalogue | 5002 | Upload, comments, playlists | | Donation Service | 5100 | Wallets, donations | +| Chat Service | 5004 | Rooms, messages, SignalR | +| RabbitMQ Management | 15672 | Default: guest/guest (override in .env) | | PostgreSQL (Account) | 5432 | | +| PostgreSQL (Video) | 5433 | | | PostgreSQL (Donation) | 5434 | | +| PostgreSQL (Chat) | 5435 | | -## Quick test with curl +## Quick test ```bash # Health checks curl http://localhost:5050/health curl http://localhost:5001/health +curl http://localhost:5002/health curl http://localhost:5100/health +curl http://localhost:5004/health -# Register a user +# Register curl -X POST http://localhost:5050/api/auth/register \ -H "Content-Type: application/json" \ - -d '{"username":"test","email":"test@test.com","password":"Password123!","confirmPassword":"Password123!","accountType":"user"}' + -d '{"username":"test","email":"test@test.com","password":"Password123!"}' # Login curl -X POST http://localhost:5050/api/auth/login \ -H "Content-Type: application/json" \ -d '{"usernameOrEmail":"test","password":"Password123!"}' -# Search users -curl "http://localhost:5050/api/profile/search?q=keki" - -# Check wallet (replace with real user ID from register/login response) -curl http://localhost:5050/api/wallet/user/USER_ID - -# Send donation (replace IDs) -curl -X POST http://localhost:5050/api/donation \ - -H "Content-Type: application/json" \ - -d '{"donorUserId":"DONOR_ID","recipientUserId":"RECIPIENT_ID","amount":10,"message":"Nice!"}' -``` +# Use the token from login response: +TOKEN="" -## Stop everything +# Profile +curl http://localhost:5050/api/profile/me -H "Authorization: Bearer $TOKEN" -```bash -docker compose down # stop containers -# Ctrl+C on gateway and frontend terminals -podman machine stop # if using podman +# Search users +curl "http://localhost:5050/api/profile/search?q=keki" ``` ## Swagger docs | Service | URL | |---------|-----| -| Gateway | http://localhost:5050/swagger | -| Account Service | http://localhost:5001/swagger | -| Donation Service | http://localhost:5100 | +| Account | http://localhost:5001/swagger | | Video Catalogue | http://localhost:5002/swagger | -| Chat Service | http://localhost:5004/swagger | +| Donation | http://localhost:5100 | +| Chat | http://localhost:5004/swagger | + +## Stop + +```bash +docker compose down # stop containers +docker compose down -v # stop + wipe database volumes +``` diff --git a/Glense.Server/DonationService/Program.cs b/Glense.Server/DonationService/Program.cs index 284c6b3..c403cd1 100644 --- a/Glense.Server/DonationService/Program.cs +++ b/Glense.Server/DonationService/Program.cs @@ -97,12 +97,15 @@ // Health check endpoint for container orchestration builder.Services.AddHealthChecks(); -// CORS policy - Allow frontend origins (both HTTP and HTTPS) +// Configure CORS — restrict to known frontend origins +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? ["http://localhost:5173", "http://localhost:50653", "http://localhost:50654", "http://localhost:3000"]; + builder.Services.AddCors(options => { - options.AddDefaultPolicy(policy => + options.AddPolicy("AllowFrontend", policy => { - policy.SetIsOriginAllowed(_ => true) // Allow any origin in development + policy.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); @@ -112,7 +115,7 @@ var app = builder.Build(); // CORS must be first middleware after exception handling -app.UseCors(); +app.UseCors("AllowFrontend"); // Swagger UI available at root path for easy API exploration if (app.Environment.IsDevelopment()) @@ -130,8 +133,7 @@ app.MapControllers(); app.MapHealthChecks("/health"); -// Auto-create database schema in development -// In production, use proper migrations +// Auto-create database schema on startup if (app.Environment.IsDevelopment()) { using var scope = app.Services.CreateScope(); diff --git a/Glense.Server/DonationService/Services/AccountServiceClient.cs b/Glense.Server/DonationService/Services/AccountServiceClient.cs index afdf2c8..433e299 100644 --- a/Glense.Server/DonationService/Services/AccountServiceClient.cs +++ b/Glense.Server/DonationService/Services/AccountServiceClient.cs @@ -18,6 +18,12 @@ public AccountServiceClient(IHttpClientFactory httpClientFactory, ILogger GetUsernameAsync(Guid userId) diff --git a/README.md b/README.md index bf1d13a..c80b49a 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,23 @@ A microservice-based video streaming platform built with .NET 8, React, and Post ## Architecture -All frontend requests go through the **API Gateway** ([YARP](https://microsoft.github.io/reverse-proxy/) reverse proxy, port 5050), which routes to the appropriate microservice based on URL path. - ``` ┌─────────────────┐ │ Frontend │ │ (React/Vite) │ └────────┬─────────┘ │ - ┌──────────▼────────────────┐ - │ API Gateway │ - │ YARP :5050 │ - │ │ - │ /api/auth/* → Account │ - │ /api/profile/* → Account │ - │ /api/videos/* → Video │ - │ /api/donation/*→ Donation │ - │ /api/chats/* → Chat │ - │ /hubs/chat → Chat(WS) │ - └──┬───┬───┬───┬────────────┘ + ┌──────────▼──────────────┐ + │ API Gateway │ + │ YARP :5050 │ + │ │ + │ /api/auth/* → Account │ + │ /api/profile/* → Account │ + │ /api/videos/* → Video │ + │ /api/donation/*→ Donation│ + │ /api/chats/* → Chat │ + │ /hubs/chat → Chat(WS)│ + └──┬───┬───┬───┬──────────┘ ┌─────────────┘ │ │ └──────────┐ ▼ ▼ ▼ ▼ ┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────────┐ @@ -38,97 +36,145 @@ All frontend requests go through the **API Gateway** ([YARP](https://microsoft.g │ ┌──────┴──────┐ │ │ │ RabbitMQ │ │ │ │ :5672/:15672│ │ - │ └──────┬──────┘ │ - │ │ │ - ├──RabbitMQ─────►│ │ wallet create on registration - │◄──RabbitMQ─────┤ │ donation notification - │◄──HTTP─────────┤ │ validate recipient - │◄──RabbitMQ─────────────────────┤ subscription notification - │◄──gRPC─────────────────────────┤ resolve usernames - Chat: JWT only (no inter-service calls) + │ └─────────────┘ │ + │ │ + │◄─── RabbitMQ ──── Donation wallet create on registration + │◄─── HTTP ──────── Donation validate recipient + │◄─── RabbitMQ ──── Donation donation notification + │◄─── gRPC ──────────────── Video resolve usernames + │◄─── RabbitMQ ─────────── Video subscription notification ``` -The gateway is config-driven — adding a new route is a few lines of JSON in `appsettings.json`. YARP handles header forwarding, WebSocket proxying (SignalR), and active health checks. +### Services -### Services and ports - -| Service | Port | Database | Description | -|---------|------|----------|-------------| -| API Gateway (YARP) | 5050 | — | Routes all frontend requests, health checks backends | -| Account | 5001 (REST), 5003 (gRPC) | PostgreSQL :5432 | Auth, profiles, notifications, gRPC server | -| Donation | 5100 | PostgreSQL :5434 | Wallets and donations | -| Video Catalogue | 5002 | PostgreSQL :5433 | Video upload, comments, playlists | -| Chat | 5004 | PostgreSQL :5435 | Chat rooms, messages, SignalR | -| RabbitMQ | 5672 (AMQP), 15672 (management UI) | — | Message broker for async events | +| Service | Port | Database | What it does | +|---------|------|----------|--------------| +| API Gateway (YARP) | 5050 | -- | Routes all requests, CORS whitelist, health checks | +| Account | 5001 (REST), 5003 (gRPC) | PostgreSQL :5432 | Auth, profiles, notifications, gRPC username server | +| Video Catalogue | 5002 | PostgreSQL :5433 | Upload, comments, playlists, subscriptions | +| Donation | 5100 | PostgreSQL :5434 | Wallets, donations, balance transfers | +| Chat | 5004 | PostgreSQL :5435 | Chat rooms, messages, real-time via SignalR | +| RabbitMQ | 5672 / 15672 | -- | Async event broker (MassTransit) | ### Inter-service communication -Services use three different protocols depending on the use case: - -| Flow | Direction | Protocol | Why | -|------|-----------|----------|-----| -| Wallet creation | Account → Donation | **RabbitMQ** | Fire-and-forget event on registration | -| Donation notification | Donation → Account | **RabbitMQ** | Async notification, doesn't block donation | -| Subscription notification | Video → Account | **RabbitMQ** | Async notification on subscribe | -| Recipient validation | Donation → Account | **HTTP** | Synchronous check before processing | -| Username resolution | Video → Account | **gRPC** | High-performance batch lookups (Protobuf) | -| Chat auth | JWT → Chat | **JWT claims** | No inter-service call needed | +| Flow | Protocol | Why | +|------|----------|-----| +| Wallet creation on registration | Account → Donation via **RabbitMQ** | Fire-and-forget | +| Donation notifications | Donation → Account via **RabbitMQ** | Async, doesn't block payment | +| Subscription notifications | Video → Account via **RabbitMQ** | Async | +| Recipient validation | Donation → Account via **HTTP** | Sync check before transfer | +| Username resolution | Video → Account via **gRPC** | High-perf batch lookups (Protobuf) | -**RabbitMQ** (MassTransit) handles fire-and-forget events where the sender doesn't need a response. **gRPC** handles high-frequency synchronous lookups with binary serialization. **HTTP** is kept for simple synchronous calls. +All inter-service calls (HTTP and gRPC) are authenticated with a shared API key (`INTERNAL_API_KEY`). ## Quick start +### Prerequisites + +- Docker or Podman +- Node.js v22 (for frontend) + +### 1. Configure environment + ```bash -# Start everything (containers + seed data) +cp .env.example .env +# Edit .env with your secrets (defaults work for local dev) +``` + +### 2. Start everything + +```bash +# Using the dev script: ./dev.sh -# Start the frontend (separate terminal) -cd glense.client && npm install && npm run dev +# Or manually: +docker compose up --build -d ``` -Other commands: +### 3. Start the frontend ```bash -./dev.sh down # Stop everything -./dev.sh restart # Full clean restart + seed -./dev.sh logs # Follow all container logs -./dev.sh logs gateway # Follow a single service -./dev.sh seed # Re-seed test data -./dev.sh prune # Nuclear option: stop + wipe all images/cache +cd glense.client && npm install && npm run dev ``` -The seed script creates 3 users (password: `Password123!`), wallets, donations, 8 videos with comments. +### Verify it works -### Manual setup +```bash +# Health checks +curl http://localhost:5050/health # Gateway +curl http://localhost:5001/health # Account +curl http://localhost:5002/health # Video +curl http://localhost:5100/health # Donation +curl http://localhost:5004/health # Chat + +# Register + login +curl -X POST http://localhost:5050/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"username":"test","email":"test@test.com","password":"Password123!"}' + +curl -X POST http://localhost:5050/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"usernameOrEmail":"test","password":"Password123!"}' +``` -If you prefer to start services individually: +### Seed test data ```bash -# Start databases, then services, then gateway -docker compose up --build -d postgres_account postgres_video postgres_donation postgres_chat -docker compose up --build -d account_service video_service donation_service chat_service gateway - -# Seed test data ./scripts/seed.sh ``` -Works with both Docker and Podman. +Creates 3 users (password: `Password123!`): `keki`, `irena`, `branko` -- each with $500 wallets. -## Prerequisites +### Other commands -- Node.js v22 -- Docker or Podman +```bash +./dev.sh down # Stop everything +./dev.sh restart # Clean restart + seed +./dev.sh logs # Follow all logs +./dev.sh logs gateway # Follow one service +./dev.sh prune # Wipe everything (containers, volumes, images) +``` -## Development workflow +## Configuration -1. Set up `git pp` so branch names are prefixed with your username -2. Use GitHub Issues for sprint tracking -3. PRs require at least one approval before merge +All secrets live in `.env` (gitignored). The `.env.example` template shows every variable: -```bash -# Setup git pp (one-time) -./scripts/setup-pp.sh yourname +| Variable | Used by | Purpose | +|----------|---------|---------| +| `JWT_SECRET_KEY` | All services | JWT token signing (min 32 chars) | +| `JWT_ISSUER` / `JWT_AUDIENCE` | All services | Token validation | +| `INTERNAL_API_KEY` | Account, Video, Donation | Service-to-service auth | +| `POSTGRES_USER` / `POSTGRES_PASSWORD` | All databases | DB credentials | +| `RABBITMQ_USER` / `RABBITMQ_PASS` | RabbitMQ + consumers | Broker credentials | +| `*_DB_CONNECTION_STRING` | Each service | Full Npgsql connection string | + +Services read from environment variables first, then fall back to `appsettings.json`. + +## Security -# Setup pre-commit hook for C# formatting (one-time) +- **CORS**: All services restrict origins to a configurable whitelist (default: `localhost:5173`, `:3000`, `:50653`, `:50654`) +- **JWT**: BCrypt password hashing, 7-day token expiry, validated on all services +- **Inter-service auth**: gRPC and HTTP calls between services require `INTERNAL_API_KEY` header +- **Secrets**: No credentials in code or config files -- all in `.env` (gitignored) + +## Swagger docs + +| Service | URL | +|---------|-----| +| Account | http://localhost:5001/swagger | +| Video Catalogue | http://localhost:5002/swagger | +| Donation | http://localhost:5100 | +| Chat | http://localhost:5004/swagger | + +## Development + +```bash +# Setup pre-commit hook for C# formatting ./scripts/setup-hooks.sh + +# Setup git pp branch prefixes +./scripts/setup-pp.sh yourname ``` + +PRs require at least one approval before merge. diff --git a/docker-compose.yml b/docker-compose.yml index 47aa33e..a326fa7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ services: image: rabbitmq:3-management container_name: glense_rabbitmq environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} ports: - "5672:5672" - "15672:15672" @@ -23,8 +23,8 @@ services: container_name: glense_postgres_account environment: POSTGRES_DB: glense_account - POSTGRES_USER: glense - POSTGRES_PASSWORD: glense123 + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5432:5432" volumes: @@ -33,7 +33,7 @@ services: networks: - glense_network healthcheck: - test: ["CMD-SHELL", "pg_isready -U glense -d glense_account"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d glense_account"] interval: 10s timeout: 5s retries: 5 @@ -48,13 +48,14 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ACCOUNT_REST_PORT=5000 - ACCOUNT_GRPC_PORT=5001 - - ConnectionStrings__DefaultConnection=Host=postgres_account;Port=5432;Database=glense_account;Username=glense;Password=glense123 - - JWT_SECRET_KEY=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm - - JWT_ISSUER=GlenseAccountService - - JWT_AUDIENCE=GlenseApp + - ConnectionStrings__DefaultConnection=${ACCOUNT_DB_CONNECTION_STRING} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - JWT_ISSUER=${JWT_ISSUER} + - JWT_AUDIENCE=${JWT_AUDIENCE} + - INTERNAL_API_KEY=${INTERNAL_API_KEY} - RabbitMQ__Host=rabbitmq - - RabbitMQ__Username=guest - - RabbitMQ__Password=guest + - RabbitMQ__Username=${RABBITMQ_USER} + - RabbitMQ__Password=${RABBITMQ_PASS} ports: - "5001:5000" - "5003:5001" @@ -73,8 +74,8 @@ services: container_name: glense_postgres_video environment: POSTGRES_DB: glense_video - POSTGRES_USER: glense - POSTGRES_PASSWORD: glense123 + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5433:5432" volumes: @@ -82,7 +83,7 @@ services: networks: - glense_network healthcheck: - test: ["CMD-SHELL", "pg_isready -U glense -d glense_video"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d glense_video"] interval: 10s timeout: 5s retries: 5 @@ -93,8 +94,8 @@ services: container_name: glense_postgres_donation environment: POSTGRES_DB: glense_donation - POSTGRES_USER: glense - POSTGRES_PASSWORD: glense123 + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5434:5432" volumes: @@ -102,7 +103,7 @@ services: networks: - glense_network healthcheck: - test: ["CMD-SHELL", "pg_isready -U glense -d glense_donation"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d glense_donation"] interval: 10s timeout: 5s retries: 5 @@ -116,14 +117,15 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - PORT=5100 - - DONATION_DB_CONNECTION_STRING=Host=postgres_donation;Port=5432;Database=glense_donation;Username=glense;Password=glense123 + - DONATION_DB_CONNECTION_STRING=${DONATION_DB_CONNECTION_STRING} - ACCOUNT_SERVICE_URL=http://account_service:5000 - - JwtSettings__Issuer=GlenseAccountService - - JwtSettings__Audience=GlenseApp - - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm + - INTERNAL_API_KEY=${INTERNAL_API_KEY} + - JwtSettings__Issuer=${JWT_ISSUER} + - JwtSettings__Audience=${JWT_AUDIENCE} + - JwtSettings__SecretKey=${JWT_SECRET_KEY} - RabbitMQ__Host=rabbitmq - - RabbitMQ__Username=guest - - RabbitMQ__Password=guest + - RabbitMQ__Username=${RABBITMQ_USER} + - RabbitMQ__Password=${RABBITMQ_PASS} ports: - "5100:5100" depends_on: @@ -141,8 +143,8 @@ services: container_name: glense_postgres_chat environment: POSTGRES_DB: glense_chat - POSTGRES_USER: glense - POSTGRES_PASSWORD: glense123 + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5435:5432" volumes: @@ -150,7 +152,7 @@ services: networks: - glense_network healthcheck: - test: ["CMD-SHELL", "pg_isready -U glense -d glense_chat"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d glense_chat"] interval: 10s timeout: 5s retries: 5 @@ -163,15 +165,16 @@ services: container_name: glense_video_service environment: - ASPNETCORE_ENVIRONMENT=Development - - ConnectionStrings__VideoCatalogue=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=glense123 + - ConnectionStrings__VideoCatalogue=${VIDEO_DB_CONNECTION_STRING} - ACCOUNT_SERVICE_URL=http://account_service:5000 - ACCOUNT_GRPC_URL=http://account_service:5001 - - JwtSettings__Issuer=GlenseAccountService - - JwtSettings__Audience=GlenseApp - - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm + - INTERNAL_API_KEY=${INTERNAL_API_KEY} + - JwtSettings__Issuer=${JWT_ISSUER} + - JwtSettings__Audience=${JWT_AUDIENCE} + - JwtSettings__SecretKey=${JWT_SECRET_KEY} - RabbitMQ__Host=rabbitmq - - RabbitMQ__Username=guest - - RabbitMQ__Password=guest + - RabbitMQ__Username=${RABBITMQ_USER} + - RabbitMQ__Password=${RABBITMQ_PASS} ports: - "5002:5002" depends_on: @@ -195,10 +198,14 @@ services: ports: - "5050:5050" depends_on: - - account_service - - video_service - - donation_service - - chat_service + account_service: + condition: service_started + video_service: + condition: service_started + donation_service: + condition: service_started + chat_service: + condition: service_started networks: - glense_network restart: unless-stopped @@ -227,10 +234,10 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://+:5000 - - ConnectionStrings__DefaultConnection=Host=postgres_chat;Port=5432;Database=glense_chat;Username=glense;Password=glense123 - - JwtSettings__Issuer=GlenseAccountService - - JwtSettings__Audience=GlenseApp - - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm + - ConnectionStrings__DefaultConnection=${CHAT_DB_CONNECTION_STRING} + - JwtSettings__Issuer=${JWT_ISSUER} + - JwtSettings__Audience=${JWT_AUDIENCE} + - JwtSettings__SecretKey=${JWT_SECRET_KEY} ports: - "5004:5000" depends_on: diff --git a/schema-Glense.svg b/schema-Glense.svg deleted file mode 100644 index 8acf89d..0000000 --- a/schema-Glense.svg +++ /dev/null @@ -1 +0,0 @@ -
dbo.CommentLikes
3 column data
commentIDINT
userIDINT
isLikedBIT
dbo.Subscriptions
2 column data
subscriberIDINT
subscribedToIDINT
dbo.Conversations
4 column data
conversationIDINT
user1IDINT
user2IDINT
createdAtDATETIME
dbo.Videos
11 column data
videoIDINT
uploaderIDINT
titleNVARCHAR
videoURLNVARCHAR
thumbnailURLNVARCHAR
uploadedAtDATETIME
viewCountINT
likeCountINT
dislikeCountINT
descriptionNVARCHAR
categoryIDINT
dbo.Category
2 column data
categoryIDINT
categoryNameNVARCHAR
dbo.Messages
6 column data
messageIDINT
conversationIDINT
senderIDINT
messageTextNVARCHAR
sentAtDATETIME
isSeenBIT
dbo.Comments
7 column data
commentIDINT
videoIDINT
userIDINT
commentTextNVARCHAR
createdAtDATETIME
parentCommentIDINT
commentLikeCountINT
dbo.VideoLikes
3 column data
videoIDINT
userIDINT
isLikedBIT
dbo.Donations
4 column data
donatorIDINT
recipientIDINT
amountINT
donatedAtDATETIME
dbo.Users
7 column data
userIDINT
usernameVARCHAR
passwordSHA256NVARCHAR
emailVARCHAR
profilePictureURLNVARCHAR
accountNVARCHAR
createdAtDATETIME
\ No newline at end of file diff --git a/services/Glense.AccountService/Controllers/NotificationController.cs b/services/Glense.AccountService/Controllers/NotificationController.cs index 01474f4..cfaa37c 100644 --- a/services/Glense.AccountService/Controllers/NotificationController.cs +++ b/services/Glense.AccountService/Controllers/NotificationController.cs @@ -23,7 +23,9 @@ public NotificationController(INotificationService notificationService, ILogger< private Guid GetCurrentUserId() { var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - return Guid.Parse(userIdClaim!); + if (!Guid.TryParse(userIdClaim, out var userId)) + throw new UnauthorizedAccessException("Invalid or missing user identity claim"); + return userId; } [HttpGet] diff --git a/services/Glense.AccountService/Controllers/ProfileController.cs b/services/Glense.AccountService/Controllers/ProfileController.cs index 6d9ee7f..38a0ed1 100644 --- a/services/Glense.AccountService/Controllers/ProfileController.cs +++ b/services/Glense.AccountService/Controllers/ProfileController.cs @@ -23,7 +23,9 @@ public ProfileController(AccountDbContext context, ILogger lo private Guid GetCurrentUserId() { var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - return Guid.Parse(userIdClaim!); + if (!Guid.TryParse(userIdClaim, out var userId)) + throw new UnauthorizedAccessException("Invalid or missing user identity claim"); + return userId; } [HttpGet("search")] diff --git a/services/Glense.AccountService/Program.cs b/services/Glense.AccountService/Program.cs index 2b87455..17c31eb 100644 --- a/services/Glense.AccountService/Program.cs +++ b/services/Glense.AccountService/Program.cs @@ -51,7 +51,11 @@ // Add services to the container builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddGrpc(); +builder.Services.AddSingleton(); +builder.Services.AddGrpc(options => +{ + options.Interceptors.Add(); +}); // Configure Swagger with JWT support builder.Services.AddSwaggerGen(c => @@ -129,14 +133,18 @@ builder.Services.AddAuthorization(); -// Configure CORS +// Configure CORS — restrict to known frontend origins +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? ["http://localhost:5173", "http://localhost:50653", "http://localhost:50654", "http://localhost:3000"]; + builder.Services.AddCors(options => { - options.AddPolicy("AllowAll", policy => + options.AddPolicy("AllowFrontend", policy => { - policy.AllowAnyOrigin() + policy.WithOrigins(allowedOrigins) .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyHeader() + .AllowCredentials(); }); }); @@ -178,7 +186,7 @@ }); } -app.UseCors("AllowAll"); +app.UseCors("AllowFrontend"); app.UseAuthentication(); app.UseAuthorization(); diff --git a/services/Glense.AccountService/Services/InternalApiKeyInterceptor.cs b/services/Glense.AccountService/Services/InternalApiKeyInterceptor.cs new file mode 100644 index 0000000..ad3780d --- /dev/null +++ b/services/Glense.AccountService/Services/InternalApiKeyInterceptor.cs @@ -0,0 +1,38 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace Glense.AccountService.Services; + +/// +/// gRPC server interceptor that validates the X-Internal-Api-Key header +/// on incoming gRPC calls from other services. +/// +public class InternalApiKeyInterceptor : Interceptor +{ + private readonly string _expectedApiKey; + private readonly ILogger _logger; + private const string ApiKeyHeader = "x-internal-api-key"; + + public InternalApiKeyInterceptor(IConfiguration configuration, ILogger logger) + { + _expectedApiKey = Environment.GetEnvironmentVariable("INTERNAL_API_KEY") + ?? configuration["InternalApiKey"] + ?? throw new InvalidOperationException("INTERNAL_API_KEY is not configured"); + _logger = logger; + } + + public override async Task UnaryServerHandler( + TRequest request, ServerCallContext context, UnaryServerMethod continuation) + { + var apiKey = context.RequestHeaders.GetValue(ApiKeyHeader); + + if (string.IsNullOrEmpty(apiKey) || apiKey != _expectedApiKey) + { + _logger.LogWarning("Rejected gRPC call to {Method} — invalid or missing API key from {Peer}", + context.Method, context.Peer); + throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid or missing internal API key")); + } + + return await continuation(request, context); + } +} diff --git a/services/Glense.ChatService/Program.cs b/services/Glense.ChatService/Program.cs index 0b97142..6274a0d 100644 --- a/services/Glense.ChatService/Program.cs +++ b/services/Glense.ChatService/Program.cs @@ -68,13 +68,16 @@ }); }); -// DEV: permissive CORS policy for local development (adjust/remove for production) +// Configure CORS — restrict to known frontend origins +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? ["http://localhost:5173", "http://localhost:50653", "http://localhost:50654", "http://localhost:3000"]; + builder.Services.AddCors(options => { - options.AddPolicy("DevCors", policy => policy + options.AddPolicy("AllowFrontend", policy => policy + .WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() - .SetIsOriginAllowed(_ => true) .AllowCredentials()); }); @@ -101,7 +104,9 @@ // JWT settings var jwtIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "GlenseAccountService"; var jwtAudience = builder.Configuration["JwtSettings:Audience"] ?? "GlenseApp"; -var jwtSecret = builder.Configuration["JwtSettings:SecretKey"] ?? "ChangeMeToA32CharSecret"; +var jwtSecret = builder.Configuration["JwtSettings:SecretKey"] + ?? Environment.GetEnvironmentVariable("JWT_SECRET_KEY") + ?? throw new InvalidOperationException("JWT SecretKey not configured. Set JwtSettings:SecretKey or JWT_SECRET_KEY env var"); // Authentication (JWT) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) @@ -166,7 +171,7 @@ }); // Enable dev CORS policy before auth so preflight succeeds -app.UseCors("DevCors"); +app.UseCors("AllowFrontend"); app.UseAuthentication(); app.UseAuthorization(); @@ -200,13 +205,9 @@ // Map SignalR hubs app.MapHub("/hubs/chat"); -// Ensure relational DB schema exists when using Postgres (not in-memory) +// Ensure database schema exists and seed demo data try { - var startupChatUseInMemory = (Environment.GetEnvironmentVariable("CHAT_USE_INMEMORY") ?? "false").ToLowerInvariant() == "true"; - var startupChatConn = app.Configuration.GetConnectionString("DefaultConnection") - ?? app.Configuration["ConnectionStrings:DefaultConnection"]; - using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetService(); if (db != null) diff --git a/services/Glense.ChatService/appsettings.json b/services/Glense.ChatService/appsettings.json index 9a97e10..e8ce325 100644 --- a/services/Glense.ChatService/appsettings.json +++ b/services/Glense.ChatService/appsettings.json @@ -1,11 +1,11 @@ { "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5435;Database=glense_chat;Username=glense;Password=glense123" + "DefaultConnection": "" }, "JwtSettings": { "Issuer": "GlenseAccountService", "Audience": "GlenseApp", - "SecretKey": "ChangeMeToA32CharSecret" + "SecretKey": "" }, "Logging": { "LogLevel": { diff --git a/services/Glense.VideoCatalogue/GrpcClients/InternalApiKeyClientInterceptor.cs b/services/Glense.VideoCatalogue/GrpcClients/InternalApiKeyClientInterceptor.cs new file mode 100644 index 0000000..5bb0084 --- /dev/null +++ b/services/Glense.VideoCatalogue/GrpcClients/InternalApiKeyClientInterceptor.cs @@ -0,0 +1,34 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace Glense.VideoCatalogue.GrpcClients; + +/// +/// gRPC client interceptor that attaches the X-Internal-Api-Key header +/// to outgoing gRPC calls to other services. +/// +public class InternalApiKeyClientInterceptor : Interceptor +{ + private readonly string _apiKey; + + public InternalApiKeyClientInterceptor(IConfiguration configuration) + { + _apiKey = Environment.GetEnvironmentVariable("INTERNAL_API_KEY") + ?? configuration["InternalApiKey"] + ?? throw new InvalidOperationException("INTERNAL_API_KEY is not configured"); + } + + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var headers = context.Options.Headers ?? new Metadata(); + headers.Add("x-internal-api-key", _apiKey); + + var newOptions = context.Options.WithHeaders(headers); + var newContext = new ClientInterceptorContext( + context.Method, context.Host, newOptions); + + return continuation(request, newContext); + } +} diff --git a/services/Glense.VideoCatalogue/Program.cs b/services/Glense.VideoCatalogue/Program.cs index 1aafcb7..c37fe7e 100644 --- a/services/Glense.VideoCatalogue/Program.cs +++ b/services/Glense.VideoCatalogue/Program.cs @@ -1,6 +1,7 @@ using Glense.VideoCatalogue.Data; using Glense.VideoCatalogue.GrpcClients; using Glense.VideoCatalogue.Protos; +using Grpc.Core.Interceptors; using Glense.VideoCatalogue.Services; using MassTransit; using Microsoft.EntityFrameworkCore; @@ -24,10 +25,17 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// CORS for frontend dev +// Configure CORS — restrict to known frontend origins +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? ["http://localhost:5173", "http://localhost:50653", "http://localhost:50654", "http://localhost:3000"]; + builder.Services.AddCors(options => { - options.AddPolicy("AllowAll", policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); + options.AddPolicy("AllowFrontend", policy => policy + .WithOrigins(allowedOrigins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials()); }); // Configure DbContext (Postgres) @@ -50,10 +58,12 @@ ?? builder.Configuration["AccountService:GrpcUrl"] ?? "http://localhost:5001"; +builder.Services.AddSingleton(); builder.Services.AddGrpcClient(options => { options.Address = new Uri(accountGrpcUrl); -}); +}) +.AddInterceptor(); builder.Services.AddScoped(); // JWT Authentication @@ -119,7 +129,7 @@ app.UseSwaggerUI(); } -app.UseCors("AllowAll"); +app.UseCors("AllowFrontend"); app.UseAuthentication(); app.UseAuthorization();