diff --git a/FunctionApp/ErrorHandler.cs b/FunctionApp/ErrorHandler.cs index 055b70f..901c32c 100644 --- a/FunctionApp/ErrorHandler.cs +++ b/FunctionApp/ErrorHandler.cs @@ -1,13 +1,14 @@ using System; +using System.Net; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace Coomes.Equipper.FunctionApp { public static class ErrorHandler { - public static async Task RunWithErrorHandling(ILogger logger, Func> method) + public static async Task RunWithErrorHandling(ILogger logger, HttpRequestData req, Func> method) { try { @@ -15,11 +16,14 @@ public static async Task RunWithErrorHandling(ILogger logger, Func } catch (BadRequestException bre) { - return new BadRequestObjectResult(bre.Message); + var response = req.CreateResponse(HttpStatusCode.BadRequest); + await response.WriteStringAsync(bre.Message); + return response; } catch(UnauthorizedException) { - return new UnauthorizedResult(); + var response = req.CreateResponse(HttpStatusCode.Unauthorized); + return response; } } } diff --git a/FunctionApp/FunctionApp.csproj b/FunctionApp/FunctionApp.csproj index 49ed59e..159d3aa 100644 --- a/FunctionApp/FunctionApp.csproj +++ b/FunctionApp/FunctionApp.csproj @@ -2,9 +2,13 @@ net8.0 v4 + Exe - + + + + diff --git a/FunctionApp/Functions/Echo.cs b/FunctionApp/Functions/Echo.cs index eae1ce9..5ccf037 100644 --- a/FunctionApp/Functions/Echo.cs +++ b/FunctionApp/Functions/Echo.cs @@ -1,24 +1,30 @@ using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System.Net; +using System.Web; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace Coomes.Equipper.FunctionApp.Functions { - public static class Echo + public class Echo { - [FunctionName("Echo")] - public static IActionResult Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) + private readonly ILogger _logger; + + public Echo(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("Echo")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req) { var correlationID = Guid.NewGuid(); var user = StaticWebAppsAuth.ParseUser(req); - log.LogInformation("{function} {status} {cid} {userId}", "Echo", "Starting", correlationID.ToString(), user.UserId); + _logger.LogInformation("{function} {status} {cid} {userId}", "Echo", "Starting", correlationID.ToString(), user.UserId); - string value = req.Query["value"]; + var query = HttpUtility.ParseQueryString(req.Url.Query); + string value = query["value"]; string responseMessage = ""; if(string.Equals("I'm an idiot", value, StringComparison.CurrentCultureIgnoreCase)) @@ -38,8 +44,11 @@ public static IActionResult Run( responseMessage = "Saying nothing, you hear only the silence of the void..."; } - log.LogInformation("{function} {status} {cid} {userId}", "Echo", "Success", correlationID.ToString(), user.UserId); - return new OkObjectResult(responseMessage); + _logger.LogInformation("{function} {status} {cid} {userId}", "Echo", "Success", correlationID.ToString(), user.UserId); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.WriteString(responseMessage); + return response; } } } diff --git a/FunctionApp/Functions/GetActivityCount.cs b/FunctionApp/Functions/GetActivityCount.cs index 79ac2af..efe6018 100644 --- a/FunctionApp/Functions/GetActivityCount.cs +++ b/FunctionApp/Functions/GetActivityCount.cs @@ -1,30 +1,38 @@ using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Coomes.Equipper.CosmosStorage; using Coomes.Equipper.Operations; -using System.Threading.Tasks; namespace Coomes.Equipper.FunctionApp.Functions { - public static class GetActivityCount + public class GetActivityCount { - [FunctionName("ActivityCount")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger logger) + private readonly ILogger _logger; + + public GetActivityCount(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("ActivityCount")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req) { var correlationID = Guid.NewGuid(); var user = StaticWebAppsAuth.ParseUser(req); - logger.LogInformation("{function} {status} {cid} {userId}", "GetActivityCount", "Starting", correlationID.ToString(), user.UserId); + _logger.LogInformation("{function} {status} {cid} {userId}", "GetActivityCount", "Starting", correlationID.ToString(), user.UserId); - var count = await ExecuteGetCount(logger); + var count = await ExecuteGetCount(_logger); - logger.LogInformation("{function} {status} {cid} {userId}", "GetActivityCount", "Success", correlationID.ToString(), user.UserId); - return new OkObjectResult(count); + _logger.LogInformation("{function} {status} {cid} {userId}", "GetActivityCount", "Success", correlationID.ToString(), user.UserId); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(count); + return response; } private static Task ExecuteGetCount(ILogger logger) diff --git a/FunctionApp/Functions/GetAthlete.cs b/FunctionApp/Functions/GetAthlete.cs index 4c66775..f89d0bc 100644 --- a/FunctionApp/Functions/GetAthlete.cs +++ b/FunctionApp/Functions/GetAthlete.cs @@ -1,33 +1,46 @@ using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Coomes.Equipper.CosmosStorage; using Coomes.Equipper.Operations; -using System.Threading.Tasks; using Coomes.Equipper.StravaApi; namespace Coomes.Equipper.FunctionApp.Functions { - public static class GetAthlete + public class GetAthlete { - [FunctionName("GetAthlete")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger logger) + private readonly ILogger _logger; + + public GetAthlete(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("GetAthlete")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req) { var correlationID = Guid.NewGuid(); var user = StaticWebAppsAuth.ParseUser(req); - logger.LogInformation("{function} {status} {cid} {userId}", "GetAthlete", "Starting", correlationID.ToString(), user.UserId); + _logger.LogInformation("{function} {status} {cid} {userId}", "GetAthlete", "Starting", correlationID.ToString(), user.UserId); - var athlete = await ExecuteGetAthlete(user, logger); + var athlete = await ExecuteGetAthlete(user, _logger); - logger.LogInformation("{function} {status} {cid} {userId}", "GetAthlete", "Success", correlationID.ToString(), user.UserId); + _logger.LogInformation("{function} {status} {cid} {userId}", "GetAthlete", "Success", correlationID.ToString(), user.UserId); - if(athlete != null) return new OkObjectResult(athlete); - else return new NotFoundResult(); + if(athlete != null) + { + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(athlete); + return response; + } + else + { + return req.CreateResponse(HttpStatusCode.NotFound); + } } private static Task ExecuteGetAthlete(EquipperUser user, ILogger logger) diff --git a/FunctionApp/Functions/SubscriptionWebhook.cs b/FunctionApp/Functions/SubscriptionWebhook.cs index 15e54e4..16de965 100644 --- a/FunctionApp/Functions/SubscriptionWebhook.cs +++ b/FunctionApp/Functions/SubscriptionWebhook.cs @@ -1,58 +1,68 @@ using System; +using System.Net; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System.Web; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Coomes.Equipper.Operations; -using Microsoft.AspNetCore.Mvc; using Coomes.Equipper.StravaApi.Models; using Coomes.Equipper.StravaApi; using Coomes.Equipper.CosmosStorage; -using System.Net; namespace Coomes.Equipper.FunctionApp.Functions { - public static class SubscriptionWebhook + public class SubscriptionWebhook { - [FunctionName("__SubscriptionWebhookPlaceholder__")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, - ILogger log) + private readonly ILogger _logger; + + public SubscriptionWebhook(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("__SubscriptionWebhookPlaceholder__")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req) { switch(req.Method) { case "GET": { - return await ConfirmSubscription(req, log); + return await ConfirmSubscription(req, _logger); } case "POST": { - return await ProcessEvent(req, log); + return await ProcessEvent(req, _logger); } default: { - log.LogWarning($"The SubscriptionWebhook function was called with the unsupported HTTP method {req.Method}"); - return new StatusCodeResult((int)HttpStatusCode.MethodNotAllowed); + _logger.LogWarning($"The SubscriptionWebhook function was called with the unsupported HTTP method {req.Method}"); + return req.CreateResponse(HttpStatusCode.MethodNotAllowed); } } } - private static Task ConfirmSubscription(HttpRequest req, ILogger logger) + private static async Task ConfirmSubscription(HttpRequestData req, ILogger logger) { var correlationID = Guid.NewGuid(); logger.LogInformation("{function} {status} {cid}", "SubscriptionWebhook - ConfirmationSubscription", "Starting", correlationID.ToString()); - var challenge = req.Query["hub.challenge"]; + var query = HttpUtility.ParseQueryString(req.Url.Query); + var challenge = query["hub.challenge"]; if(challenge == String.Empty) { - return Task.FromResult(new BadRequestObjectResult("Subscription confirmations must contain a 'hub.challenge' query parameter.")); + var errorResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await errorResponse.WriteStringAsync("Subscription confirmations must contain a 'hub.challenge' query parameter."); + return errorResponse; } - var verifyToken = req.Query["hub.verify_token"]; + var verifyToken = query["hub.verify_token"]; if(verifyToken == String.Empty) { - return Task.FromResult(new BadRequestObjectResult("Subscription confirmations must contain a 'hub.verify_token' query parameter.")); + var errorResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await errorResponse.WriteStringAsync("Subscription confirmations must contain a 'hub.verify_token' query parameter."); + return errorResponse; } // todo: better way to build dependencies? @@ -62,10 +72,13 @@ private static Task ConfirmSubscription(HttpRequest req, ILogger var confirmation = getConfirmation.Execute(challenge, verifyToken, expectedToken); logger.LogInformation("{function} {status} {cid}", "SubscriptionWebhook - ConfirmationSubscription", "Success", correlationID.ToString()); - return Task.FromResult(new OkObjectResult(confirmation)); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(confirmation); + return response; } - private static async Task ProcessEvent(HttpRequest req, ILogger log) + private static async Task ProcessEvent(HttpRequestData req, ILogger log) { var correlationID = Guid.NewGuid(); log.LogInformation("{function} {status} {cid}", "SubscriptionWebhook - ProcessEvent", "Starting", correlationID.ToString()); @@ -77,7 +90,8 @@ private static async Task ProcessEvent(HttpRequest req, ILogger l await ExecuteEventAction(stravaEvent, log); log.LogInformation("{function} {status} {cid}", "SubscriptionWebhook - ProcessEvent", "Success", correlationID.ToString()); - return new OkResult(); + + return req.CreateResponse(HttpStatusCode.OK); } private static async Task ExecuteEventAction(StravaEvent stravaEvent, ILogger log) diff --git a/FunctionApp/Functions/TokenExchange.cs b/FunctionApp/Functions/TokenExchange.cs index 9ac129b..89f5b5b 100644 --- a/FunctionApp/Functions/TokenExchange.cs +++ b/FunctionApp/Functions/TokenExchange.cs @@ -1,9 +1,9 @@ using System; +using System.Net; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System.Web; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Coomes.Equipper.StravaApi; using Coomes.Equipper.Operations; @@ -11,28 +11,37 @@ namespace Coomes.Equipper.FunctionApp { - public static class TokenExchange + public class TokenExchange { - [FunctionName("TokenExchange")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) + private readonly ILogger _logger; + + public TokenExchange(ILoggerFactory loggerFactory) { - return await ErrorHandler.RunWithErrorHandling(log, async () => { + _logger = loggerFactory.CreateLogger(); + } + + [Function("TokenExchange")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req) + { + return await ErrorHandler.RunWithErrorHandling(_logger, req, async () => { var correlationID = Guid.NewGuid(); var user = StaticWebAppsAuth.ParseUser(req); - log.LogInformation("{function} {status} {cid} {userId}", "TokenExchange", "Starting", correlationID.ToString(), user.UserId); + _logger.LogInformation("{function} {status} {cid} {userId}", "TokenExchange", "Starting", correlationID.ToString(), user.UserId); - string code = req.Query["_code"]; // see https://github.com/Azure/static-web-apps/issues/165 and auth.html - string scopeString = req.Query["scope"]; - string error = req.Query["error"]; + var query = HttpUtility.ParseQueryString(req.Url.Query); + string code = query["_code"]; // see https://github.com/Azure/static-web-apps/issues/165 and auth.html + string scopeString = query["scope"]; + string error = query["error"]; - log.LogInformation("Received auth code with scope '{scope}'", scopeString); + _logger.LogInformation("Received auth code with scope '{scope}'", scopeString); - var token = await ExecuteTokenExchange(code, scopeString, user, error, log); + var token = await ExecuteTokenExchange(code, scopeString, user, error, _logger); + + _logger.LogInformation("{function} {status} {cid} {userId}", "TokenExchange", "Success", correlationID.ToString(), user.UserId); - log.LogInformation("{function} {status} {cid} {userId}", "TokenExchange", "Success", correlationID.ToString(), user.UserId); - return new OkResult(); + var response = req.CreateResponse(HttpStatusCode.OK); + return response; }); } diff --git a/FunctionApp/Program.cs b/FunctionApp/Program.cs new file mode 100644 index 0000000..a9f3b9b --- /dev/null +++ b/FunctionApp/Program.cs @@ -0,0 +1,8 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .Build(); + +host.Run(); diff --git a/FunctionApp/StaticWebAppsAuth.cs b/FunctionApp/StaticWebAppsAuth.cs index f2a6891..35f4695 100644 --- a/FunctionApp/StaticWebAppsAuth.cs +++ b/FunctionApp/StaticWebAppsAuth.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Text; using System.Text.Json; -using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; using Coomes.Equipper; // this class derived from https://learn.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#api-functions @@ -17,13 +17,13 @@ private class ClientPrincipal public IEnumerable UserRoles { get; set; } } - public static EquipperUser ParseUser(HttpRequest req) + public static EquipperUser ParseUser(HttpRequestData req) { var principal = new ClientPrincipal(); - if (req.Headers.TryGetValue("x-ms-client-principal", out var header)) + if (req.Headers.TryGetValues("x-ms-client-principal", out var headerValues)) { - var data = header[0]; + var data = headerValues.First(); var decoded = Convert.FromBase64String(data); var json = Encoding.UTF8.GetString(decoded); principal = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });