diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8701da..dd14b5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,28 +10,27 @@ jobs: build: runs-on: ubuntu-latest - # TODO: also use ARM paramaters - # TODO: resource names should be paramaterised steps: - uses: actions/checkout@v2 - - name: Setup .NET 3.1.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 8.0.x - name: Restore dependencies - working-directory: Functions/ + working-directory: SSW.Rules.AzFuncs/ run: dotnet restore - name: Build - working-directory: Functions/ + working-directory: SSW.Rules.AzFuncs/ run: dotnet build --no-restore - name: Deploy - working-directory: Functions/ - run: dotnet publish - --configuration Release - --output ../deploy + working-directory: SSW.Rules.AzFuncs/ + run: | + dotnet publish \ + --configuration Release \ + --output ../deploy - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 @@ -45,10 +44,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET 5.0.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 8.0.x - name: Generate ARM Template working-directory: Azure/ @@ -100,15 +99,10 @@ jobs: resourceGroupName: ${{ secrets.AZURE_RG }} template: arm-template.json - - name: Setup .NET 3.1.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x - - - name: Login with Azure CLI - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + dotnet-version: 8.0.x - uses: actions/download-artifact@v2 with: @@ -120,4 +114,4 @@ jobs: id: fa with: app-name: ${{ secrets.AZURE_RG_PREFIX }}-functions - package: deploy \ No newline at end of file + package: deploy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d298cb5..b2b7bc3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,27 +9,27 @@ jobs: build: runs-on: ubuntu-latest - # TODO: also use ARM paramaters steps: - uses: actions/checkout@v2 - - name: Setup .NET 3.1.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 8.0.x - name: Restore dependencies - working-directory: Functions/ + working-directory: SSW.Rules.AzFuncs/ run: dotnet restore - name: Build - working-directory: Functions/ + working-directory: SSW.Rules.AzFuncs/ run: dotnet build --no-restore - name: Deploy - working-directory: Functions/ - run: dotnet publish - --configuration Release - --output ../deploy + working-directory: SSW.Rules.AzFuncs/ + run: | + dotnet publish \ + --configuration Release \ + --output ../deploy - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 @@ -43,10 +43,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET 5.0.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 8.0.x - name: Generate ARM Template working-directory: Azure/ @@ -97,10 +97,10 @@ jobs: resourceGroupName: ${{ secrets.AZURE_RG }} template: arm-template.json - - name: Setup .NET 3.1.x + - name: Setup .NET 8.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 8.0.x - name: Login with Azure CLI uses: azure/login@v1 @@ -117,4 +117,4 @@ jobs: id: fa with: app-name: ${{ secrets.AZURE_RG_PREFIX }}-functions - package: deploy \ No newline at end of file + package: deploy diff --git a/Azure/Azure.fsproj b/Azure/Azure.fsproj index b9ae0d9..019960f 100644 --- a/Azure/Azure.fsproj +++ b/Azure/Azure.fsproj @@ -2,7 +2,7 @@ Exe - net5.0 + net8.0 @@ -10,7 +10,7 @@ - + diff --git a/Functions/Domain/Bookmark.cs b/Functions/Domain/Bookmark.cs deleted file mode 100644 index 3c0625f..0000000 --- a/Functions/Domain/Bookmark.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions -{ - public class Bookmark : BaseEntity - { - public string RuleGuid { get; set; } - public string UserId { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Domain/Reaction.cs b/Functions/Domain/Reaction.cs deleted file mode 100644 index 0859913..0000000 --- a/Functions/Domain/Reaction.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions { - public class Reaction : BaseEntity { - public ReactionType Type { get; set; } - public string RuleGuid { get; set; } - public string UserId { get; set; } - } - - public enum ReactionType { - SuperDislike, - Dislike, - Like, - SuperLike, - } -} \ No newline at end of file diff --git a/Functions/Domain/RuleHistoryCache.cs b/Functions/Domain/RuleHistoryCache.cs deleted file mode 100644 index 201c560..0000000 --- a/Functions/Domain/RuleHistoryCache.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions -{ - public class RuleHistoryCache : BaseEntity - { - public string MarkdownFilePath { get; set; } - public DateTime ChangedAtDateTime { get; set; } - public string ChangedByDisplayName { get; set; } - public string ChangedByEmail { get; set; } - public DateTime CreatedAtDateTime { get; set; } - public string CreatedByDisplayName { get; set; } - public string CreatedByEmail { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Domain/RuleHistoryData.cs b/Functions/Domain/RuleHistoryData.cs deleted file mode 100644 index 69ddc90..0000000 --- a/Functions/Domain/RuleHistoryData.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace SSW.Rules.Functions -{ - public class RuleHistoryData - { - public string file { get; set; } - public string lastUpdated { get; set; } - public string lastUpdatedBy { get; set; } - public string lastUpdatedByEmail { get; set; } - public string created { get; set; } - public string createdBy { get; set; } - public string createdByEmail { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Domain/SecretContent.cs b/Functions/Domain/SecretContent.cs deleted file mode 100644 index 873b769..0000000 --- a/Functions/Domain/SecretContent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions -{ - public class SecretContent : BaseEntity - { - public string OrganisationId { get; set; } - public string Content { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Domain/SyncHistory.cs b/Functions/Domain/SyncHistory.cs deleted file mode 100644 index 48edd53..0000000 --- a/Functions/Domain/SyncHistory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions -{ - public class SyncHistory : BaseEntity - { - public string CommitHash { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Domain/User.cs b/Functions/Domain/User.cs deleted file mode 100644 index bc26436..0000000 --- a/Functions/Domain/User.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using AzureGems.Repository.Abstractions; - -namespace SSW.Rules.Functions -{ - public class User : BaseEntity - { - public string UserId { get; set; } - public string CommentsUserId { get; set; } - public int OrganisationId { get; set; } - } -} \ No newline at end of file diff --git a/Functions/Functions/AuthCMS/AuthenticateNetlify.cs b/Functions/Functions/AuthCMS/AuthenticateNetlify.cs deleted file mode 100644 index 2301db3..0000000 --- a/Functions/Functions/AuthCMS/AuthenticateNetlify.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Configuration; - -namespace SSW.Rules.Functions -{ - public class AuthenticateNetlify - { - [FunctionName("AuthenticateNetlify")] - public IActionResult Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth")] HttpRequest req, - ILogger log) - { - log.LogInformation($"C# HTTP trigger function {nameof(AuthenticateNetlify)} processed a request."); - - string scope = req.Query["scope"]; - - if (string.IsNullOrEmpty(scope)) - { - log.LogError("Missing scope param"); - return new BadRequestObjectResult(new - { - message = "Missing scope param", - }); - } - - string clientId = System.Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_ID", EnvironmentVariableTarget.Process); - - if (string.IsNullOrEmpty(clientId)) - { - log.LogError("Missing CMS_OAUTH_CLIENT_ID"); - throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_ID"); - } - - return new RedirectResult($"https://github.com/login/oauth/authorize?client_id={clientId}&scope={scope}", true); - } - } -} diff --git a/Functions/Functions/AuthCMS/NetlifyCallback.cs b/Functions/Functions/AuthCMS/NetlifyCallback.cs deleted file mode 100644 index 42cc516..0000000 --- a/Functions/Functions/AuthCMS/NetlifyCallback.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Configuration; -using System.Net.Http; -using System.Text; - -namespace SSW.Rules.Functions -{ - public class NetlifyCallback - { - - [FunctionName("NetlifyCallback")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "callback")] HttpRequest req, - ILogger log) - { - log.LogInformation($"C# HTTP trigger function {nameof(NetlifyCallback)} processed a request."); - - string code = req.Query["code"]; - string host = req.Headers["host"]; - - if (string.IsNullOrEmpty(code)) - { - log.LogError("Missing code param"); - return new BadRequestObjectResult(new - { - message = "Missing code param", - }); - } - - if (string.IsNullOrEmpty(host)) - { - log.LogError("Missing host param"); - return new BadRequestObjectResult(new - { - message = "Missing host param", - }); - } - - try - { - string tokenUrl = "https://github.com/login/oauth/access_token"; - HttpClient newClient = new HttpClient(); - newClient.DefaultRequestHeaders.Add("Accept", "application/json"); - HttpRequestMessage newRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl); - - string clientId = System.Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_ID", EnvironmentVariableTarget.Process); - string clientSecret = System.Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_SECRET", EnvironmentVariableTarget.Process); - - if (string.IsNullOrEmpty(clientId)) - { - log.LogError("Missing CMS_OAUTH_CLIENT_ID"); - throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_ID"); - } - - if (string.IsNullOrEmpty(clientSecret)) - { - log.LogError("Missing CMS_OAUTH_CLIENT_SECRET"); - throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_SECRET"); - } - - var body = new - { - code, - client_id = clientId, - client_secret = clientSecret - }; - - newRequest.Content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"); - - HttpResponseMessage response = await newClient.SendAsync(newRequest); - - response.EnsureSuccessStatusCode(); - - string responseBody = await response.Content.ReadAsStringAsync(); - dynamic jsonBody = JsonConvert.DeserializeObject(responseBody); - - string authorisedObject = JsonConvert.SerializeObject(new - { - token = jsonBody.access_token, - provider = "github" - }); - - string script = @""; - - return new ContentResult { Content = script, ContentType = "text/html" }; - } - catch (HttpRequestException ex) - { - log.LogError(ex.Message); - throw; - } - } - } -} diff --git a/Functions/Functions/Bookmarks/BookmarkRuleFunction.cs b/Functions/Functions/Bookmarks/BookmarkRuleFunction.cs deleted file mode 100644 index 0a7f7f9..0000000 --- a/Functions/Functions/Bookmarks/BookmarkRuleFunction.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; - -namespace SSW.Rules.Functions -{ - public class BookmarkRuleFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public BookmarkRuleFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("BookmarkRuleFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogWarning($"HTTP trigger function {nameof(BookmarkRuleFunction)} request is authorized."); - - Bookmark data; - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - data = JsonConvert.DeserializeObject(requestBody); - bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId); - if (data == null || isNull) - { - return new JsonResult(new - { - error = true, - message = "Request body is empty", - }); - } - - var results = await _dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); - var model = results.FirstOrDefault(); - - if (model == null) - { - model = await _dbContext.Bookmarks.Add(data); - log.LogInformation("Added new bookmark on rule. Id: {0}", model.Id); - } - else - { - log.LogInformation("Bookmark already exists for user {0}", model.UserId); - return new JsonResult(new - { - error = true, - message = "This rule has already been bookmarked" - }); - } - - log.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); - - return new JsonResult(new - { - error = false, - message = "", - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Bookmarks/GetAllBookmarkedFunction.cs b/Functions/Functions/Bookmarks/GetAllBookmarkedFunction.cs deleted file mode 100644 index 606cf9e..0000000 --- a/Functions/Functions/Bookmarks/GetAllBookmarkedFunction.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace SSW.Rules.Functions -{ - public class GetAllBookmarkedFunction - { - private readonly RulesDbContext _dbContext; - - public GetAllBookmarkedFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetAllBookmarkedFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetAllBookmarkedFunction)} received a request."); - - string UserId = req.Query["user_id"]; - - if (string.IsNullOrEmpty(UserId)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty user_id param", - }); - } - log.LogInformation("Checking for bookmarks by user: {0}", UserId); - var bookmarks = await _dbContext.Bookmarks.Query(q => q.Where(w => w.UserId == UserId)); - if (bookmarks.Count() == 0) - { - log.LogInformation($"Could not find results for user: {UserId}"); - return new JsonResult(new - { - error = true, - message = $"Could not find results for user: {UserId}", - bookmarkedRules = bookmarks - }); - } - return new JsonResult(new - { - error = false, - message = "", - bookmarkedRules = bookmarks, - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Bookmarks/GetBookmarkStatusFunction.cs b/Functions/Functions/Bookmarks/GetBookmarkStatusFunction.cs deleted file mode 100644 index c4fb572..0000000 --- a/Functions/Functions/Bookmarks/GetBookmarkStatusFunction.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace SSW.Rules.Functions -{ - public class GetBookmarkStatusFunction - { - private readonly RulesDbContext _dbContext; - - public GetBookmarkStatusFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetBookmarkStatusFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetBookmarkStatusFunction)} received a request."); - - string RuleGuid = req.Query["rule_guid"]; - string UserId = req.Query["user_id"]; - - if (string.IsNullOrEmpty(UserId)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty user_id param", - }); - } - if (string.IsNullOrEmpty(RuleGuid)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty rule_guid param", - }); - } - log.LogInformation("Checking for bookmark on rule: {0} and user: {1}", RuleGuid, UserId); - var bookmarks = await _dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == RuleGuid && w.UserId == UserId)); - if (bookmarks.Count() == 0) - { - log.LogInformation($"Could not find results for rule id: {RuleGuid}, and user: {UserId}"); - return new JsonResult(new - { - error = false, - message = "", - bookmarkStatus = false, - }); - } - - return new JsonResult(new - { - error = false, - message = "", - bookmarkStatus = true, - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Bookmarks/RemoveBookmarkFunction.cs b/Functions/Functions/Bookmarks/RemoveBookmarkFunction.cs deleted file mode 100644 index fbe8ce3..0000000 --- a/Functions/Functions/Bookmarks/RemoveBookmarkFunction.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; - -namespace SSW.Rules.Functions -{ - public class RemoveBookmarkFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public RemoveBookmarkFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("RemoveBookmarkFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogWarning($"HTTP trigger function {nameof(RemoveBookmarkFunction)} request is authorized."); - - Bookmark data; - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - data = JsonConvert.DeserializeObject(requestBody); - - bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId); - if (data == null || isNull) - { - return new JsonResult(new - { - error = true, - message = "Request body is empty", - }); - } - - var results = await _dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); - var model = results.FirstOrDefault(); - - if (model == null) - { - log.LogInformation("No bookmark exists for User {0} and Rule {1}", data.UserId, data.RuleGuid); - return new JsonResult(new - { - error = true, - message = "No bookmark exists for this rule and user", - data.UserId, - data.RuleGuid, - }); - } - var deleteResults = await _dbContext.Bookmarks.Delete(model); - - log.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); - - return new JsonResult(new - { - error = false, - message = "" - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/HealthCheckFunction.cs b/Functions/Functions/HealthCheckFunction.cs deleted file mode 100644 index f660a99..0000000 --- a/Functions/Functions/HealthCheckFunction.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class HealthCheckFunction - { - private readonly IApiAuthorization _apiAuthorization; - private readonly RulesDbContext _dbContext; - - public HealthCheckFunction( - IApiAuthorization apiAuthorization, - RulesDbContext dbContext) - { - _apiAuthorization = apiAuthorization; - _dbContext = dbContext; - } - - [FunctionName("HealthCheckFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(HealthCheckFunction)} received a request."); - - HealthCheckResult result = await _apiAuthorization.HealthCheckAsync(); - var reactionEntity = await _dbContext.Reactions.Add(new Reaction - { - Type = ReactionType.Like, - RuleGuid = "exampleRule123", - UserId = "exampleUser123", - Discriminator = typeof(Reaction).FullName - }); - - var bookmarkEntity = await _dbContext.Bookmarks.Add(new Bookmark - { - RuleGuid = "exampleRule123", - UserId = "exampleUser123", - Discriminator = typeof(Bookmark).FullName - }); - - var secretContentEntity = await _dbContext.SecretContents.Add(new SecretContent - { - OrganisationId = "123123", - Content = "Don't tell anyone about this", - Discriminator = typeof(SecretContent).FullName - }); - - if (result.IsHealthy && reactionEntity != null && bookmarkEntity != null && secretContentEntity != null) - { - log.LogWarning($"{nameof(HealthCheckFunction)} health check OK."); - } - else - { - log.LogError( - $"{nameof(HealthCheckFunction)} health check failed." - + $" {nameof(HealthCheckResult)}: {JsonConvert.SerializeObject(result)}" - ); - } - return new OkObjectResult(result); - } - } -} diff --git a/Functions/Functions/History/GenerateHistoryFileFunction.cs b/Functions/Functions/History/GenerateHistoryFileFunction.cs deleted file mode 100644 index 234b0bd..0000000 --- a/Functions/Functions/History/GenerateHistoryFileFunction.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System.Linq; -using System.Collections.Generic; -using Newtonsoft.Json; -using System; -using System.Globalization; - -namespace SSW.Rules.Functions.Functions -{ - public class GenerateHistoryFileFunction - { - private readonly RulesDbContext _dbContext; - private const string dateFormat = "yyyy-MM-ddTHH:mm:sszzz"; - - public GenerateHistoryFileFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GenerateHistoryFileFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GenerateHistoryFileFunction)} received a request."); - - var results = await _dbContext.RuleHistoryCache.Query(q => q); - - List ruleHistory = new List(); - - foreach(var history in results) - { - ruleHistory.Add(new RuleHistoryData - { - file = history.MarkdownFilePath, - lastUpdated = history.ChangedAtDateTime.ToString(dateFormat, CultureInfo.InvariantCulture), - lastUpdatedBy = history.ChangedByDisplayName, - lastUpdatedByEmail = history.ChangedByEmail, - created = history.CreatedAtDateTime.ToString(dateFormat, CultureInfo.InvariantCulture), - createdBy = history.CreatedByDisplayName, - createdByEmail = history.CreatedByEmail - }); - } - - string responseMessage = JsonConvert.SerializeObject(ruleHistory); - - return new OkObjectResult(responseMessage); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/History/GetHistorySyncCommitHash.cs b/Functions/Functions/History/GetHistorySyncCommitHash.cs deleted file mode 100644 index 09c712c..0000000 --- a/Functions/Functions/History/GetHistorySyncCommitHash.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System.Linq; - -namespace SSW.Rules.Functions.Functions -{ - public class GetHistorySyncCommitHash - { - private readonly RulesDbContext _dbContext; - - public GetHistorySyncCommitHash(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetHistorySyncCommitHash")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetHistorySyncCommitHash)} received a request."); - - var results = await _dbContext.SyncHistory.Query(q => q); - var syncHash = results.FirstOrDefault(); - - string responseMessage = syncHash?.CommitHash ?? string.Empty; - - return new OkObjectResult(responseMessage); - } - } -} diff --git a/Functions/Functions/History/UpdateHistorySyncCommitHash.cs b/Functions/Functions/History/UpdateHistorySyncCommitHash.cs deleted file mode 100644 index 105c578..0000000 --- a/Functions/Functions/History/UpdateHistorySyncCommitHash.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System.Linq; - -namespace SSW.Rules.Functions.Functions.History -{ - public class UpdateHistorySyncCommitHash - { - private readonly RulesDbContext _dbContext; - - public UpdateHistorySyncCommitHash(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("UpdateHistorySyncCommitHash")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(UpdateHistorySyncCommitHash)} received a request."); - - string commitHash = req.Form["commitHash"]; - - var results = await _dbContext.SyncHistory.Query(q => q); - var syncHash = results.FirstOrDefault(); - - if (syncHash == null) - { - await _dbContext.SyncHistory.Add(new SyncHistory - { - CommitHash = commitHash - }); - } - else - { - syncHash.CommitHash = commitHash; - await _dbContext.SyncHistory.Update(syncHash); - } - - return new OkResult(); - } - } -} diff --git a/Functions/Functions/History/UpdateRuleHistory.cs b/Functions/Functions/History/UpdateRuleHistory.cs deleted file mode 100644 index 46270ba..0000000 --- a/Functions/Functions/History/UpdateRuleHistory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq; -using System.Globalization; - -namespace SSW.Rules.Functions.Functions.History -{ - public class UpdateRuleHistory - { - private readonly RulesDbContext _dbContext; - private readonly CultureInfo provider = CultureInfo.InvariantCulture; - private const string dateFormat = "yyyy-MM-ddTHH:mm:sszzz"; - - public UpdateRuleHistory(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("UpdateRuleHistory")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(UpdateRuleHistory)} received a request."); - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - var data = JsonConvert.DeserializeObject>(requestBody); - - foreach(var historyEntry in data) - { - var result = await _dbContext.RuleHistoryCache.Query(q => q.Where(w => w.MarkdownFilePath == historyEntry.file)); - RuleHistoryCache historyCache = result.FirstOrDefault(); - - if (historyCache == null) - { - await _dbContext.RuleHistoryCache.Add(new RuleHistoryCache - { - MarkdownFilePath = historyEntry.file, - ChangedAtDateTime = DateTime.ParseExact(historyEntry.lastUpdated, dateFormat, provider), - ChangedByDisplayName = historyEntry.lastUpdatedBy, - ChangedByEmail = historyEntry.lastUpdatedByEmail, - CreatedAtDateTime = DateTime.ParseExact(historyEntry.created, dateFormat, provider), - CreatedByDisplayName = historyEntry.createdBy, - CreatedByEmail = historyEntry.createdByEmail - }); - } else - { - historyCache.ChangedAtDateTime = DateTime.ParseExact(historyEntry.lastUpdated, dateFormat, provider); - historyCache.ChangedByDisplayName = historyEntry.lastUpdatedBy; - historyCache.ChangedByEmail = historyEntry.lastUpdatedByEmail; - historyCache.CreatedAtDateTime = DateTime.ParseExact(historyEntry.created, dateFormat, provider); - historyCache.CreatedByDisplayName = historyEntry.createdBy; - historyCache.CreatedByEmail = historyEntry.createdByEmail; - - await _dbContext.RuleHistoryCache.Update(historyCache); - } - } - - return new OkResult(); - } - } -} diff --git a/Functions/Functions/HttpHelloWorldFunction.cs b/Functions/Functions/HttpHelloWorldFunction.cs deleted file mode 100644 index 9ca4dfb..0000000 --- a/Functions/Functions/HttpHelloWorldFunction.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; - -namespace SSW.Rules.Functions -{ - public class HttpHelloWorldFunction - { - private readonly IApiAuthorization _apiAuthorization; - - public HttpHelloWorldFunction(IApiAuthorization apiAuthorization) - { - _apiAuthorization = apiAuthorization; - } - - [FunctionName("HttpHelloWorldFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogInformation($"C# HTTP trigger function {nameof(HttpHelloWorldFunction)} processed a request."); - - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogWarning($"HTTP trigger function {nameof(HttpHelloWorldFunction)} request is authorized."); - - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - dynamic data = JsonConvert.DeserializeObject(requestBody); - string name = data?.name; - - string responseMessage = string.IsNullOrEmpty(name) - ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." - : $"Hello, {name}. This HTTP triggered function executed successfully."; - - return new OkObjectResult(responseMessage); - } - } -} diff --git a/Functions/Functions/Reactions/GetAllReactionsFunction.cs b/Functions/Functions/Reactions/GetAllReactionsFunction.cs deleted file mode 100644 index 2b07e2e..0000000 --- a/Functions/Functions/Reactions/GetAllReactionsFunction.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace SSW.Rules.Functions -{ - public class GetAllReactionsFunction - { - private readonly RulesDbContext _dbContext; - - public GetAllReactionsFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetAllReactionsFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetAllReactionsFunction)} received a request."); - - string UserId = req.Query["user_id"]; - - if (string.IsNullOrEmpty(UserId)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty user_id param", - }); - } - log.LogInformation("Checking for bookmarks by user: {0}", UserId); - var likesDislikes = await _dbContext.Reactions.Query(q => q.Where(w => w.UserId == UserId)); - if (likesDislikes.Count() == 0) - { - log.LogInformation($"Could not find results for user: {UserId}"); - return new JsonResult(new - { - error = true, - message = $"Could not find results for user: {UserId}", - likesDislikedRules = likesDislikes, - }); - } - return new JsonResult(new - { - error = false, - message = "", - likesDislikedRules = likesDislikes, - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Reactions/GetReactionsFunction.cs b/Functions/Functions/Reactions/GetReactionsFunction.cs deleted file mode 100644 index ec367da..0000000 --- a/Functions/Functions/Reactions/GetReactionsFunction.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace SSW.Rules.Functions -{ - public class GetReactionsFunction - { - private readonly RulesDbContext _dbContext; - - public GetReactionsFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetReactionsFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetReactionsFunction)} received a request."); - - string RuleGuid = req.Query["rule_guid"]; - string UserId = req.Query["user_id"]; - - if (RuleGuid == null) - { - return new JsonResult(new - { - error = true, - message = "Missing RuleGuid param", - }); - } - - var likes = await _dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == RuleGuid)); - if (likes.Count() == 0) - { - return new JsonResult(new - { - error = true, - message = "Could not find results for rule id: " + RuleGuid, - }); - } - - var results = likes - .GroupBy(l => l.Type) - .Select(g => new - { - Type = g.Key, - Count = g.Count() - }); - - ReactionType? userStatus = null; - if (!string.IsNullOrEmpty(UserId)) - { - var userReaction = likes.Where(w => w.UserId == UserId).FirstOrDefault(); - userStatus = userReaction?.Type ?? null; - log.LogInformation("Found reaction for user: '{0}' reaction: '{1}'", UserId, userStatus); - } - - return new JsonResult(new - { - error = false, - message = "", - superLikeCount = results.Where(r => r.Type == ReactionType.SuperLike).FirstOrDefault()?.Count ?? 0, - likeCount = results.Where(r => r.Type == ReactionType.Like).FirstOrDefault()?.Count ?? 0, - dislikeCount = results.Where(r => r.Type == ReactionType.Dislike).FirstOrDefault()?.Count ?? 0, - superDislikeCount = results.Where(r => r.Type == ReactionType.SuperDislike).FirstOrDefault()?.Count ?? 0, - userStatus = userStatus - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Reactions/ReactFunction.cs b/Functions/Functions/Reactions/ReactFunction.cs deleted file mode 100644 index 6be3634..0000000 --- a/Functions/Functions/Reactions/ReactFunction.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; - -namespace SSW.Rules.Functions -{ - public class ReactFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public ReactFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("ReactFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogWarning($"HTTP trigger function {nameof(ReactFunction)} request is authorized."); - - Reaction data; - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - data = JsonConvert.DeserializeObject(requestBody); - - bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId) || data?.Type == null; - if (data == null || isNull) - { - return new JsonResult(new - { - error = true, - message = "Request body is empty", - }); - } - - var results = await _dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); - var model = results.FirstOrDefault(); - log.LogInformation($"reactions on same rule by same user: {results.Count()}"); - - if (model == null) - { - model = await _dbContext.Reactions.Add(data); - log.LogInformation("Added new reaction. Id: {0}", model.Id); - } - else - { - log.LogInformation("Reaction already exists for user {0}", model.UserId); - - if (model.Type != data.Type) - { - model.Type = data.Type; - model = await _dbContext.Reactions.Update(model); - log.LogInformation("Updated reaction to " + model.Type); - } - else - { - log.LogInformation("Reaction is the same. No change"); - } - } - - log.LogInformation($"User: {model.UserId}, Type: {model.Type}, Rule: {model.RuleGuid}, Id: {model.Id}"); - - return new JsonResult(new - { - error = false, - message = "", - reaction = model.Type - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/Reactions/RemoveReactionFunction.cs b/Functions/Functions/Reactions/RemoveReactionFunction.cs deleted file mode 100644 index a4a41bf..0000000 --- a/Functions/Functions/Reactions/RemoveReactionFunction.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; - -namespace SSW.Rules.Functions -{ - public class RemoveReactionFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public RemoveReactionFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("RemoveReactionFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogWarning($"HTTP trigger function {nameof(RemoveReactionFunction)} request is authorized."); - - Reaction data; - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - data = JsonConvert.DeserializeObject(requestBody); - - bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId) || data?.Type == null; - if (data == null || isNull) - { - return new JsonResult(new - { - error = true, - message = "Request body is empty", - }); - } - - var results = await _dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); - var model = results.FirstOrDefault(); - - if (model == null) - { - log.LogInformation("No reaction exists for User {0} and Rule {1}", data.UserId, data.RuleGuid); - return new JsonResult(new - { - error = true, - message = "No reaction exists for this rule and user", - data.UserId, - data.RuleGuid, - }); - } - var deleteResults = await _dbContext.Reactions.Delete(model); - - log.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); - - return new JsonResult(new - { - error = false, - message = "" - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/SecretContent/GetSecretContentFunction.cs b/Functions/Functions/SecretContent/GetSecretContentFunction.cs deleted file mode 100644 index 3295ec3..0000000 --- a/Functions/Functions/SecretContent/GetSecretContentFunction.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using OidcApiAuthorization.Models; -using OidcApiAuthorization.Abstractions; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class GetSecretContentFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - public GetSecretContentFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - [FunctionName("GetSecretContentFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - log.LogInformation($"C# HTTP trigger function {nameof(GetSecretContentFunction)} processed a request."); - - string SecretContentId = req.Query["id"]; - - if (string.IsNullOrEmpty(SecretContentId)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty id param", - }); - } - - var SecretContents = await _dbContext.SecretContents.Query(q => q.Where(w => w.Id == SecretContentId)); - var model = SecretContents.FirstOrDefault(); - if (model == null) - { - return new JsonResult(new - { - error = true, - message = $"Could not find content with id: {SecretContentId}" - }); - } - - return new JsonResult(new - { - error = false, - message = "", - Content = model - }); - } - } -} diff --git a/Functions/Functions/User/AddUserOrganisationFunction.cs b/Functions/Functions/User/AddUserOrganisationFunction.cs deleted file mode 100644 index d28b597..0000000 --- a/Functions/Functions/User/AddUserOrganisationFunction.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class AddUserOrganisationFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public AddUserOrganisationFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("AddUserOrganisationFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - - log.LogWarning($"HTTP trigger function {nameof(AddUserOrganisationFunction)} request is authorized."); - - User data; - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - data = JsonConvert.DeserializeObject(requestBody); - - if (data == null || string.IsNullOrEmpty(data?.UserId)) - { - return new JsonResult(new - { - error = true, - message = "Request body is empty", - }); - } - - var existingOrganisation = await _dbContext.Users.Query(q => q.Where(w => w.UserId == data.UserId && w.OrganisationId == data.OrganisationId)); - var model = existingOrganisation.FirstOrDefault(); - - if (model != null) - { - return new JsonResult(new - { - error = true, - message = "User is already assigned to this organisation", - }); - } - - var result = await _dbContext.Users.Add(data); - - return new JsonResult(new - { - error = false, - message = "", - user = result, - }); - } - } -} diff --git a/Functions/Functions/User/ConnectUserToCommentsFunction.cs b/Functions/Functions/User/ConnectUserToCommentsFunction.cs deleted file mode 100644 index 21179f2..0000000 --- a/Functions/Functions/User/ConnectUserToCommentsFunction.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class ConnectUserToCommentsFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - public ConnectUserToCommentsFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("ConnectUserCommentsFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - - log.LogWarning($"HTTP trigger function {nameof(ConnectUserToCommentsFunction)} request is authorized."); - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - User data = JsonConvert.DeserializeObject(requestBody); - - if (data == null || string.IsNullOrEmpty(data?.CommentsUserId) || string.IsNullOrEmpty(data?.UserId)) - { - return new BadRequestObjectResult(new - { - message = "Request body is empty or is missing CommentsUserId or UserId fields", - }); - } - - var existingUser = await _dbContext.Users.Query(q => q.Where(w => w.UserId == data.UserId)); - User user = existingUser.FirstOrDefault(); - - if (user == null) - { - await _dbContext.Users.Add(data); - return new OkResult(); - } - - if (user?.CommentsUserId == data.CommentsUserId) - { - return new OkObjectResult(new - { - message = "User already has the same comments account associated", - }); - } - - if(!string.IsNullOrEmpty(user?.CommentsUserId)) { - return new ConflictObjectResult( new { - message = "Different comments account already connected" - }); - } - - var exisitingCommentsId = await _dbContext.Users.Query(q => q.Where(w => w.CommentsUserId == data.CommentsUserId)); - - if (exisitingCommentsId.FirstOrDefault() != null) - { - return new ConflictObjectResult(new - { - message = "This comments account is already being used by another user", - }); - } - - user.CommentsUserId = data.CommentsUserId; - await _dbContext.Users.Update(user); - - return new OkResult(); - } - } -} diff --git a/Functions/Functions/User/GetOrganisationsFunction.cs b/Functions/Functions/User/GetOrganisationsFunction.cs deleted file mode 100644 index 049461d..0000000 --- a/Functions/Functions/User/GetOrganisationsFunction.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace SSW.Rules.Functions -{ - public class GetOrganisationsFunction - { - private readonly RulesDbContext _dbContext; - - public GetOrganisationsFunction(RulesDbContext dbContext) - { - _dbContext = dbContext; - } - - [FunctionName("GetOrganisationsFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - ILogger log) - { - log.LogWarning($"HTTP trigger function {nameof(GetOrganisationsFunction)} received a request."); - - string UserId = req.Query["user_id"]; - - if (string.IsNullOrEmpty(UserId)) - { - return new JsonResult(new - { - error = true, - message = "Missing or empty user_id param", - }); - } - - var organisations = await _dbContext.Users.Query(q => q.Where(w => w.UserId == UserId)); - if (organisations.Count() == 0) - { - log.LogInformation($"Could not find results for user: {UserId}"); - } - - return new JsonResult(new - { - error = false, - message = "", - organisations = organisations, - }); - } - } -} \ No newline at end of file diff --git a/Functions/Functions/User/GetUserFunction.cs b/Functions/Functions/User/GetUserFunction.cs deleted file mode 100644 index 05cdddc..0000000 --- a/Functions/Functions/User/GetUserFunction.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class GetUserFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public GetUserFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("GetUserFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, Route = null)] HttpRequest req, - ILogger log) - { - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - - log.LogWarning($"HTTP trigger function {nameof(GetUserFunction)} request is authorized."); - - string UserId = req.Query["user_id"]; - - if (string.IsNullOrEmpty(UserId)) - { - return new BadRequestObjectResult(new - { - message = "Missing or empty user_id param", - }); - } - - var result = await _dbContext.Users.Query(q => q.Where(w => w.UserId == UserId)); - User user = result.FirstOrDefault(); - - if (user == null) - { - log.LogInformation($"Could not find results for user: {UserId}"); - return new BadRequestObjectResult(new - { - message = "User " + UserId + " was not found", - }); - } - - bool commentsConnected = !string.IsNullOrEmpty(user?.CommentsUserId); - - return new OkObjectResult(new - { - user = user, - commentsConnected = commentsConnected, - }); - } - } -} diff --git a/Functions/Functions/User/RemoveUserCommentsAccountFunction.cs b/Functions/Functions/User/RemoveUserCommentsAccountFunction.cs deleted file mode 100644 index 617f47e..0000000 --- a/Functions/Functions/User/RemoveUserCommentsAccountFunction.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using OidcApiAuthorization.Abstractions; -using OidcApiAuthorization.Models; -using System.Linq; - -namespace SSW.Rules.Functions -{ - public class RemoveUserCommentsAccountFunction - { - private readonly RulesDbContext _dbContext; - private readonly IApiAuthorization _apiAuthorization; - - public RemoveUserCommentsAccountFunction(RulesDbContext dbContext, IApiAuthorization apiAuthorization) - { - _dbContext = dbContext; - _apiAuthorization = apiAuthorization; - } - - [FunctionName("RemoveUserCommentsAccountFunction")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, - ILogger log) - { - - ApiAuthorizationResult authorizationResult = await _apiAuthorization.AuthorizeAsync(req.Headers); - - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - - log.LogWarning($"HTTP trigger function {nameof(RemoveUserCommentsAccountFunction)} request is authorized."); - - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - - User data = JsonConvert.DeserializeObject(requestBody); - - if (data == null || string.IsNullOrEmpty(data?.UserId)) - { - return new BadRequestObjectResult(new - { - message = "Request body is empty or UserId is missing", - }); - } - - var user = await _dbContext.Users.Query(q => q.Where(w => w.UserId == data.UserId)); - User model = user.FirstOrDefault(); - - if (model == null) - { - return new BadRequestObjectResult(new - { - message = "User does not exsist" - }); - } - - model.CommentsUserId = null; - - await _dbContext.Users.Update(model); - - return new OkResult(); - } - } -} diff --git a/Functions/Persistence/RulesDbContext.cs b/Functions/Persistence/RulesDbContext.cs deleted file mode 100644 index c5b03e3..0000000 --- a/Functions/Persistence/RulesDbContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AzureGems.Repository.Abstractions; -using AzureGems.Repository.CosmosDB; - -namespace SSW.Rules.Functions -{ - public class RulesDbContext : DbContext - { - public IRepository Reactions { get; set; } - public IRepository Bookmarks { get; set; } - public IRepository SecretContents { get; set; } - public IRepository Users { get; set; } - public IRepository SyncHistory { get; set; } - public IRepository RuleHistoryCache { get; set; } - } -} \ No newline at end of file diff --git a/Functions/SSW.Rules.Functions.csproj b/Functions/SSW.Rules.Functions.csproj deleted file mode 100644 index 04b6b18..0000000 --- a/Functions/SSW.Rules.Functions.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netcoreapp3.1 - v3 - <_FunctionsSkipCleanOutput>true - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - diff --git a/Functions/Startup.cs b/Functions/Startup.cs deleted file mode 100644 index 68103a9..0000000 --- a/Functions/Startup.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using OidcApiAuthorization; -using Microsoft.Extensions.Configuration; -using AzureGems.CosmosDB; -using System; -using Microsoft.Extensions.DependencyInjection; -using AzureGems.Repository.CosmosDB; - -[assembly: FunctionsStartup(typeof(SSW.Rules.Functions.Startup))] -namespace SSW.Rules.Functions -{ - public class Startup : FunctionsStartup - { - private static readonly IConfigurationRoot Configuration = new ConfigurationBuilder() - .SetBasePath(Environment.CurrentDirectory) - .AddJsonFile("appsettings.json", true) - .AddEnvironmentVariables() - .Build(); - - public override void Configure(IFunctionsHostBuilder builder) - { - builder.Services.AddOidcApiAuthorization(); - builder.Services.AddCosmosDb(builder => - { - builder - .UseConnection(endPoint: Configuration["CosmosDb:Account"], authKey: Configuration["CosmosDb:Key"]) - .UseDatabase(databaseId: Configuration["CosmosDb:DatabaseName"]) - .WithSharedThroughput(400) - .WithContainerConfig(c => - { - c.AddContainer(containerId: nameof(Reaction), partitionKeyPath: "/id"); - c.AddContainer(containerId: nameof(Bookmark), partitionKeyPath: "/id"); - c.AddContainer(containerId: nameof(SecretContent), partitionKeyPath: "/id"); - c.AddContainer(containerId: nameof(User), partitionKeyPath: "/id"); - c.AddContainer(containerId: nameof(SyncHistory), partitionKeyPath: "/id"); - c.AddContainer(containerId: nameof(RuleHistoryCache), partitionKeyPath: "/id"); - }); - }); - builder.Services.AddCosmosDbContext(); - } - } -} \ No newline at end of file diff --git a/Functions/host.json b/Functions/host.json deleted file mode 100644 index bb3b8da..0000000 --- a/Functions/host.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingExcludedTypes": "Request", - "samplingSettings": { - "isEnabled": true - } - } - } -} \ No newline at end of file diff --git a/Functions/local.settings.template.json b/Functions/local.settings.template.json deleted file mode 100644 index e89792d..0000000 --- a/Functions/local.settings.template.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - - "OidcApiAuthorizationSettings:IssuerUrl": "", - "OidcApiAuthorizationSettings:Audience": "", - - "CosmosDb:Account": "", - "CosmosDb:Key": "", - "CosmosDb:DatabaseName": "", - - "CMS_OAUTH_CLIENT_ID": "", - "CMS_OAUTH_CLIENT_SECRET": "" - }, - "Host": { - "LocalHttpPort": 7071, - "CORS": "*", - "CORSCredentials": false - } -} \ No newline at end of file diff --git a/OidcApiAuthorization/Abstractions/IApiAuthorization.cs b/OidcApiAuthorization/Abstractions/IApiAuthorization.cs index ad93584..281c92e 100644 --- a/OidcApiAuthorization/Abstractions/IApiAuthorization.cs +++ b/OidcApiAuthorization/Abstractions/IApiAuthorization.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using OidcApiAuthorization.Models; namespace OidcApiAuthorization.Abstractions diff --git a/OidcApiAuthorization/Abstractions/IJwtSecurityTokenHandlerWrapper.cs b/OidcApiAuthorization/Abstractions/IJwtSecurityTokenHandlerWrapper.cs index 18bd332..972346e 100644 --- a/OidcApiAuthorization/Abstractions/IJwtSecurityTokenHandlerWrapper.cs +++ b/OidcApiAuthorization/Abstractions/IJwtSecurityTokenHandlerWrapper.cs @@ -1,5 +1,4 @@ -using System.Security.Claims; -using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens; namespace OidcApiAuthorization.Abstractions { diff --git a/OidcApiAuthorization/Abstractions/IOidcConfigurationManager.cs b/OidcApiAuthorization/Abstractions/IOidcConfigurationManager.cs index 9d4aa1a..98b7b86 100644 --- a/OidcApiAuthorization/Abstractions/IOidcConfigurationManager.cs +++ b/OidcApiAuthorization/Abstractions/IOidcConfigurationManager.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens; namespace OidcApiAuthorization.Abstractions { diff --git a/OidcApiAuthorization/AuthorizationHeaderBearerTokenExtractor.cs b/OidcApiAuthorization/AuthorizationHeaderBearerTokenExtractor.cs index 31d8d22..c790070 100644 --- a/OidcApiAuthorization/AuthorizationHeaderBearerTokenExtractor.cs +++ b/OidcApiAuthorization/AuthorizationHeaderBearerTokenExtractor.cs @@ -1,64 +1,61 @@ -using System; -using System.Linq; using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using OidcApiAuthorization.Abstractions; -namespace OidcApiAuthorization +namespace OidcApiAuthorization; + +public class AuthorizationHeaderBearerTokenExtractor : IAuthorizationHeaderBearerTokenExtractor { - public class AuthorizationHeaderBearerTokenExtractor : IAuthorizationHeaderBearerTokenExtractor + /// + /// Extracts the Bearer token from the Authorization header of the given HTTP request headers. + /// + /// + /// The headers from an HTTP request. + /// + /// + /// The Bearer token extracted from the Authorization header (without the "Bearer " prefix), + /// or null if the Authorization header was not found, it is in an invalid format, + /// or its value is not a Bearer token. + /// + public string GetToken(IHeaderDictionary httpRequestHeaders) { - /// - /// Extracts the Bearer token from the Authorization header of the given HTTP request headers. - /// - /// - /// The headers from an HTTP request. - /// - /// - /// The Bearer token extracted from the Authorization header (without the "Bearer " prefix), - /// or null if the Authorization header was not found, it is in an invalid format, - /// or its value is not a Bearer token. - /// - public string GetToken(IHeaderDictionary httpRequestHeaders) - { - // Get a StringValues object that represents the content of the Authorization header found in the given - // headers. - // Note that the default for a IHeaderDictionary is a StringValues object with one null string. - StringValues rawAuthorizationHeaderValue = httpRequestHeaders - .SingleOrDefault(x => x.Key == "Authorization") // Case sensitive. - .Value; - - if (rawAuthorizationHeaderValue.Count != 1) - { - // StringValues' Count will be zero if there is no Authorization header - // and greater than one if there are more than one Authorization headers. - return null; - } + // Get a StringValues object that represents the content of the Authorization header found in the given + // headers. + // Note that the default for a IHeaderDictionary is a StringValues object with one null string. + StringValues rawAuthorizationHeaderValue = httpRequestHeaders + .SingleOrDefault(x => x.Key == "Authorization") // Case sensitive. + .Value; - // We got a value from the Authorization header. + if (rawAuthorizationHeaderValue.Count != 1) + { + // StringValues' Count will be zero if there is no Authorization header + // and greater than one if there are more than one Authorization headers. + return null; + } - if (!AuthenticationHeaderValue.TryParse( - rawAuthorizationHeaderValue, // StringValues automatically convert to string. - out AuthenticationHeaderValue authenticationHeaderValue)) - { - // Invalid token format. - return null; - } + // We got a value from the Authorization header. - if (!string.Equals( - authenticationHeaderValue.Scheme, - "Bearer", - StringComparison.InvariantCultureIgnoreCase)) // Case insenitive. - { - // The Authorization header's value is not a Bearer token. - return null; - } + if (!AuthenticationHeaderValue.TryParse( + rawAuthorizationHeaderValue, // StringValues automatically convert to string. + out AuthenticationHeaderValue authenticationHeaderValue)) + { + // Invalid token format. + return null; + } - // Return the token from the Athorization header. - // This is the token with the "Bearer " prefix removed. - // The Parameter will be null, if nothing followed the "Bearer " prefix. - return authenticationHeaderValue.Parameter; + if (!string.Equals( + authenticationHeaderValue.Scheme, + "Bearer", + StringComparison.InvariantCultureIgnoreCase)) // Case insenitive. + { + // The Authorization header's value is not a Bearer token. + return null; } + + // Return the token from the Athorization header. + // This is the token with the "Bearer " prefix removed. + // The Parameter will be null, if nothing followed the "Bearer " prefix. + return authenticationHeaderValue.Parameter; } -} +} \ No newline at end of file diff --git a/OidcApiAuthorization/JwtSecurityTokenHandlerWrapper.cs b/OidcApiAuthorization/JwtSecurityTokenHandlerWrapper.cs index 1703b49..68fcdfd 100644 --- a/OidcApiAuthorization/JwtSecurityTokenHandlerWrapper.cs +++ b/OidcApiAuthorization/JwtSecurityTokenHandlerWrapper.cs @@ -1,34 +1,33 @@ -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; using OidcApiAuthorization.Abstractions; -namespace OidcApiAuthorization +namespace OidcApiAuthorization; + +public class JwtSecurityTokenHandlerWrapper : IJwtSecurityTokenHandlerWrapper { - public class JwtSecurityTokenHandlerWrapper : IJwtSecurityTokenHandlerWrapper + /// + /// Reads and validates a 'JSON Web Token' (JWT) and throws an exception if + /// the token could not be validated. + /// + /// + /// A JSON Web Token (JWT) encoded as a JWS or JWE in Compact Serialized Format. + /// + /// + /// Contains parameters used in the validation of the token. + /// + public void ValidateToken( + string token, + TokenValidationParameters tokenValidationParameters) { - /// - /// Reads and validates a 'JSON Web Token' (JWT) and throws an exception if - /// the token could not be validated. - /// - /// - /// A JSON Web Token (JWT) encoded as a JWS or JWE in Compact Serialized Format. - /// - /// - /// Contains parameters used in the validation of the token. - /// - public void ValidateToken( - string token, - TokenValidationParameters tokenValidationParameters) - { - var handler = new JwtSecurityTokenHandler(); + var handler = new JwtSecurityTokenHandler(); - // Try to validate the token. - // Throws if the the token cannot be validated. - // We don't need the ClaimsPrincipal that is returned. - handler.ValidateToken( - token, - tokenValidationParameters, - out _); // Discard the output SecurityToken. We don't need it. - } + // Try to validate the token. + // Throws if the the token cannot be validated. + // We don't need the ClaimsPrincipal that is returned. + handler.ValidateToken( + token, + tokenValidationParameters, + out _); // Discard the output SecurityToken. We don't need it. } -} +} \ No newline at end of file diff --git a/OidcApiAuthorization/Models/ApiAuthorizationResult.cs b/OidcApiAuthorization/Models/ApiAuthorizationResult.cs index 24c47b4..75c542a 100644 --- a/OidcApiAuthorization/Models/ApiAuthorizationResult.cs +++ b/OidcApiAuthorization/Models/ApiAuthorizationResult.cs @@ -1,41 +1,41 @@ -namespace OidcApiAuthorization.Models +namespace OidcApiAuthorization.Models; + +/// +/// Encapsulates the results of an API authorization. +/// +public class ApiAuthorizationResult { /// - /// Encapsulates the results of an API authorization. + /// Constructs a success authorization. /// - public class ApiAuthorizationResult + public ApiAuthorizationResult() { - /// - /// Constructs a success authorization. - /// - public ApiAuthorizationResult() - { - } + } - /// - /// Constructs a failed authorization with given reason. - /// - /// - /// Describes the reason for the authorization failure. - /// - public ApiAuthorizationResult(string failureReason) - { - FailureReason = failureReason; - } + /// + /// Constructs a failed authorization with given reason. + /// + /// + /// Describes the reason for the authorization failure. + /// + public ApiAuthorizationResult(string failureReason) + { + FailureReason = failureReason; + } - /// - /// True if authorization failed. - /// - public bool Failed => FailureReason != null; + /// + /// True if authorization failed. + /// + public bool Failed => FailureReason != null; - /// - /// String describing the reason for the authorization failure. - /// - public string FailureReason { get; } + /// + /// String describing the reason for the authorization failure. + /// + public string FailureReason { get; } - /// - /// True if authorization was successful. - /// - public bool Success => !Failed; - } + /// + /// True if authorization was successful. + /// + public bool Success => !Failed; } + diff --git a/OidcApiAuthorization/Models/HealthCheckResult.cs b/OidcApiAuthorization/Models/HealthCheckResult.cs index 1863d9c..2037379 100644 --- a/OidcApiAuthorization/Models/HealthCheckResult.cs +++ b/OidcApiAuthorization/Models/HealthCheckResult.cs @@ -1,27 +1,27 @@ -namespace OidcApiAuthorization.Models +namespace OidcApiAuthorization.Models; + +public class HealthCheckResult { - public class HealthCheckResult + /// + /// Construt a HealthCheckResult that indicates good health. + /// + public HealthCheckResult() { - /// - /// Construt a HealthCheckResult that indicates good health. - /// - public HealthCheckResult() - { - } + } - /// - /// Construt a HealthCheckResult that indicates bad health. - /// - /// - /// The message describing the bad health. - /// - public HealthCheckResult(string badHealthMessage) - { - BadHealthMessage = badHealthMessage; - } + /// + /// Construt a HealthCheckResult that indicates bad health. + /// + /// + /// The message describing the bad health. + /// + public HealthCheckResult(string badHealthMessage) + { + BadHealthMessage = badHealthMessage; + } - public bool IsHealthy => BadHealthMessage == null; + public bool IsHealthy => BadHealthMessage == null; - public string BadHealthMessage { get; set; } - } + public string BadHealthMessage { get; set; } } + diff --git a/OidcApiAuthorization/Models/OidcApiAuthorizationSettings.cs b/OidcApiAuthorization/Models/OidcApiAuthorizationSettings.cs index c4b4cb9..ed4f188 100644 --- a/OidcApiAuthorization/Models/OidcApiAuthorizationSettings.cs +++ b/OidcApiAuthorization/Models/OidcApiAuthorizationSettings.cs @@ -1,58 +1,52 @@ -namespace OidcApiAuthorization +namespace OidcApiAuthorization.Models; + +/// +/// Encapsulates settings used in OpenID Connect (OIDC) API authorization. +/// +public class OidcApiAuthorizationSettings { + private string _issuerUrl; + + /// - /// Encapsulates settings used in OpenID Connect (OIDC) API authorization. + /// Identifies the API to be authorized by the Open ID Connect provider (issuer). /// - public class OidcApiAuthorizationSettings - { - private string _issuerUrl; - + /// + /// The "Audience" is the indentifer used by the authorization provider to indentify + /// the API (HTTP triggered Azure Function) being protected. This is often a URL but + /// it is not used as a URL is is simply used as an identifier. + /// + /// For Auth0 the Audience setting set here should match the API's Identifier + /// in the Auth0 Dashboard. + /// + public string Audience { get; set; } - /// - /// Identifies the API to be authorized by the Open ID Connect provider (issuer). - /// - /// - /// The "Audience" is the indentifer used by the authorization provider to indentify - /// the API (HTTP triggered Azure Function) being protected. This is often a URL but - /// it is not used as a URL is is simply used as an identifier. - /// - /// For Auth0 the Audience setting set here should match the API's Identifier - /// in the Auth0 Dashboard. - /// - public string Audience { get; set; } + /// + /// The URL of the Open ID Connect provider (issuer) that will perform API authorization. + /// + /// + /// The "Issuer" is the URL for the authorization provider's end-point. This URL will be + /// used as part of the OpenID Connect protocol to obtain the the signing keys + /// that will be used to validate the JWT Bearer tokens in incoming HTTP request headers. + /// + /// For Auth0 the URL format is: https://{Auth0-tenant-domain}.auth0.com + /// + public string IssuerUrl + { + get { return _issuerUrl; } + set { _issuerUrl = FormatUrl(value); } + } - /// - /// The URL of the Open ID Connect provider (issuer) that will perform API authorization. - /// - /// - /// The "Issuer" is the URL for the authorization provider's end-point. This URL will be - /// used as part of the OpenID Connect protocol to obtain the the signing keys - /// that will be used to validate the JWT Bearer tokens in incoming HTTP request headers. - /// - /// For Auth0 the URL format is: https://{Auth0-tenant-domain}.auth0.com - /// - public string IssuerUrl + private string FormatUrl(string url) + { + if (!string.IsNullOrWhiteSpace(url) && !url.EndsWith("/")) { - get - { - return _issuerUrl; - } - set - { - _issuerUrl = FormatUrl(value); - } + return url + "/"; } - - private string FormatUrl(string url) + else { - if (!string.IsNullOrWhiteSpace(url) && !url.EndsWith("/")) - { - return url + "/"; - } - else - { - return url; - } + return url; } } } + diff --git a/OidcApiAuthorization/OidcApiAuthorization.csproj b/OidcApiAuthorization/OidcApiAuthorization.csproj index a11a1ab..6f1b12c 100644 --- a/OidcApiAuthorization/OidcApiAuthorization.csproj +++ b/OidcApiAuthorization/OidcApiAuthorization.csproj @@ -1,16 +1,19 @@  - - netcoreapp3.1 - + + net8.0 + enable + enable + - - - - - - - - + + + + + + + + + diff --git a/OidcApiAuthorization/OidcApiAuthorizationService.cs b/OidcApiAuthorization/OidcApiAuthorizationService.cs index 415279b..cf7db7e 100644 --- a/OidcApiAuthorization/OidcApiAuthorizationService.cs +++ b/OidcApiAuthorization/OidcApiAuthorizationService.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OidcApiAuthorization.Abstractions; using OidcApiAuthorization.Models; -namespace OidcApiAuthorization -{ - /// +namespace OidcApiAuthorization; + + /// /// Encapsulates checks of OpenID Connect (OIDC) Authorization tokens in HTTP request headers. /// public class OidcApiAuthorizationService : IApiAuthorization @@ -170,5 +167,4 @@ public async Task HealthCheckAsync() return new HealthCheckResult(); // Good health. } - } -} + } \ No newline at end of file diff --git a/OidcApiAuthorization/OidcConfigurationManager.cs b/OidcApiAuthorization/OidcConfigurationManager.cs index 658a393..a2533ce 100644 --- a/OidcApiAuthorization/OidcConfigurationManager.cs +++ b/OidcApiAuthorization/OidcConfigurationManager.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; -namespace OidcApiAuthorization -{ - public class OidcConfigurationManager : IOidcConfigurationManager +namespace OidcApiAuthorization; + + public class OidcConfigurationManager : IOidcConfigurationManager { private readonly ConfigurationManager _configurationManager; @@ -70,5 +68,4 @@ public void RequestRefresh() { _configurationManager.RequestRefresh(); } - } -} + } \ No newline at end of file diff --git a/OidcApiAuthorization/ServicesConfigurationExtensions.cs b/OidcApiAuthorization/ServicesConfigurationExtensions.cs index a87eb04..4910735 100644 --- a/OidcApiAuthorization/ServicesConfigurationExtensions.cs +++ b/OidcApiAuthorization/ServicesConfigurationExtensions.cs @@ -1,34 +1,35 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; -namespace OidcApiAuthorization +namespace OidcApiAuthorization; + +public static class ServicesConfigurationExtensions { - public static class ServicesConfigurationExtensions + public static void AddOidcApiAuthorization(this IServiceCollection services) { - public static void AddOidcApiAuthorization(this IServiceCollection services) - { - // Setup injection of OidcApiAuthorizationSettings configured in the - // Function's app settings (or local.settings.json) - // as IOptions. - // See https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#working-with-options-and-settings - services.AddOptions() - .Configure((settings, configuration) => - { - configuration.GetSection(nameof(OidcApiAuthorizationSettings)).Bind(settings); - }); + // Setup injection of OidcApiAuthorizationSettings configured in the + // Function's app settings (or local.settings.json) + // as IOptions. + // See https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#working-with-options-and-settings + services.AddOptions() + .Configure((settings, configuration) => + { + // Retrieve and bind the configuration section to the settings object + configuration.GetSection(nameof(OidcApiAuthorizationSettings)).Bind(settings); + }); - // These are created as a singletons, so that only one instance of each - // is created for the lifetime of the hosting Azure Function App. - // That helps reduce the number of calls to the authorization service - // for the signing keys and other stuff that can be used across multiple - // calls to the HTTP triggered Azure Functions. + // These are created as a singletons, so that only one instance of each + // is created for the lifetime of the hosting Azure Function App. + // That helps reduce the number of calls to the authorization service + // for the signing keys and other stuff that can be used across multiple + // calls to the HTTP triggered Azure Functions. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - services.AddSingleton(); - } + services.AddSingleton(); } -} +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs.sln b/SSW.Rules.AzFuncs.sln new file mode 100644 index 0000000..3fca56a --- /dev/null +++ b/SSW.Rules.AzFuncs.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSW.Rules.AzFuncs", "SSW.Rules.AzFuncs\SSW.Rules.AzFuncs.csproj", "{AB73900D-A66A-47E1-8605-292D97F3C61D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OidcApiAuthorization", "OidcApiAuthorization\OidcApiAuthorization.csproj", "{BFFFB0FA-D784-4840-95F3-8E9C855E8FAB}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Azure", "Azure\Azure.fsproj", "{23EC6A61-47FA-4A0A-9D91-5CA2F0AADBE6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB73900D-A66A-47E1-8605-292D97F3C61D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB73900D-A66A-47E1-8605-292D97F3C61D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB73900D-A66A-47E1-8605-292D97F3C61D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB73900D-A66A-47E1-8605-292D97F3C61D}.Release|Any CPU.Build.0 = Release|Any CPU + {BFFFB0FA-D784-4840-95F3-8E9C855E8FAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFFFB0FA-D784-4840-95F3-8E9C855E8FAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFFFB0FA-D784-4840-95F3-8E9C855E8FAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFFFB0FA-D784-4840-95F3-8E9C855E8FAB}.Release|Any CPU.Build.0 = Release|Any CPU + {23EC6A61-47FA-4A0A-9D91-5CA2F0AADBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23EC6A61-47FA-4A0A-9D91-5CA2F0AADBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23EC6A61-47FA-4A0A-9D91-5CA2F0AADBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23EC6A61-47FA-4A0A-9D91-5CA2F0AADBE6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/SSW.Rules.AzFuncs/.gitignore b/SSW.Rules.AzFuncs/.gitignore new file mode 100644 index 0000000..ff5b00c --- /dev/null +++ b/SSW.Rules.AzFuncs/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/Bookmark.cs b/SSW.Rules.AzFuncs/Domain/Bookmark.cs new file mode 100644 index 0000000..32b0d4d --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/Bookmark.cs @@ -0,0 +1,9 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class Bookmark : BaseEntity +{ + public string RuleGuid { get; set; } + public string UserId { get; set; } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/FrontMatter.cs b/SSW.Rules.AzFuncs/Domain/FrontMatter.cs new file mode 100644 index 0000000..e42a2fb --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/FrontMatter.cs @@ -0,0 +1,8 @@ +namespace SSW.Rules.AzFuncs.Domain; + +public class FrontMatter +{ + public string Title { get; set; } + public DateTime Created { get; set; } + public string Uri { get; set; } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/LatestRules.cs b/SSW.Rules.AzFuncs/Domain/LatestRules.cs new file mode 100644 index 0000000..ef3ce89 --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/LatestRules.cs @@ -0,0 +1,15 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class LatestRules : BaseEntity +{ + public string CommitHash { get; set; } + public string RuleUri { get; set; } + public string RuleName { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public string CreatedBy { get; set; } + public string UpdatedBy { get; set; } + public string GitHubUsername { get; set; } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/Reaction.cs b/SSW.Rules.AzFuncs/Domain/Reaction.cs new file mode 100644 index 0000000..dab1c5c --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/Reaction.cs @@ -0,0 +1,18 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class Reaction : BaseEntity +{ + public ReactionType Type { get; set; } + public string RuleGuid { get; set; } + public string UserId { get; set; } +} + +public enum ReactionType +{ + SuperDislike, + Dislike, + Like, + SuperLike, +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/RuleHistoryCache.cs b/SSW.Rules.AzFuncs/Domain/RuleHistoryCache.cs new file mode 100644 index 0000000..797b31d --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/RuleHistoryCache.cs @@ -0,0 +1,14 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class RuleHistoryCache : BaseEntity +{ + public string MarkdownFilePath { get; set; } + public DateTime ChangedAtDateTime { get; set; } + public string ChangedByDisplayName { get; set; } + public string ChangedByEmail { get; set; } + public DateTime CreatedAtDateTime { get; set; } + public string CreatedByDisplayName { get; set; } + public string CreatedByEmail { get; set; } +} diff --git a/SSW.Rules.AzFuncs/Domain/RuleHistoryData.cs b/SSW.Rules.AzFuncs/Domain/RuleHistoryData.cs new file mode 100644 index 0000000..77bf1fe --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/RuleHistoryData.cs @@ -0,0 +1,13 @@ +namespace SSW.Rules.AzFuncs.Domain; + +public class RuleHistoryData +{ + public string file { get; set; } + public string lastUpdated { get; set; } + public string lastUpdatedBy { get; set; } + public string lastUpdatedByEmail { get; set; } + public string created { get; set; } + public string createdBy { get; set; } + public string createdByEmail { get; set; } +} + diff --git a/SSW.Rules.AzFuncs/Domain/SecretContent.cs b/SSW.Rules.AzFuncs/Domain/SecretContent.cs new file mode 100644 index 0000000..c937521 --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/SecretContent.cs @@ -0,0 +1,10 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class SecretContent : BaseEntity +{ + public string OrganisationId { get; set; } + public string Content { get; set; } +} + diff --git a/SSW.Rules.AzFuncs/Domain/SyncHistory.cs b/SSW.Rules.AzFuncs/Domain/SyncHistory.cs new file mode 100644 index 0000000..f657c29 --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/SyncHistory.cs @@ -0,0 +1,8 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class SyncHistory : BaseEntity +{ + public string CommitHash { get; set; } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Domain/User.cs b/SSW.Rules.AzFuncs/Domain/User.cs new file mode 100644 index 0000000..5ab5d10 --- /dev/null +++ b/SSW.Rules.AzFuncs/Domain/User.cs @@ -0,0 +1,11 @@ +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Domain; + +public class User : BaseEntity +{ + public string UserId { get; set; } + public string CommentsUserId { get; set; } + public int OrganisationId { get; set; } +} + diff --git a/SSW.Rules.AzFuncs/Functions/AuthCMS/AuthenticateCMS.cs b/SSW.Rules.AzFuncs/Functions/AuthCMS/AuthenticateCMS.cs new file mode 100644 index 0000000..dda8db5 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/AuthCMS/AuthenticateCMS.cs @@ -0,0 +1,42 @@ +using System.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace SSW.Rules.AzFuncs.Functions.AuthCMS; + +public class AuthenticateCms(ILoggerFactory loggerFactory) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + // TODO: Find where this is called and update the name + // Old name: AuthenticateNetlify + [Function("AuthenticateCMS")] + public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth")] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogInformation($"C# HTTP trigger function {nameof(AuthenticateCms)} processed a request."); + + var scope = req.Query["scope"]; + + if (string.IsNullOrEmpty(scope)) + { + _logger.LogError("Missing scope param"); + return new BadRequestObjectResult(new + { + message = "Missing scope param", + }); + } + + var clientId = Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_ID", EnvironmentVariableTarget.Process); + + if (!string.IsNullOrEmpty(clientId)) + return new RedirectResult($"https://github.com/login/oauth/authorize?client_id={clientId}&scope={scope}", + true); + + _logger.LogError("Missing CMS_OAUTH_CLIENT_ID"); + throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_ID"); + + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/AuthCMS/CMSCallback.cs b/SSW.Rules.AzFuncs/Functions/AuthCMS/CMSCallback.cs new file mode 100644 index 0000000..b2df614 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/AuthCMS/CMSCallback.cs @@ -0,0 +1,105 @@ +using System.Configuration; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace SSW.Rules.AzFuncs.Functions.AuthCMS; + +public class CmsCallback(ILoggerFactory loggerFactory) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + // TODO: Find where this is called and update the name + // Old name: NetlifyCallback + [Function("CMSCallback")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "callback")] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogInformation($"C# HTTP trigger function {nameof(CmsCallback)} processed a request."); + + try + { + var code = req.Query["code"]; + if (string.IsNullOrEmpty(code)) + { + _logger.LogError("Missing code param"); + return new BadRequestObjectResult(new + { + message = "Missing code param", + }); + } + + const string tokenUrl = "https://github.com/login/oauth/access_token"; + var newClient = new HttpClient(); + newClient.DefaultRequestHeaders.Add("Accept", "application/json"); + var newRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl); + + var clientId = + System.Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_ID", EnvironmentVariableTarget.Process); + var clientSecret = + System.Environment.GetEnvironmentVariable("CMS_OAUTH_CLIENT_SECRET", EnvironmentVariableTarget.Process); + + if (string.IsNullOrEmpty(clientId)) + { + _logger.LogError("Missing CMS_OAUTH_CLIENT_ID"); + throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_ID"); + } + + if (string.IsNullOrEmpty(clientSecret)) + { + _logger.LogError("Missing CMS_OAUTH_CLIENT_SECRET"); + throw new ConfigurationErrorsException("Missing CMS_OAUTH_CLIENT_SECRET"); + } + + var body = new + { + code, + client_id = clientId, + client_secret = clientSecret + }; + + newRequest.Content = + new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"); + + var response = await newClient.SendAsync(newRequest); + + response.EnsureSuccessStatusCode(); + + var responseBody = await response.Content.ReadAsStringAsync(); + dynamic jsonBody = JsonConvert.DeserializeObject(responseBody); + + var authorisedObject = JsonConvert.SerializeObject(new + { + token = jsonBody.access_token, + provider = "github" + }); + + var script = @""; + + return new ContentResult { Content = script, ContentType = "text/html" }; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex.Message); + throw; + } + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Bookmarks/BookmarkRuleFunction.cs b/SSW.Rules.AzFuncs/Functions/Bookmarks/BookmarkRuleFunction.cs new file mode 100644 index 0000000..7a94cbe --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Bookmarks/BookmarkRuleFunction.cs @@ -0,0 +1,75 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Cosmos.Linq; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Bookmarks; + +public class BookmarkRuleFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("BookmarkRuleFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + var headers = Converters.ConvertToIHeaderDictionary(req.Headers); + ApiAuthorizationResult authorizationResult = await apiAuthorization.AuthorizeAsync(headers); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateJsonErrorResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(BookmarkRuleFunction)} request is authorized."); + + Bookmark data; + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + data = JsonConvert.DeserializeObject(requestBody); + var isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId); + if (data == null || isNull) + { + return req.CreateJsonErrorResponse(HttpStatusCode.BadRequest, "Request body is empty"); + } + + var results = + await dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); + + + var model = results.FirstOrDefault(); + + if (model == null) + { + var response = await dbContext.Bookmarks.Add(data); + if (response.IsDefined()) + { + model = response; + _logger.LogInformation("Added new bookmark on rule. Id: {0}", model.Id); + } + } + else + { + _logger.LogInformation("Bookmark already exists for user {0}", model.UserId); + return req.CreateJsonErrorResponse(HttpStatusCode.BadRequest, "This rule has already been bookmarked"); + } + + _logger.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); + + return req.CreateJsonErrorResponse(); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Bookmarks/GetAllBookmarkedFunction.cs b/SSW.Rules.AzFuncs/Functions/Bookmarks/GetAllBookmarkedFunction.cs new file mode 100644 index 0000000..7deb9d5 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Bookmarks/GetAllBookmarkedFunction.cs @@ -0,0 +1,60 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Bookmarks; + +public class GetAllBookmarkedFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetAllBookmarkedFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetAllBookmarkedFunction)} received a request."); + + var userId = req.Query["user_id"]; + + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing or empty user_id param" + }, HttpStatusCode.BadRequest); + } + + _logger.LogInformation("Checking for bookmarks by user: {0}", userId); + + var bookmarks = await dbContext.Bookmarks.Query(q => q.Where(w => w.UserId == userId)); + + var bookmarksResult = bookmarks.ToList(); + if (bookmarksResult.Count != 0) + { + return req.CreateJsonResponse(new + { + error = false, + message = "", + bookmarkedRules = bookmarksResult, + }); + } + + _logger.LogInformation($"Could not find results for user: {userId}"); + return req.CreateJsonResponse(new + { + error = true, + message = $"Could not find results for user: {userId}", + bookmarkedRules = bookmarksResult + }, HttpStatusCode.NotFound); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Bookmarks/GetBookmarkStatusFunction.cs b/SSW.Rules.AzFuncs/Functions/Bookmarks/GetBookmarkStatusFunction.cs new file mode 100644 index 0000000..24c2a5b --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Bookmarks/GetBookmarkStatusFunction.cs @@ -0,0 +1,66 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Bookmarks; + +public class GetBookmarkStatusFunction(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetBookmarkStatusFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetBookmarkStatusFunction)} received a request."); + + var ruleGuid = req.Query["rule_guid"]; + var userId = req.Query["user_id"]; + + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing or empty user_id param", + }, HttpStatusCode.BadRequest); + } + + if (string.IsNullOrEmpty(ruleGuid)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing or empty rule_guid param", + }, HttpStatusCode.BadRequest); + } + + _logger.LogInformation("Checking for bookmark on rule: {0} and user: {1}", ruleGuid, userId); + var bookmarks = + await dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == ruleGuid && w.UserId == userId)); + + if (bookmarks.Any()) + { + return req.CreateJsonResponse(new + { + error = false, + message = "", + bookmarkStatus = true, + }); + } + + _logger.LogInformation($"Could not find results for rule id: {ruleGuid}, and user: {userId}"); + return req.CreateJsonResponse(new + { + error = false, + message = "", + bookmarkStatus = false, + }, HttpStatusCode.NotFound); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Bookmarks/RemoveBookmarkFunction.cs b/SSW.Rules.AzFuncs/Functions/Bookmarks/RemoveBookmarkFunction.cs new file mode 100644 index 0000000..94f74d9 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Bookmarks/RemoveBookmarkFunction.cs @@ -0,0 +1,80 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Bookmarks; + +public class RemoveBookmarkFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("RemoveBookmarkFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateJsonErrorResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(RemoveBookmarkFunction)} request is authorized."); + + Bookmark data; + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + data = JsonConvert.DeserializeObject(requestBody); + + bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId); + if (data == null || isNull) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Request body is empty", + }, HttpStatusCode.BadRequest); + } + + var results = + await dbContext.Bookmarks.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); + + var model = results.FirstOrDefault(); + + if (model == null) + { + _logger.LogInformation("No bookmark exists for User {0} and Rule {1}", data.UserId, data.RuleGuid); + return req.CreateJsonResponse(new + { + error = true, + message = "No bookmark exists for this rule and user", + data.UserId, + data.RuleGuid, + }, HttpStatusCode.NotFound); + } + + await dbContext.Bookmarks.Delete(model); + _logger.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); + + return req.CreateJsonResponse(new + { + error = false, + message = "" + }, HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Health/HealthCheckFunction.cs b/SSW.Rules.AzFuncs/Functions/Health/HealthCheckFunction.cs new file mode 100644 index 0000000..d3a7049 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Health/HealthCheckFunction.cs @@ -0,0 +1,66 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Health; + +public class HealthCheckFunction( + ILoggerFactory loggerFactory, + IApiAuthorization apiAuthorization, + RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("HealthCheckFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(HealthCheckFunction)} received a request."); + + HealthCheckResult result = await apiAuthorization.HealthCheckAsync(); + var reactionEntity = await dbContext.Reactions.Add(new Reaction + { + Type = ReactionType.Like, + RuleGuid = "exampleRule123", + UserId = "exampleUser123", + Discriminator = typeof(Reaction).FullName + }); + + var bookmarkEntity = await dbContext.Bookmarks.Add(new Bookmark + { + RuleGuid = "exampleRule123", + UserId = "exampleUser123", + Discriminator = typeof(Bookmark).FullName + }); + + var secretContentEntity = await dbContext.SecretContents.Add(new Domain.SecretContent + { + OrganisationId = "123123", + Content = "Don't tell anyone about this", + Discriminator = typeof(Domain.SecretContent).FullName + }); + + if (result.IsHealthy && reactionEntity != null && bookmarkEntity != null && secretContentEntity != null) + { + _logger.LogWarning($"{nameof(HealthCheckFunction)} health check OK."); + } + else + { + _logger.LogError( + $"{nameof(HealthCheckFunction)} health check failed." + + $" {nameof(HealthCheckResult)}: {JsonConvert.SerializeObject(result)}" + ); + } + + return req.CreateJsonResponse(result); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/History/GenerateHistoryFileFunction.cs b/SSW.Rules.AzFuncs/Functions/History/GenerateHistoryFileFunction.cs new file mode 100644 index 0000000..a04c522 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/History/GenerateHistoryFileFunction.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.History; + +public class GenerateHistoryFileFunction(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private const string DateFormat = "yyyy-MM-ddTHH:mm:sszzz"; + + [Function("GenerateHistoryFileFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GenerateHistoryFileFunction)} received a request."); + + var results = await dbContext.RuleHistoryCache.Query(q => q); + + List ruleHistory = []; + ruleHistory.AddRange(results.Select(history => new RuleHistoryData + { + file = history.MarkdownFilePath, + lastUpdated = history.ChangedAtDateTime.ToString(DateFormat, CultureInfo.InvariantCulture), + lastUpdatedBy = history.ChangedByDisplayName, + lastUpdatedByEmail = history.ChangedByEmail, + created = history.CreatedAtDateTime.ToString(DateFormat, CultureInfo.InvariantCulture), + createdBy = history.CreatedByDisplayName, + createdByEmail = history.CreatedByEmail + })); + + var responseMessage = JsonConvert.SerializeObject(ruleHistory); + + return req.CreateJsonResponse(responseMessage); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/History/GetHistorySyncCommitHash.cs b/SSW.Rules.AzFuncs/Functions/History/GetHistorySyncCommitHash.cs new file mode 100644 index 0000000..16388a8 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/History/GetHistorySyncCommitHash.cs @@ -0,0 +1,29 @@ +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.History; + +public class GetHistorySyncCommitHash(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetHistorySyncCommitHash")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetHistorySyncCommitHash)} received a request."); + + var results = await dbContext.SyncHistory.Query(q => q); + + var syncHash = results.FirstOrDefault(); + + string responseMessage = syncHash?.CommitHash ?? string.Empty; + return req.CreateJsonResponse(responseMessage); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/History/UpdateHistorySyncCommitHash.cs b/SSW.Rules.AzFuncs/Functions/History/UpdateHistorySyncCommitHash.cs new file mode 100644 index 0000000..c02d1f9 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/History/UpdateHistorySyncCommitHash.cs @@ -0,0 +1,55 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.History; + +public class UpdateHistorySyncCommitHash(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("UpdateHistorySyncCommitHash")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(UpdateHistorySyncCommitHash)} received a request."); + + var formData = await req.ReadFormDataAsync(); + var commitHash = formData["commitHash"]; + + if (string.IsNullOrEmpty(commitHash)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "No commit hash" + }, HttpStatusCode.BadRequest); + } + + + var results = await dbContext.SyncHistory.Query(q => q); + var syncHash = results.FirstOrDefault(); + + if (syncHash == null) + { + await dbContext.SyncHistory.Add(new SyncHistory + { + CommitHash = commitHash + }); + } + else + { + syncHash.CommitHash = commitHash; + await dbContext.SyncHistory.Update(syncHash); + } + + return req.CreateResponse(HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/History/UpdateRuleHistory.cs b/SSW.Rules.AzFuncs/Functions/History/UpdateRuleHistory.cs new file mode 100644 index 0000000..2aed3d4 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/History/UpdateRuleHistory.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.History; + +public class UpdateRuleHistory(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly CultureInfo _provider = CultureInfo.InvariantCulture; + private const string DateFormat = "yyyy-MM-ddTHH:mm:sszzz"; + + [Function("UpdateRuleHistory")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + try + { + _logger.LogInformation($"HTTP trigger function {nameof(UpdateRuleHistory)} received a request."); + + var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonConvert.DeserializeObject>(requestBody); + if (data == null) + { + throw new InvalidOperationException("Request body cannot be parsed."); + } + + + foreach (var historyEntry in data) + { + var result = await dbContext.RuleHistoryCache.Query(q => q.Where(w => w.MarkdownFilePath == historyEntry.file)); + var historyCache = result.FirstOrDefault(); + + if (historyCache == null) + { + await dbContext.RuleHistoryCache.Add(new RuleHistoryCache + { + MarkdownFilePath = historyEntry.file, + ChangedAtDateTime = DateTime.ParseExact(historyEntry.lastUpdated, DateFormat, _provider), + ChangedByDisplayName = historyEntry.lastUpdatedBy, + ChangedByEmail = historyEntry.lastUpdatedByEmail, + CreatedAtDateTime = DateTime.ParseExact(historyEntry.created, DateFormat, _provider), + CreatedByDisplayName = historyEntry.createdBy, + CreatedByEmail = historyEntry.createdByEmail + }); + } + else + { + historyCache.ChangedAtDateTime = DateTime.ParseExact(historyEntry.lastUpdated, DateFormat, _provider); + historyCache.ChangedByDisplayName = historyEntry.lastUpdatedBy; + historyCache.ChangedByEmail = historyEntry.lastUpdatedByEmail; + historyCache.CreatedAtDateTime = DateTime.ParseExact(historyEntry.created, DateFormat, _provider); + historyCache.CreatedByDisplayName = historyEntry.createdBy; + historyCache.CreatedByEmail = historyEntry.createdByEmail; + await dbContext.RuleHistoryCache.Update(historyCache); + } + } + + return req.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in UpdateRuleHistory function."); + return req.CreateResponse(HttpStatusCode.InternalServerError); + } + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Reactions/GetAllReactionsFunction.cs b/SSW.Rules.AzFuncs/Functions/Reactions/GetAllReactionsFunction.cs new file mode 100644 index 0000000..99adeb8 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Reactions/GetAllReactionsFunction.cs @@ -0,0 +1,56 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Reactions; + +public class GetAllReactionsFunction(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetAllReactionsFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetAllReactionsFunction)} received a request."); + + var userId = req.Query["user_id"]; + + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing or empty user_id param", + }, HttpStatusCode.BadRequest); + } + + _logger.LogInformation("Checking for bookmarks by user: {0}", userId); + var likesDislikes = await dbContext.Reactions.Query(q => q.Where(w => w.UserId == userId)); + + var likesList = likesDislikes.ToList(); + if (likesList.Count != 0) + { + return req.CreateJsonResponse(new + { + error = false, + message = "", + likesDislikedRules = likesList, + }); + } + + _logger.LogInformation($"Could not find results for user: {userId}"); + return req.CreateJsonResponse(new + { + error = true, + message = $"Could not find results for user: {userId}", + likesDislikedRules = likesList, + }, HttpStatusCode.NotFound); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Reactions/GetReactionsFunction.cs b/SSW.Rules.AzFuncs/Functions/Reactions/GetReactionsFunction.cs new file mode 100644 index 0000000..aefc3da --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Reactions/GetReactionsFunction.cs @@ -0,0 +1,87 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Reactions; + +public class GetReactionsFunction(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetReactionsFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetReactionsFunction)} received a request."); + + var ruleGuid = req.Query["rule_guid"]; + var userId = req.Query["user_id"]; + + if (ruleGuid == null) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing RuleGuid param", + }, HttpStatusCode.BadRequest); + } + + var likes = await dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == ruleGuid)); + + var likesList = likes.ToList(); + if (!likesList.Any()) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Could not find results for rule id: " + ruleGuid, + }, HttpStatusCode.NotFound); + } + + // Group and count reactions by type + var results = likesList + .GroupBy(l => l.Type) + .Select(g => new + { + Type = g.Key, + Count = g.Count() + }).ToList(); + + ReactionType? userStatus = null; + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new + { + error = false, + message = "", + superLikeCount = results.FirstOrDefault(r => r.Type == ReactionType.SuperLike)?.Count ?? 0, + likeCount = results.FirstOrDefault(r => r.Type == ReactionType.Like)?.Count ?? 0, + dislikeCount = results.FirstOrDefault(r => r.Type == ReactionType.Dislike)?.Count ?? 0, + superDislikeCount = results.FirstOrDefault(r => r.Type == ReactionType.SuperDislike)?.Count ?? 0, + userStatus + }); + } + + var userReaction = likesList.FirstOrDefault(w => w.UserId == userId); + userStatus = userReaction?.Type; + _logger.LogInformation("Found reaction for user: '{0}' reaction: '{1}'", userId, userStatus); + + return req.CreateJsonResponse(new + { + error = false, + message = "", + superLikeCount = results.FirstOrDefault(r => r.Type == ReactionType.SuperLike)?.Count ?? 0, + likeCount = results.FirstOrDefault(r => r.Type == ReactionType.Like)?.Count ?? 0, + dislikeCount = results.FirstOrDefault(r => r.Type == ReactionType.Dislike)?.Count ?? 0, + superDislikeCount = results.FirstOrDefault(r => r.Type == ReactionType.SuperDislike)?.Count ?? 0, + userStatus + }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Reactions/ReactFunction.cs b/SSW.Rules.AzFuncs/Functions/Reactions/ReactFunction.cs new file mode 100644 index 0000000..c205a5c --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Reactions/ReactFunction.cs @@ -0,0 +1,91 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Reactions; + +public class ReactFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("ReactFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateJsonErrorResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(ReactFunction)} request is authorized."); + + Reaction data; + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + data = JsonConvert.DeserializeObject(requestBody); + + bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId) || data?.Type == null; + if (data == null || isNull) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Request body is empty", + }, HttpStatusCode.BadRequest); + } + + + var results = + await dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); + var reactionsList = results.ToList(); + var model = reactionsList.FirstOrDefault(); + _logger.LogInformation($"reactions on same rule by same user: {reactionsList.Count()}"); + + if (model == null) + { + model = await dbContext.Reactions.Add(data); + _logger.LogInformation("Added new reaction. Id: {0}", model.Id); + } + else + { + _logger.LogInformation("Reaction already exists for user {0}", model.UserId); + + if (model.Type != data.Type) + { + model.Type = data.Type; + model = await dbContext.Reactions.Update(model); + _logger.LogInformation("Updated reaction to " + model.Type); + } + else + { + _logger.LogInformation("Reaction is the same. No change"); + } + } + + _logger.LogInformation($"User: {model.UserId}, Type: {model.Type}, Rule: {model.RuleGuid}, Id: {model.Id}"); + + return req.CreateJsonResponse(new + { + error = false, + message = "", + reaction = model.Type + }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Reactions/RemoveReactionFunction.cs b/SSW.Rules.AzFuncs/Functions/Reactions/RemoveReactionFunction.cs new file mode 100644 index 0000000..3daf77d --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Reactions/RemoveReactionFunction.cs @@ -0,0 +1,77 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Reactions; + +public class RemoveReactionFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("RemoveReactionFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(RemoveReactionFunction)} request is authorized."); + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + Reaction data = JsonConvert.DeserializeObject(requestBody); + + bool isNull = string.IsNullOrEmpty(data?.RuleGuid) || string.IsNullOrEmpty(data?.UserId) || data?.Type == null; + if (data == null || isNull) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Request body is empty", + }, HttpStatusCode.BadRequest); + } + + var results = + await dbContext.Reactions.Query(q => q.Where(w => w.RuleGuid == data.RuleGuid && w.UserId == data.UserId)); + var model = results.FirstOrDefault(); + + if (model == null) + { + _logger.LogInformation("No reaction exists for User {0} and Rule {1}", data.UserId, data.RuleGuid); + return req.CreateJsonResponse(new + { + error = true, + message = "No reaction exists for this rule and user", + data.UserId, + data.RuleGuid, + }, HttpStatusCode.NotFound); + } + + await dbContext.Reactions.Delete(model); + _logger.LogInformation($"User: {model.UserId}, Rule: {model.RuleGuid}, Id: {model.Id}"); + + return req.CreateJsonResponse(new + { + error = false, + message = "" + }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/SecretContent/GetSecretContentFunction.cs b/SSW.Rules.AzFuncs/Functions/SecretContent/GetSecretContentFunction.cs new file mode 100644 index 0000000..764309e --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/SecretContent/GetSecretContentFunction.cs @@ -0,0 +1,67 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.SecretContent; + +public class GetSecretContentFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetSecretContentFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", "post")] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogInformation($"C# HTTP trigger function {nameof(GetSecretContentFunction)} processed a request."); + + var secretContentId = req.Query["id"]; + + if (string.IsNullOrEmpty(secretContentId)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Missing or empty id param", + }, HttpStatusCode.BadRequest); + } + + var secretContents = await dbContext.SecretContents.Query(q => q.Where(w => w.Id == secretContentId)); + var model = secretContents.FirstOrDefault(); + + if (model == null) + { + return req.CreateJsonResponse(new + { + error = true, + message = $"Could not find content with id: {secretContentId}" + }, HttpStatusCode.NotFound); + } + + return req.CreateJsonResponse(new + { + error = false, + message = "", + Content = model + }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/User/AddUserOrganisationFunction.cs b/SSW.Rules.AzFuncs/Functions/User/AddUserOrganisationFunction.cs new file mode 100644 index 0000000..df0f8c7 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/User/AddUserOrganisationFunction.cs @@ -0,0 +1,71 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.User; + +public class AddUserOrganisationFunction( + ILoggerFactory loggerFactory, + IApiAuthorization apiAuthorization, + RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("AddUserOrganisationFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(AddUserOrganisationFunction)} received a request."); + + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(AddUserOrganisationFunction)} request is authorized."); + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonConvert.DeserializeObject(requestBody); + + if (data == null || string.IsNullOrEmpty(data.UserId)) + { + return req.CreateJsonResponse(new + { + error = true, + message = "Request body is empty" + }, + HttpStatusCode.BadRequest); + } + + var existingOrganisation = await dbContext.Users.Query(q => + q.Where(w => w.UserId == data.UserId && w.OrganisationId == data.OrganisationId)); + var model = existingOrganisation.FirstOrDefault(); + + if (model != null) + { + return req.CreateJsonResponse(new + { + error = true, + message = "User is already assigned to this organisation" + }, + HttpStatusCode.BadRequest); + } + + var result = await dbContext.Users.Add(data); + + return req.CreateJsonResponse(new { error = false, message = "", user = result }, HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/User/ConnectUserToCommentsFunction.cs b/SSW.Rules.AzFuncs/Functions/User/ConnectUserToCommentsFunction.cs new file mode 100644 index 0000000..c0e4d6e --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/User/ConnectUserToCommentsFunction.cs @@ -0,0 +1,88 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; +using DomainUser = SSW.Rules.AzFuncs.Domain.User; + +namespace SSW.Rules.AzFuncs.Functions.User; + +public class ConnectUserToCommentsFunction( + ILoggerFactory loggerFactory, + RulesDbContext dbContext, + IApiAuthorization apiAuthorization) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("ConnectUserToCommentsFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(ConnectUserToCommentsFunction)} received a request."); + + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + return req.CreateResponse(HttpStatusCode.Unauthorized); + } + + _logger.LogWarning($"HTTP trigger function {nameof(ConnectUserToCommentsFunction)} request is authorized."); + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonConvert.DeserializeObject(requestBody); + + if (data == null || string.IsNullOrEmpty(data.CommentsUserId) || string.IsNullOrEmpty(data.UserId)) + { + return req.CreateJsonResponse( + new { message = "Request body is empty or is missing CommentsUserId or UserId fields" }, + HttpStatusCode.BadRequest); + } + + + var existingUser = await dbContext.Users.Query(q => q.Where(w => w.UserId == data.UserId)); + var user = existingUser.FirstOrDefault(); + + if (user == null) + { + await dbContext.Users.Add(data); + return req.CreateResponse(HttpStatusCode.OK); + } + + if (user.CommentsUserId == data.CommentsUserId) + { + return req.CreateJsonResponse(new { message = "User already has the same comments account associated" }, + HttpStatusCode.OK); + } + + if (!string.IsNullOrEmpty(user.CommentsUserId)) + { + return req.CreateJsonResponse(new { message = "Different comments account already connected" }, + HttpStatusCode.Conflict); + } + + var existingCommentsId = + await dbContext.Users.Query(q => q.Where(w => w.CommentsUserId == data.CommentsUserId)); + + if (existingCommentsId.Any()) + { + return req.CreateJsonResponse( + new { message = "This comments account is already being used by another user" }, + HttpStatusCode.Conflict); + } + + user.CommentsUserId = data.CommentsUserId; + await dbContext.Users.Update(user); + + return req.CreateResponse(HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/User/GetOrganisationsFunction.cs b/SSW.Rules.AzFuncs/Functions/User/GetOrganisationsFunction.cs new file mode 100644 index 0000000..747c9a2 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/User/GetOrganisationsFunction.cs @@ -0,0 +1,45 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; +using DomainUser = SSW.Rules.AzFuncs.Domain.User; + +namespace SSW.Rules.AzFuncs.Functions.User; + +public class GetOrganisationsFunction(ILoggerFactory loggerFactory, RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetOrganisationsFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogWarning($"HTTP trigger function {nameof(GetOrganisationsFunction)} received a request."); + + var userId = req.Query["user_id"]; + + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new { error = true, message = "Missing or empty user_id param" }, + HttpStatusCode.BadRequest); + } + + + var organisations = await dbContext.Users.Query(q => q.Where(w => w.UserId == userId)); + + var usersList = organisations.ToList(); + if (usersList.Count != 0) + { + return req.CreateJsonResponse(new { error = false, message = "", organisations = usersList }); + } + + _logger.LogInformation($"Could not find results for user: {userId}"); + return req.CreateJsonResponse(new { error = true, message = $"Could not find results for user: {userId}" }, + HttpStatusCode.NotFound); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/User/GetUserFunction.cs b/SSW.Rules.AzFuncs/Functions/User/GetUserFunction.cs new file mode 100644 index 0000000..5051e65 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/User/GetUserFunction.cs @@ -0,0 +1,68 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; +using DomainUser = SSW.Rules.AzFuncs.Domain.User; + +namespace SSW.Rules.AzFuncs.Functions.User; + +public class GetUserFunction( + ILoggerFactory loggerFactory, + IApiAuthorization apiAuthorization, + RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetUserFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized); + return unauthorizedResponse; + } + + _logger.LogInformation($"HTTP trigger function {nameof(GetUserFunction)} request is authorized."); + + var userId = req.Query["user_id"]; + + if (string.IsNullOrEmpty(userId)) + { + return req.CreateJsonResponse(new + { + message = "Missing or empty user_id param", + }, HttpStatusCode.BadRequest); + } + + var result = await dbContext.Users.Query(q => q.Where(w => w.UserId == userId)); + var user = result.FirstOrDefault(); + + if (user == null) + { + _logger.LogInformation($"Could not find results for user: {userId}"); + return req.CreateJsonResponse(new + { + message = "User " + userId + " was not found", + }, HttpStatusCode.BadRequest); + } + + bool commentsConnected = !string.IsNullOrEmpty(user?.CommentsUserId); + + return req.CreateJsonResponse(new + { + user, commentsConnected, + }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/User/RemoveUserCommentsAccountFunction.cs b/SSW.Rules.AzFuncs/Functions/User/RemoveUserCommentsAccountFunction.cs new file mode 100644 index 0000000..2560659 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/User/RemoveUserCommentsAccountFunction.cs @@ -0,0 +1,68 @@ +using System.Net; +using AzureGems.CosmosDB; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using OidcApiAuthorization.Abstractions; +using OidcApiAuthorization.Models; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; +using DomainUser = SSW.Rules.AzFuncs.Domain.User; + +namespace SSW.Rules.AzFuncs.Functions.User; + +public class RemoveUserCommentsAccountFunction( + ILoggerFactory loggerFactory, + IApiAuthorization apiAuthorization, + RulesDbContext dbContext) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("RemoveUserCommentsAccountFunction")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + ApiAuthorizationResult authorizationResult = + await apiAuthorization.AuthorizeAsync(Converters.ConvertToIHeaderDictionary(req.Headers)); + + if (authorizationResult.Failed) + { + _logger.LogWarning(authorizationResult.FailureReason); + var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized); + return unauthorizedResponse; + } + + _logger.LogInformation( + $"HTTP trigger function {nameof(RemoveUserCommentsAccountFunction)} request is authorized."); + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + DomainUser data = JsonConvert.DeserializeObject(requestBody); + + if (data == null || string.IsNullOrEmpty(data?.UserId)) + { + return req.CreateJsonResponse(new + { + message = "Request body is empty or UserId is missing", + }, HttpStatusCode.BadRequest); + } + + var user = await dbContext.Users.Query(q => q.Where(w => w.UserId == data.UserId)); + var model = user.FirstOrDefault(); + + if (model == null) + { + return req.CreateJsonResponse(new + { + message = "User does not exist" + }, HttpStatusCode.BadRequest); + } + + model.CommentsUserId = null; + await dbContext.Users.Update(model); + + return req.CreateResponse(HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Widget/GetLatestRules.cs b/SSW.Rules.AzFuncs/Functions/Widget/GetLatestRules.cs new file mode 100644 index 0000000..3d87483 --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Widget/GetLatestRules.cs @@ -0,0 +1,40 @@ +using System.Net; +using System.Web; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Widget; + +public class GetLatestRules(ILoggerFactory loggerFactory, RulesDbContext context) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("GetLatestRules")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + var query = HttpUtility.ParseQueryString(req.Url.Query); + var skip = int.Parse(query["skip"] ?? "0"); + var take = int.Parse(query["take"] ?? "10"); + var githubUsername = query["githubUsername"]; + + _logger.LogInformation($"Fetching latest rules, Skip: {skip}, Take: {take}, GitHubUsername: {githubUsername}"); + + var rules = await context.LatestRules.GetAll(); + var filteredRules = rules + .Where(r => string.IsNullOrEmpty(githubUsername) || r.CreatedBy == githubUsername) + .OrderByDescending(r => r.UpdatedAt) + .Skip(skip) + .Take(take); + + var filteredRulesList = filteredRules.ToList(); + return filteredRulesList.Count == 0 + ? req.CreateJsonErrorResponse(HttpStatusCode.NotFound) + : req.CreateJsonResponse(filteredRules); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Functions/Widget/UpdateLatestRules.cs b/SSW.Rules.AzFuncs/Functions/Widget/UpdateLatestRules.cs new file mode 100644 index 0000000..3a58e8f --- /dev/null +++ b/SSW.Rules.AzFuncs/Functions/Widget/UpdateLatestRules.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Octokit; +using Octokit.Internal; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.helpers; +using SSW.Rules.AzFuncs.Persistence; + +namespace SSW.Rules.AzFuncs.Functions.Widget; + +public class UpdateLatestRules(ILoggerFactory loggerFactory, IGitHubClient gitHubClient, RulesDbContext context) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("UpdateLatestRules")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] + HttpRequestData req, + FunctionContext executionContext) + { + _logger.LogInformation("Processing UpdateLatestRules request."); + + // TODO: Get these from the ENV + const string repositoryOwner = "SSWConsulting"; + const string repositoryName = "SSW.Rules.Content"; + var request = new PullRequestRequest + { + State = ItemStateFilter.Closed, + }; + + var pullRequests = await gitHubClient.PullRequest.GetAllForRepository(repositoryOwner, repositoryName, request); + var syncHistory = await context.SyncHistory.GetAll(); + var existingCommitHashes = new HashSet(syncHistory.Select(sh => sh.CommitHash)); + HttpClient httpClient = new HttpClient(); + var newRules = new List(); + foreach (var pr in pullRequests) + { + if (existingCommitHashes.Contains(pr.MergeCommitSha)) break; + if (!pr.Merged) continue; + + var files = await gitHubClient.PullRequest.Files(repositoryOwner, repositoryName, pr.Number); + foreach (var file in files.Take(50)) + { + if (file.FileName.Contains("rule.md")) + { + var response = await httpClient.GetAsync(file.RawUrl); + if (!response.IsSuccessStatusCode) continue; + + var fileContent = await response.Content.ReadAsStringAsync(); + var frontMatter = Utils.ParseFrontMatter(fileContent); + + if (frontMatter is null) continue; + + var ruleHistoryCache = await context.RuleHistoryCache.Query(rhc => + rhc.Where(w => w.MarkdownFilePath == file.FileName)); + var foundRule = ruleHistoryCache.FirstOrDefault(); + + if (foundRule is not null) + { + var rule = new LatestRules + { + CommitHash = pr.MergeCommitSha, + RuleUri = frontMatter.Uri, + RuleName = frontMatter.Title, + CreatedAt = foundRule.CreatedAtDateTime, + UpdatedAt = pr.UpdatedAt.UtcDateTime, + CreatedBy = foundRule.CreatedByDisplayName, + UpdatedBy = foundRule.ChangedByDisplayName, + GitHubUsername = pr.User.Login + }; + newRules.Add(rule); + } + else + { + var rule = new LatestRules + { + CommitHash = pr.MergeCommitSha, + RuleUri = frontMatter.Uri, + RuleName = frontMatter.Title, + CreatedAt = frontMatter.Created, + UpdatedAt = pr.UpdatedAt.UtcDateTime, + CreatedBy = pr.User.Location, + UpdatedBy = pr.User.Login, + GitHubUsername = pr.User.Login + }; + newRules.Add(rule); + } + } + } + } + + foreach (var rule in newRules) + { + await context.LatestRules.Add(rule); + } + + _logger.LogInformation($"Updated Latest rules with {newRules.Count} new entries."); + + return req.CreateJsonResponse(new + { message = $"Latest rules updated successfully with {newRules.Count} new entries." }); + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Persistence/RulesDbContext.cs b/SSW.Rules.AzFuncs/Persistence/RulesDbContext.cs new file mode 100644 index 0000000..db4a390 --- /dev/null +++ b/SSW.Rules.AzFuncs/Persistence/RulesDbContext.cs @@ -0,0 +1,15 @@ +using SSW.Rules.AzFuncs.Domain; +using AzureGems.Repository.Abstractions; + +namespace SSW.Rules.AzFuncs.Persistence; + +public class RulesDbContext : CosmosContext +{ + public IRepository Reactions { get; set; } + public IRepository Bookmarks { get; set; } + public IRepository SecretContents { get; set; } + public IRepository Users { get; set; } + public IRepository SyncHistory { get; set; } + public IRepository RuleHistoryCache { get; set; } + public IRepository LatestRules { get; set; } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Program.cs b/SSW.Rules.AzFuncs/Program.cs new file mode 100644 index 0000000..7003412 --- /dev/null +++ b/SSW.Rules.AzFuncs/Program.cs @@ -0,0 +1,57 @@ +using AzureGems.CosmosDB; +using AzureGems.Repository.CosmosDB; +using Microsoft.Extensions.Hosting; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Octokit; +using OidcApiAuthorization; +using SSW.Rules.AzFuncs.Domain; +using SSW.Rules.AzFuncs.Persistence; +using Reaction = SSW.Rules.AzFuncs.Domain.Reaction; +using User = SSW.Rules.AzFuncs.Domain.User; + +var configurationRoot = new ConfigurationBuilder() + .SetBasePath(Environment.CurrentDirectory) + .AddJsonFile("appsettings.json", true) + .AddEnvironmentVariables() + .Build(); + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices(services => + { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + services.AddOidcApiAuthorization(); + services.AddSingleton(serviceProvider => + { + var client = new GitHubClient(new ProductHeaderValue("SSW.Rules")); + var githubToken = configurationRoot["GitHub:Token"]; + client.Credentials = new Credentials(githubToken); + return client; + }); + services.AddCosmosDb(builder => + { + builder + .Connect(endPoint: configurationRoot["CosmosDb:Account"], + authKey: configurationRoot["CosmosDb:Key"]) + .UseDatabase(databaseId: configurationRoot["CosmosDb:DatabaseName"]) + .WithSharedThroughput(400) + .WithContainerConfig(c => + { + c.AddContainer(containerId: nameof(Reaction), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(Bookmark), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(SecretContent), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(User), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(SyncHistory), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(RuleHistoryCache), partitionKeyPath: "/id"); + c.AddContainer(containerId: nameof(LatestRules), partitionKeyPath: "/id"); + }); + }); + services.AddCosmosContext(); + }) + .Build(); + + +host.Run(); \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/Properties/launchSettings.json b/SSW.Rules.AzFuncs/Properties/launchSettings.json new file mode 100644 index 0000000..d030b3b --- /dev/null +++ b/SSW.Rules.AzFuncs/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "SSW.Rules.AzFuncs": { + "commandName": "Project", + "commandLineArgs": "--port 7248", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/SSW.Rules.AzFuncs.csproj b/SSW.Rules.AzFuncs/SSW.Rules.AzFuncs.csproj new file mode 100644 index 0000000..de9d7a0 --- /dev/null +++ b/SSW.Rules.AzFuncs/SSW.Rules.AzFuncs.csproj @@ -0,0 +1,38 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + + + + \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/helpers/Converters.cs b/SSW.Rules.AzFuncs/helpers/Converters.cs new file mode 100644 index 0000000..e21dd0e --- /dev/null +++ b/SSW.Rules.AzFuncs/helpers/Converters.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Primitives; + +namespace SSW.Rules.AzFuncs.helpers; + +public static class Converters +{ + public static IHeaderDictionary ConvertToIHeaderDictionary(HttpHeadersCollection headers) + { + var headerDictionary = new HeaderDictionary(); + + foreach (var header in headers) + { + headerDictionary.Add(header.Key, new StringValues(header.Value.ToArray())); + } + + return headerDictionary; + } +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/helpers/Utils.cs b/SSW.Rules.AzFuncs/helpers/Utils.cs new file mode 100644 index 0000000..aa0a827 --- /dev/null +++ b/SSW.Rules.AzFuncs/helpers/Utils.cs @@ -0,0 +1,98 @@ +using System.Collections.Specialized; +using System.Net; +using System.Text.RegularExpressions; +using System.Web; +using Microsoft.Azure.Functions.Worker.Http; +using Newtonsoft.Json; +using SSW.Rules.AzFuncs.Domain; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace SSW.Rules.AzFuncs.helpers; + +public static partial class Utils +{ + /// + /// Simple Json Object { error: bool, message: string } + /// Error is true if status code >= 400 + /// + /// + /// Default 200 + /// Default empty + /// + public static HttpResponseData CreateJsonErrorResponse(this HttpRequestData req, HttpStatusCode statusCode = HttpStatusCode.OK, string message = "") + { + var response = req.CreateResponse(statusCode); + response.Headers.Add("Content-Type", "application/json"); + response.StatusCode = statusCode; + + var content = new + { + error = (int)statusCode is >= 400, + message = message + }; + + var jsonContent = JsonConvert.SerializeObject(content); + response.WriteString(jsonContent); + + return response; + } + + /// + /// Create a JSON object response + /// + /// + /// Default 200 + /// Default empty + /// + public static HttpResponseData CreateJsonResponse(this HttpRequestData req, object message, HttpStatusCode statusCode = HttpStatusCode.OK) + { + var response = req.CreateResponse(statusCode); + response.Headers.Add("Content-Type", "application/json"); + response.StatusCode = statusCode; + + var jsonContent = JsonConvert.SerializeObject(message); + response.WriteString(jsonContent); + + return response; + } + + public static async Task ReadFormDataAsync(this HttpRequestData req) + { + using var reader = new StreamReader(req.Body); + var body = await reader.ReadToEndAsync(); + var parsedForm = HttpUtility.ParseQueryString(body); + return parsedForm; + } + + public static FrontMatter? ParseFrontMatter(string markdownContent) + { + // Regular expression to extract the YAML front matter + var frontMatterRegex = FrontmatterRegex(); + var match = frontMatterRegex.Match(markdownContent); + + if (!match.Success) return null; // No front matter found + + var frontMatterYaml = match.Groups[1].Value; + + // Deserializer for YAML + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + // Deserialize YAML to FrontMatter object + try + { + return deserializer.Deserialize(frontMatterYaml); + } + catch (Exception ex) + { + // Handle parsing error + Console.WriteLine("Error parsing YAML: " + ex.Message); + return null; + } + } + + [GeneratedRegex(@"^---\s+(.*?)\s+---", RegexOptions.Singleline)] + private static partial Regex FrontmatterRegex(); +} \ No newline at end of file diff --git a/SSW.Rules.AzFuncs/host.json b/SSW.Rules.AzFuncs/host.json new file mode 100644 index 0000000..ee5cf5f --- /dev/null +++ b/SSW.Rules.AzFuncs/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file