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