From 34c3078904234e787f64f5dca8a58b94c3167d20 Mon Sep 17 00:00:00 2001 From: KevinOomen Date: Mon, 6 May 2024 09:31:18 +0200 Subject: [PATCH] Added Keycloak integrationa and added RabbitMQ --- MusicService/Controllers/SongController.cs | 28 ++--- MusicService/Data/DataContext.cs | 10 +- MusicService/MusicService.csproj | 2 + MusicService/Program.cs | 126 +++++++++++++++++-- MusicService/Services/Event/EventService.cs | 64 ++++++++++ MusicService/Services/Event/IEventService.cs | 8 ++ MusicService/Services/Song/SongService.cs | 30 ++++- docker-compose.yml | 65 ++++++++-- 8 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 MusicService/Services/Event/EventService.cs create mode 100644 MusicService/Services/Event/IEventService.cs diff --git a/MusicService/Controllers/SongController.cs b/MusicService/Controllers/SongController.cs index 3df834c..ebffd2a 100644 --- a/MusicService/Controllers/SongController.cs +++ b/MusicService/Controllers/SongController.cs @@ -10,7 +10,6 @@ namespace MusicService.Controllers [ApiController] public class SongController : ControllerBase { - private readonly ISongService _songService; public SongController(ISongService songService) @@ -18,13 +17,14 @@ public SongController(ISongService songService) _songService = songService; } + [Authorize(Roles = "Admin")] [HttpGet("all")] public async Task>>> GetAllSongs() { var response = await _songService.GetAllSongs(); if (response.Data == null) - return StatusCode(StatusCodes.Status404NotFound, response); + return StatusCode(StatusCodes.Status503ServiceUnavailable, response); return StatusCode(StatusCodes.Status200OK, response); } @@ -35,7 +35,7 @@ public async Task>> GetSong(int songId) var response = await _songService.GetSong(songId); if (response.Data == null) - return StatusCode(StatusCodes.Status404NotFound, response); + return StatusCode(StatusCodes.Status503ServiceUnavailable, response); return StatusCode(StatusCodes.Status200OK, response); } @@ -43,12 +43,12 @@ public async Task>> GetSong(int songId) [HttpPost] public async Task>> AddSong(AddSongDto request) { - var songResponse = await _songService.AddSong(request); + var response = await _songService.AddSong(request); - if (!songResponse.Success) - return StatusCode(StatusCodes.Status404NotFound, songResponse); + if (!response.Success) + return StatusCode(StatusCodes.Status503ServiceUnavailable, response); - return StatusCode(StatusCodes.Status201Created, songResponse); + return StatusCode(StatusCodes.Status201Created, response); } [HttpPut] @@ -57,20 +57,20 @@ public async Task>> UpdateSong(UpdateSo var response = await _songService.UpdateSong(request); if (response.Data == null) - return StatusCode(StatusCodes.Status404NotFound, response); + return StatusCode(StatusCodes.Status503ServiceUnavailable, response); return StatusCode(StatusCodes.Status200OK, response); } - /*[HttpDelete("{songId}")] + [HttpDelete("{songId}")] public async Task>> DeleteSong(int songId) { - /*var response = await _songService; + var response = await _songService.DeleteSong(songId); - if (response.Data == null) - return StatusCode(StatusCodes.Status404NotFound, response); + if (!response.Success) + return StatusCode(StatusCodes.Status503ServiceUnavailable, response); - return StatusCode(StatusCodes.Status200OK, response); - }*/ + return StatusCode(StatusCodes.Status204NoContent, response); + } } } diff --git a/MusicService/Data/DataContext.cs b/MusicService/Data/DataContext.cs index 109479e..6946328 100644 --- a/MusicService/Data/DataContext.cs +++ b/MusicService/Data/DataContext.cs @@ -6,7 +6,15 @@ namespace MusicService.Data public class DataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=music;Username=postgres;Password=example"); + { + IConfigurationRoot configuration = new ConfigurationBuilder() + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + + optionsBuilder.UseNpgsql(configuration.GetConnectionString("DefaultConnection")); + } + public DataContext() { diff --git a/MusicService/MusicService.csproj b/MusicService/MusicService.csproj index 1a992b0..e53abf7 100644 --- a/MusicService/MusicService.csproj +++ b/MusicService/MusicService.csproj @@ -8,11 +8,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MusicService/Program.cs b/MusicService/Program.cs index a106f44..66e99df 100644 --- a/MusicService/Program.cs +++ b/MusicService/Program.cs @@ -1,18 +1,125 @@ using MusicService.Data; using MusicService.Services.Song; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.OpenApi.Models; +using MusicService.Services.Event; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(); +var services = builder.Services; + +// Add authentication services +services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => +{ + + //authority runs behind reverse proxy + o.RequireHttpsMetadata = false; + + o.Authority = builder.Configuration["Jwt:Authority"]; + o.Audience = builder.Configuration["Jwt:Audience"]; + + + o.TokenValidationParameters = new TokenValidationParameters + { + //asp.net core seems to expect an audience with the name of the client, this disables the check + //This is primarily for architectures where certain services aren't trusted. + //The person sending the token can specify what service the token is meant for. + //This prevents "bad" services from using the token to send requests to other services. + //Since this architecture doesn't have untrusted services, this is not necessary. + ValidateAudience = false + }; + +}); + +/*.AddOpenIdConnect(options => +{ + // Configure the authority to your Keycloak realm URL + options.Authority = "http://localhost:8080/realms/SpotiCloud"; + + options.RequireHttpsMetadata = false; + // Configure the client ID and client secret + options.ClientId = "test"; + options.ClientSecret = "DRW0BGdceP1mMdO7tYo0NIycGYyPVifx"; + + // Set the callback path + options.CallbackPath = "/swagger/index.html"; + + // Configure the response type + options.ResponseType = OpenIdConnectResponseType.Code; + + // Configure the scope + options.Scope.Clear(); + options.Scope.Add("openid"); + //options.Scope.Add("profile"); + //options.Scope.Add("email"); + + // Configure the token validation parameters + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = "role" + }; +});*/ + +services.AddDbContext(); // Add services to the container. -builder.Services.AddControllers(); +services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(setup => +{ + setup.SwaggerDoc("v1", new OpenApiInfo { Title = "Music API v1.0", Version = "v1" }); + setup.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost:8080/realms/SpotiCloud/protocol/openid-connect/auth"), + } + } + }); + setup.AddSecurityRequirement(new OpenApiSecurityRequirement{ + { + new OpenApiSecurityScheme{ + Reference = new OpenApiReference{ + Type = ReferenceType.SecurityScheme, + Id = "OAuth2" //The name of the previously defined security scheme. + } + }, + new string[] {} + } +}); -builder.Services.AddAutoMapper(typeof(Program).Assembly); + /*var jwtSecurityScheme = new OpenApiSecurityScheme + { + Scheme = "bearer", + BearerFormat = "JWT", + Name = "Authentication", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, -builder.Services.AddScoped(); + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }; + + setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme); + + setup.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { jwtSecurityScheme, Array.Empty() } + });*/ +}); + +services.AddAutoMapper(typeof(Program).Assembly); + +services.AddScoped(); +services.AddSingleton(); var app = builder.Build(); @@ -23,10 +130,15 @@ app.UseSwaggerUI(); } -app.UseHttpsRedirection(); +//app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); +/*app.UseCookiePolicy(new CookiePolicyOptions{ + MinimumSameSitePolicy = SameSiteMode.Lax +});*/ + app.MapControllers(); app.Run(); diff --git a/MusicService/Services/Event/EventService.cs b/MusicService/Services/Event/EventService.cs new file mode 100644 index 0000000..60c46f3 --- /dev/null +++ b/MusicService/Services/Event/EventService.cs @@ -0,0 +1,64 @@ +using RabbitMQ.Client.Events; +using RabbitMQ.Client; +using System.Text.Json; +using System.Text; + +namespace MusicService.Services.Event +{ + public class EventService : IEventService, IDisposable + { + private readonly IConnection connection; + public EventService(IConfiguration configuration) + { + var factory = new ConnectionFactory + { + HostName = configuration.GetConnectionString("rabbitmq") + }; + + connection = factory.CreateConnection(); + } + + public void Dispose() + { + connection.Close(); + } + + public void Publish(string exchange, string topic, T data) + { + using var channel = connection.CreateModel(); + + channel.ExchangeDeclare(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + + var json = JsonSerializer.Serialize(data); + var body = Encoding.UTF8.GetBytes(json); + + channel.BasicPublish(exchange: exchange, routingKey: topic, body: body); + } + + public void subscribe(string exchange, string queue, string topic, Action handler) + { + var channel = connection.CreateModel(); + + channel.ExchangeDeclare(exchange: exchange, type: ExchangeType.Direct, durable: true, autoDelete: false); + channel.QueueDeclare(queue, exclusive: false, autoDelete: false); + channel.QueueBind(exchange: exchange, routingKey: topic, queue: queue); + + channel.BasicQos(0, 1, false); + + + var consumer = new EventingBasicConsumer(channel); + + consumer.Received += (model, eventArgs) => + { + var body = eventArgs.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + + var data = JsonSerializer.Deserialize(message); + + handler.Invoke(data); + }; + + channel.BasicConsume(queue: queue, autoAck: true, consumer: consumer); + } + } +} diff --git a/MusicService/Services/Event/IEventService.cs b/MusicService/Services/Event/IEventService.cs new file mode 100644 index 0000000..ae5d6ba --- /dev/null +++ b/MusicService/Services/Event/IEventService.cs @@ -0,0 +1,8 @@ +namespace MusicService.Services.Event +{ + public interface IEventService + { + public void Publish(string exchange, string topic, T data); + public void subscribe(string exchange, string queue, string topic, Action handler); + } +} diff --git a/MusicService/Services/Song/SongService.cs b/MusicService/Services/Song/SongService.cs index 49651d5..d5e021c 100644 --- a/MusicService/Services/Song/SongService.cs +++ b/MusicService/Services/Song/SongService.cs @@ -3,17 +3,20 @@ using MusicService.Data; using MusicService.Dtos.Song; using MusicService.Models; +using MusicService.Services.Event; namespace MusicService.Services.Song { public class SongService : ISongService { private readonly IMapper _mapper; + private readonly IEventService _eventService; private readonly DataContext _context; - public SongService(IMapper mapper, DataContext context) + public SongService(IMapper mapper, IEventService eventService, DataContext context) { _mapper = mapper; + _eventService = eventService; _context = context; } @@ -44,11 +47,11 @@ public async Task>> GetAllSongs() public async Task> GetSong(int songId) { - ServiceResponse? response = new ServiceResponse(); + ServiceResponse response = new ServiceResponse(); try { Models.Song? song = await _context.song - .Where(b => b.Id == songId) + .Where(s => s.Id == songId) .FirstAsync(); if (song != null) @@ -78,6 +81,8 @@ public async Task> AddSong(AddSongDto request) Models.Song song = _mapper.Map(request); _context.song.Add(song); await _context.SaveChangesAsync(); + var test = new { Id = song.Id, Name = song.Name }; + _eventService.Publish(exchange: "song-exchange", topic: "song-added", test); response.Data = _mapper.Map(song); } @@ -114,9 +119,24 @@ public async Task> UpdateSong(UpdateSongDto request) return response; } - public Task> DeleteSong(int songId) + public async Task> DeleteSong(int songId) { - throw new NotImplementedException(); + ServiceResponse response = new ServiceResponse(); + + try + { + await _context.song.Where(s => s.Id == songId).ExecuteDeleteAsync(); + + response.Success = true; + response.Message = "Nothing was found!"; + } + catch (Exception ex) + { + response.Success = false; + response.Message = ex.Message; + } + + return response; } } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 940f57c..3094780 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,66 @@ -# Use postgres/example user/password credentials version: '3.9' services: + rabbitmq: + image: rabbitmq:3-management-alpine + container_name: "rabbitmq" + ports: + - 5672:5672 + - 15672:15672 + # volumes: + # - ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/ + # - ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq + networks: + - rabbitmq + restart: unless-stopped db: image: postgres restart: always - # set shared memory limit when using docker-compose shm_size: 128mb - # or set shared memory limit when deploy via swarm stack - #volumes: - # - type: tmpfs - # target: /dev/shm - # tmpfs: - # size: 134217728 # 128*2^20 bytes = 128Mb environment: - POSTGRES_PASSWORD: example + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - 5432:5432 + networks: + - keycloak_network + - postgres_network - adminer: - image: adminer - restart: always + keycloak: + image: quay.io/keycloak/keycloak:23.0.6 + command: start + environment: + KC_HOSTNAME: localhost + KC_HOSTNAME_PORT: 8080 + KC_HOSTNAME_STRICT_BACKCHANNEL: false + KC_HTTP_ENABLED: true + KC_HOSTNAME_STRICT_HTTPS: false + KC_HEALTH_ENABLED: true + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db/keycloak_db + KC_DB_USERNAME: ${POSTGRES_USER} + KC_DB_PASSWORD: ${POSTGRES_PASSWORD} ports: - 8080:8080 + restart: always + depends_on: + - db + networks: + - keycloak_network + #adminer: + # image: adminer + # restart: always + # ports: + # - 8080:8080 + # networks: + # - postgres_network + +networks: + keycloak_network: + driver: bridge + postgres_network: + driver: bridge + rabbitmq: + driver: bridge