diff --git a/Glense.Server/DonationService/Consumers/UserRegisteredEventConsumer.cs b/Glense.Server/DonationService/Consumers/UserRegisteredEventConsumer.cs new file mode 100644 index 0000000..e61694f --- /dev/null +++ b/Glense.Server/DonationService/Consumers/UserRegisteredEventConsumer.cs @@ -0,0 +1,71 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using DonationService.Data; +using DonationService.Entities; +using Glense.Shared.Messages; + +namespace DonationService.Consumers +{ + public class UserRegisteredEventConsumer : IConsumer + { + private readonly DonationDbContext _context; + private readonly ILogger _logger; + + public UserRegisteredEventConsumer( + DonationDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + var msg = context.Message; + _logger.LogInformation( + "Received UserRegisteredEvent: UserId={UserId}, Username={Username}", + msg.UserId, msg.Username); + + try + { + // Check if wallet already exists (idempotency) + var existingWallet = await _context.Wallets + .FirstOrDefaultAsync(w => w.UserId == msg.UserId); + + if (existingWallet != null) + { + _logger.LogInformation( + "Wallet already exists for user {UserId}, skipping creation", msg.UserId); + return; + } + + var wallet = new Wallet + { + Id = Guid.NewGuid(), + UserId = msg.UserId, + Balance = 0, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.Wallets.Add(wallet); + await _context.SaveChangesAsync(); + + _logger.LogInformation( + "Wallet created for user {UserId} via UserRegisteredEvent", msg.UserId); + } + catch (DbUpdateException ex) + { + // Handle race condition - wallet may have been created by another consumer instance + _logger.LogWarning(ex, + "DbUpdateException creating wallet for user {UserId}, likely already exists", msg.UserId); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to create wallet for user {UserId}", msg.UserId); + throw; + } + } + } +} diff --git a/Glense.Server/DonationService/Controllers/DonationController.cs b/Glense.Server/DonationService/Controllers/DonationController.cs index 52b4ff1..c48940d 100644 --- a/Glense.Server/DonationService/Controllers/DonationController.cs +++ b/Glense.Server/DonationService/Controllers/DonationController.cs @@ -1,10 +1,12 @@ using System.Security.Claims; +using MassTransit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DonationService.Data; using DonationService.Entities; using DonationService.DTOs; +using Glense.Shared.Messages; using DonationService.Services; namespace DonationService.Controllers; @@ -18,15 +20,18 @@ public class DonationController : ControllerBase private readonly DonationDbContext _context; private readonly ILogger _logger; private readonly IAccountServiceClient _accountService; + private readonly IPublishEndpoint _publishEndpoint; public DonationController( DonationDbContext context, ILogger logger, - IAccountServiceClient accountService) + IAccountServiceClient accountService, + IPublishEndpoint publishEndpoint) { _context = context; _logger = logger; _accountService = accountService; + _publishEndpoint = publishEndpoint; } /// @@ -84,13 +89,15 @@ public async Task> CreateDonation([FromBody] Crea return BadRequest(new { message = "Cannot donate to yourself" }); } - // Validate recipient exists in Account service + // Fetch both usernames upfront to validate and reuse later for the event var recipientUsername = await _accountService.GetUsernameAsync(request.RecipientUserId); if (recipientUsername == null) { return BadRequest(new { message = "Recipient user not found" }); } + var donorUsername = await _accountService.GetUsernameAsync(request.DonorUserId) ?? "Someone"; + // Check if we can use transactions (not supported by in-memory database) var supportsTransactions = !_context.Database.IsInMemory(); var transaction = supportsTransactions @@ -160,20 +167,23 @@ public async Task> CreateDonation([FromBody] Crea "Donation created: {DonationId}, from user {DonorId} to user {RecipientId}, amount: {Amount}", donation.Id, request.DonorUserId, request.RecipientUserId, request.Amount); - // Send notification to recipient (fire-and-forget, don't fail the donation) + // Publish DonationMadeEvent so Account Service creates the notification asynchronously try { - var donorUsername = await _accountService.GetUsernameAsync(request.DonorUserId) ?? "Someone"; - await _accountService.CreateDonationNotificationAsync( - request.RecipientUserId, - donorUsername, - request.Amount, - donation.Id); + await _publishEndpoint.Publish(new DonationMadeEvent + { + DonorId = request.DonorUserId, + RecipientId = request.RecipientUserId, + Amount = request.Amount, + DonorUsername = donorUsername + }); + _logger.LogInformation( + "Published DonationMadeEvent for donation {DonationId}", donation.Id); } catch (Exception notifEx) { _logger.LogWarning(notifEx, - "Failed to send donation notification for donation {DonationId}", + "Failed to publish DonationMadeEvent for donation {DonationId}", donation.Id); } diff --git a/Glense.Server/DonationService/DonationService.csproj b/Glense.Server/DonationService/DonationService.csproj index ced406e..fa64c89 100644 --- a/Glense.Server/DonationService/DonationService.csproj +++ b/Glense.Server/DonationService/DonationService.csproj @@ -24,6 +24,7 @@ + diff --git a/Glense.Server/DonationService/Messages/DonationMadeEvent.cs b/Glense.Server/DonationService/Messages/DonationMadeEvent.cs new file mode 100644 index 0000000..8c80534 --- /dev/null +++ b/Glense.Server/DonationService/Messages/DonationMadeEvent.cs @@ -0,0 +1,10 @@ +namespace Glense.Shared.Messages +{ + public class DonationMadeEvent + { + public Guid DonorId { get; set; } + public Guid RecipientId { get; set; } + public decimal Amount { get; set; } + public string DonorUsername { get; set; } = string.Empty; + } +} diff --git a/Glense.Server/DonationService/Messages/UserRegisteredEvent.cs b/Glense.Server/DonationService/Messages/UserRegisteredEvent.cs new file mode 100644 index 0000000..6565aef --- /dev/null +++ b/Glense.Server/DonationService/Messages/UserRegisteredEvent.cs @@ -0,0 +1,9 @@ +namespace Glense.Shared.Messages +{ + public class UserRegisteredEvent + { + public Guid UserId { get; set; } + public string Username { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + } +} diff --git a/Glense.Server/DonationService/Program.cs b/Glense.Server/DonationService/Program.cs index 8ec6b8a..284c6b3 100644 --- a/Glense.Server/DonationService/Program.cs +++ b/Glense.Server/DonationService/Program.cs @@ -1,7 +1,9 @@ +using MassTransit; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Text; +using DonationService.Consumers; using DonationService.Data; using DonationService.Services; @@ -33,7 +35,7 @@ Console.WriteLine("[WARNING] No connection string found, using in-memory database"); } -// HttpClient for Account Service +// HttpClient for Account Service (kept for synchronous profile lookup/validation) builder.Services.AddHttpClient("AccountService", client => { var serviceUrl = Environment.GetEnvironmentVariable("ACCOUNT_SERVICE_URL") @@ -44,6 +46,27 @@ builder.Services.AddScoped(); +// Configure MassTransit with RabbitMQ +var rabbitHost = builder.Configuration["RabbitMQ:Host"] ?? "localhost"; +var rabbitUser = builder.Configuration["RabbitMQ:Username"] ?? "guest"; +var rabbitPass = builder.Configuration["RabbitMQ:Password"] ?? "guest"; + +builder.Services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(rabbitHost, "/", h => + { + h.Username(rabbitUser); + h.Password(rabbitPass); + }); + + cfg.ConfigureEndpoints(context); + }); +}); + // JWT Authentication var jwtIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "GlenseAccountService"; var jwtAudience = builder.Configuration["JwtSettings:Audience"] ?? "GlenseApp"; diff --git a/Glense.Server/DonationService/Services/AccountServiceClient.cs b/Glense.Server/DonationService/Services/AccountServiceClient.cs index a34507d..afdf2c8 100644 --- a/Glense.Server/DonationService/Services/AccountServiceClient.cs +++ b/Glense.Server/DonationService/Services/AccountServiceClient.cs @@ -22,33 +22,22 @@ public AccountServiceClient(IHttpClientFactory httpClientFactory, ILogger GetUsernameAsync(Guid userId) { - var response = await _httpClient.GetAsync($"/api/profile/{userId}"); - - if (response.StatusCode == HttpStatusCode.NotFound) - return null; + try + { + var response = await _httpClient.GetAsync($"/api/profile/{userId}"); - response.EnsureSuccessStatusCode(); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; - var json = await response.Content.ReadFromJsonAsync(JsonOptions); - return json.TryGetProperty("username", out var username) ? username.GetString() : null; - } + response.EnsureSuccessStatusCode(); - public async Task CreateDonationNotificationAsync( - Guid recipientUserId, - string donorUsername, - decimal amount, - Guid donationId) - { - var request = new + var json = await response.Content.ReadFromJsonAsync(JsonOptions); + return json.TryGetProperty("username", out var username) ? username.GetString() : null; + } + catch (Exception ex) { - UserId = recipientUserId, - Title = "New Donation!", - Message = $"{donorUsername} donated ${amount:F2} to you!", - Type = "donation", - RelatedEntityId = donationId - }; - - var response = await _httpClient.PostAsJsonAsync("/api/internal/notifications", request); - response.EnsureSuccessStatusCode(); + _logger.LogError(ex, "Failed to get username for user {UserId} from Account Service", userId); + return null; + } } } diff --git a/Glense.Server/DonationService/Services/IAccountServiceClient.cs b/Glense.Server/DonationService/Services/IAccountServiceClient.cs index 722e27a..5bde3cb 100644 --- a/Glense.Server/DonationService/Services/IAccountServiceClient.cs +++ b/Glense.Server/DonationService/Services/IAccountServiceClient.cs @@ -3,9 +3,8 @@ namespace DonationService.Services; public interface IAccountServiceClient { /// - /// Gets the username for a user. Returns null if the user doesn't exist. + /// Gets the username for a user via HTTP. Returns null if the user doesn't exist. + /// This is kept as HTTP for synchronous profile validation during donations. /// Task GetUsernameAsync(Guid userId); - - Task CreateDonationNotificationAsync(Guid recipientUserId, string donorUsername, decimal amount, Guid donationId); } diff --git a/Glense.Server/DonationService/appsettings.json b/Glense.Server/DonationService/appsettings.json index 30c80b9..e6a0e5d 100644 --- a/Glense.Server/DonationService/appsettings.json +++ b/Glense.Server/DonationService/appsettings.json @@ -9,5 +9,10 @@ "AllowedHosts": "*", "ConnectionStrings": { "DonationDb": "" + }, + "RabbitMQ": { + "Host": "localhost", + "Username": "guest", + "Password": "guest" } } diff --git a/README.md b/README.md index 6b3de9e..bf1d13a 100644 --- a/README.md +++ b/README.md @@ -4,79 +4,77 @@ 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. Services communicate with each other via HTTP. +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) │ - └──┬──┬──┬──┬────────┘ - ┌────────────┘ │ │ └────────────┐ - ▼ ▼ ▼ ▼ - ┌────────────┐ ┌──────────┐ ┌────────────┐ - │ Account │ │ Donation │ │ Video │ - │ :5001 │ │ :5100 │ │ Catalogue │ - │ │ │ │ │ :5002 │ - │ Auth │ │ Wallets │ │ Upload │ - │ Profiles │ │ Donations│ │ Comments │ - │ Notifs │ │ │ │ Playlists │ - └──────┬─────┘ └────┬─────┘ └─────┬─────┘ - │ ▲ │ ▲ │ - │ └─────────┘ │ │ - │ wallet create │ │ - │ on register │ │ - │ │ │ - │ validate user │ │ - │ + notify │ │ - │◄─────────────────┘ │ - │ │ - │ resolve uploader username │ - │◄────────────────────────────┘ - │ - ┌──────▼─────┐ - │ Chat │ - │ :5004 │ - │ │ - │ Rooms │ - │ Messages │ - │ SignalR │ - └────────────┘ + ┌─────────────────┐ + │ 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) │ + └──┬───┬───┬───┬────────────┘ + ┌─────────────┘ │ │ └──────────┐ + ▼ ▼ ▼ ▼ + ┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Account │ │ Donation │ │ Video │ │ Chat │ + │ :5001 │ │ :5100 │ │ Catalogue │ │ :5004 │ + │ │ │ │ │ :5002 │ │ │ + │ Auth │ │ Wallets │ │ Upload │ │ Rooms │ + │ Profiles │ │ Donations │ │ Comments │ │ Messages │ + │ Notifs │ │ │ │ Playlists │ │ SignalR │ + │ gRPC server │ │ │ │ gRPC client │ │ │ + └──────┬──────┘ └──────┬─────┘ └──────┬──────┘ └─────────────┘ + │ │ │ + │ ┌──────┴──────┐ │ + │ │ 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) ``` -The gateway is config-driven — adding a new route is a few lines of JSON in `appsettings.json`, not a new controller. YARP handles header forwarding, WebSocket proxying (SignalR), and active health checks automatically. +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 and ports | Service | Port | Database | Description | |---------|------|----------|-------------| | API Gateway (YARP) | 5050 | — | Routes all frontend requests, health checks backends | -| Account | 5001 | PostgreSQL :5432 | Auth, profiles, notifications | +| 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 | ### Inter-service communication -| Flow | Direction | Description | -|------|-----------|-------------| -| User registration | Account -> Donation | Auto-creates a wallet for the new user | -| Donation | Donation -> Account | Validates recipient exists, sends notification | -| Video listing | Video -> Account | Resolves uploader usernames | -| Chat messages | JWT -> Chat | Username extracted from JWT token | +Services use three different protocols depending on the use case: -Secondary operations (wallet creation, notifications) are non-blocking. +| 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 | + +**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. ## Quick start @@ -121,12 +119,6 @@ Works with both Docker and Podman. - Node.js v22 - Docker or Podman -## Database schema - -Each microservice owns its own database. See individual service READMEs for schema details. - -![Glense Database Schema](schema-Glense.svg) - ## Development workflow 1. Set up `git pp` so branch names are prefixed with your username diff --git a/docker-compose.yml b/docker-compose.yml index 4170e2f..47aa33e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,22 @@ services: + # RabbitMQ Message Broker + rabbitmq: + image: rabbitmq:3-management + container_name: glense_rabbitmq + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: + - "5672:5672" + - "15672:15672" + networks: + - glense_network + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 5 + # PostgreSQL for Account Service postgres_account: image: postgres:16-alpine @@ -28,17 +46,23 @@ services: container_name: glense_account_service environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://+:5000 + - ACCOUNT_REST_PORT=5000 + - ACCOUNT_GRPC_PORT=5001 - ConnectionStrings__DefaultConnection=Host=postgres_account;Port=5432;Database=glense_account;Username=glense;Password=glense123 - - DONATION_SERVICE_URL=http://donation_service:5100 - JWT_SECRET_KEY=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm - JWT_ISSUER=GlenseAccountService - JWT_AUDIENCE=GlenseApp + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=guest + - RabbitMQ__Password=guest ports: - "5001:5000" + - "5003:5001" depends_on: postgres_account: condition: service_healthy + rabbitmq: + condition: service_healthy networks: - glense_network restart: unless-stopped @@ -97,11 +121,16 @@ services: - JwtSettings__Issuer=GlenseAccountService - JwtSettings__Audience=GlenseApp - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=guest + - RabbitMQ__Password=guest ports: - "5100:5100" depends_on: postgres_donation: condition: service_healthy + rabbitmq: + condition: service_healthy networks: - glense_network restart: unless-stopped @@ -136,14 +165,20 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ConnectionStrings__VideoCatalogue=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=glense123 - ACCOUNT_SERVICE_URL=http://account_service:5000 + - ACCOUNT_GRPC_URL=http://account_service:5001 - JwtSettings__Issuer=GlenseAccountService - JwtSettings__Audience=GlenseApp - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=guest + - RabbitMQ__Password=guest ports: - "5002:5002" depends_on: postgres_video: condition: service_healthy + rabbitmq: + condition: service_healthy networks: - glense_network restart: unless-stopped diff --git a/services/Glense.AccountService/Consumers/DonationMadeEventConsumer.cs b/services/Glense.AccountService/Consumers/DonationMadeEventConsumer.cs new file mode 100644 index 0000000..d53a844 --- /dev/null +++ b/services/Glense.AccountService/Consumers/DonationMadeEventConsumer.cs @@ -0,0 +1,47 @@ +using MassTransit; +using Glense.Shared.Messages; +using Glense.AccountService.Services; + +namespace Glense.AccountService.Consumers +{ + public class DonationMadeEventConsumer : IConsumer + { + private readonly INotificationService _notificationService; + private readonly ILogger _logger; + + public DonationMadeEventConsumer( + INotificationService notificationService, + ILogger logger) + { + _notificationService = notificationService; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + var msg = context.Message; + _logger.LogInformation( + "Received DonationMadeEvent: DonorId={DonorId}, RecipientId={RecipientId}, Amount={Amount}", + msg.DonorId, msg.RecipientId, msg.Amount); + + try + { + await _notificationService.CreateNotificationAsync( + msg.RecipientId, + "New Donation!", + $"{msg.DonorUsername} donated ${msg.Amount:F2} to you!", + "donation", + msg.DonorId); + + _logger.LogInformation( + "Donation notification created for user {RecipientId}", msg.RecipientId); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to create donation notification for user {RecipientId}", msg.RecipientId); + throw; + } + } + } +} diff --git a/services/Glense.AccountService/Consumers/UserSubscribedEventConsumer.cs b/services/Glense.AccountService/Consumers/UserSubscribedEventConsumer.cs new file mode 100644 index 0000000..4bbfe47 --- /dev/null +++ b/services/Glense.AccountService/Consumers/UserSubscribedEventConsumer.cs @@ -0,0 +1,47 @@ +using MassTransit; +using Glense.Shared.Messages; +using Glense.AccountService.Services; + +namespace Glense.AccountService.Consumers +{ + public class UserSubscribedEventConsumer : IConsumer + { + private readonly INotificationService _notificationService; + private readonly ILogger _logger; + + public UserSubscribedEventConsumer( + INotificationService notificationService, + ILogger logger) + { + _notificationService = notificationService; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + var msg = context.Message; + _logger.LogInformation( + "Received UserSubscribedEvent: SubscriberId={SubscriberId}, ChannelOwnerId={ChannelOwnerId}", + msg.SubscriberId, msg.ChannelOwnerId); + + try + { + await _notificationService.CreateNotificationAsync( + msg.ChannelOwnerId, + "New Subscriber!", + $"{msg.SubscriberUsername} subscribed to your channel!", + "subscription", + msg.SubscriberId); + + _logger.LogInformation( + "Subscription notification created for user {ChannelOwnerId}", msg.ChannelOwnerId); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to create subscription notification for user {ChannelOwnerId}", msg.ChannelOwnerId); + throw; + } + } + } +} diff --git a/services/Glense.AccountService/Controllers/ProfileController.cs b/services/Glense.AccountService/Controllers/ProfileController.cs index db33517..6d9ee7f 100644 --- a/services/Glense.AccountService/Controllers/ProfileController.cs +++ b/services/Glense.AccountService/Controllers/ProfileController.cs @@ -64,6 +64,7 @@ public async Task SearchUsers([FromQuery] string? q, [FromQuery] } } + [Authorize] [HttpGet("me")] public async Task GetMyProfile() { @@ -97,7 +98,9 @@ public async Task GetById(Guid userId) { try { - var user = await _context.Users.FindAsync(userId); + var user = await _context.Users + .Where(u => u.Id == userId && u.IsActive) + .FirstOrDefaultAsync(); if (user == null) return NotFound(); var dto = new UserDto @@ -119,6 +122,7 @@ public async Task GetById(Guid userId) } } + [Authorize] [HttpPut("me")] public async Task> UpdateProfile([FromBody] UpdateProfileDto updateDto) { @@ -181,6 +185,7 @@ public async Task> UpdateProfile([FromBody] UpdateProfileD } } + [Authorize] [HttpDelete("me")] public async Task DeleteAccount() { diff --git a/services/Glense.AccountService/Glense.AccountService.csproj b/services/Glense.AccountService/Glense.AccountService.csproj index bca1059..1891ae4 100644 --- a/services/Glense.AccountService/Glense.AccountService.csproj +++ b/services/Glense.AccountService/Glense.AccountService.csproj @@ -16,10 +16,15 @@ - + + + + + + diff --git a/services/Glense.AccountService/GrpcServices/AccountGrpcService.cs b/services/Glense.AccountService/GrpcServices/AccountGrpcService.cs new file mode 100644 index 0000000..ce42b08 --- /dev/null +++ b/services/Glense.AccountService/GrpcServices/AccountGrpcService.cs @@ -0,0 +1,81 @@ +using Grpc.Core; +using Microsoft.EntityFrameworkCore; +using Glense.AccountService.Data; +using Glense.AccountService.Protos; + +namespace Glense.AccountService.GrpcServices +{ + public class AccountGrpcService : AccountGrpc.AccountGrpcBase + { + private readonly AccountDbContext _context; + private readonly ILogger _logger; + + public AccountGrpcService(AccountDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public override async Task GetUsername( + GetUsernameRequest request, ServerCallContext context) + { + _logger.LogDebug("gRPC GetUsername called for UserId={UserId}", request.UserId); + + if (!Guid.TryParse(request.UserId, out var userId)) + { + return new GetUsernameResponse { UserId = request.UserId, Username = "", Found = false }; + } + + var user = await _context.Users + .Where(u => u.Id == userId && u.IsActive) + .Select(u => new { u.Username }) + .FirstOrDefaultAsync(context.CancellationToken); + + if (user == null) + { + return new GetUsernameResponse { UserId = request.UserId, Username = "", Found = false }; + } + + return new GetUsernameResponse + { + UserId = request.UserId, + Username = user.Username, + Found = true + }; + } + + public override async Task GetUsernames( + GetUsernamesRequest request, ServerCallContext context) + { + _logger.LogDebug("gRPC GetUsernames called for {Count} user IDs", request.UserIds.Count); + + var response = new GetUsernamesResponse(); + + var guids = request.UserIds + .Select(id => Guid.TryParse(id, out var g) ? g : (Guid?)null) + .Where(g => g.HasValue) + .Select(g => g!.Value) + .Distinct() + .ToList(); + + if (guids.Count == 0) + return response; + + var users = await _context.Users + .Where(u => guids.Contains(u.Id) && u.IsActive) + .Select(u => new { u.Id, u.Username }) + .ToListAsync(context.CancellationToken); + + foreach (var user in users) + { + response.Users.Add(new UserMapping + { + UserId = user.Id.ToString(), + Username = user.Username + }); + } + + return response; + } + } +} diff --git a/services/Glense.AccountService/Messages/DonationMadeEvent.cs b/services/Glense.AccountService/Messages/DonationMadeEvent.cs new file mode 100644 index 0000000..8c80534 --- /dev/null +++ b/services/Glense.AccountService/Messages/DonationMadeEvent.cs @@ -0,0 +1,10 @@ +namespace Glense.Shared.Messages +{ + public class DonationMadeEvent + { + public Guid DonorId { get; set; } + public Guid RecipientId { get; set; } + public decimal Amount { get; set; } + public string DonorUsername { get; set; } = string.Empty; + } +} diff --git a/services/Glense.AccountService/Messages/UserRegisteredEvent.cs b/services/Glense.AccountService/Messages/UserRegisteredEvent.cs new file mode 100644 index 0000000..6565aef --- /dev/null +++ b/services/Glense.AccountService/Messages/UserRegisteredEvent.cs @@ -0,0 +1,9 @@ +namespace Glense.Shared.Messages +{ + public class UserRegisteredEvent + { + public Guid UserId { get; set; } + public string Username { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + } +} diff --git a/services/Glense.AccountService/Messages/UserSubscribedEvent.cs b/services/Glense.AccountService/Messages/UserSubscribedEvent.cs new file mode 100644 index 0000000..3ac9a4d --- /dev/null +++ b/services/Glense.AccountService/Messages/UserSubscribedEvent.cs @@ -0,0 +1,9 @@ +namespace Glense.Shared.Messages +{ + public class UserSubscribedEvent + { + public Guid SubscriberId { get; set; } + public Guid ChannelOwnerId { get; set; } + public string SubscriberUsername { get; set; } = string.Empty; + } +} diff --git a/services/Glense.AccountService/Program.cs b/services/Glense.AccountService/Program.cs index 2470efe..2b87455 100644 --- a/services/Glense.AccountService/Program.cs +++ b/services/Glense.AccountService/Program.cs @@ -1,10 +1,14 @@ using System.Text; using DotNetEnv; +using MassTransit; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Glense.AccountService.Consumers; using Glense.AccountService.Data; +using Glense.AccountService.GrpcServices; using Glense.AccountService.Services; // Load environment variables from a .env file if present in this directory or any parent directory @@ -26,13 +30,28 @@ var builder = WebApplication.CreateBuilder(args); -// Allow overriding URLs from environment: use ACCOUNT_URLS first, then ASPNETCORE_URLS -var accountUrls = Environment.GetEnvironmentVariable("ACCOUNT_URLS") ?? Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); -if (!string.IsNullOrEmpty(accountUrls)) builder.WebHost.UseUrls(accountUrls); +// Configure Kestrel with two ports: HTTP/1.1 for REST API, HTTP/2 for gRPC +var restPort = int.Parse(Environment.GetEnvironmentVariable("ACCOUNT_REST_PORT") ?? "5000"); +var grpcPort = int.Parse(Environment.GetEnvironmentVariable("ACCOUNT_GRPC_PORT") ?? "5001"); + +builder.WebHost.ConfigureKestrel(options => +{ + // REST API endpoint (HTTP/1.1) + options.ListenAnyIP(restPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1; + }); + // gRPC endpoint (HTTP/2 cleartext) + options.ListenAnyIP(grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); +}); // Add services to the container builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddGrpc(); // Configure Swagger with JWT support builder.Services.AddSwaggerGen(c => @@ -121,19 +140,31 @@ }); }); -// HttpClient for Donation Service -builder.Services.AddHttpClient("DonationService", client => +// Configure MassTransit with RabbitMQ +var rabbitHost = builder.Configuration["RabbitMQ:Host"] ?? "localhost"; +var rabbitUser = builder.Configuration["RabbitMQ:Username"] ?? "guest"; +var rabbitPass = builder.Configuration["RabbitMQ:Password"] ?? "guest"; + +builder.Services.AddMassTransit(x => { - var serviceUrl = Environment.GetEnvironmentVariable("DONATION_SERVICE_URL") - ?? "http://localhost:5100"; - client.BaseAddress = new Uri(serviceUrl); - client.Timeout = TimeSpan.FromSeconds(10); + x.AddConsumer(); + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(rabbitHost, "/", h => + { + h.Username(rabbitUser); + h.Password(rabbitPass); + }); + + cfg.ConfigureEndpoints(context); + }); }); // Register services builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); var app = builder.Build(); @@ -153,6 +184,7 @@ app.UseAuthorization(); app.MapControllers(); +app.MapGrpcService(); // Health check endpoint app.MapGet("/health", () => Results.Ok(new { status = "healthy", service = "account", timestamp = DateTime.UtcNow })); diff --git a/services/Glense.AccountService/Protos/account.proto b/services/Glense.AccountService/Protos/account.proto new file mode 100644 index 0000000..48daab5 --- /dev/null +++ b/services/Glense.AccountService/Protos/account.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +option csharp_namespace = "Glense.AccountService.Protos"; + +package account; + +service AccountGrpc { + rpc GetUsername (GetUsernameRequest) returns (GetUsernameResponse); + rpc GetUsernames (GetUsernamesRequest) returns (GetUsernamesResponse); +} + +message GetUsernameRequest { + string user_id = 1; +} + +message GetUsernameResponse { + string user_id = 1; + string username = 2; + bool found = 3; +} + +message GetUsernamesRequest { + repeated string user_ids = 1; +} + +message GetUsernamesResponse { + repeated UserMapping users = 1; +} + +message UserMapping { + string user_id = 1; + string username = 2; +} diff --git a/services/Glense.AccountService/Services/AuthService.cs b/services/Glense.AccountService/Services/AuthService.cs index 34f21b0..7a8f863 100644 --- a/services/Glense.AccountService/Services/AuthService.cs +++ b/services/Glense.AccountService/Services/AuthService.cs @@ -2,10 +2,12 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Glense.AccountService.Data; using Glense.AccountService.DTOs; +using Glense.Shared.Messages; using Glense.AccountService.Models; namespace Glense.AccountService.Services @@ -18,18 +20,18 @@ public class AuthService : IAuthService { private readonly AccountDbContext _context; private readonly IConfiguration _configuration; - private readonly IWalletServiceClient _walletServiceClient; + private readonly IPublishEndpoint _publishEndpoint; private readonly ILogger _logger; public AuthService( AccountDbContext context, IConfiguration configuration, - IWalletServiceClient walletServiceClient, + IPublishEndpoint publishEndpoint, ILogger logger) { _context = context; _configuration = configuration; - _walletServiceClient = walletServiceClient; + _publishEndpoint = publishEndpoint; _logger = logger; } @@ -52,15 +54,21 @@ public AuthService( _context.Users.Add(user); await _context.SaveChangesAsync(); - // Auto-create wallet in Donation service (non-blocking) + // Publish UserRegisteredEvent so Donation Service creates the wallet asynchronously try { - await _walletServiceClient.CreateWalletAsync(user.Id); + await _publishEndpoint.Publish(new UserRegisteredEvent + { + UserId = user.Id, + Username = user.Username, + Email = user.Email + }); + _logger.LogInformation("Published UserRegisteredEvent for user {UserId}", user.Id); } catch (Exception ex) { _logger.LogWarning(ex, - "Failed to create wallet for user {UserId} during registration. Wallet can be created later.", + "Failed to publish UserRegisteredEvent for user {UserId}. Wallet can be created later.", user.Id); } diff --git a/services/Glense.AccountService/Services/IWalletServiceClient.cs b/services/Glense.AccountService/Services/IWalletServiceClient.cs deleted file mode 100644 index 4a88b5a..0000000 --- a/services/Glense.AccountService/Services/IWalletServiceClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Glense.AccountService.Services -{ - public interface IWalletServiceClient - { - Task CreateWalletAsync(Guid userId, decimal initialBalance = 0); - } -} diff --git a/services/Glense.AccountService/Services/WalletServiceClient.cs b/services/Glense.AccountService/Services/WalletServiceClient.cs deleted file mode 100644 index d9ca123..0000000 --- a/services/Glense.AccountService/Services/WalletServiceClient.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Net.Http.Json; - -namespace Glense.AccountService.Services -{ - public class WalletServiceClient : IWalletServiceClient - { - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - - public WalletServiceClient(IHttpClientFactory httpClientFactory, ILogger logger) - { - _httpClient = httpClientFactory.CreateClient("DonationService"); - _logger = logger; - } - - public async Task CreateWalletAsync(Guid userId, decimal initialBalance = 0) - { - try - { - var request = new { UserId = userId, InitialBalance = initialBalance }; - var response = await _httpClient.PostAsJsonAsync("/api/wallet", request); - - if (response.IsSuccessStatusCode) - { - _logger.LogInformation("Wallet created for user {UserId}", userId); - return true; - } - - _logger.LogWarning( - "Failed to create wallet for user {UserId}. Status: {StatusCode}", - userId, response.StatusCode); - return false; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error creating wallet for user {UserId}", userId); - return false; - } - } - } -} diff --git a/services/Glense.AccountService/appsettings.json b/services/Glense.AccountService/appsettings.json index eea04fb..296cfde 100644 --- a/services/Glense.AccountService/appsettings.json +++ b/services/Glense.AccountService/appsettings.json @@ -13,5 +13,10 @@ "SecretKey": "", "Issuer": "GlenseAccountService", "Audience": "GlenseApp" + }, + "RabbitMQ": { + "Host": "localhost", + "Username": "guest", + "Password": "guest" } } diff --git a/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs b/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs index dcb5513..fe07c16 100644 --- a/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs +++ b/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs @@ -1,8 +1,11 @@ using System.Security.Claims; using System.Threading.Tasks; +using MassTransit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Glense.VideoCatalogue.Data; +using Glense.VideoCatalogue.GrpcClients; +using Glense.Shared.Messages; using Glense.VideoCatalogue.Models; using Microsoft.EntityFrameworkCore; @@ -12,10 +15,20 @@ namespace Glense.VideoCatalogue.Controllers; public class SubscriptionsController : ControllerBase { private readonly VideoCatalogueDbContext _db; + private readonly IPublishEndpoint _publishEndpoint; + private readonly IAccountGrpcClient _accountClient; + private readonly ILogger _logger; - public SubscriptionsController(VideoCatalogueDbContext db) + public SubscriptionsController( + VideoCatalogueDbContext db, + IPublishEndpoint publishEndpoint, + IAccountGrpcClient accountClient, + ILogger logger) { _db = db; + _publishEndpoint = publishEndpoint; + _accountClient = accountClient; + _logger = logger; } private Guid GetCurrentUserId() @@ -38,6 +51,27 @@ public async Task Subscribe([FromBody] DTOs.SubscribeRequestDTO d _db.Subscriptions.Add(s); await _db.SaveChangesAsync(); + // Publish UserSubscribedEvent so Account Service creates the notification + try + { + var subscriberUsername = await _accountClient.GetUsernameAsync(subscriberId) ?? "Someone"; + await _publishEndpoint.Publish(new UserSubscribedEvent + { + SubscriberId = subscriberId, + ChannelOwnerId = dto.SubscribedToId, + SubscriberUsername = subscriberUsername + }); + _logger.LogInformation( + "Published UserSubscribedEvent: SubscriberId={SubscriberId}, ChannelOwnerId={ChannelOwnerId}", + subscriberId, dto.SubscribedToId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Failed to publish UserSubscribedEvent for subscription {SubscriberId} -> {ChannelOwnerId}", + subscriberId, dto.SubscribedToId); + } + var resp = new DTOs.SubscribeResponseDTO { SubscriberId = s.SubscriberId, SubscribedToId = s.SubscribedToId, SubscriptionDate = s.SubscriptionDate }; return Created(string.Empty, resp); } diff --git a/services/Glense.VideoCatalogue/Controllers/VideosController.cs b/services/Glense.VideoCatalogue/Controllers/VideosController.cs index 64e2b5a..f711edb 100644 --- a/services/Glense.VideoCatalogue/Controllers/VideosController.cs +++ b/services/Glense.VideoCatalogue/Controllers/VideosController.cs @@ -1,14 +1,12 @@ using System; using System.IO; -using System.Net; -using System.Net.Http.Json; using System.Security.Claims; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Glense.VideoCatalogue.Data; +using Glense.VideoCatalogue.GrpcClients; using Glense.VideoCatalogue.Services; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -21,17 +19,20 @@ public class VideosController : ControllerBase private readonly Upload _uploader; private readonly VideoCatalogueDbContext _db; private readonly IVideoStorage _storage; - private readonly HttpClient _accountClient; + private readonly IAccountGrpcClient _accountClient; private readonly ILogger _logger; - private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; - - public VideosController(Upload uploader, VideoCatalogueDbContext db, IVideoStorage storage, IHttpClientFactory httpClientFactory, ILogger logger) + public VideosController( + Upload uploader, + VideoCatalogueDbContext db, + IVideoStorage storage, + IAccountGrpcClient accountClient, + ILogger logger) { _uploader = uploader; _db = db; _storage = storage; - _accountClient = httpClientFactory.CreateClient("AccountService"); + _accountClient = accountClient; _logger = logger; } @@ -48,32 +49,6 @@ private Guid GetCurrentUserId() return $"/api/videos/{videoId}/thumbnail"; } - private async Task GetUsernameAsync(Guid userId) - { - if (userId == Guid.Empty) return null; - try - { - var resp = await _accountClient.GetAsync($"/api/profile/{userId}"); - if (!resp.IsSuccessStatusCode) return null; - var json = await resp.Content.ReadFromJsonAsync(JsonOpts); - return json.TryGetProperty("username", out var u) ? u.GetString() : null; - } - catch { return null; } - } - - private async Task> ResolveUsernamesAsync(IEnumerable userIds) - { - var map = new Dictionary(); - var unique = userIds.Where(id => id != Guid.Empty).Distinct().ToList(); - var tasks = unique.Select(async id => - { - var name = await GetUsernameAsync(id); - if (name != null) map[id] = name; - }); - await Task.WhenAll(tasks); - return map; - } - [Authorize] [HttpPost("upload")] public async Task Upload([FromForm] DTOs.UploadRequestDTO dto) @@ -105,7 +80,8 @@ public async Task Upload([FromForm] DTOs.UploadRequestDTO dto) public async Task List() { var videos = await _db.Videos.ToListAsync(); - var usernames = await ResolveUsernamesAsync(videos.Select(v => v.UploaderId)); + var uploaderIds = videos.Select(v => v.UploaderId).ToList(); + var usernames = await _accountClient.GetUsernamesAsync(uploaderIds); var vids = videos.Select(video => new DTOs.UploadResponseDTO { @@ -132,7 +108,7 @@ public async Task Get(Guid id) var video = await _db.Videos.FirstOrDefaultAsync(v => v.Id == id); if (video == null) return NotFound(); - var username = await GetUsernameAsync(video.UploaderId); + var username = await _accountClient.GetUsernameAsync(video.UploaderId); var resp = new DTOs.UploadResponseDTO { diff --git a/services/Glense.VideoCatalogue/Glense.VideoCatalogue.csproj b/services/Glense.VideoCatalogue/Glense.VideoCatalogue.csproj index 66e5ecb..405b49b 100644 --- a/services/Glense.VideoCatalogue/Glense.VideoCatalogue.csproj +++ b/services/Glense.VideoCatalogue/Glense.VideoCatalogue.csproj @@ -12,6 +12,18 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/services/Glense.VideoCatalogue/GrpcClients/AccountGrpcClient.cs b/services/Glense.VideoCatalogue/GrpcClients/AccountGrpcClient.cs new file mode 100644 index 0000000..238aabb --- /dev/null +++ b/services/Glense.VideoCatalogue/GrpcClients/AccountGrpcClient.cs @@ -0,0 +1,73 @@ +using Glense.VideoCatalogue.Protos; + +namespace Glense.VideoCatalogue.GrpcClients +{ + public interface IAccountGrpcClient + { + Task GetUsernameAsync(Guid userId); + Task> GetUsernamesAsync(IEnumerable userIds); + } + + public class AccountGrpcClient : IAccountGrpcClient + { + private readonly AccountGrpc.AccountGrpcClient _client; + private readonly ILogger _logger; + + public AccountGrpcClient( + AccountGrpc.AccountGrpcClient client, + ILogger logger) + { + _client = client; + _logger = logger; + } + + public async Task GetUsernameAsync(Guid userId) + { + try + { + var response = await _client.GetUsernameAsync(new GetUsernameRequest + { + UserId = userId.ToString() + }); + + return response.Found ? response.Username : null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "gRPC call to GetUsername failed for UserId={UserId}", userId); + return null; + } + } + + public async Task> GetUsernamesAsync(IEnumerable userIds) + { + var result = new Dictionary(); + var uniqueIds = userIds.Where(id => id != Guid.Empty).Distinct().ToList(); + + if (uniqueIds.Count == 0) + return result; + + try + { + var request = new GetUsernamesRequest(); + request.UserIds.AddRange(uniqueIds.Select(id => id.ToString())); + + var response = await _client.GetUsernamesAsync(request); + + foreach (var mapping in response.Users) + { + if (Guid.TryParse(mapping.UserId, out var id)) + { + result[id] = mapping.Username; + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "gRPC call to GetUsernames failed for {Count} user IDs", uniqueIds.Count); + } + + return result; + } + } +} diff --git a/services/Glense.VideoCatalogue/Messages/UserSubscribedEvent.cs b/services/Glense.VideoCatalogue/Messages/UserSubscribedEvent.cs new file mode 100644 index 0000000..3ac9a4d --- /dev/null +++ b/services/Glense.VideoCatalogue/Messages/UserSubscribedEvent.cs @@ -0,0 +1,9 @@ +namespace Glense.Shared.Messages +{ + public class UserSubscribedEvent + { + public Guid SubscriberId { get; set; } + public Guid ChannelOwnerId { get; set; } + public string SubscriberUsername { get; set; } = string.Empty; + } +} diff --git a/services/Glense.VideoCatalogue/Program.cs b/services/Glense.VideoCatalogue/Program.cs index 9c9d4c3..1aafcb7 100644 --- a/services/Glense.VideoCatalogue/Program.cs +++ b/services/Glense.VideoCatalogue/Program.cs @@ -1,5 +1,8 @@ using Glense.VideoCatalogue.Data; +using Glense.VideoCatalogue.GrpcClients; +using Glense.VideoCatalogue.Protos; using Glense.VideoCatalogue.Services; +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -42,14 +45,16 @@ options.UseInMemoryDatabase("VideoCatalogue")); } -// HttpClient for Account Service (resolve uploader usernames) -builder.Services.AddHttpClient("AccountService", client => +// gRPC client for Account Service (replaces HTTP-based username resolution) +var accountGrpcUrl = Environment.GetEnvironmentVariable("ACCOUNT_GRPC_URL") + ?? builder.Configuration["AccountService:GrpcUrl"] + ?? "http://localhost:5001"; + +builder.Services.AddGrpcClient(options => { - var serviceUrl = Environment.GetEnvironmentVariable("ACCOUNT_SERVICE_URL") - ?? "http://localhost:5001"; - client.BaseAddress = new Uri(serviceUrl); - client.Timeout = TimeSpan.FromSeconds(10); + options.Address = new Uri(accountGrpcUrl); }); +builder.Services.AddScoped(); // JWT Authentication var jwtIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "GlenseAccountService"; @@ -78,6 +83,25 @@ builder.Services.AddAuthorization(); +// Configure MassTransit with RabbitMQ +var rabbitHost = builder.Configuration["RabbitMQ:Host"] ?? "localhost"; +var rabbitUser = builder.Configuration["RabbitMQ:Username"] ?? "guest"; +var rabbitPass = builder.Configuration["RabbitMQ:Password"] ?? "guest"; + +builder.Services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(rabbitHost, "/", h => + { + h.Username(rabbitUser); + h.Password(rabbitPass); + }); + + cfg.ConfigureEndpoints(context); + }); +}); + // Health checks builder.Services.AddHealthChecks(); @@ -95,8 +119,6 @@ app.UseSwaggerUI(); } -app.UseHttpsRedirection(); - app.UseCors("AllowAll"); app.UseAuthentication(); diff --git a/services/Glense.VideoCatalogue/Protos/account.proto b/services/Glense.VideoCatalogue/Protos/account.proto new file mode 100644 index 0000000..31c3eff --- /dev/null +++ b/services/Glense.VideoCatalogue/Protos/account.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +option csharp_namespace = "Glense.VideoCatalogue.Protos"; + +package account; + +service AccountGrpc { + rpc GetUsername (GetUsernameRequest) returns (GetUsernameResponse); + rpc GetUsernames (GetUsernamesRequest) returns (GetUsernamesResponse); +} + +message GetUsernameRequest { + string user_id = 1; +} + +message GetUsernameResponse { + string user_id = 1; + string username = 2; + bool found = 3; +} + +message GetUsernamesRequest { + repeated string user_ids = 1; +} + +message GetUsernamesResponse { + repeated UserMapping users = 1; +} + +message UserMapping { + string user_id = 1; + string username = 2; +} diff --git a/services/Glense.VideoCatalogue/appsettings.json b/services/Glense.VideoCatalogue/appsettings.json index b802754..21af43f 100644 --- a/services/Glense.VideoCatalogue/appsettings.json +++ b/services/Glense.VideoCatalogue/appsettings.json @@ -14,5 +14,13 @@ "VideoStorage": { "BasePath": "Videos", "RequestBufferSize": 81920 + }, + "RabbitMQ": { + "Host": "localhost", + "Username": "guest", + "Password": "guest" + }, + "AccountService": { + "GrpcUrl": "http://localhost:5001" } } diff --git a/start-dev.sh b/start-dev.sh deleted file mode 100755 index 8a3d41a..0000000 --- a/start-dev.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -# Glense Development Startup Script -# Starts all services needed for local development - -set -e - -echo "🚀 Starting Glense Development Environment..." -echo "" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get the directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Function to check if a port is in use -check_port() { - lsof -i :$1 >/dev/null 2>&1 -} - -# Function to wait for a service to be ready -wait_for_service() { - local url=$1 - local name=$2 - local max_attempts=30 - local attempt=1 - - echo -n " Waiting for $name..." - while ! curl -s "$url" >/dev/null 2>&1; do - if [ $attempt -ge $max_attempts ]; then - echo -e " ${RED}FAILED${NC}" - return 1 - fi - sleep 1 - attempt=$((attempt + 1)) - echo -n "." - done - echo -e " ${GREEN}OK${NC}" -} - -# Stop any existing docker containers that might conflict -echo "📦 Setting up Docker services..." -docker stop glense_account_service 2>/dev/null || true - -# Start PostgreSQL via docker-compose -echo " Starting PostgreSQL..." -cd "$SCRIPT_DIR" -docker-compose up postgres_account -d 2>/dev/null -sleep 2 - -# Check if PostgreSQL is ready -echo -n " Waiting for PostgreSQL..." -max_attempts=30 -attempt=1 -while ! docker exec glense_postgres_account pg_isready -U glense -d glense_account >/dev/null 2>&1; do - if [ $attempt -ge $max_attempts ]; then - echo -e " ${RED}FAILED${NC}" - echo "PostgreSQL failed to start. Check docker logs." - exit 1 - fi - sleep 1 - attempt=$((attempt + 1)) - echo -n "." -done -echo -e " ${GREEN}OK${NC}" - -echo "" -echo "🔧 Starting Microservices..." - -# Start Account Service -echo " Starting Account Service (port 5001)..." -cd "$SCRIPT_DIR/services/Glense.AccountService" -dotnet run --urls "http://localhost:5001" > /tmp/account-service.log 2>&1 & -ACCOUNT_PID=$! -echo " PID: $ACCOUNT_PID" - -# Start Donation Service -echo " Starting Donation Service (port 5100)..." -cd "$SCRIPT_DIR/Glense.Server/DonationService" -dotnet run > /tmp/donation-service.log 2>&1 & -DONATION_PID=$! -echo " PID: $DONATION_PID" - -# Start Gateway -echo " Starting Gateway (port 5050)..." -cd "$SCRIPT_DIR/Glense.Server" -dotnet run --urls "http://localhost:5050" > /tmp/gateway.log 2>&1 & -GATEWAY_PID=$! -echo " PID: $GATEWAY_PID" - -# Wait for services to be ready -echo "" -echo "⏳ Waiting for services to be ready..." -sleep 3 - -wait_for_service "http://localhost:5001/health" "Account Service" -wait_for_service "http://localhost:5100/health" "Donation Service" -wait_for_service "http://localhost:5050/health" "Gateway" - -# Start Frontend -echo "" -echo "🌐 Starting Frontend (port 5173)..." -cd "$SCRIPT_DIR/glense.client" -npm run dev > /tmp/frontend.log 2>&1 & -FRONTEND_PID=$! -echo " PID: $FRONTEND_PID" - -sleep 3 - -echo "" -echo -e "${GREEN}✅ All services started!${NC}" -echo "" -echo "📍 Service URLs:" -echo " Frontend: http://localhost:5173" -echo " Gateway: http://localhost:5050" -echo " Account Service: http://localhost:5001" -echo " Donation Service: http://localhost:5100" -echo "" -echo "📋 Logs:" -echo " Account Service: /tmp/account-service.log" -echo " Donation Service: /tmp/donation-service.log" -echo " Gateway: /tmp/gateway.log" -echo " Frontend: /tmp/frontend.log" -echo "" -echo "🛑 To stop all services, run: ./stop-dev.sh" -echo "" - -# Save PIDs to file for stop script -echo "$ACCOUNT_PID" > /tmp/glense-pids.txt -echo "$DONATION_PID" >> /tmp/glense-pids.txt -echo "$GATEWAY_PID" >> /tmp/glense-pids.txt -echo "$FRONTEND_PID" >> /tmp/glense-pids.txt - -# Keep script running and show combined logs -echo "📜 Showing combined logs (Ctrl+C to stop viewing, services will keep running)..." -echo "===========================================================================" -tail -f /tmp/account-service.log /tmp/donation-service.log /tmp/gateway.log /tmp/frontend.log 2>/dev/null diff --git a/stop-dev.sh b/stop-dev.sh deleted file mode 100755 index be9f3a2..0000000 --- a/stop-dev.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# Glense Development Stop Script -# Stops all development services - -echo "🛑 Stopping Glense Development Environment..." - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -# Kill processes from PID file -if [ -f /tmp/glense-pids.txt ]; then - while read pid; do - if kill -0 "$pid" 2>/dev/null; then - kill "$pid" 2>/dev/null && echo " Stopped process $pid" - fi - done < /tmp/glense-pids.txt - rm /tmp/glense-pids.txt -fi - -# Kill any remaining dotnet processes for our services -pkill -f "Glense.AccountService" 2>/dev/null && echo " Stopped Account Service" -pkill -f "DonationService" 2>/dev/null && echo " Stopped Donation Service" -pkill -f "Glense.Server" 2>/dev/null && echo " Stopped Gateway" - -# Kill frontend dev server -pkill -f "vite.*glense" 2>/dev/null && echo " Stopped Frontend" - -# Optionally stop PostgreSQL container -read -p "Stop PostgreSQL container? (y/N) " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - docker stop glense_postgres_account 2>/dev/null && echo " Stopped PostgreSQL" -fi - -echo "" -echo -e "${GREEN}✅ All services stopped${NC}"