Skip to content

Commit

Permalink
Major refactoring work across multiple services and controllers. Adde…
Browse files Browse the repository at this point in the history
…d middleware for sql user data validation
  • Loading branch information
Torkelsen committed Nov 21, 2024
1 parent be9be29 commit a169ebe
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -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 + "/";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,13 @@

namespace HandlenettAPI.Controllers
{
public abstract class BaseController : ControllerBase
public abstract class BaseController : ControllerBase //not in use yet
{
protected readonly ILogger<BaseController> _logger;
protected readonly IConfiguration _config;
protected readonly GraphServiceClient _graphClient;
protected readonly SlackService _slackService;

protected BaseController(ILogger<BaseController> logger, GraphServiceClient graphServiceClient, IConfiguration config, SlackService slackService)
protected BaseController(ILogger<BaseController> 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");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserController> 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<List<UserDTO>> Get()
public async Task<ActionResult<List<UserDTO>>> 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<UserDTO> Get(Guid id)
public async Task<ActionResult<UserDTO>> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UserInitializationService>();

// Ensure the user exists using the service
await userInitializationService.EnsureUserExistsAsync();
}

// Call the next middleware in the pipeline
await _next(context);
}
}
}
49 changes: 43 additions & 6 deletions handlenett-backend/web-api/HandlenettAPI/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<SlackSettings>()
.Bind(builder.Configuration.GetSection("SlackSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();

// Add services to the container.
//TODO: null handling errors for config sections
Expand All @@ -81,7 +85,6 @@
.AddInMemoryTokenCaches();

//Dependency Injection
builder.Services.AddScoped<SlackService>();
builder.Services.AddScoped<ICosmosDBService>(provider =>
{
var settings = provider.GetRequiredService<IOptions<AzureCosmosDBSettings>>().Value;
Expand All @@ -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<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(redisConnString)); //heavy resource and is designed to be reused
builder.Services.AddHttpClient<WeatherService>();
builder.Services.AddHttpClient("SlackClient", client =>
builder.Services.AddHttpClient<SlackService>("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<SlackService>();

//User
builder.Services.AddScoped<UserService>(sp =>
{
var dbContext = sp.GetRequiredService<AzureSQLContext>();
var blobStorageService = sp.GetRequiredService<AzureBlobStorageService>();
var slackSettings = sp.GetRequiredService<IOptions<SlackSettings>>().Value;

return new UserService(dbContext, blobStorageService, slackSettings.ContainerNameUserImages); //get from builder.configuration instead of strongly typed?
});
builder.Services.AddScoped<AzureSQLContext>();
builder.Services.AddScoped<AzureBlobStorageService>(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<UserInitializationService>();

//builder.Services.AddSingleton(); // A single instance is shared across the entire application lifetime
//builder.Services.AddHttpClient<T>() //DI container registers T as a transient service by default.
//builder.Services.AddScoped<T>() // T
Expand Down Expand Up @@ -142,8 +178,9 @@
app.UseCors(MyAllowSpecificOrigins);

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAuthentication(); //validates tokens
app.UseAuthorization(); //enforces [Authorize] attributes
app.UseMiddleware<UserInitializationMiddleware>(); //Custom implementation of SQL database user verification
app.MapControllers().RequireCors(MyAllowSpecificOrigins);

ConfigurationHelper.Initialize(app.Configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Stream> GetBlobAsync(string blobName)
public async Task<Stream> 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<string>("AzureStorage:AccountKey"))).ToString();
//Old code that worked, test new solution before deleting
//var sasToken = sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential(
// _blobServiceClient.AccountName,
// _config.GetValue<string>("AzureStorage:AccountKey"))).ToString();

return sasToken;
//return sasToken;
}

}
Expand Down
Loading

0 comments on commit a169ebe

Please sign in to comment.