diff --git a/handlenett-backend/web-api/HandlenettAPI/Configurations/SlackSettings.cs b/handlenett-backend/web-api/HandlenettAPI/Configurations/SlackSettings.cs new file mode 100644 index 0000000..d6400e7 --- /dev/null +++ b/handlenett-backend/web-api/HandlenettAPI/Configurations/SlackSettings.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace HandlenettAPI.Configurations +{ + public class SlackSettings + { + [Required] + public required string SlackBotUserOAuthToken { get; set; } + [Required] + public required string ContainerNameUserImages { get; set; } + [Required] + public required string BlobStorageUri { get; set; } + public string BlobStoragePathIncludingContainer => BlobStorageUri + ContainerNameUserImages + "/"; + } +} diff --git a/handlenett-backend/web-api/HandlenettAPI/Controllers/BaseController.cs b/handlenett-backend/web-api/HandlenettAPI/Controllers/BaseController.cs index 986f310..314bc18 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Controllers/BaseController.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Controllers/BaseController.cs @@ -4,33 +4,13 @@ namespace HandlenettAPI.Controllers { - public abstract class BaseController : ControllerBase + public abstract class BaseController : ControllerBase //not in use yet { protected readonly ILogger _logger; - protected readonly IConfiguration _config; - protected readonly GraphServiceClient _graphClient; - protected readonly SlackService _slackService; - protected BaseController(ILogger logger, GraphServiceClient graphServiceClient, IConfiguration config, SlackService slackService) + protected BaseController(ILogger logger) { _logger = logger; - _config = config; - _graphClient = graphServiceClient; - _slackService = slackService; - } - - protected async Task InitializeAsync() - { - try - { - var dbService = new UserService(_config); - await dbService.AddUserIfNotExists(_graphClient, _slackService); - } - catch (Exception ex) - { - _logger.LogError(ex, "Not valid user"); - throw new Exception("Not valid user"); - } } } } diff --git a/handlenett-backend/web-api/HandlenettAPI/Controllers/ItemController.cs b/handlenett-backend/web-api/HandlenettAPI/Controllers/ItemController.cs index dc43762..c244cde 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Controllers/ItemController.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Controllers/ItemController.cs @@ -1,12 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Web.Resource; -using Microsoft.Graph; using HandlenettAPI.Models; -using HandlenettAPI.Services; -using Microsoft.Azure.Cosmos; using HandlenettAPI.DTO; -using Microsoft.Extensions.Configuration; using HandlenettAPI.Interfaces; namespace HandlenettAPI.Controllers diff --git a/handlenett-backend/web-api/HandlenettAPI/Controllers/UserController.cs b/handlenett-backend/web-api/HandlenettAPI/Controllers/UserController.cs index d358f13..ffbbd24 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Controllers/UserController.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Controllers/UserController.cs @@ -1,54 +1,34 @@ -using Azure.Identity; -using HandlenettAPI.DTO; -using HandlenettAPI.Models; +using HandlenettAPI.DTO; using HandlenettAPI.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Graph; namespace HandlenettAPI.Controllers { [Authorize] [ApiController] [Route("[controller]")] - public class UserController : BaseController + public class UserController : ControllerBase { + private readonly UserService _userService; + private readonly UserInitializationService _userInitializationService; - public UserController(ILogger logger, GraphServiceClient graphServiceClient, IConfiguration config, SlackService slackService) - : base(logger, graphServiceClient, config, slackService) + public UserController(UserService userService, UserInitializationService userInitializationService) { + _userService = userService; + _userInitializationService = userInitializationService; } [HttpGet(Name = "GetUsers")] - public async Task> Get() + public async Task>> Get() { - try - { - await InitializeAsync(); - var dbService = new UserService(_config); - return dbService.GetUsers(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get users"); - throw new Exception("Failed to get users"); - } + return Ok(_userService.GetUsers()); } [HttpGet("{id}", Name = "GetUser")] - public async Task Get(Guid id) + public async Task> Get(Guid id) { - try - { - await InitializeAsync(); - var dbService = new UserService(_config); - return dbService.GetUser(id); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get user"); - throw new Exception("Failed to get user"); - } + return Ok(_userService.GetUser(id)); } } } diff --git a/handlenett-backend/web-api/HandlenettAPI/Middleware/UserInitializationMiddleware.cs b/handlenett-backend/web-api/HandlenettAPI/Middleware/UserInitializationMiddleware.cs new file mode 100644 index 0000000..f74901a --- /dev/null +++ b/handlenett-backend/web-api/HandlenettAPI/Middleware/UserInitializationMiddleware.cs @@ -0,0 +1,30 @@ +using HandlenettAPI.Services; + +namespace HandlenettAPI.Middleware +{ + public class UserInitializationMiddleware + { + private readonly RequestDelegate _next; + + public UserInitializationMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, IServiceProvider serviceProvider) + { + // Create a scope for resolving scoped services + using (var scope = serviceProvider.CreateScope()) + { + // Resolve the scoped UserInitializationService + var userInitializationService = scope.ServiceProvider.GetRequiredService(); + + // Ensure the user exists using the service + await userInitializationService.EnsureUserExistsAsync(); + } + + // Call the next middleware in the pipeline + await _next(context); + } + } +} diff --git a/handlenett-backend/web-api/HandlenettAPI/Program.cs b/handlenett-backend/web-api/HandlenettAPI/Program.cs index 6c2a417..5116f5d 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Program.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Program.cs @@ -1,6 +1,7 @@ using Azure.Identity; using HandlenettAPI.Configurations; using HandlenettAPI.Interfaces; +using HandlenettAPI.Middleware; using HandlenettAPI.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Azure.Cosmos; @@ -70,7 +71,10 @@ .Bind(builder.Configuration.GetSection("AzureCosmosDBSettings")) .ValidateDataAnnotations() // Validates [Required] attributes at runtime .ValidateOnStart(); // Ensures validation happens at application startup - +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("SlackSettings")) + .ValidateDataAnnotations() + .ValidateOnStart(); // Add services to the container. //TODO: null handling errors for config sections @@ -81,7 +85,6 @@ .AddInMemoryTokenCaches(); //Dependency Injection -builder.Services.AddScoped(); builder.Services.AddScoped(provider => { var settings = provider.GetRequiredService>().Value; @@ -92,15 +95,48 @@ settings.ContainerName ); }); -var redisConnString = builder.Configuration.GetConnectionString("AzureRedisCache") ?? throw new InvalidOperationException("Missing redis config"); +var redisConnString = builder.Configuration.GetConnectionString("AzureRedisCache") ?? throw new InvalidOperationException("Missing AzureRedisCache config"); builder.Services.AddSingleton(sp => ConnectionMultiplexer.Connect(redisConnString)); //heavy resource and is designed to be reused builder.Services.AddHttpClient(); -builder.Services.AddHttpClient("SlackClient", client => +builder.Services.AddHttpClient("SlackClient", client => { client.BaseAddress = new Uri("https://slack.com/api/"); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Configure static Authorization header + var slackToken = builder.Configuration["SlackSettings:SlackBotUserOAuthToken"]; + if (string.IsNullOrEmpty(slackToken)) + { + throw new InvalidOperationException("Slack Bot User OAuth token is missing."); + } + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", slackToken); }); +builder.Services.AddScoped(); + +//User +builder.Services.AddScoped(sp => +{ + var dbContext = sp.GetRequiredService(); + var blobStorageService = sp.GetRequiredService(); + var slackSettings = sp.GetRequiredService>().Value; + + return new UserService(dbContext, blobStorageService, slackSettings.ContainerNameUserImages); //get from builder.configuration instead of strongly typed? +}); +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => +{ + var connectionString = builder.Configuration["ConnectionStrings:AzureStorageUsingAccessKey"]; + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("Azure Storage connection string is missing."); + } + + return new AzureBlobStorageService(connectionString); +}); + +builder.Services.AddScoped(); + //builder.Services.AddSingleton(); // A single instance is shared across the entire application lifetime //builder.Services.AddHttpClient() //DI container registers T as a transient service by default. //builder.Services.AddScoped() // T @@ -142,8 +178,9 @@ app.UseCors(MyAllowSpecificOrigins); app.UseHttpsRedirection(); -app.UseAuthentication(); -app.UseAuthorization(); +app.UseAuthentication(); //validates tokens +app.UseAuthorization(); //enforces [Authorize] attributes +app.UseMiddleware(); //Custom implementation of SQL database user verification app.MapControllers().RequireCors(MyAllowSpecificOrigins); ConfigurationHelper.Initialize(app.Configuration); diff --git a/handlenett-backend/web-api/HandlenettAPI/Services/AzureBlobStorageService.cs b/handlenett-backend/web-api/HandlenettAPI/Services/AzureBlobStorageService.cs index 50496c2..84f99a3 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Services/AzureBlobStorageService.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Services/AzureBlobStorageService.cs @@ -8,51 +8,56 @@ public class AzureBlobStorageService { private readonly BlobServiceClient _blobServiceClient; - private readonly IConfiguration _config; - private readonly string _containerName; - - public AzureBlobStorageService(string containerName, IConfiguration config) + public AzureBlobStorageService(string connectionString) { - _config = config; - _containerName = containerName; - var asd = _config.GetConnectionString("AzureStorageUsingAccessKey"); - _blobServiceClient = new BlobServiceClient(_config.GetConnectionString("AzureStorageUsingAccessKey")); + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException("Azure Storage connection string is missing."); + } + _blobServiceClient = new BlobServiceClient(connectionString); } - public async Task UploadBlobAsync(string blobName, Stream content) + public async Task UploadBlobAsync(string containerName, string blobName, Stream content) { - var blobContainerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobContainerClient = _blobServiceClient.GetBlobContainerClient(containerName); var blobClient = blobContainerClient.GetBlobClient(blobName); await blobClient.UploadAsync(content, overwrite: true); } // Get existing blob - public async Task GetBlobAsync(string blobName) + public async Task GetBlobAsync(string containerName, string blobName) { - var blobContainerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobContainerClient = _blobServiceClient.GetBlobContainerClient(containerName); var blobClient = blobContainerClient.GetBlobClient(blobName); BlobDownloadInfo download = await blobClient.DownloadAsync(); return download.Content; } - public string GenerateContainerSasToken() + public string GenerateContainerSasToken(string containerName, int hoursValid = 48) { - var blobServiceClient = new BlobServiceClient(_config.GetConnectionString("AzureStorageUsingAccessKey")); + var blobContainerClient = _blobServiceClient.GetBlobContainerClient(containerName); var sasBuilder = new BlobSasBuilder { - BlobContainerName = _containerName, + BlobContainerName = containerName, Resource = "c", // container-level SAS - ExpiresOn = DateTimeOffset.UtcNow.AddHours(48) + ExpiresOn = DateTimeOffset.UtcNow.AddHours(hoursValid) }; sasBuilder.SetPermissions(BlobContainerSasPermissions.Read); + if (!_blobServiceClient.CanGenerateAccountSasUri) + { + throw new InvalidOperationException("The provided client cannot generate SAS tokens."); + } + + return blobContainerClient.GenerateSasUri(sasBuilder).Query; - var sasToken = sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential( - blobServiceClient.AccountName, - _config.GetValue("AzureStorage:AccountKey"))).ToString(); + //Old code that worked, test new solution before deleting + //var sasToken = sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential( + // _blobServiceClient.AccountName, + // _config.GetValue("AzureStorage:AccountKey"))).ToString(); - return sasToken; + //return sasToken; } } diff --git a/handlenett-backend/web-api/HandlenettAPI/Services/SlackService.cs b/handlenett-backend/web-api/HandlenettAPI/Services/SlackService.cs index 56e4653..7555860 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Services/SlackService.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Services/SlackService.cs @@ -1,7 +1,6 @@ -using Azure.Storage.Blobs; +using HandlenettAPI.Configurations; using HandlenettAPI.Models; -using System.Net.Http; -using System.Net.Http.Headers; +using Microsoft.Extensions.Options; using System.Text.Json; namespace HandlenettAPI.Services @@ -9,47 +8,50 @@ namespace HandlenettAPI.Services public class SlackService { private readonly HttpClient _httpClient; - private readonly IConfiguration _config; + private readonly AzureBlobStorageService _blobStorageService; + private readonly SlackSettings _settings; - public SlackService(IHttpClientFactory httpClientFactory, IConfiguration config) + public SlackService(HttpClient httpClient, AzureBlobStorageService blobStorageService, IOptions options) { - _httpClient = httpClientFactory.CreateClient("SlackClient"); - _config = config; + _httpClient = httpClient; + _blobStorageService = blobStorageService; + _settings = options.Value; } - public async Task CopyImageToAzureBlobStorage(string oauthToken, SlackUser slackUser) + public string GetSlackToken() { - try + if (string.IsNullOrEmpty(_settings.SlackBotUserOAuthToken)) { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauthToken); + throw new InvalidOperationException("Slack Bot User OAuth token is not configured."); + } + return _settings.SlackBotUserOAuthToken; + } + public async Task CopyImageToAzureBlobStorage(SlackUser slackUser) + { + try + { using (HttpResponseMessage response = await _httpClient.GetAsync(slackUser.ImageUrlSlack, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); - //TODO: Mer ryddig å ha get function her og upload i azureService, men hvordan håndtere stream da? using (Stream imageStream = await response.Content.ReadAsStreamAsync()) { - var containerName = _config.GetValue("AzureStorage:ContainerNameUserImages"); - var accountName = _config.GetValue("AzureStorage:AccountName"); - if (string.IsNullOrEmpty(containerName) || string.IsNullOrEmpty(accountName)) throw new InvalidOperationException("Missing storage config"); + var blobName = $"{slackUser.Id}.jpg"; + await _blobStorageService.UploadBlobAsync(_settings.ContainerNameUserImages, blobName, imageStream); - var blobService = new AzureBlobStorageService(containerName, _config); - await blobService.UploadBlobAsync(slackUser.Id + ".jpg", imageStream); - return $"https://{accountName}.blob.core.windows.net/{containerName}/{slackUser.Id}.jpg"; + return $"{_settings.BlobStoragePathIncludingContainer}{blobName}"; } } } catch (Exception ex) { - throw new Exception(ex.Message, ex); + throw new Exception("Error copying image to Azure Blob Storage", ex); } } - public async Task GetUserByEmailAsync(string oauthToken, string email) + public async Task GetUserByEmailAsync(string email) { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauthToken); - var response = await _httpClient.GetAsync("users.list"); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); diff --git a/handlenett-backend/web-api/HandlenettAPI/Services/UserInitializationService.cs b/handlenett-backend/web-api/HandlenettAPI/Services/UserInitializationService.cs new file mode 100644 index 0000000..bef61ed --- /dev/null +++ b/handlenett-backend/web-api/HandlenettAPI/Services/UserInitializationService.cs @@ -0,0 +1,23 @@ +using Microsoft.Graph; + +namespace HandlenettAPI.Services +{ + public class UserInitializationService + { + private readonly UserService _userService; + private readonly GraphServiceClient _graphClient; + private readonly SlackService _slackService; + + public UserInitializationService(UserService userService, GraphServiceClient graphClient, SlackService slackService) + { + _userService = userService; + _graphClient = graphClient; + _slackService = slackService; + } + + public async Task EnsureUserExistsAsync() + { + await _userService.AddUserIfNotExists(_graphClient, _slackService); + } + } +} diff --git a/handlenett-backend/web-api/HandlenettAPI/Services/UserService.cs b/handlenett-backend/web-api/HandlenettAPI/Services/UserService.cs index 591c7a5..cbb6e8b 100644 --- a/handlenett-backend/web-api/HandlenettAPI/Services/UserService.cs +++ b/handlenett-backend/web-api/HandlenettAPI/Services/UserService.cs @@ -2,44 +2,39 @@ using Microsoft.Graph; using HandlenettAPI.Helpers; using HandlenettAPI.DTO; -using Newtonsoft.Json.Linq; using HandlenettAPI.Models; namespace HandlenettAPI.Services { - public class UserService { - private IConfiguration _config; - public UserService(IConfiguration config) + private readonly AzureSQLContext _dbContext; + private readonly AzureBlobStorageService _blobStorageService; + private readonly string _containerName; + + public UserService(AzureSQLContext dbContext, AzureBlobStorageService blobStorageService, string containerName) { - _config = config; + _dbContext = dbContext; + _blobStorageService = blobStorageService; + _containerName = containerName; } public List GetUsers() { try { - var SASToken = GetAzureBlobStorageUserImageSASToken(); + var users = _dbContext.Users + .Where(u => u.IsDeleted == false) + .OrderBy(u => u.Name) + .ToList(); - using (var db = new AzureSQLContext(_config)) - { - var users = db.Users - .Where(u => u.IsDeleted == false) - .OrderBy(u => u.Name) - .ToList(); - if (users == null) return []; + if (users == null || users.Count == 0) return new List(); - var usersDTO = new List(); + var usersDTO = users + .Select(user => ConvertUserToUserDTO(user)) + .ToList(); - foreach (var user in users) - { - var userDTO = ConvertUserToUserDTO(user, SASToken); - usersDTO.Add(userDTO); - } - - return usersDTO; - } + return usersDTO; } catch (SqlException ex) { @@ -48,17 +43,10 @@ public List GetUsers() } } - private string GetAzureBlobStorageUserImageSASToken() - { - var containerName = _config.GetValue("AzureStorage:ContainerNameUserImages") ?? throw new InvalidOperationException("Missing storage config"); - var azureService = new AzureBlobStorageService(containerName, _config); - return azureService.GenerateContainerSasToken(); - } - - private UserDTO ConvertUserToUserDTO(Models.User user, string SASToken) + private UserDTO ConvertUserToUserDTO(Models.User user) { var userDTO = user.ConvertTo(); - userDTO.ImageUrl = $"{user.ImageUrl}?{SASToken}"; + userDTO.ImageUrl = $"{user.ImageUrl}?{_blobStorageService.GenerateContainerSasToken(_containerName)}"; return userDTO; } @@ -66,17 +54,13 @@ public UserDTO GetUser(Guid id) { try { - using (var db = new AzureSQLContext(_config)) - { - var user = db.Users - .Where(u => u.Id == id) - .FirstOrDefault(); + var user = _dbContext.Users + .Where(u => u.Id == id) + .FirstOrDefault(); - if (user == null) throw new InvalidOperationException("User not found"); + if (user == null) throw new InvalidOperationException("User not found"); - var SASToken = GetAzureBlobStorageUserImageSASToken(); - return ConvertUserToUserDTO(user, SASToken); - } + return ConvertUserToUserDTO(user); } catch (SqlException ex) { @@ -93,17 +77,15 @@ public async Task AddUserIfNotExists(GraphServiceClient graphServiceClient, Slac { throw new Exception("Could not get Ad profile"); } - var slackToken = _config.GetValue("SlackBotUserOAuthToken") ?? throw new Exception("Missing config"); - var SQLUser = GetUserWithDetails(new Guid(ADUser.Id)); - //AzureSQL og slack + //AzureSQL and slack if (SQLUser == null) { - var slackUser = await slackService.GetUserByEmailAsync(slackToken, ADUser.Mail); + var slackUser = await slackService.GetUserByEmailAsync(ADUser.Mail); if (slackUser != null) { - slackUser.ImageUrlBlobStorage = await slackService.CopyImageToAzureBlobStorage(slackToken, slackUser); + slackUser.ImageUrlBlobStorage = await slackService.CopyImageToAzureBlobStorage(slackUser); } AddUser(ADUser, slackUser); } @@ -111,31 +93,25 @@ public async Task AddUserIfNotExists(GraphServiceClient graphServiceClient, Slac private Models.User? GetUserWithDetails(Guid userId) { - using (var db = new AzureSQLContext(_config)) - { - var user = db.Users - .Where(u => u.Id == userId) - .FirstOrDefault(); + var user = _dbContext.Users + .Where(u => u.Id == userId) + .FirstOrDefault(); - return user; - } + return user; } private void AddUser(Microsoft.Graph.User user, SlackUser? slackUser) { - using (var db = new AzureSQLContext(_config)) + var newUser = new Models.User { - var newUser = new Models.User - { - Id = new Guid(user.Id), - FirstName = user.GivenName, - LastName = user.Surname, - SlackUserId = slackUser?.Id, - ImageUrl = slackUser?.ImageUrlBlobStorage, - }; - db.Users.Add(newUser); - db.SaveChanges(); - } + Id = new Guid(user.Id), + FirstName = user.GivenName, + LastName = user.Surname, + SlackUserId = slackUser?.Id, + ImageUrl = slackUser?.ImageUrlBlobStorage, + }; + _dbContext.Users.Add(newUser); + _dbContext.SaveChanges(); } } } diff --git a/handlenett-backend/web-api/HandlenettAPI/appsettings.json b/handlenett-backend/web-api/HandlenettAPI/appsettings.json index 5d2ad56..77f7445 100644 --- a/handlenett-backend/web-api/HandlenettAPI/appsettings.json +++ b/handlenett-backend/web-api/HandlenettAPI/appsettings.json @@ -25,8 +25,11 @@ }, "AzureStorage": { "AccountName": "handlenettsa", - "ContainerNameItemImages": "item-images", - "ContainerNameUserImages": "user-images" + "ContainerNameItemImages": "item-images" + }, + "SlackSettings": { + "ContainerNameUserImages": "user-images", + "BlobStorageUri": "https://handlenettsa.blob.core.windows.net/" }, "AzureKeyVaultNameProd": "handlenett-prod-kv", "AzureKeyVaultNameDev": "handlenett-dev-kv" //not created yet